- 기둥·말뚝 입력을 한 카드로 통합(공통 입력, 구간별 예외 보존, 횡철근 표) - 현장타설 실배근 · 삽도 · 수량을 화면에 연결하고 프로젝트 저장 · 복원 - 상단 헤더를 한 줄로 축약(상태 축약, 파일 메뉴, 말줄임) - 디자인 검토 반영(가져오기 범위, ③ 모델 높이 · 접기, 검토 항목 이동) - 화면 검증 스크립트(verify-unified-input · verify-design-fixes 등) 보관 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
131 lines
11 KiB
JavaScript
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();
|
|
});
|