| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- /** Local Edge acceptance. Non-local browser requests are intercepted, never sent. */
- import assert from 'node:assert/strict';
- import { createServer } from 'node:http';
- import { spawn } from 'node:child_process';
- import { readFile, stat, mkdir, writeFile } from 'node:fs/promises';
- import { existsSync, readFileSync } from 'node:fs';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { indexHtml } from './config.mjs';
- const root = fileURLToPath(new URL('../', import.meta.url));
- const run = path.join(root, 'work', `showcase-browser-${Date.now()}`);
- await mkdir(run, { recursive: true });
- const checks = [], errors = [], requests = [], forbidden = [], observations = [];
- const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml', '.woff2': 'font/woff2' };
- const mount = '/preview-test/';
- let seedUnavailable = false;
- const server = createServer(async (req, res) => {
- try {
- const url = new URL(req.url, 'http://localhost');
- const sub = url.pathname.startsWith(mount);
- const rel = decodeURIComponent(sub ? url.pathname.slice(mount.length) : url.pathname.slice(1));
- if (/^(?:api|parse)\//.test(rel)) { res.writeHead(451); res.end('No business proxy'); return; }
- if (seedUnavailable && rel === 'src/assets/showcase/preview-seed.json') { res.writeHead(503); res.end('sample unavailable'); return; }
- const file = path.resolve(root, rel || 'index.html');
- if (!file.startsWith(path.resolve(root) + path.sep)) { res.writeHead(403); res.end(); return; }
- let body;
- let ext = path.extname(file);
- if (ext === '.html' || !ext) { body = indexHtml(sub ? mount : '/', { dev: false }); ext = '.html'; }
- else { assert.ok((await stat(file)).isFile()); body = await readFile(file); }
- res.writeHead(200, { 'content-type': mime[ext] || 'application/octet-stream', 'cache-control': 'no-store' });
- res.end(body);
- } catch { res.writeHead(404); res.end('missing resource'); }
- });
- await new Promise((done) => server.listen(0, '127.0.0.1', done));
- const origin = `http://127.0.0.1:${server.address().port}`;
- const profile = path.join(run, 'profile');
- await mkdir(profile);
- const executable = process.env.CHROME_PATH || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
- assert.ok(existsSync(executable), 'Edge/Chromium executable required');
- const browser = spawn(executable, ['--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank'], { windowsHide: true, stdio: 'ignore' });
- const pause = (ms) => new Promise((done) => setTimeout(done, ms));
- let socket;
- const check = (name, passed, detail) => { checks.push({ name, passed: Boolean(passed), ...(detail ? { detail } : {}) }); assert.ok(passed, name); };
- try {
- let port;
- for (let i = 0; i < 100; i++) {
- try { port = Number(readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').split('\n')[0]); } catch {}
- if (port) break;
- await pause(100);
- }
- assert.ok(port);
- const tabs = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
- socket = new WebSocket(tabs.find((x) => x.type === 'page').webSocketDebuggerUrl);
- await new Promise((done, reject) => { socket.addEventListener('open', done, { once: true }); socket.addEventListener('error', reject, { once: true }); });
- let seq = 0;
- const pending = new Map();
- const cdp = (method, params = {}) => new Promise((resolve, reject) => {
- const id = ++seq;
- const timer = setTimeout(() => { pending.delete(id); reject(new Error(`${method} timeout`)); }, 15000);
- pending.set(id, { resolve, reject, timer });
- socket.send(JSON.stringify({ id, method, params }));
- });
- const evaluate = async (expression) => {
- const value = await cdp('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
- if (value.exceptionDetails) throw new Error(value.exceptionDetails.exception?.description || value.exceptionDetails.text);
- return value.result.value;
- };
- const fixture = { result: { ok: true, data: { project: [], artifact: [], insight: [], source: [], session: [], opportunity: [], asOf: '2026-11-03T00:00:00.000Z', nextCursor: null } } };
- fixture.result.data.artifact.push({ objectId: 'fixture-report', title: '原有报告回归', stageCode: 'S4', artifactType: 'competitor_report', report: { title: '原有报告回归', chapters: [{ id: 'scope', title: '核对原文', sections: [{ kind: 'para', text: '公开报告阅读回归正文。' }] }], evidence: {} } });
- for (const type of ['category_report', 'voc_report', 'pain_list', 'concept']) fixture.result.data.artifact.push({ objectId: 'fixture-' + type, artifactType: type, title: '中文标题完整显示与多行排版验证', stageCode: 'S1', rawText: '本机回放排版测试资料。', createdAt: '2026-11-03T00:00:00.000Z' });
- fixture.result.data.opportunity.push({ objectId: 'fixture-opportunity', title: '原有机会卡回归', status: 'candidate', linkedArtifacts: ['fixture-report'], scores: [], updatedAt: '2026-11-03T00:00:00.000Z' });
- const fixtureEmployee = { key: 'employee:' + 'a'.repeat(64), name: '测试成员甲', displayName: '测试昵称', organizationName: '测试企业', departmentNames: ['测试产品部'], position: '产品经理', source: 'wecom_directory', nameVerified: true, verifiedAt: '2026-11-03T00:00:00Z' };
- fixture.result.data.session.push(...['one', 'two'].map((id) => ({ objectId: 'identity-session-' + id, studentName: '旧占位身份', employee: fixtureEmployee, meta: { participantKey: 'workshop-participant-' + id } })));
- Object.assign(fixture.result.data.artifact[0], { ownerSession: 'identity-session-one', ownerName: '旧占位身份' });
- socket.addEventListener('message', ({ data }) => {
- const m = JSON.parse(data);
- if (m.id && pending.has(m.id)) {
- const p = pending.get(m.id); clearTimeout(p.timer); pending.delete(m.id);
- if (m.error) p.reject(new Error(m.error.message)); else p.resolve(m.result);
- }
- if (m.method === 'Runtime.exceptionThrown') errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
- if (m.method === 'Network.requestWillBeSent') requests.push({ url: m.params.request.url, method: m.params.request.method, body: m.params.request.postData || '' });
- if (m.method === 'Fetch.requestPaused') {
- const { requestId, request } = m.params;
- if (request.url.startsWith(origin + '/')) cdp('Fetch.continueRequest', { requestId }).catch((e) => errors.push(e.message));
- else {
- forbidden.push({ url: request.url, method: request.method, body: request.postData || '' });
- const isPublicRead = request.postData?.includes('ksfGetPublicBoard');
- const isPreflight = request.method === 'OPTIONS';
- const action = request.postData ? JSON.parse(request.postData) : {};
- const isPublicAction = ['/ksfSetPublicOpportunityStatus', '/ksfRatePublicOpportunity'].includes(action.path);
- if (isPublicAction) {
- const opp = fixture.result.data.opportunity[0], params = action.params;
- opp.updatedAt = new Date().toISOString();
- if (action.path.includes('SetPublic')) opp.status = params.status;
- else opp.scores = [{ user: params.by, score: params.score, at: opp.updatedAt }];
- }
- cdp('Fetch.fulfillRequest', { requestId, responseCode: isPreflight ? 204 : isPublicRead || isPublicAction ? 200 : 451,
- responseHeaders: [{ name: 'Content-Type', value: 'application/json' }, { name: 'Access-Control-Allow-Origin', value: '*' }, { name: 'Access-Control-Allow-Headers', value: 'content-type,x-parse-application-id' }, { name: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS' }],
- body: Buffer.from(JSON.stringify(isPublicRead ? fixture : isPublicAction ? { result: { ok: true, data: { opportunity: fixture.result.data.opportunity[0] } } } : { error: 'No external traffic in acceptance' })).toString('base64'),
- }).catch((e) => errors.push(e.message));
- }
- }
- });
- await cdp('Page.enable'); await cdp('Runtime.enable'); await cdp('Network.enable');
- await cdp('Fetch.enable', { patterns: [{ urlPattern: '*' }] });
- await cdp('Page.addScriptToEvaluateOnNewDocument', { source: `const originalInterval=window.setInterval;window.setInterval=(fn,ms,...args)=>originalInterval(fn,ms===30000?100:ms,...args);` });
- const host = `document.querySelector('kf-app')?.shadowRoot?.querySelector('view-showcase')`;
- const shadow = `${host}?.shadowRoot`;
- const ready = async () => {
- for (let i = 0; i < 100; i++) { if (await evaluate(`Boolean(${host}?._model && !${host}?._busy)`)) return; await pause(100); }
- throw new Error('showcase did not load');
- };
- const go = async (route) => { await cdp('Page.navigate', { url: origin + route }); await ready(); await pause(150); };
- const shot = async (name, full = false) => {
- const params = { format: 'png', captureBeyondViewport: full };
- if (full) {
- const metrics = await cdp('Page.getLayoutMetrics');
- params.clip = { x: 0, y: 0, width: metrics.cssContentSize.width, height: metrics.cssContentSize.height, scale: 1 };
- }
- const result = await cdp('Page.captureScreenshot', params);
- await writeFile(path.join(run, name + '.png'), Buffer.from(result.data, 'base64'));
- };
- await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
- for (const key of ['overview', 'leadership', 'research', 'collaboration', 'validation', 'classroom']) {
- const route = key === 'overview' ? '/showcase' : '/showcase/' + key;
- await go(route);
- const info = await evaluate(`(()=>{const el=${host},root=el.shadowRoot;return {system:el.system,total:el._model.total,title:root.querySelector('h1')?.textContent,counts:[...root.querySelectorAll('.count-grid dd')].map(x=>Number(x.textContent)),width:document.documentElement.scrollWidth,viewport:innerWidth,images:[...root.images||root.querySelectorAll('img')].map(x=>({loaded:x.complete&&x.naturalWidth>0,src:x.src})),pending:root.querySelector('.pending')?.textContent};})()`);
- check(`${key}: correct route, real totals and loaded image`, info.system === key && info.total === 334 && info.counts.length === 12 && info.images.every((x) => x.loaded));
- check(`${key}: no page horizontal overflow`, info.width <= info.viewport + 1);
- observations.push(info);
- await shot(`desktop-${key}`, key === 'overview' || key === 'classroom');
- await cdp('Page.reload', { ignoreCache: true }); await ready();
- check(`${key}: survives direct refresh`, await evaluate(`${host}.system===${JSON.stringify(key)}`));
- }
- await go('/showcase');
- await evaluate(`${shadow}.querySelector('a[href$="/showcase/research"]').click()`); await pause(200);
- check('Shadow DOM links navigate to the correct page', await evaluate(`location.pathname==='/showcase/research'&&${host}.system==='research'`));
- await evaluate(`${shadow}.querySelector('.image-button').click()`); await pause(100);
- check('preview opens in a real modal dialog', await evaluate(`${shadow}.querySelector('dialog').open`));
- await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
- await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); await pause(100);
- check('Escape closes preview', await evaluate(`!${shadow}.querySelector('dialog').open`));
- await go('/showcase?case=P01');
- for (let i = 0; i < 7; i++) {
- const observation = await evaluate(`(()=>{const el=${host};return {step:el._step,text:el.shadowRoot.querySelector('.story-body').textContent};})()`);
- check(`P01 step ${i + 1} displays its real record`, observation.step === i && observation.text.trim().length > 10);
- if (i < 6) await evaluate(`${shadow}.querySelector('.story-controls .primary').click()`);
- await pause(60);
- }
- check('P01 retains six questions after classroom completion', await evaluate(`${shadow}.querySelectorAll('.question-list li').length===6`));
- await go('/showcase/validation?case=P03');
- await evaluate(`${shadow}.querySelectorAll('.story-steps button')[1].click()`); await pause(80);
- check('P03 shows preparation conflict', await evaluate(`${shadow}.querySelector('.story-body').textContent.includes('无灶具')`));
- await evaluate(`${shadow}.querySelectorAll('.story-steps button')[2].click()`); await pause(80);
- check('P03 questions retain owners', await evaluate(`${shadow}.querySelectorAll('.question-list li').length===6&&${shadow}.querySelector('.question-list').textContent.includes('演示学员')`));
- await go('/showcase/validation?case=P10');
- await evaluate(`${shadow}.querySelectorAll('.story-steps button')[3].click()`); await pause(80);
- check('P10 does not fabricate missing validation tasks', await evaluate(`${shadow}.querySelector('.empty-inline').textContent.includes('尚无验证任务')`));
- for (const key of ['overview', 'leadership', 'research', 'collaboration', 'validation', 'classroom']) {
- await cdp('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
- await go(key === 'overview' ? '/showcase' : '/showcase/' + key);
- check(`${key}: mobile has no horizontal page overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
- if (key === 'overview' || key === 'research') await shot('mobile-' + key, true);
- }
- await go(mount + 'showcase/collaboration?case=P01');
- check('nested deployment preserves route and image URLs', await evaluate(`${host}.system==='collaboration'&&${shadow}.querySelector('img').src.includes('/preview-test/src/assets/showcase/')`));
- await evaluate(`${shadow}.querySelector('a[href$="/showcase/validation"]').click()`); await pause(150);
- check('nested navigation preserves base path', await evaluate(`location.pathname==='/preview-test/showcase/validation'`));
- const before = forbidden.length;
- await evaluate(`(()=>{const app=document.querySelector('kf-app');for(const type of ['refresh','status-change','score-submit','artifact-open'])app.dispatchEvent(new CustomEvent(type,{detail:{id:'DEMO-O01',status:'approved',by:'模拟'}}));})()`);
- await pause(500);
- check('sample shell ignores global live business events', forbidden.length === before);
- check('all introduction interactions made zero external business requests', forbidden.length === 0);
- const sampleHost = `document.querySelector('kf-app').shadowRoot.querySelector('sample-shell')`;
- const viewHost = `${sampleHost}.shadowRoot.querySelector('main').firstElementChild`;
- const viewRoot = `${viewHost}.shadowRoot`;
- const detailHost = `${sampleHost}.shadowRoot.querySelector('sample-detail')`;
- const detailRoot = `${detailHost}.shadowRoot`;
- const sampleReady = async () => {
- for (let i = 0; i < 160; i++) { if (await evaluate(`(()=>{const shell=document.querySelector('kf-app')?.shadowRoot?.querySelector('sample-shell');return Boolean(shell?._model && shell.shadowRoot?.querySelector('main')?.firstElementChild?.shadowRoot?.querySelector('h1'));})()`)) { await pause(100); return; } await pause(100); }
- throw new Error('sample did not load');
- };
- const sampleGo = async (route) => { await cdp('Page.navigate', { url: origin + route }); await sampleReady(); };
- const clickRecord = async (id) => { await evaluate(`${viewRoot}.querySelector('button[data-record="${id}"]').click()`); await pause(150); };
- const submit = async (action, fields) => {
- await evaluate(`(()=>{const form=${detailRoot}.querySelector('[data-action="${action}"]').closest('form');for(const [key,value] of Object.entries(${JSON.stringify(fields)}))form.elements.namedItem(key).value=value;form.requestSubmit();})()`);
- await pause(200);
- };
- await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
- for (const name of ['cockpit', 'intel', 'coop', 'review', 'class']) {
- await sampleGo('/' + name);
- check(`${name}: independent sample shell and actual extension`, await evaluate(`${sampleHost}._model.baselineEntityCount===334&&${sampleHost}._model.extension.voices.length===12000`));
- check(`${name}: no page horizontal overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
- await shot('demo-' + name);
- await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
- check(`${name}: refresh preserves route`, await evaluate(`${sampleHost}.view===${JSON.stringify(name)}`));
- }
- await sampleGo('/cockpit?group=DEMO-G2');
- check('group filter scopes both project and report counts', await evaluate(`${viewHost}.filtered.projects.length===2&&${viewHost}.filtered.reports.every(r=>r.groupId==='DEMO-G2')`));
- await evaluate(`${viewRoot}.querySelector('[data-filter="group"]').value='';${viewRoot}.querySelector('[data-filter="group"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(100);
- await evaluate(`${viewRoot}.querySelector('[data-filter="member"]').value='DEMO-M31';${viewRoot}.querySelector('[data-filter="member"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(150);
- check('member selection filters owned and collaborating projects', await evaluate(`${viewHost}.filtered.projects.length<12&&${viewHost}.filtered.projects.some(p=>p.id==='DEMO-P10')`));
- await sampleGo('/intel');
- await evaluate(`${viewRoot}.querySelector('[data-filter="search"]').value='完全无此关键词XYZ';${viewRoot}.querySelector('[data-filter="search"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(150);
- check('search has a real zero-result state', await evaluate(`${viewHost}.filtered.voices.length===0&&${viewRoot}.textContent.includes('没有符合条件')`));
- await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
- check('search is preserved in URL after refresh', await evaluate(`${viewHost}.filters.search==='完全无此关键词XYZ'`));
- await sampleGo('/intel');
- await evaluate(`${viewRoot}.querySelector('[data-page="voicePage-next"]').click()`); await pause(150);
- check('pagination changes the visible voice records', await evaluate(`location.search.includes('voicePage=2')&&${viewRoot}.querySelector('[data-record^="SIM-VOICE"]').dataset.record==='SIM-VOICE-1-0011'`));
- await clickRecord('SIM-OBS-0001');
- check('product detail shows four distinct price snapshots', await evaluate(`${detailRoot}.querySelectorAll('tbody tr').length===4`));
- await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
- await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); await pause(150);
- check('Escape closes record detail and removes deep-link query', await evaluate(`!${detailRoot}.querySelector('dialog').open&&!location.search.includes('record=')`));
- await sampleGo('/coop');
- await evaluate(`localStorage.setItem('unrelated-demo-sentinel','preserve')`);
- await clickRecord('DEMO-O01');
- check('opportunity opens local dialog', await evaluate(`${detailRoot}.querySelector('dialog').open&&${detailHost}.recordId==='DEMO-O01'`));
- await submit('status', { user: '模拟甲', status: 'candidate' });
- check('status action mutates only sample state', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='candidate'`));
- await submit('score', { user: '模拟甲', score: '80', comment: '先核制备条件' });
- await submit('score', { user: '模拟乙', score: '60' });
- await submit('score', { user: '模拟甲', score: '100' });
- check('two voters and same-name replacement produce correct average', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.avg===80&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.n===2`));
- await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
- check('status, scores and open record survive refresh', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='candidate'&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.avg===80&&${detailRoot}.querySelector('dialog').open`));
- await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-P01"]').click()`); await pause(150);
- await submit('comment', { user: '模拟甲', body: '请补复水与沥水条件。' });
- check('project comment is retained with its project', await evaluate(`${sampleHost}._model.comments['DEMO-P01'].some(c=>c.body==='请补复水与沥水条件。')`));
- await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-R-P01-S3"]').click()`); await pause(150);
- check('report detail opens actual stage record and pending questions', await evaluate(`${detailHost}.recordId==='DEMO-R-P01-S3'&&${detailRoot}.textContent.includes('依据与适用边界')&&${detailRoot}.querySelectorAll('[data-record^="DEMO-Q01"]').length===6`));
- await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-S01"]').click()`); await pause(150);
- check('report source opens original sample text', await evaluate(`${detailRoot}.querySelector('.raw').textContent.includes('中午只有热水')`));
- await sampleGo('/coop?detail=source&record=UNKNOWN');
- check('unknown sample detail shows local error with no production lookup', await evaluate(`${detailRoot}.textContent.includes('不会向线上接口补查')`));
- await sampleGo('/review?stage=S3');
- check('S3 does not fabricate later validation tasks', await evaluate(`${viewHost}.filtered.validationTasks.length===0&&${viewHost}.filtered.questions.length===12`));
- await sampleGo('/class'); await clickRecord('DEMO-A-DEMO-G1-2');
- check('class assignment resolves its own report references', await evaluate(`${detailRoot}.querySelectorAll('[data-record^="DEMO-R-"]').length===4`));
- for (const [key, count] of [['P01', 7], ['P03', 4], ['P10', 4]]) {
- await sampleGo(`/review?case=${key}&step=${count - 1}`);
- check(`${key}: clickable guided path resolves to local project records`, await evaluate(`${viewRoot}.querySelectorAll('.steps button').length===${count}&&${viewRoot}.querySelectorAll('.story-records [data-record]').length>0`));
- }
- await sampleGo('/coop');
- await evaluate(`${sampleHost}.shadowRoot.querySelector('.sample-strip button').click()`); await pause(200);
- check('reset restores statuses and removes local votes while preserving unrelated storage', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='approved'&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats===null&&localStorage.getItem('unrelated-demo-sentinel')==='preserve'`));
- for (const name of ['cockpit', 'intel', 'coop', 'review', 'class']) {
- await cdp('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
- await sampleGo('/' + name);
- check(`${name}: mobile layout permits table scrolling without page overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
- if (name === 'coop') await shot('demo-mobile-coop');
- }
- await sampleGo(mount + 'coop?detail=opportunity&record=DEMO-O01');
- check('nested demo detail loads without production routing', await evaluate(`${detailHost}.recordId==='DEMO-O01'&&${detailRoot}.querySelector('dialog').open`));
- await evaluate(`${detailHost}.close()`); await pause(100);
- await evaluate(`(()=>{const app=document.querySelector('kf-app');for(const type of ['refresh','status-change','score-submit'])app.dispatchEvent(new CustomEvent(type,{detail:{id:'DEMO-O01',status:'approved'}}));})()`); await pause(350);
- check('five views and local operations made zero external business requests', forbidden.length === 0);
- // Existing live route regression uses a local interception fixture, never production.
- await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate('/'))`); await pause(600);
- check('existing live route resumes loading', forbidden.some((r) => r.body.includes('ksfGetPublicBoard')));
- check('existing navigation preserves its original scope without an extra demonstration entry', await evaluate(`(()=>{const root=document.querySelector('kf-app').shadowRoot.querySelector('kf-nav').shadowRoot;return !root.querySelector('a[data-path="/showcase"]')&&!root.querySelector('.showcase-entry')&&root.querySelectorAll('.tabbar a').length===5&&[...root.querySelectorAll('a[data-path]')].every(a=>a.href.includes('/preview-test/'));})()`));
- const legacyNavigate = async (route) => { await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate(${JSON.stringify(route)}))`); await pause(450); };
- for (const width of [1440, 390]) {
- await cdp('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: width < 768 });
- await legacyNavigate('/members');
- const identityInfo = await evaluate(`(()=>{const page=document.querySelector('kf-app').shadowRoot.querySelector('view-members');const rows=[...page.shadowRoot.querySelectorAll('[data-member]')];return {names:rows.map(r=>r.dataset.member),text:page.shadowRoot.textContent,width:document.documentElement.scrollWidth,viewport:innerWidth};})()`);
- check(`employee ${width}px: same member appears once across workshops, with available directory fields`, identityInfo.names.length === 1 && identityInfo.names[0] === '测试成员甲' && ['测试产品部', '产品经理', '企业微信通讯录姓名', '姓名同步于'].every((s) => identityInfo.text.includes(s)));
- check(`employee ${width}px: no page overflow or old identity label`, identityInfo.width <= identityInfo.viewport + 1 && !identityInfo.text.includes('旧占位身份'));
- await shot('employee-profile-' + width);
- }
- const measureTitles = async () => evaluate(`(()=>{
- const root=document.querySelector('kf-app').shadowRoot;
- const headings=[root.querySelector('kf-topbar').shadowRoot.querySelector('h1'),...root.querySelector('main').firstElementChild.shadowRoot.querySelectorAll('[data-type-card] h3,.head h2')];
- const canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');
- return headings.map(el=>{const s=getComputedStyle(el),r=el.getBoundingClientRect();ctx.font=s.fontWeight+' '+s.fontSize+' '+s.fontFamily;const glyph=ctx.measureText(el.textContent);return {text:el.textContent,height:r.height,glyphHeight:glyph.actualBoundingBoxAscent+glyph.actualBoundingBoxDescent,trim:s.textBoxTrim,clipped:r.height+0.5<glyph.actualBoundingBoxAscent+glyph.actualBoundingBoxDescent};});
- })()`);
- for (const width of [2048, 1280, 1024, 768, 390]) {
- await cdp('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: width < 768 });
- await legacyNavigate('/reports');
- await evaluate('document.fonts.ready');
- const headings = await measureTitles(); observations.push({ reportWidth: width, headings });
- if (width === 2048 || width === 390) await shot('reports-typography-' + width);
- check(`reports ${width}px: Chinese heading boxes retain the complete glyph height`, headings.length === 6 && headings.every(x => !x.clipped), headings);
- check(`reports ${width}px: no page horizontal overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
- await evaluate(`document.querySelector('kf-app').shadowRoot.querySelector('view-reports').shadowRoot.querySelector('[data-type-card="voc"]').click()`); await pause(250);
- const typeHeadings = await measureTitles();
- check(`report category ${width}px: heading and classification retain full glyphs`, typeHeadings.length === 2 && typeHeadings.every(x => !x.clipped), typeHeadings);
- check(`report category ${width}px: active tab is exposed correctly`, await evaluate(`document.querySelector('kf-app').shadowRoot.querySelector('view-report-type').shadowRoot.querySelector('.tab.cur').getAttribute('aria-current')==='page'`));
- }
- await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
- await legacyNavigate('/pool/fixture-report');
- check('legacy wrong report link recovers to report route', await evaluate(`location.pathname==='/preview-test/report/fixture-report'&&Boolean(document.querySelector('kf-app').shadowRoot.querySelector('view-report'))`));
- await legacyNavigate('/pool/fixture-opportunity');
- const legacyDrawer = `document.querySelector('kf-app').shadowRoot.querySelector('kf-drawer').shadowRoot`;
- check('existing public opportunity drawer resolves its record', await evaluate(`${legacyDrawer}.textContent.includes('决策与状态')&&!${legacyDrawer}.textContent.includes('未找到这张机会卡')`));
- await evaluate(`${legacyDrawer}.querySelectorAll('.st-row button')[1].click()`); await pause(250);
- check('existing state button retains public function contract', forbidden.some((r) => r.body.includes('ksfSetPublicOpportunityStatus') && r.body.includes('fixture-opportunity')));
- await evaluate(`document.querySelector('kf-app').dispatchEvent(new CustomEvent('score-submit',{detail:{user:'回归访客',score:85,comment:'本机拦截回放'}}))`); await pause(250);
- check('existing rating retains public function contract', forbidden.some((r) => r.body.includes('ksfRatePublicOpportunity') && r.body.includes('fixture-opportunity')));
- await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate('/showcase'))`); await ready(); await pause(200);
- const afterLive = forbidden.length;
- await pause(500);
- check('returning to introduction stops accelerated live polling', forbidden.length === afterLive);
- seedUnavailable = true;
- await cdp('Page.navigate', { url: origin + '/showcase' });
- for (let i = 0; i < 60; i++) { if (await evaluate(`Boolean(${host}?._error)`)) break; await pause(100); }
- check('failed sample fetch shows error instead of live fallback', await evaluate(`${host}._error.includes('503')`));
- seedUnavailable = false;
- await evaluate(`${shadow}.querySelector('.loading button').click()`); await ready();
- check('retry recovers the local sample', await evaluate(`${host}._model.total===334`));
- check('no uncaught browser exceptions', errors.length === 0, errors);
- await writeFile(path.join(run, 'acceptance.json'), JSON.stringify({ ok: true, checks, observations, requests, interceptedExternalRequests: forbidden, errors, productionRequestsSent: 0, deployed: false }, null, 2));
- console.log(JSON.stringify({ ok: true, checks: checks.length, output: run, productionRequestsSent: 0 }));
- } catch (error) {
- await writeFile(path.join(run, 'acceptance.json'), JSON.stringify({ ok: false, error: error.stack, checks, observations, requests, interceptedExternalRequests: forbidden, errors, deployed: false }, null, 2));
- console.error(error);
- console.error(`Evidence: ${run}`);
- process.exitCode = 1;
- } finally {
- socket?.close(); browser.kill(); server.closeAllConnections(); server.close();
- }
|