import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { pathToFileURL } from 'node:url'; import { WORKSHOP_OUTPUTS, workshopReview } from '../src/core/workshop-outputs.js'; import { buildModel } from '../src/core/kfs/build.js'; const out = path.resolve('docs/workshop-report-coverage-r13-20261110'); const raw = JSON.parse(await fs.readFile(path.join(out, 'local-public-projection.json'))); const groups = JSON.parse(await fs.readFile(path.join(out, 'local-shared-groups.json'))); const review = workshopReview(buildModel(raw), groups, { mode: 'sample' }); const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href); const server = spawn(process.execPath, ['src/dev/serve.mjs', '--port', '4186'], { cwd: process.cwd(), windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }); let log = ''; server.stdout.on('data', b => { log += b; });server.stderr.on('data', b => { log += b; }); const origin = 'http://127.0.0.1:4186'; const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' }); const checks = [], errors = [], requests = []; try { for (let attempt = 0; attempt < 50; attempt++) { if (server.exitCode !== null) throw Error('Local test server failed: ' + log); try { const r = await fetch(origin); if (r.ok) break; } catch {} if (attempt === 49) throw Error('Local test server not ready: ' + log); await new Promise(resolve => setTimeout(resolve, 100)); } const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); // Synthetic data is only supplied inside this browser. Nothing reaches production. await context.route('**/*', route => { const request = route.request(), url = new URL(request.url()); if (url.pathname === '/api/functions' || url.pathname === '/__dev/member-groups') { const body = request.postDataJSON(); requests.push(body?.path); const response = body?.path === '/ksfGetPublicBoard' ? raw : body?.path === '/ksfGetPublicMemberGroups' ? groups : null; if (!response) { errors.push('Unexpected action: ' + body?.path); return route.abort(); } return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ result: { ok: true, data: response } }) }); } if (url.origin === origin && request.method() === 'GET') return route.continue(); errors.push('Unexpected network request: ' + url.origin + url.pathname);return route.abort(); }); const page = await context.newPage();page.on('pageerror', error => errors.push(error.message)); 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); }; await page.goto(origin + '/workshop');await ready(); assert.equal(await page.locator('[data-workshop-artifact]').count(), 0); checks.push('样例不进入默认真实资料列表'); await page.getByLabel('资料范围', { exact: true }).selectOption('sample'); await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?.renderRoot.querySelectorAll('[data-workshop-artifact]').length === 16); assert.equal(await page.locator('[data-workshop-artifact]').count(), 16); assert.equal(await page.locator('.matrix thead th').count(), 9); checks.push('16份本地草稿显示,含3份独立机会卡报告;七类矩阵存在'); for (const output of WORKSHOP_OUTPUTS) { await page.getByLabel('成果类别', { exact: true }).selectOption(output.key); const expected = review.rows.filter(row => row.refs.some(ref => ref.key === output.key)).length; await page.waitForFunction(n => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?.renderRoot.querySelectorAll('[data-workshop-artifact]').length === n, expected); const row = review.rows.find(row => row.refs.some(ref => ref.key === output.key && ref.chapterId)); assert.ok(row); const ref = row.refs.find(ref => ref.key === output.key); await page.locator(`[data-workshop-artifact="${row.id}"] .refs a`).filter({ hasText: output.name }).click(); await page.locator(`view-report [data-chapter="${ref.chapterId}"]`).waitFor(); const chapter = row.artifact.report.chapters.find(ch => ch.id === ref.chapterId); const text = await page.locator(`view-report [data-chapter="${ref.chapterId}"]`).textContent(); const normalize = s => s.replace(/\s/g, ''); assert.ok(normalize(text).includes(normalize(chapter.sections[0].text).slice(0, 90))); assert.equal(new URL(page.url()).searchParams.get('ch'), ref.chapterId); checks.push(output.name + ':筛选数量、正文、章节链接通过(不代表内容已完整)'); await page.goBack();await ready(); } await page.getByLabel('成果类别', { exact: true }).selectOption(''); for (const width of [1280, 390]) { await page.setViewportSize({ width, height: 900 }); const size = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth })); assert.ok(size.scroll <= size.width, JSON.stringify(size)); await page.screenshot({ path: path.join(out, 'local-board-' + width + '.png'), fullPage: true }); checks.push(width + 'px页面无横向溢出'); } await context.close(); } finally { await browser.close(); server.kill(); } assert.deepEqual(errors, []); await fs.writeFile(path.join(out, 'browser-check.json'), JSON.stringify({ checkedAt: new Date().toISOString(), method: 'Actual local board components with in-memory API projection intercepted in browser only', productionWrites: 0, deployed: false, checks, errors, requests: [...new Set(requests)] }, null, 2) + '\n'); console.log(JSON.stringify({ checks: checks.length, errors, productionWrites: 0 }));