import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { buildModel } from '../docs/ksf-workshop-board-20261110-r1/package/test-support/site/src/core/kfs/build.js'; import { workshopReview, WORKSHOP_OUTPUTS } from '../docs/ksf-workshop-board-20261110-r1/package/test-support/site/src/core/workshop-outputs.js'; const root = path.resolve('docs/ksf-workshop-board-20261110-r1/package'); const work = path.resolve('work/ksf-workshop-board-20261110-r1'); const manifest = JSON.parse(await fs.readFile(path.join(root, 'deploy/industry-ui-files.json'))); const raw = JSON.parse(await fs.readFile(path.join(work, 'public-after.json'))); const groups = JSON.parse(await fs.readFile(path.join(work, 'groups-after.json'))); const db = buildModel(raw), origin = 'https://kangshifu.fmode.cn'; const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href); const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' }); const errors = [], checks = [], requests = [], replaced = new Set(); const record = (name, data = {}) => checks.push({ name, ...data }); try { const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); const page = await context.newPage(); page.on('pageerror', e => errors.push(e.message)); await context.route(origin + '/**', async route => { const request = route.request(), url = new URL(request.url()), relative = url.pathname.slice(1); if (manifest.files.some(f => f.path === relative)) { replaced.add(relative); return route.fulfill({ contentType: 'application/javascript', body: await fs.readFile(path.join(root, 'payload', relative)) }); } if (request.method() !== 'GET' && request.method() !== 'HEAD') { const body = request.postDataJSON(); requests.push(body?.path || url.pathname); if (url.pathname !== '/api/functions' || !['/ksfGetPublicBoard', '/ksfGetPublicMemberGroups'].includes(body?.path)) { errors.push('unexpected write request: ' + url.pathname); return route.abort(); } } return route.continue(); }); const ready = async () => { await page.locator('view-workshop-outputs .filters').waitFor(); await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?._loading === false); }; const countIs = async expected => { await page.waitForFunction(n => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?.renderRoot.querySelectorAll('[data-workshop-artifact]').length === n, expected); assert.equal(await page.locator('[data-workshop-artifact]').count(), expected); }; await page.goto(origin + '/workshop'); await ready(); const live = workshopReview(db, groups); await countIs(live.rows.length); record('default-live-count-matches-real-public-data', { rows: live.rows.length }); assert.equal(await page.getByLabel('资料范围', { exact: true }).inputValue(), 'live'); assert.equal(await page.locator('.matrix thead th').count(), 9); assert.equal(await page.locator('[data-output-group]').count(), groups.groups.length + 1); for (const output of WORKSHOP_OUTPUTS) assert.ok((await page.locator('.matrix thead').textContent()).includes(output.name)); record('seven-output-matrix-and-real-shared-groups'); const text = await page.locator('view-workshop-outputs').evaluate(el => el.renderRoot.textContent); assert.ok(text.includes(`${live.ungrouped} 份成果未分组`)); record('ungrouped-live-artifacts-not-assigned-by-guess', { rows: live.ungrouped }); await page.getByLabel('资料范围', { exact: true }).selectOption('all'); await countIs(workshopReview(db, groups, { mode: 'all' }).rows.length); await page.getByLabel('显示历史版本', { exact: true }).check(); await countIs(raw.artifact.length); record('all-53-real-artifacts-retained-with-history', { artifacts: raw.artifact.length }); for (const mode of ['sample', 'unknown', 'manual']) { await page.getByLabel('资料范围', { exact: true }).selectOption(mode); await countIs(workshopReview(db, groups, { mode, history: '1' }).rows.length); record('mode-filter', { mode }); } await page.goto(origin + '/workshop?mode=all&history=1&group=__ungrouped'); await ready(); assert.equal(await page.getByLabel('小组', { exact: true }).inputValue(), '__ungrouped'); await page.reload(); await ready(); assert.equal(await page.getByLabel('小组', { exact: true }).inputValue(), '__ungrouped'); assert.equal(await page.getByLabel('显示历史版本', { exact: true }).isChecked(), true); record('scope-preserved-after-refresh'); const candidate = workshopReview(db, groups, { mode: 'all', history: '1', group: '__ungrouped' }).rows.find(row => row.artifact.report && row.refs.some(ref => ref.chapterId)); if (candidate) { const ref = candidate.refs.find(ref => ref.chapterId); const locator = page.locator(`[data-workshop-artifact="${candidate.id}"] .refs a`).filter({ hasText: WORKSHOP_OUTPUTS.find(o => o.key === ref.key).name }); await locator.click(); await page.locator(`view-report [data-chapter="${ref.chapterId}"]`).waitFor(); assert.equal(new URL(page.url()).searchParams.get('ch'), ref.chapterId); assert.ok(new URL(page.url()).searchParams.get('returnTo').includes('group=__ungrouped')); await page.goBack(); await ready(); record('real-report-chapter-link-and-return'); } else { const row = workshopReview(db, groups, { mode: 'all', history: '1', group: '__ungrouped' }).rows.find(row => row.artifact.report); assert.ok(row); await page.locator(`[data-workshop-artifact="${row.id}"] h3 a`).click(); await page.locator('view-report').waitFor(); assert.ok(new URL(page.url()).searchParams.get('returnTo').includes('group=__ungrouped')); await page.goBack(); await ready(); record('real-report-link-and-return; existing-data-has-no-explicit-output-chapter'); } await page.getByLabel('研究课题', { exact: true }).selectOption(live.workshops[0]); await page.reload(); await ready(); assert.equal(await page.getByLabel('研究课题', { exact: true }).inputValue(), live.workshops[0]); record('real-topic-filter-and-refresh'); for (const width of [1440, 390]) { await page.setViewportSize({ width, height: 1000 }); const size = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth })); assert.ok(size.scroll <= size.width, JSON.stringify(size)); assert.ok(await page.getByLabel('资料范围', { exact: true }).isVisible()); await page.screenshot({ path: path.join(work, `release-preview-${width}.png`), fullPage: true }); record('responsive-no-page-overflow', { width }); } await page.goto(origin + '/reports/assessment'); await page.locator('view-report-type').waitFor(); await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-report-type')?.renderRoot.textContent.includes('评估与论证')); record('assessment-route-compatible-with-production-dependencies'); await page.goto(origin + '/reports'); await page.locator('view-reports').waitFor(); await page.locator('view-reports a[href*="/workshop"]').waitFor(); record('report-center-links-to-workshop-on-mobile'); assert.deepEqual([...replaced].sort(), manifest.files.map(f => f.path).sort()); record('only-nine-candidate-files-overridden-all-other-modules-actual-production', { staticFiles: replaced.size }); await context.close(); } finally { await browser.close(); } assert.deepEqual(errors, []); const result = { schema: 'ksf.workshop-board.release-browser.v1', checkedAt: new Date().toISOString(), productionDeployment: false, method: 'production-origin browser with exactly nine candidate script overrides; real read-only public APIs and all other real production dependencies', checks, checksPassed: checks.length, errors, businessWrites: 0, requests: [...new Set(requests)], screenshotDirectory: 'local work directory; not included in public release' }; await fs.writeFile(path.join(root, 'evidence/release-browser.json'), JSON.stringify(result, null, 2) + '\n'); console.log(JSON.stringify({ checksPassed: checks.length, errors: errors.length, productionDeployment: false, businessWrites: 0 }));