// 교대만 가져오기 (2026-11-06): CDP 9444 Chrome + Vite 5173. Imports the sample workbook with P1 unchecked and A1 only, // then walks ① ② ③, and checks for runtime exceptions; screenshots go to .verify/abut-only-20261106. const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); const out = path.resolve(__dirname, '../.verify/abut-only-20261106'); fs.mkdirSync(out, { recursive: true }); const report = { checks: [], errors: [], console: [] }; const delay = ms => new Promise(r => setTimeout(r, ms)); (async () => { const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json(); const ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; }); let seq = 0; const pending = new Map(); ws.onmessage = ({ data }) => { const m = JSON.parse(data), p = pending.get(m.id); if (p) { pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); } if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text); if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') report.console.push(m.params.args.map(a => a.value ?? a.description).join(' ')); }; const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); }); 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 expr => { for (let i = 0; i < 100; i++) { if (await ev(expr)) return; await delay(150); } throw new Error(`Wait failed: ${expr}`); }; const click = expr => ev(`(() => { const e = ${expr}; if (!e || e.disabled) throw new Error('Missing/disabled: ' + ${JSON.stringify(expr)}); e.click(); })()`); const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); }; const shot = async name => { await delay(400); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); }; try { await send('Page.enable'); await send('Runtime.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); await send('Page.navigate', { url: 'http://127.0.0.1:5173/' }); await until(`typeof window.loadBytesToApp === 'function'`); await ev(`(async () => { const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer(); window.loadBytesToApp(new Uint8Array(b), '운곡교P1입력.xlsx'); })()`); await until(`!!document.getElementById('import-abut-A1')`); // Uncheck every pier, keep A1. await ev(`(() => { for (const c of document.querySelectorAll('input[data-import-kind="pier"]')) if (c.checked) { c.checked = false; c.dispatchEvent(new Event('change', { bubbles: true })); } })()`); await ev(`(() => { const c = document.getElementById('import-abut-A1'); if (!c.checked) { c.checked = true; c.dispatchEvent(new Event('change', { bubbles: true })); } })()`); await ev(`(() => { const e = document.getElementById('project-name'); e.value = '교대만 검증'; e.dispatchEvent(new Event('input', { bubbles: true })); })()`); const label = await ev(`document.getElementById('dlg-apply').textContent`); check(`apply button allows A1 only: "${label}"`, !(await ev(`document.getElementById('dlg-apply').disabled`)) && label.includes('교각 0기 + 교대 1기')); check('dialog note says abutments only', (await ev(`document.getElementById('dlg-selected').textContent`)).includes('교대만 가져옵니다')); await click(`document.getElementById('dlg-apply')`); await until(`!document.querySelector('.app').hidden`); await shot('1-shape'); check('① list shows A1', (await ev(`document.body.innerText`)).includes('A1 · A1 교대')); // WebGL canvases cannot be read back without preserveDrawingBuffer: the 3D view is judged on the screenshots. for (const [nav, name] of [['nav-plan', '2-plan'], ['nav-rebar', '3-rebar'], ['nav-shape', '4-shape-again']]) { await click(`document.getElementById('${nav}')`); await shot(name); // Pick the abutment in the visible list of this stage, when there is one. await ev(`(() => { const b = [...document.querySelectorAll('main:not([hidden]) button')].find(x => x.textContent.startsWith('A1 ·')); if (b) b.click(); })()`); await shot(name + '-A1'); } check('no runtime exceptions (교대만)', report.errors.length === 0); // Regression: P1 + A1 together still import, and P1 front-row solplates pair with A1. await send('Page.navigate', { url: 'http://127.0.0.1:5173/' }); await until(`typeof window.loadBytesToApp === 'function'`); await ev(`(async () => { const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer(); window.loadBytesToApp(new Uint8Array(b), '운곡교P1입력.xlsx'); })()`); await until(`!!document.getElementById('import-abut-A1')`); await ev(`(() => { const e = document.getElementById('project-name'); e.value = '교각+교대 검증'; e.dispatchEvent(new Event('input', { bubbles: true })); })()`); check('P1 + A1 apply label', (await ev(`document.getElementById('dlg-apply').textContent`)).includes('교각 1기 + 교대 1기')); await click(`document.getElementById('dlg-apply')`); await until(`!document.querySelector('.app').hidden`); await click(`document.getElementById('nav-plan')`); await shot('5-pier-plan'); check('no runtime exceptions (교각 + 교대)', report.errors.length === 0); } catch (err) { report.failure = String(err && err.stack || err); console.error(report.failure); } finally { fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify({ checks: report.checks.length, errors: report.errors, console: report.console.slice(0, 5), failure: report.failure }, null, 2)); ws.close(); } })();