feat: release Find-Word v0.2.3
This commit is contained in:
+86
-13
@@ -31,7 +31,8 @@ function fixture() {
|
||||
sampleRate: rate,
|
||||
numberOfChannels: 1,
|
||||
getChannelData: () => data,
|
||||
copyToChannel: vi.fn(),
|
||||
// 실제 AudioBuffer처럼 복사를 반영해야 정규화 뒤 피크를 검증할 수 있다.
|
||||
copyToChannel: vi.fn((samples: Float32Array) => data.set(samples)),
|
||||
};
|
||||
}
|
||||
const sources: ReturnType<typeof makeSource>[] = [];
|
||||
@@ -95,8 +96,9 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
f.sources.at(-1)!.onended?.();
|
||||
await done;
|
||||
expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
|
||||
"/audio/intro.wav?v=0.1.1",
|
||||
`/audio/${question.id}.wav?v=0.1.1`,
|
||||
"/audio/intro.wav?v=0.2.3",
|
||||
`/audio/${question.id}.wav?v=0.2.3`,
|
||||
question.environment.audioUrl,
|
||||
]);
|
||||
f.engine.dispose();
|
||||
});
|
||||
@@ -128,10 +130,12 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
f.sources.at(-1)!.onended?.();
|
||||
await done;
|
||||
expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
|
||||
"/audio/intro.wav?v=0.1.1",
|
||||
`/audio/${question.id}.wav?v=0.1.1`,
|
||||
"/audio/intro.wav?v=0.1.1",
|
||||
`/audio/${question.id}.wav?v=0.1.1`,
|
||||
"/audio/intro.wav?v=0.2.3",
|
||||
`/audio/${question.id}.wav?v=0.2.3`,
|
||||
question.environment.audioUrl,
|
||||
"/audio/intro.wav?v=0.2.3",
|
||||
`/audio/${question.id}.wav?v=0.2.3`,
|
||||
question.environment.audioUrl,
|
||||
]);
|
||||
f.engine.dispose();
|
||||
},
|
||||
@@ -150,7 +154,11 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
f.fetcher.mock.calls.map(
|
||||
([url]) => new URL(String(url), "http://localhost").pathname,
|
||||
),
|
||||
).toEqual(["/audio/intro.wav", `/audio/${question.id}.wav`]);
|
||||
).toEqual([
|
||||
"/audio/intro.wav",
|
||||
`/audio/${question.id}.wav`,
|
||||
new URL(question.environment.audioUrl, "http://localhost").pathname,
|
||||
]);
|
||||
// master, 배경, 목표, 안내의 gain을 분리한다. 안내는 점차 소음에 묻힌다.
|
||||
expect(f.gains[1]!.gain.setValueAtTime).toHaveBeenCalledWith(
|
||||
1,
|
||||
@@ -160,10 +168,9 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
10 ** (question.db / 20),
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(f.gains[3]!.gain.linearRampToValueAtTime).toHaveBeenCalledWith(
|
||||
0.12,
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(
|
||||
f.gains[3]!.gain.linearRampToValueAtTime.mock.calls[0]![0],
|
||||
).toBeCloseTo(0.12, 10);
|
||||
expect(f.sources[2]!.start.mock.calls[0]![0]!).toBeGreaterThan(
|
||||
f.sources[1]!.start.mock.calls[0]![0]!,
|
||||
);
|
||||
@@ -221,7 +228,7 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
);
|
||||
f.context.decodeAudioData.mockRejectedValueOnce(new Error("decode"));
|
||||
await expect(f.engine.play(question, false, vi.fn())).rejects.toThrow(
|
||||
"decode",
|
||||
"해석",
|
||||
);
|
||||
f.context.resume.mockRejectedValueOnce(new Error("권한"));
|
||||
await expect(f.engine.play(question, false, vi.fn())).rejects.toThrow(
|
||||
@@ -246,3 +253,69 @@ describe("오디오 재생 수명과 믹서", () => {
|
||||
await done;
|
||||
});
|
||||
});
|
||||
|
||||
// 배경 파일은 길이가 짧아도 루프 소스 하나로 유지하며 목표 ended에서 함께 정리한다.
|
||||
it("선택한 녹음을 디코드·루프하고 목표 종료에서 모든 소스와 fetch를 정리한다", async () => {
|
||||
const f = fixture();
|
||||
const done = f.engine.play(question, false, vi.fn());
|
||||
await flush();
|
||||
expect(f.fetcher).toHaveBeenCalledWith(
|
||||
question.environment.audioUrl,
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
expect(f.context.decodeAudioData).toHaveBeenCalledTimes(2);
|
||||
expect(f.sources[0]!.loop).toBe(true);
|
||||
expect(f.sources[1]!.loop).toBe(false);
|
||||
f.sources[1]!.onended?.();
|
||||
await done;
|
||||
expect(f.sources.every((s) => s.stop.mock.calls.length === 1)).toBe(true);
|
||||
for (const [, options] of f.fetcher.mock.calls)
|
||||
expect(options!.signal!.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it("배경 404·디코드 실패는 한국어 복구 메시지이고 재시도로 성공한다", async () => {
|
||||
const f = fixture();
|
||||
f.fetcher.mockImplementationOnce(
|
||||
async () =>
|
||||
({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) }) as Response,
|
||||
);
|
||||
f.fetcher.mockResolvedValueOnce({ ok: false } as Response);
|
||||
await expect(f.engine.play(question, false, vi.fn())).rejects.toThrow(
|
||||
/배경.*다시/,
|
||||
);
|
||||
f.context.decodeAudioData.mockRejectedValueOnce(new Error("EncodingError"));
|
||||
await expect(f.engine.play(question, false, vi.fn())).rejects.toThrow(
|
||||
/해석.*다시/,
|
||||
);
|
||||
const done = f.engine.play(question, false, vi.fn());
|
||||
await flush();
|
||||
f.sources.at(-1)!.onended?.();
|
||||
await done;
|
||||
});
|
||||
|
||||
it("로드 시간초과는 fetch를 취소하고 늦은 디코드가 소스를 만들지 않는다", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const f = fixture();
|
||||
let release!: (buffer: ReturnType<typeof f.context.createBuffer>) => void;
|
||||
f.context.decodeAudioData.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
);
|
||||
const done = f.engine.play(question, false, vi.fn());
|
||||
const failed = expect(done).rejects.toThrow("준비 시간이 초과");
|
||||
await flush();
|
||||
await vi.advanceTimersByTimeAsync(15000);
|
||||
await failed;
|
||||
expect(f.fetcher.mock.calls[0]![1]!.signal!.aborted).toBe(true);
|
||||
release(f.context.createBuffer(1, 100, 24000));
|
||||
await flush();
|
||||
expect(f.sources).toHaveLength(0);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
f.engine.dispose();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user