verify-workshop-report-parity-browser.mjs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import fs from 'node:fs/promises';
  2. import path from 'node:path';
  3. import assert from 'node:assert/strict';
  4. import crypto from 'node:crypto';
  5. import { pathToFileURL } from 'node:url';
  6. import { buildModel } from '../src/core/kfs/build.js';
  7. import { seedMemberGroups } from '../src/core/member-groups.js';
  8. import { CORE_DELIVERABLES, DELIVERY_KINDS, DELIVERY_SCHEMA } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs';
  9. const out = path.resolve('docs/workshop-report-parity-20261111');
  10. const origin = 'http://127.0.0.1:4181';
  11. const raw = { artifact: [], session: [], project: [], insight: [], opportunity: [{ objectId: 'separate-opportunity', title: '浏览器隔离夹具机会' }], source: [] };
  12. const at = new Date().toISOString();
  13. const htmlFile = Buffer.from('<!doctype html><meta charset="utf-8"><h1>浏览器隔离夹具提案</h1><script>parent.postMessage("unexpected-report-script","*")</script>');
  14. const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5YkAAAAASUVORK5CYII=', 'base64');
  15. const publicFiles = new Map();
  16. for (let i = 0; i < 35; i++) {
  17. const participant = 'browser-participant-' + i, sessionId = 'browser-session-' + i;
  18. raw.session.push({ objectId: sessionId, studentName: '浏览器夹具组员' + i, workshopKey: 'browser-topic', groupId: 'G' + (i % 5 + 1), meta: { participantKey: participant }, createdAt: at });
  19. for (const output of i === 0 ? CORE_DELIVERABLES : CORE_DELIVERABLES.slice(0, 1)) {
  20. const id = 'browser-report-' + i + '-' + output.key;
  21. const artifact = { objectId: id, title: '浏览器隔离夹具:' + output.name, ownerName: '浏览器夹具组员' + i,
  22. ownerSession: sessionId, uploadedByKey: participant, agentName: '隔离测试共用智能体', artifactType: DELIVERY_KINDS[output.key].types[0],
  23. stageCode: 'S5', dataMode: 'sample', workshopKey: 'browser-topic', version: 1, reportSeriesKey: 'browser-topic:D2-08:' + output.key,
  24. createdAt: at, rawText: '浏览器隔离测试正文,不代表真实研究成果。',
  25. report: { title: '浏览器隔离夹具:' + output.name, executive: ['浏览器隔离测试,不代表真实研究成果。'],
  26. delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D2-08', kind: output.key, contentStatus: 'ready', outputs: [output.key] },
  27. chapters: [{ id: output.anchor, title: output.name, sections: [{ kind: 'para', text: '浏览器隔离测试正文:核对同一人的分类、正文、文件与报告跳转。' }] }] } };
  28. if (output.key === 'proposal') {
  29. artifact.report.attachments = [['报告.html','text/html',htmlFile],['示意图.png','image/png',png]].map(([name,contentType,bytes], index) => {
  30. const file = { id: 'browser-file-' + index, name, contentType, sizeBytes: bytes.length, sha256: crypto.createHash('sha256').update(bytes).digest('hex') };
  31. file.url = origin + '/api/functions?path=%2Fksf%2Ffile&fileId=' + file.id + '&artifactId=' + id;
  32. publicFiles.set(file.id, { ...file, bytes });return file;
  33. });
  34. }
  35. raw.artifact.push(artifact);
  36. }
  37. }
  38. const db = buildModel(raw), groups = seedMemberGroups(db.members);
  39. const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
  40. const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' });
  41. const checks = [], errors = [], requests = [];
  42. try {
  43. const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
  44. await context.route('**/*', route => {
  45. const request = route.request(), url = new URL(request.url());
  46. if (url.pathname === '/api/functions' && url.searchParams.get('path') === '/ksf/file') {
  47. const file = publicFiles.get(url.searchParams.get('fileId'));
  48. if (!file) return route.abort();
  49. return route.fulfill({ contentType: file.contentType, body: file.bytes,
  50. headers: { 'Content-Security-Policy': "default-src 'none'; sandbox", 'Content-Disposition': url.searchParams.has('preview') ? 'inline' : 'attachment' } });
  51. }
  52. if (url.pathname === '/api/functions' || url.pathname === '/__dev/member-groups') {
  53. const body = request.postDataJSON();requests.push(body?.path);
  54. const response = body?.path === '/ksfGetPublicBoard' ? raw : body?.path === '/ksfGetPublicMemberGroups' ? groups : null;
  55. if (!response) { errors.push('Unexpected action ' + body?.path);return route.abort(); }
  56. return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ result: { ok: true, data: response } }) });
  57. }
  58. if (url.origin === origin && request.method() === 'GET') return route.continue();
  59. errors.push('Unexpected network request ' + url.origin + url.pathname);return route.abort();
  60. });
  61. const page = await context.newPage();page.on('pageerror', error => errors.push(error.message));
  62. await page.goto(origin + '/workshop?mode=sample');
  63. await page.locator('[data-output-member]').first().waitFor();
  64. assert.equal(await page.locator('[data-output-member]').count(), 35);
  65. assert.equal(await page.locator('[data-output-group]').count(), 6); // Includes empty, explicitly ungrouped section.
  66. const memberId = db.artifactById['browser-report-0-market'].memberId;
  67. const member = page.locator(`[data-output-member="${memberId}"]`);
  68. await member.locator(':scope > summary').click();
  69. assert.equal(await member.locator('[data-delivery-state="saved"]').count(), 7);
  70. assert.equal(await member.locator('[data-workshop-artifact]').count(), 7);
  71. checks.push('35名隔离夹具组员、5组、个人七类交付状态与报告链接');
  72. for (const width of [1280,390]) {
  73. await page.setViewportSize({ width, height: 900 });
  74. const size = await page.evaluate(() => ({ innerWidth, scrollWidth: document.documentElement.scrollWidth }));
  75. assert.ok(size.scrollWidth <= size.innerWidth, JSON.stringify(size));
  76. await page.screenshot({ path: path.join(out, 'personal-reports-' + width + '.png'), fullPage: false });
  77. checks.push(width + 'px个人报告页无横向溢出');
  78. }
  79. for (const output of CORE_DELIVERABLES) {
  80. await page.goto(origin + '/workshop?mode=sample&reportType=' + DELIVERY_KINDS[output.key].category);
  81. await page.locator(`[data-output-member="${memberId}"] > summary`).click();
  82. const row = page.locator('[data-workshop-artifact="browser-report-0-' + output.key + '"]');
  83. await row.locator('h3 a').click();
  84. await page.locator('view-report [data-chapter="' + output.anchor + '"]').waitFor();
  85. assert.ok((await page.locator('view-report [data-chapter="' + output.anchor + '"]').textContent()).includes('浏览器隔离测试正文'));
  86. checks.push(output.name + ':个人分类报告点击进入同编号正文');
  87. }
  88. assert.equal(await page.locator('[data-report-attachments] img').count(), 1);
  89. assert.equal(await page.locator('[data-report-attachments] a').filter({ hasText: '下载文件' }).count(), 2);
  90. await page.evaluate(() => { window.__reportScriptExecuted = false;window.addEventListener('message', e => { if (e.data === 'unexpected-report-script') window.__reportScriptExecuted = true; }); });
  91. await page.locator('[data-report-attachments] details summary').click();
  92. await page.frameLocator('[data-report-attachments] iframe').locator('h1').waitFor();
  93. assert.equal(await page.locator('[data-report-attachments] iframe').getAttribute('sandbox'), '');
  94. assert.equal(await page.evaluate(() => window.__reportScriptExecuted), false);
  95. assert.ok(await page.locator('[data-report-attachments] img').evaluate(img => img.complete && img.naturalWidth > 0));
  96. await page.screenshot({ path: path.join(out, 'report-files-mobile.png'), fullPage: true });
  97. checks.push('图片实际加载、HTML受限预览和文件下载入口;HTML脚本未执行');
  98. await page.goto(origin + '/reports');await page.locator('[data-types]').waitFor();
  99. assert.equal(await page.locator('[data-type-card]').count(), 11);
  100. checks.push('报告中心11类入口可渲染,独立机会不混入报告数');
  101. await page.goto(origin + '/docs/workshop-quality-r13-20261110/' + encodeURIComponent('final-康师傅工作坊操作手册.html') + '#topic');
  102. await page.locator('#prompt-topic-1').waitFor();
  103. assert.match(await page.locator('#prompt-topic-1').textContent(), /按当前登录员工个人保存/);
  104. checks.push('手册课题收尾的复制正文包含后续个人保存、分类和文件关联约定');
  105. await context.close();
  106. } finally { await browser.close(); }
  107. assert.deepEqual(errors, []);
  108. await fs.writeFile(path.join(out, 'browser-check.json'), JSON.stringify({ checkedAt: new Date().toISOString(), productionWrites: 0,
  109. method: 'Local real components, isolated browser-only fixtures, all API requests intercepted; not real classroom results',
  110. checks, errors, requestFunctions: [...new Set(requests)] }, null, 2) + '\n');
  111. console.log(JSON.stringify({ checks: checks.length, errors, productionWrites: 0 }));