| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import http from 'node:http';
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import { fileURLToPath, pathToFileURL } from 'node:url';
- import assert from 'node:assert/strict';
- import crypto from 'node:crypto';
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const folder = path.join(root, 'docs/workshop-manual-20261109');
- const standalonePath = path.join(root, 'docs/workshop-manual-20261110-r1/ksf-workshop-manual-r10.html');
- const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
- const { chromium } = await import(pathToFileURL(path.join(root, 'work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
- const allowed = new Map([['/', ['index.html', 'text/html']], ['/index.html', ['index.html', 'text/html']], ['/manual.css', ['manual.css', 'text/css']], ['/content.js', ['content.js', 'application/javascript']], ['/manual.js', ['manual.js', 'application/javascript']]]);
- const server = http.createServer(async (req, res) => {
- if (req.url === '/standalone.html') {
- try { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(await fs.readFile(standalonePath)); }
- catch { res.writeHead(404); res.end(); }
- return;
- }
- const item = allowed.get(new URL(req.url, 'http://127.0.0.1').pathname);
- if (!item) { res.writeHead(404); res.end(); return; }
- try { res.writeHead(200, { 'content-type': item[1] + '; charset=utf-8' }); res.end(await fs.readFile(path.join(folder, item[0]))); }
- catch { res.writeHead(500); res.end(); }
- });
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
- const base = `http://127.0.0.1:${server.address().port}`;
- const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' });
- const errors = [];
- const checks = [];
- try {
- const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, permissions: ['clipboard-read', 'clipboard-write'] });
- const page = await context.newPage();
- page.on('pageerror', error => errors.push(error.message));
- await page.goto(base, { waitUntil: 'networkidle' });
- await page.locator('#chapter-title').waitFor();
- assert.equal(await page.locator('#chapter-title').textContent(), '安装与初始化');
- const steps = await page.evaluate(() => window.WORKSHOP_MANUAL_DATA.steps);
- assert.equal(steps.length, 15);
- assert.equal(await page.locator('.nav-item').count(), steps.length);
- assert.equal(await page.locator('.hero, .overview, .roadmap').count(), 0);
- assert.equal(await page.locator('#prompt-text').isVisible(), true);
- assert.ok((await page.locator('#prompt-text').boundingBox()).y < 520);
- checks.push('Compact task view starts directly with chapter content and expanded prompt');
- await page.locator('#copy-prompt').click();
- let copied = await page.evaluate(() => navigator.clipboard.readText());
- const release = await page.evaluate(() => window.WORKSHOP_MANUAL_DATA.release);
- assert.ok(copied.includes(release.guide) && copied.includes(release.version));
- assert.ok((await page.locator('.version-note').textContent()).includes(release.version));
- if (release.localDraftPublished === false) assert.ok((await page.locator('.version-note').textContent()).includes('待联合发布'));
- checks.push('Installation prompt copies the current fixed release and preserves migration status distinction');
- await page.locator('#completed').check();
- await page.reload({ waitUntil: 'networkidle' });
- assert.equal(await page.locator('#completed').isChecked(), true);
- await page.locator('#completed').uncheck();
- checks.push('Personal progress survives reload without any backend request');
- const overflow = [];
- for (const width of [1440, 390]) {
- await page.setViewportSize({ width, height: 960 });
- for (const step of steps) {
- await page.evaluate(id => { location.hash = id; }, step.id);
- await page.waitForFunction(title => document.getElementById('chapter-title')?.textContent === title, step.title);
- assert.equal(await page.locator('.nav-item[aria-current="step"]').count(), 1);
- assert.equal(await page.locator('#prompt-text').isVisible(), true);
- for (const output of step.deliverables) assert.ok((await page.locator('.outputs').textContent()).includes(output));
- const prompt = await page.locator('#prompt-text').textContent();
- assert.equal(/49\s*天|四十九天/.test(prompt), false);
- if (step.task) {
- assert.ok(prompt.includes(step.question) && prompt.includes(step.inputs) && prompt.includes(step.task));
- assert.ok(prompt.includes(step.skillReference));
- const reference = path.resolve(root, 'skills/ksf-workshop-runtime', step.skillReference);
- assert.ok(reference.startsWith(path.join(root, 'skills/ksf-workshop-runtime/references') + path.sep));
- assert.ok((await fs.stat(reference)).isFile());
- for (const output of step.deliverables) assert.ok(prompt.includes(output));
- }
- const dimensions = await page.evaluate(() => ({ client: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
- if (dimensions.scroll > dimensions.client) overflow.push({ width, id: step.id, ...dimensions });
- }
- }
- assert.deepEqual(overflow, []);
- checks.push('All 15 task panels render their outputs with no page overflow at 1440px and 390px');
- checks.push('All 13 research prompts include their question, reusable inputs, existing Skill reference, task and deliverables');
- await page.evaluate(() => { location.hash = 'competitors'; });
- await page.waitForFunction(() => document.getElementById('chapter-title').textContent === '比较竞品与产品取舍');
- await page.locator('#copy-prompt').click();
- copied = await page.evaluate(() => navigator.clipboard.readText());
- const competitor = steps.find(step => step.id === 'competitors');
- assert.ok(copied.includes(competitor.question) && copied.includes(competitor.inputs) && copied.includes(competitor.skillReference) && copied.includes(competitor.task));
- assert.ok(copied.includes('接续摘要') && copied.includes(competitor.deliverables[0]));
- checks.push('Research prompt copies shared evidence rules, chapter-specific work and deliverables together');
- await page.evaluate(() => { location.hash = 'proposal'; });
- const proposal = steps.find(step => step.id === 'proposal');
- await page.waitForFunction(title => document.getElementById('chapter-title').textContent === title, proposal.title);
- await page.locator('#copy-prompt').click();
- copied = await page.evaluate(() => navigator.clipboard.readText());
- assert.ok(copied.includes(proposal.task) && copied.includes('决定性待验证项'));
- assert.equal(/49\s*天|四十九天/.test(await page.locator('#chapter').textContent()), false);
- assert.equal(await page.locator('#next').isDisabled(), true);
- checks.push('Workshop ends with concept review and evidence gaps; final prompt excludes the post-workshop 49-day plan');
- await page.locator('.menu-button').click();
- assert.equal(await page.locator('.menu-button').getAttribute('aria-expanded'), 'true');
- await page.locator('.nav-item[href="#topic"]').click();
- assert.equal(await page.locator('.menu-button').getAttribute('aria-expanded'), 'false');
- await page.waitForFunction(() => document.getElementById('chapter-title').textContent === '确定本组研究课题');
- checks.push('Mobile menu navigates to the distinct topic-start step');
- await page.evaluate(() => { location.hash = 'install'; });
- await page.waitForFunction(() => document.getElementById('chapter-title').textContent === '安装与初始化');
- await page.waitForFunction(() => !document.getElementById('toast').classList.contains('show'));
- await page.evaluate(() => { document.documentElement.style.scrollBehavior = 'auto'; window.scrollTo(0, 0); });
- await page.screenshot({ path: path.join(folder, 'mobile.png'), fullPage: true, animations: 'disabled' });
- await page.setViewportSize({ width: 1440, height: 1000 });
- await page.evaluate(() => window.scrollTo(0, 0));
- await page.screenshot({ path: path.join(folder, 'desktop.png'), fullPage: true, animations: 'disabled' });
- assert.deepEqual(errors, []);
- checks.push('No browser execution errors');
- const standalone = await context.newPage();
- standalone.on('pageerror', error => errors.push(error.message));
- await standalone.goto(base + '/standalone.html', { waitUntil: 'networkidle' });
- await standalone.locator('#chapter-title').waitFor();
- assert.equal(await standalone.locator('#chapter-title').textContent(), '安装与初始化');
- assert.equal(await standalone.locator('.nav-item').count(), steps.length);
- await standalone.locator('#copy-prompt').click();
- const standalonePrompt = await standalone.evaluate(() => navigator.clipboard.readText());
- // Windows clipboard normalizes line endings; compare the complete text after that normalization.
- assert.equal(standalonePrompt.replaceAll('\r\n', '\n'), steps[0].prompt.replaceAll('\r\n', '\n'));
- await standalone.evaluate(() => { location.hash = 'proposal'; });
- await standalone.waitForFunction(() => document.getElementById('chapter-title').textContent === '完成概念提案与评审汇总');
- assert.deepEqual(errors, []);
- checks.push('Standalone HTML opens, copies the same complete prompt and navigates to the final chapter');
- const sourceFiles = Object.fromEntries(await Promise.all(['index.html', 'content.js', 'manual.js', 'manual.css'].map(async name => [name, digest(await fs.readFile(path.join(folder, name)))])));
- const receipt = { ok: true, panels: steps.length, widths: [1440, 390], checks, browserErrors: errors,
- sourceFiles, standaloneVerified: true, standaloneSha256: digest(await fs.readFile(standalonePath)),
- businessDataWritten: false, employeeWorkshopTrialCompleted: false, manualPublished: false };
- await fs.writeFile(path.join(folder, 'browser-verification.json'), JSON.stringify(receipt, null, 2) + '\n');
- console.log(JSON.stringify(receipt));
- } finally {
- await browser.close();
- await new Promise(resolve => server.close(resolve));
- }
|