// Browser acceptance against a real, already-running read-only panel. // Failure/empty checks below intercept responses only inside a fresh browser context. import assert from 'node:assert/strict' import { mkdir } from 'node:fs/promises' const { chromium } = await import(process.env.PLAYWRIGHT_MODULE) const url = process.env.DASHBOARD_URL assert(url, 'DASHBOARD_URL is required') const out = process.env.DASHBOARD_OUTPUT || '/tmp/software-dashboard-check' await mkdir(out, { recursive: true }) const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_EXECUTABLE, headless: true }) try { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 }) const errors = [] const mutations = [] page.on('pageerror', e => errors.push(e.message)) page.on('request', r => { if (r.method() !== 'GET') mutations.push(r.url()) }) await page.goto(url) await page.locator('#summary h1').waitFor() await page.waitForFunction(() => document.querySelector('#sync').textContent.startsWith('已同步')) assert.equal(await page.locator('.metric').count(), 4) assert(await page.locator('.directory').count() > 0, 'server must provide real repository inventory') await page.evaluate(() => window.scrollTo(0, 0)) await page.screenshot({ path: out + '/01-overview.png', fullPage: true }) await page.locator('nav [data-tab="impact"]').click() assert(await page.locator('svg[aria-label="候选的静态可达关联图"]').count()) await page.locator('details').first().locator('summary').click() assert(!(await page.locator('details').first().innerText()).includes('[object Object]')) await page.evaluate(() => window.scrollTo(0, 0)) await page.screenshot({ path: out + '/02-impact.png', fullPage: true }) await page.locator('nav [data-tab="changes"]').click() await page.locator('[data-comparison]').click() await page.locator('#comparison .minimetrics').waitFor() await page.evaluate(() => window.scrollTo(0, 0)) await page.screenshot({ path: out + '/03-changes.png', fullPage: true }) await page.locator('[data-evidence="changes.patch"]').click() await page.waitForFunction(() => document.querySelector('#artifact-text').textContent.startsWith('---')) await page.locator('#close').click() await page.locator('nav [data-tab="tests"]').click() await page.locator('tbody tr').first().waitFor() assert.equal(await page.locator('tbody tr').count(), 2) await page.evaluate(() => window.scrollTo(0, 0)) await page.screenshot({ path: out + '/04-tests.png', fullPage: true }) await page.locator('nav [data-tab="risks"]').click() await page.locator('#filter').selectOption('selected') assert(await page.locator('.candidate-row').count() > 0) await page.locator('#search').fill('not-a-real-file-xyz') assert.equal(await page.locator('.candidate-row').count(), 0) await page.setViewportSize({ width: 390, height: 844 }) await page.locator('nav [data-tab="overview"]').click() assert(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), 'no mobile overflow') await page.evaluate(() => window.scrollTo(0, 0)) await page.screenshot({ path: out + '/05-mobile.png', fullPage: true }) assert.deepEqual(errors, []) assert.deepEqual(mutations, []) await page.close() // Synthetic boundaries: unknown data, failed tests, update, disconnection and hostile text. const boundary = await browser.newPage({ viewport: { width: 1200, height: 900 } }) let state = { id: 'boundary', revision: 0, phase: 'analyzing', project: '', candidates: [], decisions: {}, coverage: {} } let fail = false let liveEvidence = null boundary.on('pageerror', e => errors.push(e.message)) await boundary.route('**/api/**', async route => { if (fail) return route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'Synthetic connection failure' }) }) const evidence = liveEvidence || { revision: state.revision, files: [], checks: state.phase === 'verification_failed' ? { 'baseline/checks.json': { passed: true, case_counts: { passed: 10, failed: 0, skipped: 2, unknown: 0 } }, 'round-1/checks.json': { passed: false, case_counts: { passed: 9, failed: 1, skipped: 2, unknown: 0 } }, } : {} } return route.fulfill({ contentType: 'application/json', body: JSON.stringify(new URL(route.request().url()).pathname === '/api/review' ? state : evidence) }) }) await boundary.goto(url) await boundary.locator('#summary h1').waitFor() assert.equal(await boundary.locator('#summary img').count(), 0, 'untrusted data must be escaped') assert.equal(await boundary.locator('.metric strong').last().textContent(), '—') await boundary.locator('nav [data-tab="tests"]').click() await boundary.getByText('本轮尚无测试检查报告;未执行不计通过。', { exact: true }).waitFor() state = { ...state, revision: 1, phase: 'verification_failed', execution: { result: { generation: 1, changed_files: [], verification_passed: false } } } await boundary.locator('#refresh').click() await boundary.getByText('改后验证失败', { exact: true }).waitFor() await boundary.locator('tbody tr').nth(1).waitFor() assert((await boundary.locator('tbody tr').nth(1).innerText()).includes('未通过')) fail = true await boundary.locator('#refresh').click() await boundary.getByText('连接中断 · 展示上次快照', { exact: true }).waitFor() assert((await boundary.locator('#error').innerText()).includes('不是最新状态')) assert.equal(await boundary.locator('tbody tr').count(), 2, 'preserve last snapshot on disconnect') fail = false for (const example of [ { name: '财务核算服务', label: 'Python 文件', adapter: 'synthetic-python', path: 'ledger/rules.py', title: '费用边界规则', extension: '.py' }, { name: '仓储调度前端', label: 'TypeScript 文件', adapter: 'synthetic-typescript', path: 'dispatch/index.ts', title: '接口职责边界', extension: '.ts' }, ]) { state = { id: 'boundary', revision: state.revision + 1, phase: 'awaiting_selection', project: example.name, analysis_scope: { label: example.label, total_files: 12, adapter: example.adapter }, coverage: { analyzed_files: 9 }, decisions: {}, repository_summary: { files: 20, directory_group_basis: 'snapshot_files', directory_groups: [{ name: 'src', files: 12 }, { name: 'docs', files: 8 }], facts: [{ label: '纳入快照的文件', value: 20 }], file_types: [{ extension: example.extension, files: 12 }] }, candidates: [{ id: 'new-kind', kind: 'adapter_specific_rule', title: example.title, path: example.path, source_range: { start: 3, end: 9 }, ontology_context: { dependency_paths: [{ path: 'contracts/contract'+example.extension, via: [{ origin: { path: example.path, line: 3 } }] }], dependency_paths_total: 1, unresolved_references_total: 2, unresolved_references: [{ path: example.path, line: 4, target: "ExternalPolicy", reason: "external_or_unindexed_symbol" }] } }], } await boundary.locator('#refresh').click() await boundary.getByRole('heading', { name: example.name, exact: true }).waitFor() await boundary.locator('nav [data-tab="overview"]').click() assert((await boundary.locator('.metric').first().innerText()).includes(example.label)) assert(!(await boundary.locator('#content').innerText()).includes('Maven')) assert.equal(await boundary.locator('.directory').count(), 2) await boundary.locator('nav [data-tab="impact"]').click() await boundary.getByRole('heading', { name: example.title, exact: true }).waitFor() await boundary.getByRole('heading', { name: '下游依赖 · 需要保持哪些组件契约', exact: true }).waitFor() assert((await boundary.locator('#content').innerText()).includes('contracts/contract'+example.extension)) assert((await boundary.locator('#content').innerText()).includes('2 条未解析引用')) await boundary.getByText('待查证引用 · 2 条', { exact: true }).click() await boundary.getByText('ExternalPolicy', { exact: true }).waitFor() await boundary.getByText('外部库或未索引类型', { exact: true }).waitFor() assert(!(await boundary.locator('#content').innerText()).includes('[object Object]')) await boundary.locator('nav [data-tab="risks"]').click() assert((await boundary.locator('#content').innerText()).includes(example.adapter)) } state = { ...state, revision: state.revision + 1, phase: 'running', execution: { result: { generation: 1 } } } liveEvidence = { revision: state.revision, token: 'first', files: [], checks: { 'baseline/checks.json': { passed: true, build_status: 'passed', test_status: 'passed', case_counts: { passed: 7 } }, } } await boundary.locator('#refresh').click() await boundary.locator('nav [data-tab="tests"]').click() await boundary.locator('tbody tr').waitFor() assert.equal(await boundary.locator('tbody tr').count(), 1) liveEvidence = { ...liveEvidence, token: 'second', checks: { ...liveEvidence.checks, 'round-1/checks.json': { passed: false, build_status: 'passed', test_status: 'failed', test_reason: 'Synthetic assertion regression', case_counts: { passed: 6, failed: 1 } }, } } await boundary.locator('#refresh').click() await boundary.locator('tbody tr').nth(1).waitFor() assert((await boundary.locator('tbody tr').nth(1).innerText()).includes('通过 / 失败')) await boundary.getByText('Synthetic assertion regression', { exact: true }).waitFor() state = { ...state, revision: state.revision + 1, phase: 'changes_requested', quality_review: { decision: 'revise', notes: { reason: '保留原有错误顺序,补充边界依据。' } }, candidates: Array.from({ length: 65 }, (_, i) => ({ id: 'entry-' + i, path: 'src/Entry' + i + '.ts', symbol: 'Entry' + i, kind: 'synthetic' })), } liveEvidence = { ...liveEvidence, revision: state.revision } await boundary.locator('#refresh').click() await boundary.locator('nav [data-tab="risks"]').click() await boundary.getByText('保留原有错误顺序,补充边界依据。', { exact: true }).waitFor() assert.equal(await boundary.locator('.candidate-row').count(), 20) for (let i = 0; i < 3; i++) await boundary.locator('[data-candidate-page="1"]').click() assert.equal(await boundary.locator('.candidate-row').count(), 5) await boundary.getByRole('button', { name: '下一页', exact: true }).isDisabled().then(value => assert(value)) await boundary.locator('#search').fill('Entry64') assert.equal(await boundary.locator('.candidate-row').count(), 1) state = { ...state, revision: state.revision + 1 } liveEvidence = { ...liveEvidence, revision: state.revision } await boundary.evaluate(() => poll()) assert.equal(await boundary.locator('#search').inputValue(), 'Entry64') assert(await boundary.locator('#search').evaluate(el => el === document.activeElement), 'refresh must preserve active search') state = { ...state, revision: state.revision + 1, phase: 'analyzing', candidates: [], coverage: { analyzed_files: 240, java_files: 240 }, progress: { stage: 'link', completed_batches: 2, total_batches: 2 } } liveEvidence = { ...liveEvidence, revision: state.revision } await boundary.locator('#refresh').click() await boundary.getByText('源码扫描完成,正在关联跨文件本体与约束。', { exact: true }).waitFor() assert.equal(await boundary.locator('.metric strong').nth(1).textContent(), '—', 'unfinished findings are not zero') assert.deepEqual(errors, []) // A running task may publish evidence without changing its review revision. // Never concatenate pages from different manifests, including modal next-page reads. const race = await browser.newPage() let evidenceToken = 'v1', replaceDuringRead = false const pageTokens = [] const documentText = '{"changed":["Example.java"]}' race.on('pageerror', e => errors.push(e.message)) await race.route('**/api/**', async route => { const target = new URL(route.request().url()) if (target.pathname === '/api/review') return route.fulfill({ json: { id: 'race', revision: 1, phase: 'running', project: 'Evidence pagination fixture', candidates: [], decisions: {}, coverage: {}, } }) if (target.pathname === '/api/evidence') return route.fulfill({ json: { id: 'race', revision: 1, token: evidenceToken, files: [{ name: 'comparison.json', bytes: documentText.length }], checks: {}, } }) const requested = target.searchParams.get('token'), offset = Number(target.searchParams.get('offset')) pageTokens.push(requested) if (requested !== evidenceToken) return route.fulfill({ status: 409, json: { error: 'Evidence version changed; reopen the file.' } }) if (offset === 0 && replaceDuringRead) { evidenceToken += '-updated' await race.evaluate(() => poll()) } return route.fulfill({ json: { text: offset === 0 ? documentText.slice(0, 12) : documentText.slice(12), offset, next_offset: offset === 0 ? 12 : null, total_chars: documentText.length } }) }) await race.goto(url) await race.waitForFunction(() => index !== null && busy === false) assert.deepEqual(await race.evaluate(() => readDocument('comparison.json')), { changed: ['Example.java'] }) assert.deepEqual(pageTokens.splice(0), ['v1', 'v1']) replaceDuringRead = true const failure = await race.evaluate(() => readDocument('comparison.json').catch(e => e.message)) assert.equal(failure, 'Evidence version changed; reopen the file.') assert.deepEqual(pageTokens.splice(0), ['v1', 'v1'], 'JSON pages must retain the first token') const beforeModal = evidenceToken await race.evaluate(() => showArtifact('comparison.json')) await race.locator('#more').click() await race.getByText('Evidence version changed; reopen the file.', { exact: true }).waitFor() assert.deepEqual(pageTokens, [beforeModal, beforeModal], 'modal next page must retain the first token') assert.deepEqual(errors, []) await race.close() console.log('PASS: real review 5 views; unknown/failure/disconnection; Python/TypeScript schemas; live evidence at unchanged revision; rejection notes; all 65 paginated candidates; search focus; measured progress; version-pinned evidence pagination') } finally { await browser.close() }