@@ -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" ;