verify-workshop-r12-browser.mjs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  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 { chromium } from '../work/workshop-manual-tools/node_modules/playwright/index.mjs';
  6. const root=path.resolve('docs/workshop-validated-final-20261110');
  7. const old=path.resolve('docs/workshop-self-run-20261110');
  8. const baseline=JSON.parse(await fs.readFile(path.join(old,'对话与产出.json'),'utf8'));
  9. const rerun=JSON.parse(await fs.readFile(path.join(root,'rerun/对话与产出.json'),'utf8'));
  10. const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'});
  11. const context=await browser.newContext({permissions:['clipboard-read','clipboard-write']});
  12. const page=await context.newPage();
  13. const errors=[];page.on('pageerror',e=>errors.push(e.message));
  14. const cases=[];
  15. async function noOverflow(name){
  16. const sizes=await page.evaluate(()=>({viewport:innerWidth,scroll:document.documentElement.scrollWidth}));
  17. assert.ok(sizes.scroll<=sizes.viewport,JSON.stringify({name,...sizes}));
  18. }
  19. try {
  20. const originals=[
  21. {name:'original-manual',file:path.resolve('docs/workshop-final-local-20261110/康师傅工作坊操作手册-本地定稿.html'),steps:baseline.steps},
  22. {name:'r12-manual',file:path.join(root,'康师傅工作坊操作手册-r12本地定稿.html'),steps:rerun.steps}
  23. ];
  24. let prompts=0,copied=0;
  25. for(const doc of originals){
  26. await page.goto(pathToFileURL(doc.file).href);
  27. assert.equal(await page.locator('.nav-item').count(),15);
  28. for(const width of [1440,390]){
  29. await page.setViewportSize({width,height:1000});
  30. for(const s of doc.steps){
  31. await page.evaluate(id=>{location.hash=id},s.stepId);
  32. await page.waitForFunction(id=>document.querySelector('.nav-item[aria-current="step"]')?.getAttribute('href')==='#'+id,s.stepId);
  33. const text=await page.locator('#prompt-text').textContent();
  34. assert.equal(text,s.prompt,doc.name+' '+s.stepId);prompts++;
  35. assert.ok(await page.locator('#prompt-text').isVisible());
  36. await page.locator('#copy-prompt').click();
  37. // Read only the known content immediately after this button wrote it.
  38. const clipboard=await page.evaluate(()=>navigator.clipboard.readText());
  39. assert.equal(clipboard.replaceAll('\r\n','\n'),s.prompt.replaceAll('\r\n','\n'));copied++;
  40. await noOverflow(doc.name+' '+s.stepId+' '+width);
  41. }
  42. if(doc.name==='r12-manual'){
  43. await page.evaluate(()=>{location.hash='topic'});
  44. await page.waitForFunction(()=>document.querySelector('.nav-item[aria-current="step"]')?.getAttribute('href')==='#topic');
  45. await page.waitForFunction(()=>!document.getElementById('toast').classList.contains('show'));
  46. await page.screenshot({path:path.join(root,`手册-${width}.png`),fullPage:true});
  47. }
  48. }
  49. }
  50. cases.push({check:'all-15-prompts-match-records-at-both-widths',comparisons:prompts,passed:true});
  51. cases.push({check:'copy-button-actual-browser-clipboard-content',comparisons:copied,passed:true});
  52. for(const rel of ['index.html','first-run/index.html','rerun/index.html']){
  53. await page.goto(pathToFileURL(path.join(root,rel)).href);
  54. for(const width of [1440,390]){
  55. await page.setViewportSize({width,height:1000});await noOverflow(rel+' '+width);
  56. if(rel==='index.html')await page.screenshot({path:path.join(root,`总览-${width}.png`)});
  57. }
  58. if(rel!=='index.html'){
  59. assert.equal(await page.locator('article[data-step]').count(),15);
  60. assert.equal(await page.locator('[data-output]').count(),7);
  61. await page.locator('#collapse').click();assert.equal(await page.locator('article details[open]').count(),0);
  62. await page.locator('#expand').click();assert.equal(await page.locator('article details[open]').count(),15);
  63. const steps=rel.startsWith('first-run')?baseline.steps:rerun.steps;
  64. for(const s of steps)assert.equal(await page.locator('#p-'+s.stepId).textContent(),s.prompt);
  65. const localSources=await page.locator('article a[href*="证据索引.html#"]').evaluateAll(els=>els.map(e=>e.getAttribute('href')));
  66. const sourcePage=await context.newPage();await sourcePage.goto(pathToFileURL(path.join(root,path.dirname(rel),'证据索引.html')).href);
  67. for(const src of new Set(localSources))assert.equal(await sourcePage.locator('#'+src.split('#')[1]).count(),1,src);
  68. await sourcePage.close();
  69. }
  70. const links=await page.locator('a[href]').evaluateAll(els=>els.map(e=>e.getAttribute('href')));
  71. for(const href of links){
  72. if(href.startsWith('#')){assert.equal(await page.locator('[id="'+href.slice(1)+'"]').count(),1,href);continue}
  73. if(/^[a-z]+:/i.test(href))continue;
  74. if(['本地定稿与两轮记录.zip','核验结果.json','浏览器核验.json'].includes(href))continue; // result files and archive are sealed after checks
  75. const p=path.resolve(root,path.dirname(rel),decodeURIComponent(href.split('#')[0]));await fs.access(p);
  76. }
  77. cases.push({check:'offline-report-navigation-and-links',file:rel,passed:true});
  78. }
  79. await page.goto(pathToFileURL(path.join(root,'rerun/index.html')).href+'#s-proposal');
  80. await page.setViewportSize({width:1440,height:1000});
  81. await page.locator('#s-proposal').evaluate(el=>el.scrollIntoView({behavior:'instant',block:'start'}));
  82. await page.waitForFunction(()=>{const y=document.getElementById('s-proposal').getBoundingClientRect().top;return y>=0&&y<=40});
  83. await page.screenshot({path:path.join(root,'第二轮提案-1440.png')});
  84. cases.push({check:'proposal-anchor-opens-at-decision-heading',passed:true});
  85. assert.deepEqual(errors,[]);
  86. const result={ok:true,widths:[1440,390],cases,scriptErrors:errors,realEmployeeTrial:false,nativeHostSkillDiscovery:false};
  87. await fs.writeFile(path.join(root,'浏览器核验.json'),JSON.stringify(result,null,2)+'\n');
  88. console.log(JSON.stringify(result));
  89. }finally{await browser.close()}