import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { chromium } from '../work/workshop-manual-tools/node_modules/playwright/index.mjs'; const root=path.resolve('docs/workshop-validated-final-20261110'); const old=path.resolve('docs/workshop-self-run-20261110'); const baseline=JSON.parse(await fs.readFile(path.join(old,'对话与产出.json'),'utf8')); const rerun=JSON.parse(await fs.readFile(path.join(root,'rerun/对话与产出.json'),'utf8')); const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'}); const context=await browser.newContext({permissions:['clipboard-read','clipboard-write']}); const page=await context.newPage(); const errors=[];page.on('pageerror',e=>errors.push(e.message)); const cases=[]; async function noOverflow(name){ const sizes=await page.evaluate(()=>({viewport:innerWidth,scroll:document.documentElement.scrollWidth})); assert.ok(sizes.scroll<=sizes.viewport,JSON.stringify({name,...sizes})); } try { const originals=[ {name:'original-manual',file:path.resolve('docs/workshop-final-local-20261110/康师傅工作坊操作手册-本地定稿.html'),steps:baseline.steps}, {name:'r12-manual',file:path.join(root,'康师傅工作坊操作手册-r12本地定稿.html'),steps:rerun.steps} ]; let prompts=0,copied=0; for(const doc of originals){ await page.goto(pathToFileURL(doc.file).href); assert.equal(await page.locator('.nav-item').count(),15); for(const width of [1440,390]){ await page.setViewportSize({width,height:1000}); for(const s of doc.steps){ await page.evaluate(id=>{location.hash=id},s.stepId); await page.waitForFunction(id=>document.querySelector('.nav-item[aria-current="step"]')?.getAttribute('href')==='#'+id,s.stepId); const text=await page.locator('#prompt-text').textContent(); assert.equal(text,s.prompt,doc.name+' '+s.stepId);prompts++; assert.ok(await page.locator('#prompt-text').isVisible()); await page.locator('#copy-prompt').click(); // Read only the known content immediately after this button wrote it. const clipboard=await page.evaluate(()=>navigator.clipboard.readText()); assert.equal(clipboard.replaceAll('\r\n','\n'),s.prompt.replaceAll('\r\n','\n'));copied++; await noOverflow(doc.name+' '+s.stepId+' '+width); } if(doc.name==='r12-manual'){ await page.evaluate(()=>{location.hash='topic'}); await page.waitForFunction(()=>document.querySelector('.nav-item[aria-current="step"]')?.getAttribute('href')==='#topic'); await page.waitForFunction(()=>!document.getElementById('toast').classList.contains('show')); await page.screenshot({path:path.join(root,`手册-${width}.png`),fullPage:true}); } } } cases.push({check:'all-15-prompts-match-records-at-both-widths',comparisons:prompts,passed:true}); cases.push({check:'copy-button-actual-browser-clipboard-content',comparisons:copied,passed:true}); for(const rel of ['index.html','first-run/index.html','rerun/index.html']){ await page.goto(pathToFileURL(path.join(root,rel)).href); for(const width of [1440,390]){ await page.setViewportSize({width,height:1000});await noOverflow(rel+' '+width); if(rel==='index.html')await page.screenshot({path:path.join(root,`总览-${width}.png`)}); } if(rel!=='index.html'){ assert.equal(await page.locator('article[data-step]').count(),15); assert.equal(await page.locator('[data-output]').count(),7); await page.locator('#collapse').click();assert.equal(await page.locator('article details[open]').count(),0); await page.locator('#expand').click();assert.equal(await page.locator('article details[open]').count(),15); const steps=rel.startsWith('first-run')?baseline.steps:rerun.steps; for(const s of steps)assert.equal(await page.locator('#p-'+s.stepId).textContent(),s.prompt); const localSources=await page.locator('article a[href*="证据索引.html#"]').evaluateAll(els=>els.map(e=>e.getAttribute('href'))); const sourcePage=await context.newPage();await sourcePage.goto(pathToFileURL(path.join(root,path.dirname(rel),'证据索引.html')).href); for(const src of new Set(localSources))assert.equal(await sourcePage.locator('#'+src.split('#')[1]).count(),1,src); await sourcePage.close(); } const links=await page.locator('a[href]').evaluateAll(els=>els.map(e=>e.getAttribute('href'))); for(const href of links){ if(href.startsWith('#')){assert.equal(await page.locator('[id="'+href.slice(1)+'"]').count(),1,href);continue} if(/^[a-z]+:/i.test(href))continue; if(['本地定稿与两轮记录.zip','核验结果.json','浏览器核验.json'].includes(href))continue; // result files and archive are sealed after checks const p=path.resolve(root,path.dirname(rel),decodeURIComponent(href.split('#')[0]));await fs.access(p); } cases.push({check:'offline-report-navigation-and-links',file:rel,passed:true}); } await page.goto(pathToFileURL(path.join(root,'rerun/index.html')).href+'#s-proposal'); await page.setViewportSize({width:1440,height:1000}); await page.locator('#s-proposal').evaluate(el=>el.scrollIntoView({behavior:'instant',block:'start'})); await page.waitForFunction(()=>{const y=document.getElementById('s-proposal').getBoundingClientRect().top;return y>=0&&y<=40}); await page.screenshot({path:path.join(root,'第二轮提案-1440.png')}); cases.push({check:'proposal-anchor-opens-at-decision-heading',passed:true}); assert.deepEqual(errors,[]); const result={ok:true,widths:[1440,390],cases,scriptErrors:errors,realEmployeeTrial:false,nativeHostSkillDiscovery:false}; await fs.writeFile(path.join(root,'浏览器核验.json'),JSON.stringify(result,null,2)+'\n'); console.log(JSON.stringify(result)); }finally{await browser.close()}