| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- // Fresh anonymous browser regression for opportunity card details and linked reports.
- import fs from 'node:fs';
- import path from 'node:path';
- import assert from 'node:assert/strict';
- import { spawn } from 'node:child_process';
- const base = process.argv[2] || 'http://127.0.0.1:4178';
- const profile = fs.mkdtempSync(path.resolve('work/opportunity-browser-'));
- const browser = spawn('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', [
- '--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
- '--disable-background-networking', '--remote-debugging-address=127.0.0.1',
- '--remote-debugging-port=0', '--user-data-dir=' + profile, 'about:blank',
- ], { windowsHide: true, stdio: 'ignore' });
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
- let socket;
- const requests = [];
- const checks = [];
- const errors = [];
- const screenshots = [];
- const runId = Date.now();
- const output = process.env.KF_OPPORTUNITY_RESULT || path.join(profile, 'result.json');
- try {
- let port;
- for (let i = 0; i < 100; i++) {
- try { port = Number(fs.readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').split('\n')[0]); } catch {}
- if (port) break;
- await sleep(100);
- }
- assert.ok(port, 'Edge CDP unavailable');
- const tabs = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
- socket = new WebSocket(tabs.find((tab) => tab.type === 'page').webSocketDebuggerUrl);
- await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', reject, { once: true }); });
- let seq = 0;
- const pending = new Map();
- socket.addEventListener('message', ({ data }) => {
- const msg = JSON.parse(data);
- if (msg.id && pending.has(msg.id)) {
- const call = pending.get(msg.id);
- clearTimeout(call.timer);
- pending.delete(msg.id);
- msg.error ? call.reject(Error(msg.error.message)) : call.resolve(msg.result);
- }
- if (msg.method === 'Network.requestWillBeSent') requests.push({ method: msg.params.request.method, url: msg.params.request.url, body: msg.params.request.postData, headerNames: Object.keys(msg.params.request.headers || {}) });
- if (msg.method === 'Runtime.exceptionThrown') errors.push({ kind: 'runtime', text: msg.params.exceptionDetails.text });
- if (msg.method === 'Log.entryAdded' && msg.params.entry.level === 'error' && !msg.params.entry.url?.endsWith('/favicon.ico')) errors.push({ kind: 'console', text: msg.params.entry.text });
- if (msg.method === 'Network.loadingFailed' && msg.params.errorText !== 'net::ERR_ABORTED') errors.push({ kind: 'network', text: msg.params.errorText });
- });
- const cdp = (method, params = {}) => new Promise((resolve, reject) => {
- const id = ++seq;
- const timer = setTimeout(() => { pending.delete(id); reject(Error(`${method} timed out`)); }, 30000);
- pending.set(id, { resolve, reject, timer });
- socket.send(JSON.stringify({ id, method, params }));
- });
- const evaluate = async (expression) => {
- const result = await cdp('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
- if (result.exceptionDetails) throw Error(result.exceptionDetails.text);
- return result.result.value;
- };
- const find = `function find(root, selector) { const hit=root.querySelector(selector); if(hit)return hit; for(const node of root.querySelectorAll('*')) if(node.shadowRoot){const nested=find(node.shadowRoot,selector);if(nested)return nested;} return null; }`;
- const shot = async (label) => {
- const result = await cdp('Page.captureScreenshot', { format: 'png' });
- const file = `work/live-opportunity-ui-${label}-${runId}.png`;
- fs.writeFileSync(file, Buffer.from(result.data, 'base64'), { flag: 'wx' }); screenshots.push(file);
- };
- const waitFor = async (expression) => {
- for (let i = 0; i < 80; i++) {
- const value = await evaluate(expression).catch(() => false);
- if (value) return value;
- await sleep(150);
- }
- throw Error(`Condition not reached: ${expression.slice(0, 80)}`);
- };
- await cdp('Page.enable'); await cdp('Runtime.enable'); await cdp('Network.enable'); await cdp('Log.enable');
- await cdp('Network.setCacheDisabled', { cacheDisabled: true });
- await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
- await cdp('Page.navigate', { url: base + '/pool' });
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready')`);
- const card = await waitFor(`(()=>{${find}const c=find(document,'kf-opportunity-card[key=\"7TnUg76JOI\"]');return c?.shadowRoot.querySelector('a.report')?.href})()`);
- assert.equal(new URL(card).pathname, '/report/mX1FapeOVB');
- await evaluate(`(()=>{${find}find(document,'kf-opportunity-card[key=\"7TnUg76JOI\"]').shadowRoot.querySelector('a.report').click();return true})()`);
- await waitFor(`location.pathname==='/report/mX1FapeOVB'`);
- const report = await waitFor(`(()=>{${find}const v=find(document,'view-report');return v?.shadowRoot?.textContent.includes('【样例】')||v?.shadowRoot?.textContent.includes('拌面')})()`);
- assert.ok(report);
- await shot('report-desktop');
- await cdp('Page.navigate', { url: base + '/pool/mX1FapeOVB' });
- await waitFor(`location.pathname==='/report/mX1FapeOVB'`);
- checks.push('旧的 /pool/:报告编号 链接可恢复为正确报告');
- await cdp('Page.navigate', { url: base + '/pool' });
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready')`);
- await evaluate(`(()=>{${find}find(document,'kf-opportunity-card[key=\"7TnUg76JOI\"]').shadowRoot.querySelector('.acts .kf-btn').click();return true})()`);
- await waitFor(`location.pathname==='/pool/7TnUg76JOI'`);
- const heading = await waitFor(`(()=>{${find}const d=find(document,'kf-drawer');return d?.open&&d.heading})()`);
- assert.ok(heading.includes('拌面方向待验证'));
- await shot('drawer-desktop');
- checks.push('关联报告路由正确,真实报告可读', '详情使用机会卡编号,标题匹配');
- const cards = await evaluate(`import('/src/services/board-service.js').then(m=>m.boardService.db.get().opportunities.map(o=>({id:o.id,title:o.title,reportId:o.reportArtifact?.id})))`);
- for (const card of cards) {
- await evaluate(`import('/src/utils/router.js').then(m=>m.navigate('/pool/${card.id}'))`);
- await waitFor(`(()=>{${find}const d=find(document,'kf-drawer');return d?.open&&d.heading===${JSON.stringify(card.title)}})()`);
- if (card.reportId) {
- await evaluate(`(()=>{${find}find(document,'kf-opportunity-card[key="${card.id}"]').shadowRoot.querySelector('a.report').click();return true})()`);
- await waitFor(`location.pathname==='/report/${card.reportId}'`);
- await waitFor(`(()=>{${find}const v=find(document,'view-report');return v?.artifactId==='${card.reportId}'&&Boolean(v?.shadowRoot?.querySelector('.cover h1'))})()`);
- }
- checks.push(`机会卡 ${card.id} 详情${card.reportId ? '/关联报告' : ''}可读`);
- }
- if (process.env.KF_ACTION_TEST_ID) {
- const id = process.env.KF_ACTION_TEST_ID;
- for (const width of [1440, 390]) {
- await cdp('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: width === 390 });
- const signature = width === 1440 ? '' : '公开操作验收样例';
- await evaluate(`import('/src/services/board-service.js').then(m=>m.boardService.setMe(${JSON.stringify(signature)}))`);
- for (const status of ['approved', 'rejected', 'candidate']) {
- await cdp('Page.navigate', { url: base + '/pool' });
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready')`);
- const index = ['candidate', 'approved', 'rejected'].indexOf(status);
- const before = requests.length;
- await evaluate(`(()=>{${find}const card=find(document,'kf-opportunity-card[key="${id}"]');if(!card)throw Error('Test card absent');card.scrollIntoView();card.shadowRoot.querySelectorAll('.st button')[${index}].click();return true})()`);
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.db.get().opportunityById['${id}']?.status==='${status}')`);
- await sleep(150);
- const writes = requests.slice(before).filter((r) => r.url.endsWith('/api/functions') && JSON.parse(r.body || '{}').path === '/ksfSetPublicOpportunityStatus');
- assert.equal(writes.length, 1, 'One click must send exactly one state action');
- assert.equal(JSON.parse(writes[0].body).params.opportunityId, id);
- await cdp('Page.reload', { ignoreCache: true });
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready'&&m.boardService.db.get().opportunityById['${id}']?.status==='${status}')`);
- const persistedBy = await evaluate(`import('/src/services/board-service.js').then(m=>m.boardService.db.get().opportunityById['${id}']?.decision?.by)`);
- assert.equal(persistedBy, signature || '匿名访客');
- checks.push(`${width}px ${status} 单次点击/真实保存/刷新一致`);
- }
- await evaluate(`import('/src/utils/router.js').then(m=>m.navigate('/pool/${id}'))`);
- await waitFor(`(()=>{${find}return find(document,'kf-drawer')?.open})()`);
- assert.ok(await evaluate('document.documentElement.scrollWidth<=innerWidth'), 'Viewport overflow');
- await shot('state-drawer-' + width);
- }
- await evaluate(`import('/src/utils/router.js').then(m=>m.navigate('/pool/${id}'))`);
- await waitFor(`(()=>{${find}return find(document,'kf-drawer')?.open})()`);
- await evaluate(`import('/src/services/board-service.js').then(m=>m.boardService.setMe('公开操作验收样例'))`);
- await sleep(150);
- const before = requests.length;
- await evaluate(`(()=>{${find}const panel=find(document,'kf-score-panel');const input=panel.shadowRoot.querySelector('input[type=number]');for(let i=0;i<2;i++){input.value='76';input.dispatchEvent(new Event('input',{bubbles:true}));panel.shadowRoot.querySelector('form').requestSubmit();}return true})()`);
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.db.get().opportunityById['${id}']?.scores.some(s=>s.user==='公开操作验收样例'&&s.score===76))`);
- assert.equal(requests.slice(before).filter((r) => r.url.endsWith('/api/functions') && JSON.parse(r.body || '{}').path === '/ksfRatePublicOpportunity').length, 1);
- await cdp('Page.reload', { ignoreCache: true });
- await waitFor(`import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready'&&m.boardService.db.get().opportunityById['${id}']?.scores.some(s=>s.user==='公开操作验收样例'&&s.score===76))`);
- checks.push('抽屉评分快速双提交只有一次写入,刷新回读一致');
- await shot('score-mobile');
- }
- assert.ok(!requests.some((r) => r.method === 'PUT' || r.method === 'DELETE'), 'Browser must not write raw tables');
- assert.ok(!requests.some((r) => r.headerNames.some((key) => /master|authorization|session.token|ksf.service.token/i.test(key))), 'Browser must remain anonymous');
- assert.ok(!requests.some((r) => r.url.includes('/parse/classes/')), 'Browser must use public projection/actions only');
- assert.equal(errors.length, 0, 'Browser runtime/network errors');
- checks.push('浏览器无凭据、无直接业务表访问、无运行或网络错误');
- const result = { ok: true, at: new Date().toISOString(), base, checks, errors, screenshots, productionFrontendBrowserVerified: base === 'https://kangshifu.fmode.cn', moduleOverrides: false, reportPath: '/report/mX1FapeOVB', opportunityPath: '/pool/7TnUg76JOI', drawerHeading: heading, rawTableWrites: 0, testOpportunityId: process.env.KF_ACTION_TEST_ID || null };
- fs.writeFileSync(output, JSON.stringify(result, null, 2));
- console.log(JSON.stringify(result));
- } catch (error) {
- const failure = { ok: false, checks, errors, screenshots, error: error.message };
- fs.writeFileSync(output, JSON.stringify(failure, null, 2));
- console.error(JSON.stringify(failure));
- process.exitCode = 1;
- } finally {
- socket?.close();
- browser.kill();
- }
|