fix: preserve analysis bands across input workflows

This commit is contained in:
2026-08-29 08:05:15 +09:00
parent 122b6cf7be
commit 62971e07ec
10 changed files with 282 additions and 24 deletions
+2 -1
View File
@@ -25,7 +25,8 @@ Unit과 integration test가 같은 페이지에서 실행됩니다. WebGPU 장
- `array-builder-ui.test.js`: 프리셋·직접 추가·좌표·보정·채널 편집, undo/redo,
중복 좌표 차단, 표준 Array JSON 저장·재가져오기와 390px layout
- `camera-ui.test.js`: Dedicated Worker CPU/WebGPU 실행, 취소, Result schema, rectangular/Fermat와
DAS/DAS-PHAT 비교, GCC-PHAT CPU·협대역 제약; 이동 예제 로드, 8×8 CPU·2 Hz live map의
DAS/DAS-PHAT 비교, GCC-PHAT CPU·협대역 제약, Builder 새 탭과 Experiment 선로드·Scene
재import의 다중 band 보존; 이동 예제 전체 교체 확인, 8×8 CPU·2 Hz live map의
Auto→CPU fallback 뒤 공통 analysis plan 재사용, 복수 frame, scrub, 재생 시간 증가와 일시정지
- `workflow-ui.test.js`: Dataset Builder의 Scene JSON과 원본 assets를 Acoustic Camera에 넘겨
Result JSON을 저장하는 end-to-end 흐름
+207
View File
@@ -15,6 +15,13 @@ function dispatchValue(windowObject, element, value, eventName = "change") {
element.dispatchEvent(new windowObject.Event(eventName, { bubbles: true }));
}
function dispatchFiles(windowObject, input, files) {
const transfer = new windowObject.DataTransfer();
for (const file of files) transfer.items.add(file);
input.files = transfer.files;
input.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
}
async function loadJson(path) {
const response = await fetch(path);
assert(response.ok, `${path} load failed`);
@@ -337,6 +344,206 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
}
});
test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서 다중 분석 대역을 보존한다", async () => {
const frame = document.createElement("iframe");
frame.src = "/apps/acoustic-camera/?integration-test=1&band-preservation=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 band-preservation 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);
const originalConfirm = windowObject.confirm.bind(windowObject);
windowObject.HTMLAnchorElement.prototype.click = function captureDownload() {
if (this.download) downloads.push({ fileName: this.download, url: this.href });
};
windowObject.URL.revokeObjectURL = (url) => objectUrls.push(url);
const readBandEditors = () =>
[...documentObject.querySelectorAll("#band-list .band-editor")].map((editor) =>
Object.fromEntries(
[...editor.querySelectorAll("input[data-band-key]")].map((input) => [
input.dataset.bandKey,
input.type === "number" ? Number(input.value) : input.value,
]),
));
try {
await waitFor(
() => documentObject.documentElement.dataset.appReady === "true" &&
documentObject.documentElement.dataset.cameraValid === "true",
"band-preservation Camera initialization",
);
const builderLinks = [...documentObject.querySelectorAll("a.builder-link")];
assertEqual(builderLinks.length, 2, "Scene and Array Builder links");
for (const link of builderLinks) {
assertEqual(link.target, "_blank", "Builder opens without unloading Camera");
assert(link.relList.contains("noopener"), "Builder tab cannot replace Camera through opener");
}
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
scene.id = "two-tone-band-preservation";
scene.name = "Two-tone band preservation";
const secondSource = structuredClone(scene.sources[0]);
secondSource.id = "source-2";
secondSource.name = "1.2 kHz tone";
secondSource.position = [-0.12, 0.09, 2];
secondSource.signal.frequency = 1200;
secondSource.signal.phaseDeg = 45;
scene.sources.push(secondSource);
const arrayDefinition = await loadJson("/assets/array-geometries/rectangular-16x12.array.json");
arrayDefinition.id = "custom-array-band-preservation";
arrayDefinition.name = "Custom array band preservation";
const experiment = await loadJson("/assets/sample-scenes/single-tone.experiment.json");
experiment.id = "two-band-import-first";
experiment.name = "Two-band import first";
experiment.sceneId = scene.id;
experiment.arrayId = arrayDefinition.id;
experiment.frequencyBands.push({
id: "tone-1.2k",
minHz: 1100,
maxHz: 1300,
color: "#58c7ff",
});
const expectedBands = structuredClone(experiment.frequencyBands);
const experimentFile = new windowObject.File(
[JSON.stringify(experiment)],
"two-band-import-first.experiment.json",
{ type: "application/json" },
);
dispatchFiles(windowObject, documentObject.querySelector("#experiment-input"), [experimentFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false",
"clean-state Experiment-first import",
);
assertEqual(
documentObject.querySelector("#experiment-id").value,
experiment.id,
"Experiment loads before its referenced Scene",
);
assertEqual(readBandEditors().length, 2, "Experiment-first import renders every analysis band");
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"Experiment-first import preserves band IDs, ranges, and colors",
);
await waitFor(
() => documentObject.documentElement.dataset.cameraValid === "false" &&
documentObject.querySelector("#validation-message").textContent.includes("sceneId") &&
documentObject.querySelector("#validation-message").textContent.includes("arrayId"),
"pending Scene and Array linkage validation",
);
assert(
documentObject.querySelector("#validation-message").textContent.includes("다릅니다"),
"Experiment-first import explains the pending document ID mismatch",
);
assert(
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개") &&
documentObject.querySelector("#run-status").textContent.includes(scene.id) &&
documentObject.querySelector("#run-status").textContent.includes(arrayDefinition.id) &&
documentObject.querySelector("#run-status").textContent.includes("연결 대기"),
"Experiment-first status reports every band and pending reference",
);
let replacementConfirmCount = 0;
windowObject.confirm = () => {
replacementConfirmCount += 1;
return false;
};
documentObject.querySelector("#load-moving-example-button").click();
assertEqual(replacementConfirmCount, 1, "full moving example asks before replacing bands");
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"declining the full example keeps every analysis band",
);
assert(
documentObject.querySelector("#run-status").textContent.includes("유지"),
"declined replacement explains that inputs were kept",
);
windowObject.confirm = originalConfirm;
const sceneFile = new windowObject.File(
[JSON.stringify(scene)],
"two-tone-band-preservation.scene.json",
{ type: "application/json" },
);
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [sceneFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.querySelectorAll("#source-summary .source-line").length === 2 &&
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
documentObject.documentElement.dataset.cameraValid === "false",
"source-added Scene reimport",
);
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"Scene reimport preserves band IDs, ranges, and colors",
);
assert(
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개"),
"Camera status keeps the preserved band count visible",
);
const arrayFile = new windowObject.File(
[JSON.stringify(arrayDefinition)],
"custom-array-band-preservation.array.json",
{ type: "application/json" },
);
dispatchFiles(windowObject, documentObject.querySelector("#array-input"), [arrayFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
documentObject.documentElement.dataset.cameraValid === "true",
"referenced Array reimport",
);
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"Array reimport also preserves every analysis band",
);
assert(
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개"),
"ready status keeps the connected Experiment band count visible",
);
documentObject.querySelector("#export-experiment-button").click();
const experimentDownload = await waitFor(
() => downloads.find((download) => download.fileName.endsWith(".experiment.json")),
"preserved Experiment export",
);
const exportedExperiment = await fetch(experimentDownload.url).then((response) => response.json());
assertEqual(
JSON.stringify(exportedExperiment.frequencyBands),
JSON.stringify(expectedBands),
"Experiment export preserves every band after Scene reimport",
);
assertEqual(exportedExperiment.sceneId, scene.id, "Experiment rebinds only the Scene ID");
assertEqual(exportedExperiment.arrayId, arrayDefinition.id, "Experiment rebinds the Array ID");
} finally {
windowObject.confirm = originalConfirm;
windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick;
windowObject.URL.revokeObjectURL = originalRevoke;
for (const url of objectUrls) originalRevoke(url);
frame.remove();
}
});
test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source example", async () => {
const frame = document.createElement("iframe");
frame.src = "/apps/acoustic-camera/?integration-test=1";