fix: preserve analysis bands across input workflows
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user