feat: add independent full-band activity workflow
This commit is contained in:
@@ -853,3 +853,579 @@ test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source exam
|
||||
frame.remove();
|
||||
}
|
||||
});
|
||||
|
||||
test("Acoustic Camera full-band Activity keeps raw ADC evidence separate from localization", async () => {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = "/apps/acoustic-camera/?integration-test=activity-full-band-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 Activity iframe load failed")),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
const windowObject = frame.contentWindow;
|
||||
const documentObject = frame.contentDocument;
|
||||
const downloads = [];
|
||||
const objectUrls = [];
|
||||
let confirmCount = 0;
|
||||
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() {
|
||||
downloads.push({ fileName: this.download, url: this.href });
|
||||
};
|
||||
windowObject.URL.revokeObjectURL = (url) => objectUrls.push(url);
|
||||
windowObject.confirm = () => {
|
||||
confirmCount += 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.appReady === "true",
|
||||
"Acoustic Camera Activity initialization",
|
||||
);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.cameraValid === "true",
|
||||
"Acoustic Camera Activity initial validation",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#activity-export-button").disabled,
|
||||
"Activity export starts disabled without a report",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#activity-frame").disabled,
|
||||
"Activity frame scrubber starts disabled without a report",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#load-music-example-button").click();
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.documentElement.dataset.cameraValid === "true" &&
|
||||
documentObject.querySelector("#algorithm").value === "music" &&
|
||||
documentObject.querySelector("#microphone-count").textContent === "12 ch",
|
||||
"Activity quick MUSIC example load",
|
||||
);
|
||||
const sceneWithMissingBackground = await loadJson(
|
||||
"/assets/sample-scenes/music-single-tone.scene.json",
|
||||
);
|
||||
sceneWithMissingBackground.mix.normalization = "peak";
|
||||
sceneWithMissingBackground.assets = [
|
||||
{
|
||||
id: "unbound-background",
|
||||
kind: "image",
|
||||
fileName: "unbound-background.png",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
];
|
||||
sceneWithMissingBackground.image = {
|
||||
assetId: "unbound-background",
|
||||
widthPixels: 640,
|
||||
heightPixels: 480,
|
||||
plane: {
|
||||
z: 2,
|
||||
xMin: -1,
|
||||
xMax: 1,
|
||||
yMin: -0.75,
|
||||
yMax: 0.75,
|
||||
},
|
||||
};
|
||||
dispatchFiles(
|
||||
windowObject,
|
||||
documentObject.querySelector("#scene-input"),
|
||||
[
|
||||
new windowObject.File(
|
||||
[JSON.stringify(sceneWithMissingBackground)],
|
||||
"music-with-unbound-background.scene.json",
|
||||
{ type: "application/json" },
|
||||
),
|
||||
],
|
||||
);
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.documentElement.dataset.cameraValid === "false" &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes(
|
||||
"unbound-background",
|
||||
) &&
|
||||
!documentObject.querySelector("#activity-run-button").disabled,
|
||||
"missing background image does not block audio Activity analysis",
|
||||
);
|
||||
const activityMonitor = documentObject.querySelector("#activity-monitor");
|
||||
activityMonitor.open = true;
|
||||
assertEqual(
|
||||
documentObject.querySelector("#activity-algorithm").value,
|
||||
"os-cfar",
|
||||
"default full-band detector is OS-CFAR",
|
||||
);
|
||||
assert(
|
||||
activityMonitor.querySelector(".activity-monitor__copy").textContent.includes(
|
||||
"Scene",
|
||||
) &&
|
||||
activityMonitor.querySelector(".activity-monitor__copy").textContent.includes(
|
||||
"검출기에 전달하지",
|
||||
) &&
|
||||
activityMonitor.querySelector(".activity-monitor__copy").textContent.includes(
|
||||
"ADC",
|
||||
) &&
|
||||
activityMonitor.querySelector(".activity-monitor__copy").textContent.includes(
|
||||
"alias order",
|
||||
),
|
||||
"Activity copy separates ADC observation from Scene truth and analog alias order",
|
||||
);
|
||||
const musicBandMaximum = documentObject.querySelector(
|
||||
'#band-list .band-editor input[data-band-key="maxHz"]',
|
||||
);
|
||||
const originalMusicBandMaximum = musicBandMaximum.valueAsNumber;
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
musicBandMaximum,
|
||||
originalMusicBandMaximum + 2 * (48000 / 512),
|
||||
);
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.cameraValid === "false" &&
|
||||
documentObject.querySelector("#run-button").disabled &&
|
||||
!documentObject.querySelector("#activity-run-button").disabled,
|
||||
"Activity remains ready when an unused MUSIC band is invalid",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#activity-run-button").click();
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.activityState === "done" &&
|
||||
documentObject.documentElement.dataset.activityAlgorithm === "os-cfar",
|
||||
"default OS-CFAR Activity run",
|
||||
45000,
|
||||
);
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector(
|
||||
'#band-list .band-editor input[data-band-key="maxHz"]',
|
||||
),
|
||||
originalMusicBandMaximum,
|
||||
);
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.cameraValid === "false" &&
|
||||
documentObject.documentElement.dataset.activityState === "done" &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes(
|
||||
"unbound-background",
|
||||
),
|
||||
"restore localization-only MUSIC band after independent Activity run",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.documentElement.dataset.runState,
|
||||
"idle",
|
||||
"Activity run does not create a localization result",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#export-result-button").disabled,
|
||||
"localization Result export remains unavailable",
|
||||
);
|
||||
const caveatText =
|
||||
documentObject.querySelector("#activity-result-list .activity-caveat")
|
||||
?.textContent ?? "";
|
||||
assert(
|
||||
caveatText.includes("IID exponential") &&
|
||||
caveatText.includes("tested CUT") &&
|
||||
caveatText.includes("confidence") &&
|
||||
caveatText.includes("frame 전체"),
|
||||
"UI states the nominal per-CUT statistical limits",
|
||||
);
|
||||
const activityResultText =
|
||||
documentObject.querySelector("#activity-result-list").textContent;
|
||||
assert(
|
||||
documentObject.querySelector("#activity-frame-label").textContent.includes(
|
||||
"start",
|
||||
) &&
|
||||
documentObject.querySelector("#activity-frame-label").textContent.includes(
|
||||
"available",
|
||||
),
|
||||
"Activity frame distinguishes descriptive start from causal availability",
|
||||
);
|
||||
assert(
|
||||
activityResultText.includes("CFAR tested 범위") &&
|
||||
activityResultText.includes("full reference window") &&
|
||||
activityResultText.includes("untested"),
|
||||
"UI discloses the actual CFAR coverage and untouched edge bins",
|
||||
);
|
||||
assert(
|
||||
activityResultText.includes("end-to-end 전처리는 미래 sample에 의존"),
|
||||
"UI discloses whole-render peak-normalization causality",
|
||||
);
|
||||
|
||||
const toneHypothesisInput = await waitFor(
|
||||
() => documentObject.querySelector(
|
||||
'#activity-result-list input[data-activity-analog-hypothesis="true"]' +
|
||||
'[data-activity-bin="12"]',
|
||||
),
|
||||
"detected 1125 Hz raw Activity row",
|
||||
15000,
|
||||
);
|
||||
const toneRow = toneHypothesisInput.closest(".activity-result-row");
|
||||
assert(
|
||||
toneRow.querySelector("strong").textContent.includes("ADC bin 12") &&
|
||||
toneRow.querySelector("strong").textContent.includes("1125"),
|
||||
"detected tone is expressed as an observed ADC bin",
|
||||
);
|
||||
const rawText = [...toneRow.children]
|
||||
.find((child) => child.tagName === "SPAN")?.textContent ?? "";
|
||||
assert(
|
||||
rawText.includes("CUT") &&
|
||||
rawText.includes("ref") &&
|
||||
rawText.includes("threshold") &&
|
||||
rawText.includes("ratio"),
|
||||
"detected row exposes raw CUT, reference, threshold, and ratio",
|
||||
);
|
||||
assertEqual(
|
||||
toneHypothesisInput.valueAsNumber,
|
||||
1125,
|
||||
"analog hypothesis starts at the observed ADC center without claiming alias order",
|
||||
);
|
||||
|
||||
dispatchValue(windowObject, toneHypothesisInput, 49125, "input");
|
||||
toneRow.querySelector("button[data-add-activity-band]").click();
|
||||
const addedBandEditor = await waitFor(
|
||||
() => [...documentObject.querySelectorAll("#band-list .band-editor")]
|
||||
.find((editor) =>
|
||||
editor.querySelector('input[data-band-key="id"]')?.value ===
|
||||
"activity-1"),
|
||||
"above-Nyquist Activity hypothesis band addition",
|
||||
);
|
||||
const addedMinimumHz = Number(
|
||||
addedBandEditor.querySelector('input[data-band-key="minHz"]').value,
|
||||
);
|
||||
const addedMaximumHz = Number(
|
||||
addedBandEditor.querySelector('input[data-band-key="maxHz"]').value,
|
||||
);
|
||||
assert(
|
||||
Math.abs((addedMinimumHz + addedMaximumHz) / 2 - 49125) <= 1e-9,
|
||||
"explicit analog hypothesis center",
|
||||
);
|
||||
assert(
|
||||
Math.abs(addedMaximumHz - addedMinimumHz - 48000 / 512) <= 1e-9,
|
||||
"one FFT-cell candidate band width",
|
||||
);
|
||||
assert(
|
||||
(addedMinimumHz + addedMaximumHz) / 2 > 48000 / 2,
|
||||
"above-Nyquist analog hypothesis is retained",
|
||||
);
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.cameraValid === "false" &&
|
||||
documentObject.documentElement.dataset.activityState === "done" &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes(
|
||||
"unbound-background",
|
||||
),
|
||||
"above-Nyquist Activity band with unrelated missing image",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#run-status").textContent.includes("ADC bin 12") &&
|
||||
documentObject.querySelector("#run-status").textContent.includes(
|
||||
"다른 alias order",
|
||||
),
|
||||
"band addition labels ADC folding and keeps alias-order choice explicit",
|
||||
);
|
||||
|
||||
const activityDownloadStart = downloads.length;
|
||||
documentObject.querySelector("#activity-export-button").click();
|
||||
const activityDownload = await waitFor(
|
||||
() => downloads.slice(activityDownloadStart)
|
||||
.find((download) => download.fileName.endsWith(".activity.json")),
|
||||
"OS-CFAR Activity JSON download",
|
||||
);
|
||||
const activityDocument = await fetch(activityDownload.url).then(
|
||||
(response) => response.json(),
|
||||
);
|
||||
assertEqual(activityDocument.kind, "micarrsim.activity-report", "Activity document kind");
|
||||
assertEqual(activityDocument.provenanceOnlyIds, true, "Scene/Array/Experiment are provenance IDs");
|
||||
assertEqual(
|
||||
activityDocument.detectorSceneTruthInput,
|
||||
false,
|
||||
"outer Activity report denies Scene-truth detector input",
|
||||
);
|
||||
const workerReport = activityDocument.report;
|
||||
const analysis = workerReport.analysis;
|
||||
assertEqual(workerReport.algorithm, "os-cfar", "exported detector identity");
|
||||
assertEqual(
|
||||
workerReport.channel.selection,
|
||||
"user-selected-single-physical-microphone",
|
||||
"one explicit physical microphone",
|
||||
);
|
||||
assertEqual(workerReport.channel.microphoneFusion, "none", "no microphone fusion");
|
||||
assertEqual(
|
||||
workerReport.input.sceneSourceTruthPassedToDetector,
|
||||
false,
|
||||
"worker does not receive Scene source truth as detector input",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.input.sceneSourceTruthUsedByPhysicalSimulator,
|
||||
true,
|
||||
"report distinguishes physical Scene synthesis from detector input",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.input.synthesizedMicrophoneCount,
|
||||
1,
|
||||
"only the explicitly selected microphone is synthesized for Activity",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.input.analogAliasOrderResolved,
|
||||
false,
|
||||
"worker reports ADC observation without resolving analog alias order",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.input.adcRangeHz.join(","),
|
||||
"0,24000",
|
||||
"Activity export reports only the observable ADC range",
|
||||
);
|
||||
assertEqual(workerReport.localization, undefined, "worker report has no localization");
|
||||
assertEqual(workerReport.scene, undefined, "worker report contains no Scene source list");
|
||||
assertEqual(activityDocument.localization, undefined, "document has no localization payload");
|
||||
assertEqual(
|
||||
workerReport.signalStats.windowing.arrivingSourceIds,
|
||||
undefined,
|
||||
"Activity artifact omits arriving source identities",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats
|
||||
.explicitSceneSourceIdentitiesAndArrivalsIncluded,
|
||||
false,
|
||||
"report narrows its truth-free claim to explicit identities and arrivals",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.sourceDerivedAggregatePhysicalAuditIncluded,
|
||||
true,
|
||||
"report discloses aggregate source-derived physical audit statistics",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.preprocessing.normalizationScope,
|
||||
"selected-physical-microphone-whole-render",
|
||||
"peak normalization is scoped to the selected microphone",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.preprocessing.futureDependentNormalization,
|
||||
true,
|
||||
"whole-render peak normalization is marked future-dependent",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.preprocessing.endToEndCausal,
|
||||
false,
|
||||
"peak-normalized Activity pipeline does not claim end-to-end causality",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.windowing.sourceArrivals,
|
||||
undefined,
|
||||
"Activity artifact omits source-arrival truth",
|
||||
);
|
||||
assertEqual(
|
||||
workerReport.signalStats.workingSet.maximumTransientId,
|
||||
undefined,
|
||||
"Activity artifact omits source-scoped working-set identity",
|
||||
);
|
||||
|
||||
const model = analysis.detectorModel;
|
||||
assertEqual(
|
||||
model.parameters.nominalPfaPerTestedCutUnderIidExponentialModel,
|
||||
0.001,
|
||||
"nominal Pfa remains explicitly model-qualified",
|
||||
);
|
||||
assertEqual(
|
||||
model.calibration.status,
|
||||
"nominal-model-only-not-empirically-calibrated",
|
||||
"nominal Pfa is not represented as empirical calibration",
|
||||
);
|
||||
assertEqual(
|
||||
model.calibration.globalFrameFalseAlarmProbability,
|
||||
null,
|
||||
"no invented frame-global false-alarm probability",
|
||||
);
|
||||
assert(
|
||||
model.calibration.notCalibratedReasons.includes(
|
||||
"hann-window-bin-independence-is-not-guaranteed",
|
||||
),
|
||||
"Hann bin dependence limitation is exported",
|
||||
);
|
||||
assertEqual(model.processing.clustering, false, "no bin clustering");
|
||||
assertEqual(model.processing.temporalPersistence, false, "no temporal persistence");
|
||||
assertEqual(model.processing.detectorFusion, false, "no detector fusion");
|
||||
assertEqual(model.processing.microphoneFusion, false, "no microphone fusion metadata");
|
||||
assertEqual(model.processing.bandFusion, false, "no band fusion");
|
||||
|
||||
const expectedFrameCount =
|
||||
1 + Math.floor(
|
||||
(workerReport.input.sampleCount - workerReport.input.fftSize) /
|
||||
workerReport.input.hopSize,
|
||||
);
|
||||
const expectedBinCount = workerReport.input.fftSize / 2 + 1;
|
||||
assertEqual(analysis.frames.length, expectedFrameCount, "every complete frame retained");
|
||||
assertEqual(analysis.summary.frameCount, expectedFrameCount, "frame summary is lossless");
|
||||
let exportedBinCount = 0;
|
||||
for (const exportedFrame of analysis.frames) {
|
||||
assertEqual(exportedFrame.bins.length, expectedBinCount, "every ADC bin retained");
|
||||
assertEqual(
|
||||
exportedFrame.testedBinCount + exportedFrame.untestedBinCount,
|
||||
expectedBinCount,
|
||||
"tested and untested states cover a whole frame",
|
||||
);
|
||||
for (let binIndex = 0; binIndex < exportedFrame.bins.length; binIndex += 1) {
|
||||
const bin = exportedFrame.bins[binIndex];
|
||||
assertEqual(bin.adcBin, binIndex, "ADC bin order is preserved");
|
||||
assert(Number.isFinite(bin.cutPower) && bin.cutPower >= 0, "raw CUT power is finite");
|
||||
assert(
|
||||
bin.status === "detected" ||
|
||||
bin.status === "not-detected" ||
|
||||
bin.status === "untested",
|
||||
"every bin has an explicit decision state",
|
||||
);
|
||||
}
|
||||
exportedBinCount += exportedFrame.bins.length;
|
||||
}
|
||||
assertEqual(
|
||||
exportedBinCount,
|
||||
expectedFrameCount * expectedBinCount,
|
||||
"Activity JSON preserves the full frame/bin Cartesian product",
|
||||
);
|
||||
assertEqual(
|
||||
analysis.summary.binRecordCount,
|
||||
exportedBinCount,
|
||||
"raw bin-record summary matches the payload",
|
||||
);
|
||||
assertEqual(analysis.frames[0].bins[0].reason, "real-only-dc-bin", "DC remains untested");
|
||||
assertEqual(
|
||||
analysis.frames[0].bins.at(-1).reason,
|
||||
"real-only-nyquist-bin",
|
||||
"Nyquist remains untested",
|
||||
);
|
||||
const exportedTone = analysis.frames
|
||||
.map((exportedFrame) => exportedFrame.bins[12])
|
||||
.find((bin) => bin.status === "detected");
|
||||
assert(exportedTone, "the UI row comes from a retained raw detected-bin record");
|
||||
assert(
|
||||
Number.isFinite(exportedTone.referencePower) &&
|
||||
Number.isFinite(exportedTone.thresholdPower),
|
||||
"detected tone retains raw reference and threshold statistics",
|
||||
);
|
||||
assert(
|
||||
exportedTone.thresholdRatio === null
|
||||
? exportedTone.thresholdRatioStatus ===
|
||||
"positive-infinity-zero-threshold"
|
||||
: Number.isFinite(exportedTone.thresholdRatio) &&
|
||||
exportedTone.thresholdRatio > 1,
|
||||
"threshold ratio is raw and JSON-safe",
|
||||
);
|
||||
|
||||
const experimentDownloadStart = downloads.length;
|
||||
documentObject.querySelector("#export-experiment-button").click();
|
||||
const experimentDownload = await waitFor(
|
||||
() => downloads.slice(experimentDownloadStart)
|
||||
.find((download) => download.fileName.endsWith(".experiment.json")),
|
||||
"Activity hypothesis Experiment JSON download",
|
||||
);
|
||||
const experimentDocument = await fetch(experimentDownload.url).then(
|
||||
(response) => response.json(),
|
||||
);
|
||||
const addedBand = experimentDocument.frequencyBands.find(
|
||||
(band) => band.id === "activity-1",
|
||||
);
|
||||
assert(addedBand, "explicit Activity hypothesis is exported as an Experiment band");
|
||||
assert(
|
||||
Math.abs((addedBand.minHz + addedBand.maxHz) / 2 - 49125) <= 1e-9,
|
||||
"exported analog center",
|
||||
);
|
||||
assert(
|
||||
(addedBand.minHz + addedBand.maxHz) / 2 > workerReport.input.adcRangeHz[1],
|
||||
"Experiment retains the above-Nyquist analog hypothesis",
|
||||
);
|
||||
const [musicArray, musicScene] = await Promise.all([
|
||||
loadJson("/assets/array-geometries/rectangular-4x3-450mm.array.json"),
|
||||
loadJson("/assets/sample-scenes/music-single-tone.scene.json"),
|
||||
]);
|
||||
assertEqual(
|
||||
(await validateExperiment(experimentDocument, {
|
||||
arrayDefinition: musicArray,
|
||||
scene: musicScene,
|
||||
})).length,
|
||||
0,
|
||||
"above-Nyquist explicit hypothesis remains a valid Experiment",
|
||||
);
|
||||
assertEqual(confirmCount, 0, "saved Activity report is replaced without a dirty-report prompt");
|
||||
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#activity-algorithm"),
|
||||
"dixon-spectral-flux",
|
||||
);
|
||||
assert(
|
||||
!documentObject.querySelector("#activity-peak-picker-enabled").checked,
|
||||
"spectral-flux peak picker remains opt-in",
|
||||
);
|
||||
await waitFor(
|
||||
() =>
|
||||
!documentObject.querySelector("#activity-run-button").disabled &&
|
||||
!documentObject.querySelector("#activity-flux-options").hidden,
|
||||
"raw-only spectral flux readiness",
|
||||
);
|
||||
documentObject.querySelector("#activity-run-button").click();
|
||||
await waitFor(
|
||||
() =>
|
||||
documentObject.documentElement.dataset.activityState === "done" &&
|
||||
documentObject.documentElement.dataset.activityAlgorithm ===
|
||||
"dixon-spectral-flux",
|
||||
"raw-only spectral flux Activity run",
|
||||
45000,
|
||||
);
|
||||
const fluxText = documentObject.querySelector("#activity-result-list").textContent;
|
||||
assert(
|
||||
fluxText.includes("raw onset novelty") &&
|
||||
fluxText.includes("probability") &&
|
||||
fluxText.includes("Peak picker를 적용하지 않아"),
|
||||
"spectral flux UI remains raw novelty without an invented event threshold",
|
||||
);
|
||||
const fluxDownloadStart = downloads.length;
|
||||
documentObject.querySelector("#activity-export-button").click();
|
||||
const fluxDownload = await waitFor(
|
||||
() => downloads.slice(fluxDownloadStart)
|
||||
.find((download) => download.fileName.endsWith(".activity.json")),
|
||||
"raw spectral-flux Activity JSON download",
|
||||
);
|
||||
const fluxDocument = await fetch(fluxDownload.url).then(
|
||||
(response) => response.json(),
|
||||
);
|
||||
assertEqual(
|
||||
fluxDocument.report.algorithm,
|
||||
"dixon-spectral-flux",
|
||||
"raw spectral-flux identity",
|
||||
);
|
||||
assertEqual(
|
||||
fluxDocument.report.peakPicking,
|
||||
undefined,
|
||||
"no implicit spectral-flux peak picker",
|
||||
);
|
||||
assertEqual(
|
||||
fluxDocument.report.analysis.rawOdf.length,
|
||||
fluxDocument.report.analysis.frameCount,
|
||||
"all raw spectral-flux frame values retained",
|
||||
);
|
||||
assertEqual(
|
||||
fluxDocument.report.analysis.positiveDifferences.length,
|
||||
fluxDocument.report.analysis.frameCount *
|
||||
fluxDocument.report.analysis.binCount,
|
||||
"all raw per-frame/per-bin positive differences retained",
|
||||
);
|
||||
assertEqual(confirmCount, 0, "Activity exports clear report-dirty state");
|
||||
} finally {
|
||||
windowObject.confirm = originalConfirm;
|
||||
windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick;
|
||||
windowObject.URL.revokeObjectURL = originalRevoke;
|
||||
for (const url of objectUrls) originalRevoke(url);
|
||||
frame.remove();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user