/** 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 !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(); }