| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- 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/report-quality-html-20261111');
- const raw = JSON.parse(await fs.readFile(path.join(out, 'browser-fixture.json'), 'utf8'));
- const html = await fs.readFile(path.join(out, '报告版式预览.html'), 'utf8');
- const task = JSON.parse(await fs.readFile(path.join(out, 'preview-task.json'), 'utf8'));
- 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 = [], sizes = [];
- await fs.mkdir(path.join(out, 'screenshots'), { recursive: true });
- try {
- const context = await browser.newContext({ viewport: { width: 1440, height: 1050 } });
- let filesEnabled = true;
- await context.route('**/*', async route => {
- const request = route.request(), url = new URL(request.url());
- if (request.method() === 'GET' && url.pathname === '/api/functions' && url.searchParams.get('path') === '/ksf/file') {
- assert.equal(url.searchParams.get('artifactId'), 'argument-preview');
- return route.fulfill({ contentType: 'text/html', body: html, headers: {
- 'content-security-policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; sandbox allow-popups allow-popups-to-escape-sandbox",
- } });
- }
- if (url.pathname === '/api/functions' || url.pathname === '/__dev/member-groups') {
- const body = request.postDataJSON(); requests.push(body?.path);
- const data = body?.path === '/ksfGetPublicBoard' ? structuredClone(raw)
- : body?.path === '/ksfGetPublicMemberGroups' ? { schema: 'ksf.workshop-member-groups.v1', groups: [], members: [] } : null;
- if (!data) { errors.push('Unexpected API ' + body?.path); return route.abort(); }
- if (!filesEnabled && data.artifact) delete data.artifact[0].report.attachments;
- return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ result: { ok: true, data } }) });
- }
- if (url.origin === origin && request.method() === 'GET') return route.continue();
- // Source link navigation is intercepted to prove it can open from the sandbox.
- if (url.hostname === 'sh.jd.com') return route.fulfill({ contentType: 'text/html', body: '<h1>原始来源链接打开验证(未访问线上来源)</h1>' });
- errors.push('Unexpected request ' + request.method() + ' ' + url.origin + url.pathname); return route.abort();
- });
- const page = await context.newPage(); page.on('pageerror', e => errors.push(e.message));
- for (const width of [1440, 390, 320]) {
- await page.setViewportSize({ width, height: 1050 });
- await page.goto(origin + '/docs/report-quality-html-20261111/' + encodeURIComponent('报告版式预览.html'));
- const measure = await page.evaluate(() => ({
- width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
- quotes: [...document.querySelectorAll('.source-group .quote-card')].map(el => ({ height: el.getBoundingClientRect().height, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight })),
- tableScrolls: [...document.querySelectorAll('.table-wrap')].map(el => ({ client: el.clientWidth, scroll: el.scrollWidth })),
- text: document.body.innerText,
- dividers: [...document.querySelectorAll('.quote-card')].every(el => getComputedStyle(el.querySelector('figcaption')).borderBottomWidth === '1px' && getComputedStyle(el.querySelector('.quote-ref')).borderTopWidth === '1px'),
- }));
- assert.ok(measure.scrollWidth <= width, JSON.stringify(measure));
- assert.equal(measure.dividers, true);
- assert.equal(measure.quotes.length, 3);
- assert.ok(measure.quotes.every(q => q.scrollHeight <= q.clientHeight + 1));
- assert.ok(measure.quotes[2].height > measure.quotes[0].height + 30);
- for (const entry of Object.values(task.report.evidence)) assert.ok(measure.text.includes(entry.quotes[0].text));
- await page.screenshot({ path: path.join(out, `screenshots/html-cover-${width}.png`) });
- await page.locator('.finding').first().scrollIntoViewIfNeeded();
- await page.screenshot({ path: path.join(out, `screenshots/html-evidence-${width}.png`) });
- await page.locator('#chapter-3').scrollIntoViewIfNeeded();
- await page.screenshot({ path: path.join(out, `screenshots/html-comparison-${width}.png`) });
- sizes.push({ type: 'html', width, ...measure, text: undefined });
- checks.push(`${width}px 独立HTML:摘要/正文/原话保留、卡片自适应、来源分段、表格局部滚动、整页无溢出`);
- await page.goto(origin + '/report/argument-preview?mode=sample');
- await page.locator('[data-html-report]').waitFor();
- const frame = page.frameLocator('view-report iframe.html-report-preview');
- await frame.locator('h1').waitFor();
- assert.equal(await frame.locator('h1').textContent(), task.title);
- assert.equal(await page.locator('[data-chapter]:visible').count(), 0, '默认读HTML,避免正文重复堆叠');
- assert.equal(await page.locator('.reading-formats button[aria-pressed="true"]').textContent(), 'HTML 排版');
- const screen = await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
- assert.ok(screen.scrollWidth <= width, JSON.stringify(screen));
- await page.locator('[data-html-report]').scrollIntoViewIfNeeded();
- await page.screenshot({ path: path.join(out, `screenshots/board-html-${width}.png`) });
- await page.getByRole('button', { name: '看板正文与证据', exact: true }).click();
- await page.locator('[data-evidence-reading]').waitFor();
- assert.equal(await page.locator('[data-chapter]:visible').count(), 3);
- assert.equal(await page.locator('.html-report-preview').count(), 0);
- assert.deepEqual(await page.locator('[data-evidence-reading] blockquote').allTextContents(), Object.values(task.report.evidence).map(e => e.quotes[0].text));
- checks.push(`${width}px 看板:默认HTML、切换正文/证据、保留章节与原话、无溢出`);
- }
- await page.setViewportSize({ width: 1440, height: 1050 });
- await page.getByRole('button', { name: 'HTML 排版', exact: true }).click();
- const frame = page.frameLocator('view-report iframe.html-report-preview');
- const popupWait = page.waitForEvent('popup');
- await frame.locator('.quote-ref a').first().click();
- const popup = await popupWait; await popup.waitForLoadState();
- assert.equal(new URL(popup.url()).hostname, 'sh.jd.com');
- await popup.close();
- checks.push('HTML内来源链接可从受限预览打开;仅测试拦截,未访问外部来源');
- const sandbox = await page.locator('.html-report-preview').getAttribute('sandbox');
- assert.ok(!sandbox.includes('allow-scripts') && !sandbox.includes('allow-same-origin'));
- assert.ok((await page.locator('.html-open').getAttribute('href')).includes('preview=1'));
- assert.ok((await page.getByRole('link', { name: '下载 HTML', exact: true }).getAttribute('href')).includes('artifactId=argument-preview'));
- checks.push('独立阅读、下载关联同报告文件;预览不授予脚本或同源权限');
- await page.emulateMedia({ media: 'print' });
- assert.equal(await page.locator('[data-html-report]:visible').count(), 0);
- assert.equal(await page.locator('[data-chapter]:visible').count(), 3);
- assert.equal(await page.locator('[data-evidence-reading]:visible').count(), 1);
- await page.emulateMedia({ media: 'screen' });
- await page.goto(origin + '/report/argument-preview?mode=sample&ch=next-test');
- await page.locator('#ch-next-test').waitFor();
- assert.equal(await page.locator('.reading-formats button[aria-pressed="true"]').textContent(), '看板正文与证据');
- checks.push('带章节深链接自动切到看板正文,保留原章节阅读');
- filesEnabled = false;
- await page.goto(origin + '/report/argument-preview?mode=sample');
- await page.locator('[data-evidence-reading]').waitFor();
- assert.equal(await page.locator('[data-html-report]').count(), 0);
- assert.equal(await page.locator('[data-chapter]:visible').count(), 3);
- checks.push('没有HTML附件的历史报告保留可读正文,不造文件或链接');
- await page.goto(origin + '/docs/workshop-quality-r13-20261110/' + encodeURIComponent('final-康师傅工作坊操作手册.html'));
- assert.ok(await page.locator('body').innerText());
- const manual = await page.evaluate(() => window.WORKSHOP_MANUAL_DATA);
- assert.equal(manual.steps.length, 14);
- const bank = JSON.parse(await fs.readFile('docs/workshop-quality-r13-20261110/manual/workshop-dialogue-prompts.json', 'utf8'));
- assert.match(bank.shared.delivery, /不合格先补研究或修正文/);
- checks.push('final手册可读取,课表14页、42轮提示词结构保留,含内容自检与默认HTML规则');
- assert.deepEqual(errors, []);
- assert.ok(requests.every(x => ['/ksfGetPublicBoard','/ksfGetPublicMemberGroups'].includes(x)));
- const result = { ok: true, checks, sizes, errors, fixtureOnly: true, externalCollectionRequests: 0, productionWrites: 0 };
- await fs.writeFile(path.join(out, 'browser-verification.json'), JSON.stringify(result, null, 2) + '\n');
- console.log(JSON.stringify({ ok: true, checks: checks.length, errors, productionWrites: 0 }));
- } finally { await browser.close(); }
|