import { validateResult } from "/packages/contracts/validation.js"; async function waitFor(predicate, label, timeoutMs = 30000) { const deadline = performance.now() + timeoutMs; while (performance.now() < deadline) { const value = predicate(); if (value) return value; await new Promise((resolve) => setTimeout(resolve, 40)); } throw new Error(`${label} 대기 시간이 초과되었습니다.`); } function dispatchValue(windowObject, element, value, eventName = "change") { element.value = String(value); element.dispatchEvent(new windowObject.Event(eventName, { bubbles: true })); } async function loadJson(path) { const response = await fetch(path); assert(response.ok, `${path} load failed`); return response.json(); } async function nextPaint(windowObject) { await new Promise((resolve) => windowObject.requestAnimationFrame(() => windowObject.requestAnimationFrame(resolve))); } test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름을 완주한다", async () => { const frame = document.createElement("iframe"); frame.src = "/apps/acoustic-camera/?integration-test=1"; frame.style.cssText = "position:fixed;left:-20000px;top:0;width:1400px;height:1000px;border:0"; document.body.append(frame); await new Promise((resolve, reject) => { frame.addEventListener("load", resolve, { once: true }); frame.addEventListener("error", () => reject(new Error("Acoustic Camera iframe load failed")), { once: true, }); }); const windowObject = frame.contentWindow; const documentObject = frame.contentDocument; const downloads = []; const objectUrls = []; const originalAnchorClick = windowObject.HTMLAnchorElement.prototype.click; const originalRevoke = windowObject.URL.revokeObjectURL.bind(windowObject.URL); windowObject.HTMLAnchorElement.prototype.click = function captureDownload() { downloads.push({ fileName: this.download, url: this.href }); }; windowObject.URL.revokeObjectURL = (url) => objectUrls.push(url); try { await waitFor( () => documentObject.documentElement.dataset.appReady === "true", "Acoustic Camera 초기화", ); await waitFor( () => documentObject.documentElement.dataset.cameraValid === "true", "기본 Camera 입력 검증", ); const canvas = documentObject.querySelector("#scene-canvas"); await waitFor(() => canvas.width > 0 && canvas.height > 0, "scene canvas 초기 렌더"); await nextPaint(windowObject); const initialCanvas = canvas.toDataURL(); dispatchValue(windowObject, documentObject.querySelector("#scan-columns"), 8); dispatchValue(windowObject, documentObject.querySelector("#scan-rows"), 8); dispatchValue(windowObject, documentObject.querySelector("#backend"), "cpu"); await waitFor( () => documentObject.documentElement.dataset.cameraValid === "true" && !documentObject.querySelector("#run-button").disabled, "8×8 CPU 입력 검증", ); assertEqual(documentObject.querySelector("#grid-preset").value, "custom", "custom grid preset"); documentObject.querySelector("#run-button").click(); await waitFor( () => documentObject.documentElement.dataset.runState === "done", "8×8 CPU simulation", 30000, ); assertEqual(documentObject.documentElement.dataset.runBackend, "cpu", "actual backend"); assertEqual(documentObject.documentElement.dataset.resultBandCount, "1", "result band count"); assert(documentObject.querySelectorAll("#metric-grid .metric-card").length >= 9, "result metrics rendered"); assert(!documentObject.querySelector("#metric-grid").classList.contains("is-hidden"), "metrics visible"); assert(documentObject.querySelector("#result-badge").textContent.includes("CPU"), "backend badge rendered"); await nextPaint(windowObject); assert(canvas.toDataURL() !== initialCanvas, "result heatmap changed the scene canvas"); documentObject.querySelector("#export-result-button").click(); const resultDownload = await waitFor( () => downloads.find((download) => download.fileName.endsWith(".result.json")), "Result JSON download", ); const resultDocument = await fetch(resultDownload.url).then((response) => response.json()); const [scene, experiment] = await Promise.all([ loadJson("/assets/sample-scenes/single-tone.scene.json"), loadJson("/assets/sample-scenes/single-tone.experiment.json"), ]); experiment.scan.columns = 8; experiment.scan.rows = 8; experiment.beamforming.backend = "cpu"; const resultIssues = await validateResult(resultDocument, { scene, experiment }); assertEqual(resultIssues.length, 0, `downloaded Result validation: ${resultIssues .map((issue) => `${issue.path}: ${issue.message}`) .join(" · ")}`); assertEqual(resultDocument.backend, "cpu", "exported backend"); assertEqual(resultDocument.bands[0].rawMap.rows, 8, "exported map rows"); assertEqual(resultDocument.bands[0].rawMap.columns, 8, "exported map columns"); assertEqual(resultDocument.bands[0].rawMap.values.length, 64, "exported map value count"); assert(/^[a-f0-9]{64}$/.test(resultDocument.inputHash), "exported input hash"); dispatchValue(windowObject, documentObject.querySelector("#dynamic-range"), 24); await waitFor( () => documentObject.querySelector("#result-badge").textContent.includes("이전 설정"), "changed input stale badge", ); assert(documentObject.querySelector("#canvas-badge").textContent.includes("이전 설정"), "canvas stale badge"); dispatchValue(windowObject, documentObject.querySelector("#dynamic-range"), 30); await waitFor( () => documentObject.documentElement.dataset.cameraValid === "true" && !documentObject.querySelector("#run-button").disabled, "original comparison configuration restore", ); documentObject.querySelector("#run-button").click(); await waitFor( () => documentObject.documentElement.dataset.runState === "running", "rerun start", ); assert(!documentObject.querySelector("#cancel-button").classList.contains("is-hidden"), "cancel available"); await waitFor( () => documentObject.documentElement.dataset.runState === "done", "8×8 CPU rerun", 30000, ); assert(!documentObject.querySelector("#result-badge").textContent.includes("이전 설정"), "fresh result badge"); assertEqual(documentObject.querySelectorAll("#history-list .history-card").length, 2, "rerun history"); } finally { windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick; windowObject.URL.revokeObjectURL = originalRevoke; for (const url of objectUrls) originalRevoke(url); frame.remove(); } });