교대 이름을 전체 명칭으로 해석하고, 엑셀 가져오기 문제를 목록으로 모은다. 교각 이름과의 중복을 저장 파일 검사에서도 막는다(checkStoredAbutments). 검사 스크립트 verify-abut-names · verify-abut-workflow 포함. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
167 lines
11 KiB
JavaScript
167 lines
11 KiB
JavaScript
// Build non-destructive fixtures and verify full-name identity and error disclosure via CDP.
|
|
// Run with Windows Node while the local app and a dedicated Chrome CDP port 9444 are running.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const assert = require('node:assert/strict');
|
|
const XLSX = require('xlsx');
|
|
const root = path.resolve(__dirname, '..');
|
|
const out = path.join(root, '.verify', 'abut-names-20261105');
|
|
const report = { checks: [], errors: [], started: new Date().toISOString() };
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
fs.mkdirSync(out, { recursive: true });
|
|
const base = XLSX.readFile(path.resolve(root, '../../RAG/운곡교P1입력.xlsx'));
|
|
const blocks = { info: [], ips: [], bearings: [] };
|
|
for (const sheet of base.SheetNames) {
|
|
const rows = XLSX.utils.sheet_to_json(base.Sheets[sheet], { header: 1, defval: null });
|
|
for (let h = 0; h < rows.length; h++) {
|
|
if (rows[h][0] !== '교대명') continue;
|
|
const kind = ({ 형식: 'info', IP점: 'ips', 받침번호: 'bearings' })[rows[h][1]];
|
|
if (!kind) continue;
|
|
let name;
|
|
for (let i = h + 1; i < rows.length && rows[i][1] != null && rows[i][1] !== ''; i++) {
|
|
name = rows[i][0] || name;
|
|
if (name === 'A1') blocks[kind].push(['A2-완도', ...rows[i].slice(1)]);
|
|
}
|
|
}
|
|
for (const row of rows) if (row[0] === 'A1') row[0] = 'A1-완도';
|
|
base.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
|
|
}
|
|
assert.equal(blocks.info.length, 1);
|
|
blocks.info[0][1] = 'A2';
|
|
XLSX.utils.book_append_sheet(base, XLSX.utils.aoa_to_sheet([
|
|
['교대명', '형식'], ...blocks.info, [],
|
|
['교대명', 'IP점'], ...blocks.ips, [],
|
|
['교대명', '받침번호'], ...blocks.bearings,
|
|
]), '종점교대');
|
|
const validFile = path.join(out, 'full-names.xlsx');
|
|
XLSX.writeFile(base, validFile);
|
|
const bad = XLSX.readFile(validFile);
|
|
for (const sheet of bad.SheetNames) {
|
|
const rows = XLSX.utils.sheet_to_json(bad.Sheets[sheet], { header: 1, defval: null });
|
|
for (const row of rows) if (row[0] === 'A1-완도' && row[1] === 'A1') row[1] = 'A3';
|
|
bad.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
|
|
}
|
|
const badFile = path.join(out, 'invalid-type.xlsx');
|
|
XLSX.writeFile(bad, badFile);
|
|
let ws;
|
|
let send;
|
|
async function main() {
|
|
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
|
ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
|
let seq = 0;
|
|
let loaded;
|
|
let downloadGuid;
|
|
const done = new Set();
|
|
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 === 'Page.loadEventFired') loaded?.();
|
|
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(' '));
|
|
else if (msg.method === 'Browser.downloadWillBegin') downloadGuid = msg.params.guid;
|
|
else if (msg.method === 'Browser.downloadProgress' && msg.params.state === 'completed') done.add(msg.params.guid);
|
|
};
|
|
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 target'); e.click(); })()`);
|
|
const check = (name, condition) => { assert.ok(condition, name); report.checks.push(name); console.log('PASS', name); };
|
|
const shot = async (name) => {
|
|
await delay(250);
|
|
const r = await send('Page.captureScreenshot', { format: 'png' });
|
|
fs.writeFileSync(path.join(out, `${name}.png`), Buffer.from(r.data, 'base64'));
|
|
};
|
|
const file = async (selector, name) => {
|
|
const doc = await send('DOM.getDocument');
|
|
const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
|
|
await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [name] });
|
|
};
|
|
const navigate = async () => {
|
|
let timer;
|
|
const load = new Promise((resolve, reject) => { loaded = resolve; timer = setTimeout(() => reject(new Error('Load timeout')), 20000); });
|
|
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
|
try { await load; } finally { clearTimeout(timer); loaded = undefined; }
|
|
await until(`typeof window.loadBytesToApp === 'function' && !!document.querySelector('[data-card="excel"] button')`);
|
|
};
|
|
const select = (name) => click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes(${JSON.stringify(name)}) && b.textContent.includes('교대'))`);
|
|
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
|
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
|
const downloads = 'C:\\Users\\User\\AppData\\Local\\Temp\\pierzainer-abut-name-20261105\\downloads';
|
|
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
|
await navigate();
|
|
await click(`document.querySelector('[data-card="excel"] button')`);
|
|
await ev(`document.getElementById('project-name').value = '복합 교대명 검증'; document.getElementById('project-name').dispatchEvent(new Event('input'));`);
|
|
await file('#file', validFile);
|
|
await until(`document.getElementById('dlg-apply').textContent.includes('교대 2기')`);
|
|
check('both full names recognized without cascade errors', await ev(`document.getElementById('pv-abuts').textContent.includes('A1-완도') && document.getElementById('pv-abuts').textContent.includes('A2-완도') && !document.querySelector('#sheets .bad')`));
|
|
check('full-name checkboxes stay distinct', await ev(`document.getElementById('import-abut-A1-완도').checked && document.getElementById('import-abut-A2-완도').checked`));
|
|
await shot('01-full-names-import');
|
|
await click(`document.getElementById('dlg-apply')`);
|
|
await click(`document.getElementById('nav-rebar')`);
|
|
await select('A1-완도');
|
|
check('full name selects the start abutment', await ev(`document.getElementById('rebar-note').textContent.startsWith('A1-완도') && document.querySelectorAll('#rebar-tabs button').length === 6`));
|
|
await select('A2-완도');
|
|
check('full name selects the end abutment independently', await ev(`document.getElementById('rebar-note').textContent.startsWith('A2-완도')`));
|
|
const order = await ev(`[...document.querySelectorAll('#rebar-pier-items button')].map(b => b.textContent)`);
|
|
check('type-based order is preserved', order[0].includes('A1-완도') && order[1].includes('P1') && order[2].includes('A2-완도'));
|
|
await shot('02-independent-abutments');
|
|
await click(`document.getElementById('save-project')`);
|
|
await until(`document.getElementById('save-dialog').open`);
|
|
await click(`document.getElementById('sv-save')`);
|
|
for (let i = 0; i < 100 && !(downloadGuid && done.has(downloadGuid)); i++) await delay(150);
|
|
assert.ok(downloadGuid && done.has(downloadGuid), 'actual download completes');
|
|
const text = fs.readFileSync(path.join(downloads, downloadGuid), 'utf8');
|
|
const project = JSON.parse(text);
|
|
const expectedNames = ['A1-완도', 'A2-완도'];
|
|
assert.deepEqual(project.abutments.map((a) => a.name), expectedNames);
|
|
assert.deepEqual(project.abutDefaults.map((a) => a.name), expectedNames);
|
|
report.checks.push('actual download preserves both names and defaults');
|
|
const saved = path.join(out, 'full-names.pzr.json'); fs.writeFileSync(saved, text);
|
|
await navigate();
|
|
await ev('window.openSampleProject()');
|
|
await click(`document.getElementById('open-project')`);
|
|
await file('#project-file', saved);
|
|
await until(`document.querySelector('#toast-host').textContent.includes('열었습니다')`);
|
|
await click(`document.getElementById('nav-rebar')`);
|
|
await select('A1-완도'); await select('A2-완도');
|
|
check('fresh reopen regenerates full-name abutments', await ev(`document.getElementById('save-status').textContent === '저장됨' && document.getElementById('rebar-note').textContent.startsWith('A2-완도')`));
|
|
await shot('03-reopened');
|
|
await click(`document.getElementById('open-import')`);
|
|
await file('#file', badFile);
|
|
await until(`document.getElementById('dlg-apply').textContent.includes('교대 1기') && !!document.querySelector('#sheets details')`);
|
|
check('one bad definition does not disable another name', await ev(`document.getElementById('import-abut-A2-완도').checked && !document.getElementById('import-abut-A2-완도').disabled`));
|
|
check('dependent rows are collapsed under the root', await ev(`document.querySelectorAll('#sheets details').length === 1 && !document.querySelector('#sheets details').open && document.querySelector('#sheets details summary').textContent.includes('8개 행') && document.querySelectorAll('#sheets details p').length === 8`));
|
|
await shot('04-root-error-collapsed');
|
|
await click(`document.querySelector('#sheets details summary')`);
|
|
check('dependent rows remain available for diagnosis', await ev(`document.querySelector('#sheets details').open && document.querySelector('#sheets details').textContent.includes('IP') && document.querySelector('#sheets details').textContent.includes('받침')`));
|
|
await shot('05-related-rows');
|
|
check('no browser 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 { /* Keep the original error. */ }
|
|
}
|
|
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 { /* The tab may already be closed. */ } }
|
|
ws?.close();
|
|
});
|