// CDP regression checks for the four scoped design improvements. Run with Windows Node. const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert/strict'); const root = path.resolve(__dirname, '..'); const out = path.join(root, '.verify', 'design-fixes-20261105'); const report = { checks: [], errors: [], captures: [], started: new Date().toISOString() }; fs.mkdirSync(out, { recursive: true }); let ws; let send; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { const target = await (await fetch('http://127.0.0.1:9444/json/new?http://127.0.0.1:5173/', { method: 'PUT' })).json(); ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; }); let seq = 0; const pending = new Map(); send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout ${method}`)); }, 20000); pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params })); }); ws.onmessage = ({ data }) => { const msg = JSON.parse(data); if (msg.id) { const wait = pending.get(msg.id); if (!wait) return; clearTimeout(wait.timer); pending.delete(msg.id); if (msg.error) wait.reject(new Error(JSON.stringify(msg.error))); else wait.resolve(msg.result); } else if (msg.method === 'Runtime.exceptionThrown') report.errors.push(msg.params.exceptionDetails.exception?.description || msg.params.exceptionDetails.text); else if (msg.method === 'Runtime.consoleAPICalled' && msg.params.type === 'error') report.errors.push(msg.params.args.map((a) => a.value ?? a.description).join(' ')); }; const ev = async (expression) => { const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text); return r.result.value; }; const until = async (expression) => { for (let i = 0; i < 100; i++) { if (await ev(expression)) return; await delay(150); } throw new Error(`Timeout: ${expression}`); }; const click = (expression) => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled control'); e.click(); })()`); const shot = async (name) => { await delay(200); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, `${name}.png`), Buffer.from(r.data, 'base64')); report.captures.push(name); }; const check = (name, condition) => { assert.ok(condition, name); report.checks.push(name); console.log('PASS', name); }; const upload = async () => { const doc = await send('DOM.getDocument'); const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#file' }); await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [] }); await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [path.resolve(root, '../../RAG/운곡교P1입력.xlsx')] }); await until(`!document.getElementById('dlg-apply').disabled && document.getElementById('import-abut-A1')?.checked === true`); }; const member = async (label) => { await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(label)}))`); await delay(100); }; const lower = () => ev(`document.querySelector('#review-workspace > section').getBoundingClientRect().height`); await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); await until(`!!document.querySelector('[data-card="excel"] button') && typeof window.loadBytesToApp === 'function'`); await click(`document.querySelector('[data-card="excel"] button')`); await ev(`document.getElementById('project-name').value = '디자인 개선 검증'; document.getElementById('project-name').dispatchEvent(new Event('input'));`); await upload(); check('combined import scope', await ev(`document.getElementById('dlg-apply').textContent === '교각 1기 + 교대 1기 가져오기'`)); check('A1 source and dimensions preview', await ev(`document.getElementById('pv-abuts').textContent.includes('A1') && document.getElementById('pv-abuts').textContent.includes('12.197') && document.querySelector('#sheets > li:first-child').textContent.includes('교대 인식 결과')`)); await shot('01-import-both'); await click(`document.getElementById('import-abut-A1')`); check('uncheck updates preview and count', await ev(`document.getElementById('dlg-apply').textContent.includes('교대 0기') && document.getElementById('pv-abuts').textContent.includes('아니오')`)); await click(`document.getElementById('dlg-apply')`); check('unchecked abutment is not applied', await ev(`![...document.querySelectorAll('#pier-items button')].some(b => b.textContent.includes('교대'))`)); await click(`document.getElementById('open-import')`); await upload(); await click(`document.getElementById('dlg-apply')`); await click(`document.getElementById('nav-rebar')`); await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('A1') && b.textContent.includes('교대'))`); await member('확대기초'); check('A1 footing excludes pier review questions', await ev(`document.getElementById('review-summary').textContent.includes('A1') && document.getElementById('review-summary').textContent.includes('미결 0건') && !document.getElementById('review-tasks').textContent.includes('Q-C9')`)); await delay(4500); await shot('02-workspace-desktop'); await send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false }); await delay(300); if (await ev(`document.getElementById('review-model-toggle').getAttribute('aria-expanded') === 'false'`)) await click(`document.getElementById('review-model-toggle')`); await ev(`const size = document.getElementById('review-model-height'); size.value = size.max; size.dispatchEvent(new Event('input'));`); await delay(200); report.layoutBefore = await lower(); await ev(`const range = document.getElementById('review-model-height'); range.value = '180'; range.dispatchEvent(new Event('input'));`); await delay(200); report.layoutResized = await lower(); check('height control expands evidence area', report.layoutResized >= report.layoutBefore); check('compact viewer tools stay inside model', await ev(`document.querySelector('.viewer-stack-right').getBoundingClientRect().bottom <= document.getElementById('viewer-block').getBoundingClientRect().bottom + 1`)); await click(`document.getElementById('review-model-toggle')`); await delay(200); report.layoutCollapsed = await lower(); check('collapse adds model area to evidence', report.layoutCollapsed > report.layoutResized + 150); await click(`document.getElementById('review-table')`); check('table shortcut focuses and reveals table', await ev(`document.activeElement.matches('#rebar-table table.rebar-table') && document.activeElement.getBoundingClientRect().top >= document.querySelector('#review-workspace > section').getBoundingClientRect().top - 2`)); await shot('03-table-laptop'); await click(`document.getElementById('review-inputs')`); check('input shortcut preserves keyboard tab order', await ev(`document.activeElement.matches('#rebar-table input') && document.activeElement.tabIndex === 0`)); await shot('04-inputs-laptop'); check('layout preference persisted', await ev(`JSON.parse(localStorage.getItem('pierzainer.review-layout')).collapsed === true`)); await click(`document.getElementById('review-model-toggle')`); await member('받침블럭'); check('block review is scoped', await ev(`document.getElementById('review-tasks').textContent.includes('Q-S4') && !document.getElementById('review-tasks').textContent.includes('Q-C9')`)); await click(`document.querySelector('#review-tasks button')`); check('review action selects correct bar and row', await ev(`document.querySelector('#rebar-table .rebar-select').textContent.includes('선택 1 /') && document.activeElement.matches('tr') && document.activeElement.textContent.includes('S1')`)); await member('날개벽'); check('wing review shows only relevant unresolved work', await ev(`document.getElementById('review-summary').textContent.includes('미결 2건') && document.getElementById('review-tasks').textContent.includes('E19') && document.getElementById('review-tasks').textContent.includes('V-A2')`)); await shot('05-review-and-materials'); report.fields = await ev(`[...document.querySelectorAll('.material-fields .mat')].map(label => { const r = label.getBoundingClientRect(); const c = label.querySelector('input,select').getBoundingClientRect(); return { label: label.textContent, left: r.left, top: r.top, width: r.width, controlLeft: c.left, controlRight: c.right, bottom: r.bottom }; })`); check('material fields align without overlap', report.fields.every((f, i, list) => f.controlRight <= f.left + f.width + 1 && (i === 0 || f.top >= list[i - 1].bottom))); check('document has no horizontal overflow at laptop width', await ev('document.documentElement.scrollWidth === innerWidth')); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('P1') && !b.textContent.includes('교대'))`); await member('기둥'); check('existing decision is not presented as unresolved', await ev(`document.getElementById('review-tasks').textContent.includes('Q-C9') && document.getElementById('review-tasks').textContent.includes('결정됨') && document.getElementById('review-summary').textContent.includes('미결 0건')`)); check('no runtime errors', report.errors.length === 0); report.result = 'PASS'; } main().catch((error) => { report.result = 'FAIL'; report.failure = error.stack; process.exitCode = 1; }).finally(async () => { if (report.result === 'FAIL' && send) { try { const shot = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, 'failure.png'), Buffer.from(shot.data, 'base64')); } catch { /* preserve original failure */ } } report.finished = new Date().toISOString(); fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify({ result: report.result, checks: report.checks.length, errors: report.errors, failure: report.failure })); if (send) { try { await send('Page.close'); } catch { /* tab may already be closed */ } } ws?.close(); });