verify-workshop-board-release-browser.mjs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import fs from 'node:fs/promises';
  2. import path from 'node:path';
  3. import assert from 'node:assert/strict';
  4. import { pathToFileURL } from 'node:url';
  5. import { buildModel } from '../docs/ksf-workshop-board-20261110-r1/package/test-support/site/src/core/kfs/build.js';
  6. import { workshopReview, WORKSHOP_OUTPUTS } from '../docs/ksf-workshop-board-20261110-r1/package/test-support/site/src/core/workshop-outputs.js';
  7. const root = path.resolve('docs/ksf-workshop-board-20261110-r1/package');
  8. const work = path.resolve('work/ksf-workshop-board-20261110-r1');
  9. const manifest = JSON.parse(await fs.readFile(path.join(root, 'deploy/industry-ui-files.json')));
  10. const raw = JSON.parse(await fs.readFile(path.join(work, 'public-after.json')));
  11. const groups = JSON.parse(await fs.readFile(path.join(work, 'groups-after.json')));
  12. const db = buildModel(raw), origin = 'https://kangshifu.fmode.cn';
  13. const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
  14. const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' });
  15. const errors = [], checks = [], requests = [], replaced = new Set();
  16. const record = (name, data = {}) => checks.push({ name, ...data });
  17. try {
  18. const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
  19. const page = await context.newPage(); page.on('pageerror', e => errors.push(e.message));
  20. await context.route(origin + '/**', async route => {
  21. const request = route.request(), url = new URL(request.url()), relative = url.pathname.slice(1);
  22. if (manifest.files.some(f => f.path === relative)) {
  23. replaced.add(relative);
  24. return route.fulfill({ contentType: 'application/javascript', body: await fs.readFile(path.join(root, 'payload', relative)) });
  25. }
  26. if (request.method() !== 'GET' && request.method() !== 'HEAD') {
  27. const body = request.postDataJSON(); requests.push(body?.path || url.pathname);
  28. if (url.pathname !== '/api/functions' || !['/ksfGetPublicBoard', '/ksfGetPublicMemberGroups'].includes(body?.path)) {
  29. errors.push('unexpected write request: ' + url.pathname); return route.abort();
  30. }
  31. }
  32. return route.continue();
  33. });
  34. const ready = async () => {
  35. await page.locator('view-workshop-outputs .filters').waitFor();
  36. await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?._loading === false);
  37. };
  38. const countIs = async expected => {
  39. await page.waitForFunction(n => document.querySelector('kf-app')?.shadowRoot.querySelector('view-workshop-outputs')?.renderRoot.querySelectorAll('[data-workshop-artifact]').length === n, expected);
  40. assert.equal(await page.locator('[data-workshop-artifact]').count(), expected);
  41. };
  42. await page.goto(origin + '/workshop'); await ready();
  43. const live = workshopReview(db, groups);
  44. await countIs(live.rows.length); record('default-live-count-matches-real-public-data', { rows: live.rows.length });
  45. assert.equal(await page.getByLabel('资料范围', { exact: true }).inputValue(), 'live');
  46. assert.equal(await page.locator('.matrix thead th').count(), 9);
  47. assert.equal(await page.locator('[data-output-group]').count(), groups.groups.length + 1);
  48. for (const output of WORKSHOP_OUTPUTS) assert.ok((await page.locator('.matrix thead').textContent()).includes(output.name));
  49. record('seven-output-matrix-and-real-shared-groups');
  50. const text = await page.locator('view-workshop-outputs').evaluate(el => el.renderRoot.textContent);
  51. assert.ok(text.includes(`${live.ungrouped} 份成果未分组`)); record('ungrouped-live-artifacts-not-assigned-by-guess', { rows: live.ungrouped });
  52. await page.getByLabel('资料范围', { exact: true }).selectOption('all');
  53. await countIs(workshopReview(db, groups, { mode: 'all' }).rows.length);
  54. await page.getByLabel('显示历史版本', { exact: true }).check(); await countIs(raw.artifact.length);
  55. record('all-53-real-artifacts-retained-with-history', { artifacts: raw.artifact.length });
  56. for (const mode of ['sample', 'unknown', 'manual']) {
  57. await page.getByLabel('资料范围', { exact: true }).selectOption(mode);
  58. await countIs(workshopReview(db, groups, { mode, history: '1' }).rows.length);
  59. record('mode-filter', { mode });
  60. }
  61. await page.goto(origin + '/workshop?mode=all&history=1&group=__ungrouped'); await ready();
  62. assert.equal(await page.getByLabel('小组', { exact: true }).inputValue(), '__ungrouped');
  63. await page.reload(); await ready();
  64. assert.equal(await page.getByLabel('小组', { exact: true }).inputValue(), '__ungrouped');
  65. assert.equal(await page.getByLabel('显示历史版本', { exact: true }).isChecked(), true);
  66. record('scope-preserved-after-refresh');
  67. const candidate = workshopReview(db, groups, { mode: 'all', history: '1', group: '__ungrouped' }).rows.find(row => row.artifact.report && row.refs.some(ref => ref.chapterId));
  68. if (candidate) {
  69. const ref = candidate.refs.find(ref => ref.chapterId);
  70. const locator = page.locator(`[data-workshop-artifact="${candidate.id}"] .refs a`).filter({ hasText: WORKSHOP_OUTPUTS.find(o => o.key === ref.key).name });
  71. await locator.click(); await page.locator(`view-report [data-chapter="${ref.chapterId}"]`).waitFor();
  72. assert.equal(new URL(page.url()).searchParams.get('ch'), ref.chapterId);
  73. assert.ok(new URL(page.url()).searchParams.get('returnTo').includes('group=__ungrouped'));
  74. await page.goBack(); await ready(); record('real-report-chapter-link-and-return');
  75. } else {
  76. const row = workshopReview(db, groups, { mode: 'all', history: '1', group: '__ungrouped' }).rows.find(row => row.artifact.report);
  77. assert.ok(row); await page.locator(`[data-workshop-artifact="${row.id}"] h3 a`).click();
  78. await page.locator('view-report').waitFor(); assert.ok(new URL(page.url()).searchParams.get('returnTo').includes('group=__ungrouped'));
  79. await page.goBack(); await ready(); record('real-report-link-and-return; existing-data-has-no-explicit-output-chapter');
  80. }
  81. await page.getByLabel('研究课题', { exact: true }).selectOption(live.workshops[0]);
  82. await page.reload(); await ready(); assert.equal(await page.getByLabel('研究课题', { exact: true }).inputValue(), live.workshops[0]);
  83. record('real-topic-filter-and-refresh');
  84. for (const width of [1440, 390]) {
  85. await page.setViewportSize({ width, height: 1000 });
  86. const size = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
  87. assert.ok(size.scroll <= size.width, JSON.stringify(size));
  88. assert.ok(await page.getByLabel('资料范围', { exact: true }).isVisible());
  89. await page.screenshot({ path: path.join(work, `release-preview-${width}.png`), fullPage: true }); record('responsive-no-page-overflow', { width });
  90. }
  91. await page.goto(origin + '/reports/assessment'); await page.locator('view-report-type').waitFor();
  92. await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-report-type')?.renderRoot.textContent.includes('评估与论证'));
  93. record('assessment-route-compatible-with-production-dependencies');
  94. await page.goto(origin + '/reports'); await page.locator('view-reports').waitFor();
  95. await page.locator('view-reports a[href*="/workshop"]').waitFor();
  96. record('report-center-links-to-workshop-on-mobile');
  97. assert.deepEqual([...replaced].sort(), manifest.files.map(f => f.path).sort());
  98. record('only-nine-candidate-files-overridden-all-other-modules-actual-production', { staticFiles: replaced.size });
  99. await context.close();
  100. } finally { await browser.close(); }
  101. assert.deepEqual(errors, []);
  102. const result = { schema: 'ksf.workshop-board.release-browser.v1', checkedAt: new Date().toISOString(),
  103. productionDeployment: false, method: 'production-origin browser with exactly nine candidate script overrides; real read-only public APIs and all other real production dependencies',
  104. checks, checksPassed: checks.length, errors, businessWrites: 0, requests: [...new Set(requests)],
  105. screenshotDirectory: 'local work directory; not included in public release' };
  106. await fs.writeFile(path.join(root, 'evidence/release-browser.json'), JSON.stringify(result, null, 2) + '\n');
  107. console.log(JSON.stringify({ checksPassed: checks.length, errors: errors.length, productionDeployment: false, businessWrites: 0 }));