Files
Pier_v1/scripts/verify-design-fixes.cjs
T
ypshimandClaude Haiku 5.5 da7b13a568 feat(ui): 기둥·말뚝 통합 입력 · 현장타설 화면 · 헤더 한 줄 정리
- 기둥·말뚝 입력을 한 카드로 통합(공통 입력, 구간별 예외 보존, 횡철근 표)
- 현장타설 실배근 · 삽도 · 수량을 화면에 연결하고 프로젝트 저장 · 복원
- 상단 헤더를 한 줄로 축약(상태 축약, 파일 메뉴, 말줄임)
- 디자인 검토 반영(가져오기 범위, ③ 모델 높이 · 접기, 검토 항목 이동)
- 화면 검증 스크립트(verify-unified-input · verify-design-fixes 등) 보관

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-06 13:15:01 +09:00

131 lines
11 KiB
JavaScript

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