| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- // 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: '<img src=x onerror=alert(1)>', 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() }
|