verify-browser.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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. const out=path.resolve('docs/workshop-report-layout-20261111');
  6. const raw=JSON.parse(await fs.readFile('docs/workshop-argument-quality-20261111/example-fixture.json','utf8'));
  7. const original=JSON.stringify(raw);
  8. const origin='http://127.0.0.1:4181';
  9. const {chromium}=await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
  10. const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'});
  11. const checks=[],errors=[],requests=[],layouts=[];
  12. try {
  13. const context=await browser.newContext({viewport:{width:1440,height:1050}});
  14. await context.route('**/*',route=>{
  15. const request=route.request(),url=new URL(request.url());
  16. if(url.pathname==='/api/functions'||url.pathname==='/__dev/member-groups') {
  17. const body=request.postDataJSON();requests.push(body?.path);
  18. const data=body?.path==='/ksfGetPublicBoard'?raw:body?.path==='/ksfGetPublicMemberGroups'?{schema:'ksf.workshop-member-groups.v1',groups:[],members:[]}:null;
  19. if(!data){errors.push('Unexpected API '+body?.path);return route.abort();}
  20. return route.fulfill({contentType:'application/json',body:JSON.stringify({result:{ok:true,data}})});
  21. }
  22. if(url.origin===origin&&request.method()==='GET')return route.continue();
  23. errors.push('Unexpected request '+url.origin+url.pathname);return route.abort();
  24. });
  25. const page=await context.newPage();page.on('pageerror',error=>errors.push(error.message));
  26. // Before captures use the exact pre-edit report component and the same read-only fixture.
  27. await page.route('**/src/pages/view-report.js',route=>route.fulfill({contentType:'text/javascript',path:path.join(out,'before/view-report.js')}));
  28. await page.goto(origin+'/report/argument-preview?mode=sample');
  29. await page.locator('[data-evidence-reading]').waitFor();
  30. await page.screenshot({path:path.join(out,'before-overview-1440.png')});
  31. await page.locator('[data-evidence-reading]').scrollIntoViewIfNeeded();
  32. await page.screenshot({path:path.join(out,'before-evidence-1440.png')});
  33. await page.unroute('**/src/pages/view-report.js');
  34. for(const width of [1440,1280,768,390,320]) {
  35. await page.setViewportSize({width,height:1050});
  36. await page.goto(origin+'/report/argument-preview?mode=sample');
  37. await page.locator('[data-evidence-reading]').waitFor();
  38. await page.evaluate(()=>document.fonts.ready);
  39. const report=page.locator('view-report');
  40. const measurements=await report.evaluate(host=>{
  41. const root=host.shadowRoot;
  42. 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)};};
  43. const groups=[...root.querySelectorAll('.evidence-catalog>.evidence-group')].map(rect);
  44. const cards=[...root.querySelectorAll('.evidence-catalog .evidence-excerpt')].map(el=>({...rect(el),scrollHeight:el.scrollHeight,clientHeight:el.clientHeight}));
  45. return {width:innerWidth,scrollWidth:document.documentElement.scrollWidth,groups,cards,
  46. emptyHeadings:[...root.querySelectorAll('h4')].filter(el=>!el.textContent.trim()).length,
  47. dividers:[...root.querySelectorAll('.evidence-excerpt')].every(el=>getComputedStyle(el.querySelector('.evidence-caption')).borderBottomWidth==='1px'&&getComputedStyle(el.querySelector('.evidence-footer')).borderTopWidth==='1px')};
  48. });
  49. assert.ok(measurements.scrollWidth<=width,JSON.stringify(measurements));
  50. assert.equal(measurements.emptyHeadings,0);
  51. assert.equal(measurements.dividers,true);
  52. assert.equal(measurements.cards.length,3);
  53. assert.ok(measurements.cards.every(card=>card.scrollHeight<=card.clientHeight+1),'Quote content must not scroll inside a fixed-height card');
  54. assert.ok(measurements.cards[2].height>measurements.cards[0].height+30,'Long quotation must naturally increase its card height');
  55. const cols=new Set(measurements.groups.map(group=>group.x)).size;
  56. assert.equal(cols,width>=1280?2:1,JSON.stringify({width,groups:measurements.groups}));
  57. const excerpts=await page.locator('[data-evidence-reading] blockquote').allTextContents();
  58. assert.deepEqual(excerpts,Object.values(raw.artifact[0].report.evidence).map(e=>e.quotes[0].text));
  59. assert.equal(await page.locator('.source-meta[open]').count(),0);
  60. const sourceMeta=page.locator('[data-evidence-reading] .source-meta').first();
  61. await sourceMeta.locator('summary').click();
  62. const detailsText=await sourceMeta.textContent();
  63. assert.ok(detailsText.replace(/\s/g,'').includes(raw.artifact[0].report.evidence.R05.quotes[0].ref.replace(/\s/g,'')),detailsText);
  64. await sourceMeta.locator('summary').click();
  65. await page.evaluate(()=>window.scrollTo(0,0));
  66. await page.screenshot({path:path.join(out,'overview-'+width+'.png')});
  67. await page.locator('[data-evidence-reading]').scrollIntoViewIfNeeded();
  68. await page.screenshot({path:path.join(out,'evidence-'+width+'.png')});
  69. if([1440,390].includes(width)) {
  70. await page.locator('[data-chapter="original-evidence"]').scrollIntoViewIfNeeded();
  71. await page.screenshot({path:path.join(out,'chapter-'+width+'.png')});
  72. }
  73. layouts.push({...measurements,columns:cols});
  74. checks.push(width+'px:引用卡自适应高度、分隔线、原句完整、出处展开、无横向溢出');
  75. }
  76. await page.locator('.finding .evidence-actions button').first().click();
  77. await page.locator('kf-drawer .panel').waitFor();
  78. assert.ok((await page.locator('kf-drawer .panel').innerText()).replace(/\s/g,'').includes('R05京东三味六碗商品评价'));
  79. await page.keyboard.press('Escape');
  80. await page.locator('kf-drawer .panel').waitFor({state:'hidden'});
  81. await page.locator('.finding [data-anchor]').first().click();
  82. await page.locator('kf-drawer .panel').waitFor();
  83. assert.ok((await page.locator('kf-drawer .panel').innerText()).includes(raw.artifact[0].report.evidence.R05.quotes[0].text));
  84. checks.push('来源记录与证据链入口仍可用');
  85. await page.keyboard.press('Escape');
  86. await page.locator('kf-drawer .panel').waitFor({state:'hidden'});
  87. await page.setViewportSize({width:1280,height:1050});
  88. await page.locator('[data-toc] a').nth(2).click();
  89. await page.waitForURL('**ch=next-test');
  90. await page.waitForFunction(()=>{
  91. const r=document.querySelector('kf-app')?.shadowRoot?.querySelector('view-report')?.shadowRoot;
  92. const target=r?.querySelector('#ch-next-test');return target&&target.getBoundingClientRect().top>0&&target.getBoundingClientRect().top<150;
  93. });
  94. checks.push('章节导航定位与表格阅读保持可用');
  95. await page.goto(origin+'/docs/workshop-report-layout-20261111/preview.html');
  96. await page.locator('[data-evidence-reading]').waitFor();
  97. await page.locator('.finding .evidence-actions button').first().click();
  98. await page.locator('kf-drawer .panel').waitFor();
  99. await page.keyboard.press('Escape');
  100. await page.locator('kf-drawer .panel').waitFor({state:'hidden'});
  101. await page.locator('[data-toc] a').nth(2).click();
  102. await page.waitForURL('**/preview.html?ch=next-test');
  103. await page.reload();
  104. await page.locator('[data-evidence-reading]').waitFor();
  105. assert.equal(new URL(page.url()).searchParams.get('ch'),'next-test');
  106. assert.equal(await page.locator('#load-state').count(),0);
  107. checks.push('独立本地预览可打开、查看来源、切换章节并刷新');
  108. assert.equal(JSON.stringify(raw),original);
  109. assert.deepEqual(errors,[]);
  110. assert.ok(requests.every(item=>['/ksfGetPublicBoard','/ksfGetPublicMemberGroups'].includes(item)));
  111. await fs.writeFile(path.join(out,'verification.json'),JSON.stringify({ok:true,checks,layouts,errors,fixtureOnly:true,contentUnchanged:true,productionWrites:0},null,2)+'\n');
  112. console.log(JSON.stringify({ok:true,checks,contentUnchanged:true,productionWrites:0}));
  113. } finally {await browser.close();}