verify-workshop-report-coverage-r13-browser.mjs 5.9 KB

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