| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- import * as fs from 'node:fs'
- import * as path from 'node:path'
- import { pathToFileURL } from 'node:url'
- import assert from 'node:assert/strict'
- const runtimePath = path.resolve(process.argv[2])
- const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf8').replace(/^\uFEFF/, ''))
- const evidence = path.dirname(runtimePath)
- const { chromium } = await import(pathToFileURL(path.join(process.env.WEBNOVEL_PLAYWRIGHT, 'index.mjs')).href)
- const browser = await chromium.launch({ headless: true, executablePath: process.env.WEBNOVEL_CHROMIUM })
- const context = await browser.newContext({ viewport: { width: 1440, height: 960 } })
- const page = await context.newPage()
- const errors = []
- const requests = []
- const failedResponses = []
- const layoutSamples = []
- async function settleLayout() {
- await page.evaluate(async () => {
- const deadline = performance.now() + 5000
- let previous = '', stable = 0
- while (performance.now() < deadline) {
- await new Promise(resolve => requestAnimationFrame(resolve))
- const frame = document.querySelector('[data-shell-overlay]')?.parentElement
- const column = document.querySelector('.nw-native-writing')
- const geometry = JSON.stringify([frame, frame?.children[0], column].map(element => element?.getBoundingClientRect().toJSON()))
- const moving = document.getAnimations().some(animation => animation instanceof CSSTransition && animation.playState === 'running')
- stable = !moving && geometry === previous ? stable + 1 : 0
- if (stable >= 8) return
- previous = geometry
- }
- throw new Error('Writing layout did not settle')
- })
- }
- page.on('response', async response => {
- if (response.status() < 400) return
- const url = new URL(response.url()).pathname
- failedResponses.push({ url, status: response.status(), ...(url.startsWith('/webnovel/') ? { body: (await response.text()).slice(0,500) } : {}) })
- })
- page.on('request', request => { if (request.method() === 'POST') requests.push({ url: new URL(request.url()).pathname, body: request.postData()?.slice(0,800) }) })
- page.on('pageerror', error => errors.push(error.message))
- page.on('console', message => { if (message.type() === 'error') errors.push(message.text()) })
- try {
- const url = fs.readFileSync(path.join(runtime.root, 'stdout.log'), 'utf8').match(/dsh web: (http\S+)/)?.[1]
- if (!url) throw new Error('Acceptance host is not ready')
- await page.goto(url)
- await page.getByRole('tab', { name: '书房', exact: true }).waitFor({ state: 'visible', timeout: 30000 })
- const welcome = page.getByRole('button', { name: '继续', exact: true })
- await welcome.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {})
- if (await welcome.isVisible()) { await welcome.click(); await welcome.waitFor({ state: 'hidden' }) }
- if (['verify', 'production'].includes(process.argv[3])) {
- const launch = page.getByRole('button', { name: '打开书稿与资料', exact: true })
- const column = page.getByRole('complementary', { name: '书稿与资料', exact: true })
- await launch.click()
- await column.getByText('暂无打开的文档', { exact: true }).waitFor()
- for (const width of [1920, 1680, 1440, 1280, 1190, 1100, 1024, 960, 1100, 1440]) {
- await page.setViewportSize({ width, height: 900 })
- await settleLayout()
- const geometry = await column.evaluate(element => {
- const box = element.getBoundingClientRect()
- const frame = document.querySelector('[data-shell-overlay]')?.parentElement
- const center = frame.children[1].getBoundingClientRect()
- return { left: box.left, right: box.right, width: box.width, frameWidth: frame.getBoundingClientRect().width, sidebarWidth: frame.children[0].getBoundingClientRect().width, centerRight: center.right, overflow: document.documentElement.scrollWidth > window.innerWidth }
- })
- layoutSamples.push({ viewport: width, ...geometry })
- assert.ok(geometry.width >= 240, `Native writing width at viewport ${width}`)
- assert.ok(geometry.right <= width + 1)
- assert.ok(geometry.left >= 0)
- assert.equal(geometry.overflow, false)
- }
- // Native Sidebar owns resize, fullscreen and collapse; no custom frame reservation.
- await page.getByRole('button', { name: '收起右侧边栏', exact: true }).click()
- await column.waitFor({ state: 'hidden' })
- await launch.click(); await column.waitFor({ state: 'visible' })
- await page.getByRole('button', { name: '收起右侧边栏', exact: true }).click()
- await column.waitFor({ state: 'hidden' })
- assert.equal(await page.locator('[data-webnovel-writing-frame]').count(), 0)
- }
- if (process.argv[3] === 'production') {
- await page.screenshot({ path: path.join(evidence, 'production-desktop.png'), fullPage: true })
- assert.equal(await page.getByRole('tab', { name: '工作区', exact: true }).isVisible(), true)
- assert.equal(await page.getByRole('tab', { name: '书房', exact: true }).isVisible(), true)
- assert.equal(await page.getByRole('button', { name: '刷新书房', exact: true }).isVisible(), true)
- console.log(JSON.stringify({ productionClient: true, url: page.url().split('?')[0], layoutSamples, failedResponses, errors }))
- process.exitCode = errors.length ? 1 : 0
- } else {
- if (!await page.getByRole('tab', { name: '工作区', exact: true }).isVisible()) {
- const expand = page.getByRole('button', { name: '展开书房', exact: true })
- if (await expand.isVisible()) await expand.click()
- else await page.getByRole('button', { name: '打开侧边栏', exact: true }).click()
- }
- await page.getByRole('tab', { name: '工作区', exact: true }).click()
- await page.getByText('S5 临时验收书房', { exact: true }).click()
- await page.getByText('S5 书房验收', { exact: true }).click()
- await page.getByRole('tab', { name: '书房', exact: true }).click()
- await page.getByRole('button', { name: /验收作品甲/ }).waitFor()
- if (process.argv[3] === 'verify') {
- const checks = { blankSessionPanel: true, widthsAndNoOverlay: true, sidebarAndWritingToggle: true, keyboardAndPointerResize: true }
- const initialReport = JSON.parse(fs.readFileSync(path.join(evidence, 'browser-host-report.json'), 'utf8'))
- const firstBook = page.locator('.nw-book').filter({ hasText: '验收作品甲' })
- await firstBook.getByRole('button', { name: /验收作品甲/ }).click()
- await firstBook.getByRole('button', { name: /^草稿区/ }).click()
- await firstBook.getByRole('button', { name: '草稿', exact: true }).click()
- await firstBook.getByRole('button', { name: '卷01-来信', exact: true }).click()
- await firstBook.getByRole('button', { name: /^稿1.md/ }).click()
- await page.getByRole('tab', { name: '稿1.md', exact: true }).waitFor()
- await page.getByRole('button', { name: '关闭 稿1.md', exact: true }).click()
- checks.nestedBookTree = true
- await page.getByText('打开原文', { exact: true }).first().click()
- await page.getByRole('tab', { name: '稿1.md', exact: true }).waitFor()
- checks.nativeDocumentLink = true
- await page.getByRole('button', { name: '编辑', exact: true }).click()
- const editor = page.getByRole('textbox', { name: '文档正文', exact: true })
- await editor.click()
- await page.keyboard.press('Control+Home')
- await page.keyboard.press('Control+Shift+End')
- const selection = await editor.innerText()
- await page.getByRole('button', { name: /^引用到对话/ }).click()
- const composer = page.locator('[contenteditable="true"]:not([aria-label="文档正文"])').filter({ visible: true }).first()
- const quote = await composer.innerText()
- assert.ok(quote.includes('【书房原文引用】') && quote.includes('acceptance-a'))
- assert.ok(quote.includes(selection.trim().split('\n').filter(Boolean).at(-1)))
- assert.ok(quote.length > 1500)
- checks.fullQuote = true
- await composer.fill('')
- await page.getByRole('button', { name: '阅读', exact: true }).click()
- await page.getByRole('button', { name: '知识库', exact: true }).click()
- await page.getByRole('button', { name: '写作笔记.md', exact: true }).click()
- await page.getByRole('button', { name: '编辑', exact: true }).click()
- const sharedEditor = page.getByRole('textbox', { name: '文档正文', exact: true })
- await sharedEditor.click()
- await page.keyboard.press('Control+End')
- const marker = '浏览器保存验收 ' + Date.now()
- await page.keyboard.insertText('\n' + marker + '\n')
- await page.getByRole('button', { name: '保存', exact: true }).click()
- await page.getByText('文件已保存', { exact: true }).waitFor()
- await page.getByText('已交给主控,处理结果见对话', { exact: true }).waitFor()
- assert.ok(fs.readFileSync(path.join(runtime.workspace, '书房/知识库/写作笔记.md'), 'utf8').includes(marker))
- checks.authorSave = true
- await page.getByRole('button', { name: '改动', exact: true }).click()
- assert.ok((await page.locator('.nw-changes').innerText()).includes(marker))
- checks.savedDiff = true
- await page.getByRole('button', { name: '阅读', exact: true }).click()
- await page.getByRole('tab', { name: '工作区', exact: true }).click()
- const native = page.locator('.nw-native-workspaces')
- await native.getByText('S5 空工作范围', { exact: true }).hover()
- await native.getByRole('button', { name: '在“S5 空工作范围”中新建会话', exact: true }).click()
- await page.getByRole('tab', { name: '书房', exact: true }).click()
- await page.getByText(/当前工作范围还没有作品/).waitFor()
- assert.equal(await page.getByRole('tab', { name: '写作笔记.md', exact: true }).count(), 0)
- await page.getByRole('tab', { name: '工作区', exact: true }).click()
- await native.getByText('S5 书房验收', { exact: true }).click()
- await page.getByRole('tab', { name: '书房', exact: true }).click()
- await page.getByRole('tab', { name: '写作笔记.md', exact: true }).waitFor()
- checks.sessionIsolation = true
- await page.getByRole('button', { name: '编辑', exact: true }).click()
- const retainedEditor = page.getByRole('textbox', { name: '文档正文', exact: true })
- await retainedEditor.click()
- await page.keyboard.press('Control+End')
- await page.keyboard.insertText('\n收起后保留的编辑')
- const retainedText = await retainedEditor.innerText()
- await page.getByRole('button', { name: '收起编辑器', exact: true }).click()
- await settleLayout()
- await page.getByRole('button', { name: '打开书稿与资料', exact: true }).click()
- await settleLayout()
- assert.equal(await retainedEditor.innerText(), retainedText)
- await page.getByRole('button', { name: '撤销', exact: true }).click()
- assert.ok(!(await retainedEditor.innerText()).includes('收起后保留的编辑'))
- await page.getByRole('button', { name: '重做', exact: true }).click()
- assert.equal(await retainedEditor.innerText(), retainedText)
- await page.getByRole('button', { name: '撤销', exact: true }).click()
- await page.getByRole('button', { name: '阅读', exact: true }).click()
- checks.collapsePreservesEditing = true
- await page.screenshot({ path: path.join(evidence, 'study-desktop.png'), fullPage: true })
- await page.setViewportSize({ width: 1190, height: 900 })
- await settleLayout()
- await page.screenshot({ path: path.join(evidence, 'study-medium.png'), fullPage: true })
- await page.setViewportSize({ width: 1440, height: 960 })
- await settleLayout()
- await page.getByRole('tab', { name: '可视化', exact: true }).click()
- await page.getByRole('heading', { name: '章节进度' }).waitFor()
- await page.locator('.nw-chapters>button').first().waitFor()
- await page.screenshot({ path: path.join(evidence, 'study-chapters.png'), fullPage: true })
- checks.chapters = true
- await page.setViewportSize({ width: 960, height: 900 })
- await settleLayout()
- const narrow = page.getByRole('complementary', { name: '书稿与资料', exact: true })
- await narrow.waitFor()
- assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth), false)
- await page.screenshot({ path: path.join(evidence, 'study-narrow-desktop.png'), fullPage: true })
- await narrow.getByRole('button', { name: '编辑', exact: true }).click()
- const narrowEditor = narrow.getByRole('textbox', { name: '文档正文', exact: true })
- await narrowEditor.click()
- await page.keyboard.press('Control+End')
- await page.keyboard.insertText('\n尚未保存的修改')
- page.once('dialog', dialog => dialog.dismiss())
- await narrow.getByRole('button', { name: '关闭 写作笔记.md', exact: true }).click()
- assert.ok(await narrow.getByRole('tab', { name: /写作笔记.md/ }).count())
- page.once('dialog', dialog => dialog.accept())
- await narrow.getByRole('button', { name: '关闭 写作笔记.md', exact: true }).click()
- await narrow.getByRole('tab', { name: '稿1.md', exact: true }).waitFor()
- checks.desktopWindowAndUnsavedGuard = true
- await narrow.getByRole('button', { name: '收起编辑器', exact: true }).click()
- await narrow.waitFor({ state: 'hidden' })
- await settleLayout()
- checks.closeRestoresHost = true
- const hostReport = JSON.parse(fs.readFileSync(path.join(evidence, 'browser-host-report.json'), 'utf8'))
- assert.ok(hostReport.savesReceived > initialReport.savesReceived)
- assert.equal(hostReport.customEvents, 0)
- checks.nativeFollowup = true
- assert.deepEqual(errors, [])
- fs.writeFileSync(path.join(evidence, 'browser-acceptance.json'), JSON.stringify({ checks, layoutSamples, hostReport, errors }, null, 2) + '\n')
- console.log(JSON.stringify({ checks, layoutSamples, hostReport, errors }, null, 2))
- }
- await page.screenshot({ path: path.join(evidence, 'study-browser-inspect.png'), fullPage: true })
- if (process.argv[3] !== 'verify') console.log(JSON.stringify({ url: page.url().split('?')[0], text: (await page.locator('body').innerText()).slice(0,12000), buttons: await page.getByRole('button').evaluateAll(elements => elements.map(element => ({ text: element.innerText, title: element.title, aria: element.getAttribute('aria-label') }))), errors }, null, 2))
- }
- } catch (error) {
- await page.screenshot({ path: path.join(evidence, 'study-browser-failure.png'), fullPage: true })
- console.log(JSON.stringify({ text: (await page.locator('body').innerText()).slice(0,10000), links: await page.locator('a').evaluateAll(elements => elements.map(element => ({ text: element.innerText, href: element.getAttribute('href'), html: element.outerHTML.slice(0,1000) }))), requests: requests.slice(-15), failedResponses, errors }, null, 2))
- throw error
- } finally {
- await browser.close()
- }
|