Files
workinf_Blender_Wasm/web/tests/unit/sequencer-audio-recovery.test.mjs
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

165 lines
5.8 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { pathToFileURL } from "node:url";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "sequencer-audio-recovery-unit-"));
const protocolPath = path.join(root, "web/protocol/sequencer-audio-session.ts");
const runtimePath = path.join(root, "web/app/src/sequencer/SequencerAudioSession.ts");
function transpile(source, fileName) {
const result = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
return result.outputText;
}
fs.writeFileSync(
path.join(temporary, "sequencer-audio-session.mjs"),
transpile(fs.readFileSync(protocolPath, "utf8").replace('import type { ErrorCode } from "./error";\n', ""), protocolPath),
);
fs.writeFileSync(
path.join(temporary, "SequencerAudioSession.mjs"),
transpile(
fs.readFileSync(runtimePath, "utf8").replace(
'from "../../../protocol/sequencer-audio-session";',
'from "./sequencer-audio-session.mjs";',
),
runtimePath,
),
);
const protocol = await import(pathToFileURL(path.join(temporary, "sequencer-audio-session.mjs")));
const runtime = await import(pathToFileURL(path.join(temporary, "SequencerAudioSession.mjs")));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-13/sequencer-audio-recovery.json"), "utf8"));
class FakeAudioParam {
value = 1;
events = [];
cancelScheduledValues(time) { this.events.push(["cancel", time]); }
setValueAtTime(value, time) { this.value = value; this.events.push(["set", value, time]); }
}
class FakeGainNode {
gain = new FakeAudioParam();
connected = false;
disconnected = false;
connect() { this.connected = true; }
disconnect() { this.disconnected = true; }
}
class FakeAudioContext {
static instances = [];
state = "suspended";
currentTime = 1;
destination = {};
output = new FakeGainNode();
closeCount = 0;
constructor() { FakeAudioContext.instances.push(this); }
createGain() { return this.output; }
async resume() { this.state = "running"; }
async suspend() { this.state = "suspended"; }
async close() { this.closeCount += 1; this.state = "closed"; }
}
const summary = (report) => [
report.contextState,
report.outputState,
report.muted,
report.outputGain,
report.issueCode,
];
test("M11-13 restores output after real session suspend and mute transitions", async () => {
FakeAudioContext.instances.length = 0;
const session = new runtime.SequencerAudioSession({
scope: { AudioContext: FakeAudioContext },
outputGain: golden.outputGain,
});
const reports = [];
reports.push(await session.initialize());
reports.push(session.setMuted(true));
reports.push(await session.resume());
reports.push(session.setMuted(false));
reports.push(await session.suspend());
reports.push(await session.resume());
reports.push(await session.close());
assert.deepEqual(reports.map(summary), golden.lifecycle);
assert.deepEqual(reports.map((report) => report.revision), [1, 2, 3, 4, 5, 6, 7]);
const context = FakeAudioContext.instances[0];
assert.equal(context.output.gain.value, 0);
assert.equal(context.output.connected, true);
assert.equal(context.output.disconnected, true);
assert.equal(context.closeCount, 1);
assert.ok(context.output.gain.events.some((event) => event[0] === "set" && event[1] === 0));
assert.ok(context.output.gain.events.some((event) => event[0] === "set" && event[1] === golden.outputGain));
});
test("M11-13 blocks a missing audio device and can retry after the runtime becomes available", async () => {
FakeAudioContext.instances.length = 0;
const scope = {};
const session = new runtime.SequencerAudioSession({ scope, outputGain: golden.outputGain });
const missing = await session.initialize();
assert.deepEqual(
[missing.contextState, missing.outputState, missing.issueCode],
golden.missingDevice,
);
assert.deepEqual(session.snapshot(), missing);
scope.AudioContext = FakeAudioContext;
const recovered = await session.recoverDevice();
assert.deepEqual(summary(recovered), golden.lifecycle[0]);
const resumed = await session.resume();
assert.deepEqual(summary(resumed), ["RUNNING", "ENABLED", false, golden.outputGain, null]);
await session.close();
});
test("M11-13 keeps failed resume and malformed reports silent and structured", async () => {
class ResumeFailureContext extends FakeAudioContext {
async resume() { throw new Error("injected resume failure"); }
}
const failed = new runtime.SequencerAudioSession({
contextFactory: () => new ResumeFailureContext(),
outputGain: golden.outputGain,
});
await failed.initialize();
const resumeFailure = await failed.resume();
assert.deepEqual(
[resumeFailure.contextState, resumeFailure.outputState, resumeFailure.issueCode],
golden.resumeFailure,
);
await failed.close();
const unavailable = new runtime.SequencerAudioSession({
contextFactory: () => { throw new Error("no output device"); },
});
assert.deepEqual(
[
(await unavailable.initialize()).contextState,
unavailable.snapshot().outputState,
unavailable.snapshot().issueCode,
],
golden.missingDevice,
);
assert.throws(() => new runtime.SequencerAudioSession({ outputGain: 0 }), {
code: "SEQUENCER_AUDIO_CONTEXT_INVALID",
});
assert.throws(() => protocol.parseSequencerAudioSessionReport({
schemaVersion: 1,
revision: 1,
contextState: "RUNNING",
outputState: "ENABLED",
muted: true,
outputGain: 1,
issueCode: null,
}), { code: "SEQUENCER_AUDIO_CONTEXT_INVALID" });
});