Files
Pier_v1/scripts/verify-design-fixes.cjs
T
ypshimandClaude Haiku 5.5 7d36c4b64a feat(view): ②·③ 모델과 입력창/철근표를 상하·좌우로 전환하고 분할선으로 크기 조절
- 중앙 도구줄 '상하 | 좌우' 전환(② 도구줄 신설), 분할선 드래그·키보드(←→↑↓, Shift×5, Home/End)·더블클릭 기본값
- 상하: 모델 높이 180–560 px, 좌우: 모델 폭 30–70 %(모델 왼쪽), 모델 접기 시 창 전체 사용
- 중앙 열 900 px 미만·1100 px 이하 화면은 자동 상하 + 안내, 넓어지면 복귀
- 단계별 표시 설정을 localStorage에 저장(기존 pierzainer.review-layout 이관), 프로젝트 파일 미포함
- 오른쪽 창 묶음(.workspace-pane) 도입에 맞춰 기존 검증 스크립트 선택자 갱신
- 단위시험 4개, verify-workspace-layout.cjs Chrome 27개 검사

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 18:01:20 +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 .workspace-pane > 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 .workspace-pane > 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();
});