166 lines
7.7 KiB
JavaScript
166 lines
7.7 KiB
JavaScript
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");
|
||
assertEqual(resultDocument.bands[0].evaluation.version, "1.0.0", "evaluation version");
|
||
assert(Number.isFinite(resultDocument.bands[0].evaluation.matchToleranceM), "match tolerance exported");
|
||
assert(Number.isFinite(resultDocument.bands[0].metrics.mainLobeWidthM), "main-lobe metric exported");
|
||
assertEqual(resultDocument.environment.scanXMin, experiment.scan.xMin, "scan bounds exported");
|
||
|
||
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",
|
||
);
|
||
dispatchValue(
|
||
windowObject,
|
||
documentObject.querySelector("#array-preset"),
|
||
"fermat-spiral-192",
|
||
);
|
||
await waitFor(
|
||
() => documentObject.querySelector("#array-title").textContent.includes("Fermat") &&
|
||
documentObject.documentElement.dataset.cameraValid === "true" &&
|
||
!documentObject.querySelector("#run-button").disabled,
|
||
"Fermat comparison array",
|
||
);
|
||
|
||
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");
|
||
const historyText = documentObject.querySelector("#history-list").textContent;
|
||
assert(historyText.includes("rectangular baseline"), "baseline history retained");
|
||
assert(historyText.includes("Fermat spiral"), "comparison array history added");
|
||
} finally {
|
||
windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick;
|
||
windowObject.URL.revokeObjectURL = originalRevoke;
|
||
for (const url of objectUrls) originalRevoke(url);
|
||
frame.remove();
|
||
}
|
||
});
|