import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; const out=path.resolve('docs/workshop-report-layout-20261111'); const raw=JSON.parse(await fs.readFile('docs/workshop-argument-quality-20261111/example-fixture.json','utf8')); const original=JSON.stringify(raw); const origin='http://127.0.0.1:4181'; const {chromium}=await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href); const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'}); const checks=[],errors=[],requests=[],layouts=[]; try { const context=await browser.newContext({viewport:{width:1440,height:1050}}); await context.route('**/*',route=>{ const request=route.request(),url=new URL(request.url()); if(url.pathname==='/api/functions'||url.pathname==='/__dev/member-groups') { const body=request.postDataJSON();requests.push(body?.path); const data=body?.path==='/ksfGetPublicBoard'?raw:body?.path==='/ksfGetPublicMemberGroups'?{schema:'ksf.workshop-member-groups.v1',groups:[],members:[]}:null; if(!data){errors.push('Unexpected API '+body?.path);return route.abort();} return route.fulfill({contentType:'application/json',body:JSON.stringify({result:{ok:true,data}})}); } if(url.origin===origin&&request.method()==='GET')return route.continue(); errors.push('Unexpected request '+url.origin+url.pathname);return route.abort(); }); const page=await context.newPage();page.on('pageerror',error=>errors.push(error.message)); // Before captures use the exact pre-edit report component and the same read-only fixture. await page.route('**/src/pages/view-report.js',route=>route.fulfill({contentType:'text/javascript',path:path.join(out,'before/view-report.js')})); await page.goto(origin+'/report/argument-preview?mode=sample'); await page.locator('[data-evidence-reading]').waitFor(); await page.screenshot({path:path.join(out,'before-overview-1440.png')}); await page.locator('[data-evidence-reading]').scrollIntoViewIfNeeded(); await page.screenshot({path:path.join(out,'before-evidence-1440.png')}); await page.unroute('**/src/pages/view-report.js'); for(const width of [1440,1280,768,390,320]) { await page.setViewportSize({width,height:1050}); await page.goto(origin+'/report/argument-preview?mode=sample'); await page.locator('[data-evidence-reading]').waitFor(); await page.evaluate(()=>document.fonts.ready); const report=page.locator('view-report'); const measurements=await report.evaluate(host=>{ const root=host.shadowRoot; const rect=el=>{const r=el.getBoundingClientRect();return {x:Math.round(r.x),y:Math.round(r.y),width:Math.round(r.width),height:Math.round(r.height)};}; const groups=[...root.querySelectorAll('.evidence-catalog>.evidence-group')].map(rect); const cards=[...root.querySelectorAll('.evidence-catalog .evidence-excerpt')].map(el=>({...rect(el),scrollHeight:el.scrollHeight,clientHeight:el.clientHeight})); return {width:innerWidth,scrollWidth:document.documentElement.scrollWidth,groups,cards, emptyHeadings:[...root.querySelectorAll('h4')].filter(el=>!el.textContent.trim()).length, dividers:[...root.querySelectorAll('.evidence-excerpt')].every(el=>getComputedStyle(el.querySelector('.evidence-caption')).borderBottomWidth==='1px'&&getComputedStyle(el.querySelector('.evidence-footer')).borderTopWidth==='1px')}; }); assert.ok(measurements.scrollWidth<=width,JSON.stringify(measurements)); assert.equal(measurements.emptyHeadings,0); assert.equal(measurements.dividers,true); assert.equal(measurements.cards.length,3); assert.ok(measurements.cards.every(card=>card.scrollHeight<=card.clientHeight+1),'Quote content must not scroll inside a fixed-height card'); assert.ok(measurements.cards[2].height>measurements.cards[0].height+30,'Long quotation must naturally increase its card height'); const cols=new Set(measurements.groups.map(group=>group.x)).size; assert.equal(cols,width>=1280?2:1,JSON.stringify({width,groups:measurements.groups})); const excerpts=await page.locator('[data-evidence-reading] blockquote').allTextContents(); assert.deepEqual(excerpts,Object.values(raw.artifact[0].report.evidence).map(e=>e.quotes[0].text)); assert.equal(await page.locator('.source-meta[open]').count(),0); const sourceMeta=page.locator('[data-evidence-reading] .source-meta').first(); await sourceMeta.locator('summary').click(); const detailsText=await sourceMeta.textContent(); assert.ok(detailsText.replace(/\s/g,'').includes(raw.artifact[0].report.evidence.R05.quotes[0].ref.replace(/\s/g,'')),detailsText); await sourceMeta.locator('summary').click(); await page.evaluate(()=>window.scrollTo(0,0)); await page.screenshot({path:path.join(out,'overview-'+width+'.png')}); await page.locator('[data-evidence-reading]').scrollIntoViewIfNeeded(); await page.screenshot({path:path.join(out,'evidence-'+width+'.png')}); if([1440,390].includes(width)) { await page.locator('[data-chapter="original-evidence"]').scrollIntoViewIfNeeded(); await page.screenshot({path:path.join(out,'chapter-'+width+'.png')}); } layouts.push({...measurements,columns:cols}); checks.push(width+'px:引用卡自适应高度、分隔线、原句完整、出处展开、无横向溢出'); } await page.locator('.finding .evidence-actions button').first().click(); await page.locator('kf-drawer .panel').waitFor(); assert.ok((await page.locator('kf-drawer .panel').innerText()).replace(/\s/g,'').includes('R05京东三味六碗商品评价')); await page.keyboard.press('Escape'); await page.locator('kf-drawer .panel').waitFor({state:'hidden'}); await page.locator('.finding [data-anchor]').first().click(); await page.locator('kf-drawer .panel').waitFor(); assert.ok((await page.locator('kf-drawer .panel').innerText()).includes(raw.artifact[0].report.evidence.R05.quotes[0].text)); checks.push('来源记录与证据链入口仍可用'); await page.keyboard.press('Escape'); await page.locator('kf-drawer .panel').waitFor({state:'hidden'}); await page.setViewportSize({width:1280,height:1050}); await page.locator('[data-toc] a').nth(2).click(); await page.waitForURL('**ch=next-test'); await page.waitForFunction(()=>{ const r=document.querySelector('kf-app')?.shadowRoot?.querySelector('view-report')?.shadowRoot; const target=r?.querySelector('#ch-next-test');return target&&target.getBoundingClientRect().top>0&&target.getBoundingClientRect().top<150; }); checks.push('章节导航定位与表格阅读保持可用'); await page.goto(origin+'/docs/workshop-report-layout-20261111/preview.html'); await page.locator('[data-evidence-reading]').waitFor(); await page.locator('.finding .evidence-actions button').first().click(); await page.locator('kf-drawer .panel').waitFor(); await page.keyboard.press('Escape'); await page.locator('kf-drawer .panel').waitFor({state:'hidden'}); await page.locator('[data-toc] a').nth(2).click(); await page.waitForURL('**/preview.html?ch=next-test'); await page.reload(); await page.locator('[data-evidence-reading]').waitFor(); assert.equal(new URL(page.url()).searchParams.get('ch'),'next-test'); assert.equal(await page.locator('#load-state').count(),0); checks.push('独立本地预览可打开、查看来源、切换章节并刷新'); assert.equal(JSON.stringify(raw),original); assert.deepEqual(errors,[]); assert.ok(requests.every(item=>['/ksfGetPublicBoard','/ksfGetPublicMemberGroups'].includes(item))); await fs.writeFile(path.join(out,'verification.json'),JSON.stringify({ok:true,checks,layouts,errors,fixtureOnly:true,contentUnchanged:true,productionWrites:0},null,2)+'\n'); console.log(JSON.stringify({ok:true,checks,contentUnchanged:true,productionWrites:0})); } finally {await browser.close();}