Files
portifolio-evandrodevbr/app/jogo/som.test.ts
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
2026-11-10 19:37:10 -03:00

184 lines
5.3 KiB
TypeScript

import { criarSom } from "./som";
class GanhoFalso {
gain = { value: 1 };
connect = jest.fn();
disconnect = jest.fn();
}
class FonteFalsa {
buffer: unknown = null;
playbackRate = { value: 1 };
onended: (() => void) | null = null;
connect = jest.fn();
start = jest.fn();
stop = jest.fn();
}
class ContextoFalso {
static ultimo: ContextoFalso | null = null;
destination = {};
ganhos: GanhoFalso[] = [];
fontes: FonteFalsa[] = [];
close = jest.fn(() => Promise.resolve());
constructor() {
ContextoFalso.ultimo = this;
}
state = "running";
resume = jest.fn(() => Promise.resolve());
decodeAudioData() {
return Promise.resolve({});
}
createGain() {
const g = new GanhoFalso();
this.ganhos.push(g);
return g;
}
createBufferSource() {
const f = new FonteFalsa();
this.fontes.push(f);
return f;
}
}
const g = globalThis as unknown as { AudioContext?: unknown; fetch?: unknown };
function respostaOk() {
return Promise.resolve({
ok: true,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
});
}
beforeEach(() => {
ContextoFalso.ultimo = null;
g.AudioContext = ContextoFalso;
g.fetch = jest.fn(respostaOk);
});
afterEach(() => {
delete g.AudioContext;
delete g.fetch;
});
it("tocar antes de liberar não quebra nem cria contexto", () => {
const som = criarSom();
expect(() => som.tocar("pistola")).not.toThrow();
expect(ContextoFalso.ultimo).toBeNull();
expect(g.fetch).not.toHaveBeenCalled();
});
it("liberar cria o contexto e baixa os dez sons", async () => {
const som = criarSom();
await som.liberar();
expect(ContextoFalso.ultimo).not.toBeNull();
expect(g.fetch).toHaveBeenCalledTimes(10);
expect(g.fetch).toHaveBeenCalledWith("/jogo/sons/gemido-1.mp3");
som.tocar("acerto", { volume: 0.4 });
const ctx = ContextoFalso.ultimo as ContextoFalso;
expect(ctx.fontes).toHaveLength(1);
expect(ctx.fontes[0].start).toHaveBeenCalled();
expect(ctx.ganhos.at(-1)?.gain.value).toBe(0.4);
});
it("liberar de novo retoma o contexto parado, sem lançar se a retomada falhar", async () => {
const som = criarSom();
await som.liberar();
const ctx = ContextoFalso.ultimo as ContextoFalso;
ctx.resume.mockClear();
await som.liberar();
expect(ctx.resume).not.toHaveBeenCalled();
for (const parado of ["suspended", "interrupted"]) {
ctx.state = parado;
ctx.resume.mockImplementationOnce(() => Promise.reject(new Error("não")));
await expect(som.liberar()).resolves.toBeUndefined();
}
expect(ctx.resume).toHaveBeenCalledTimes(2);
expect(ContextoFalso.ultimo).toBe(ctx);
});
it("no máximo 8 vozes: a mais antiga é cortada", async () => {
const som = criarSom();
await som.liberar();
for (let i = 0; i < 10; i++) som.tocar("automatica");
const fontes = (ContextoFalso.ultimo as ContextoFalso).fontes;
expect(fontes).toHaveLength(10);
expect(fontes.filter((f) => f.stop.mock.calls.length > 0)).toEqual([
fontes[0],
fontes[1],
]);
});
it("voz que terminou libera a vaga", async () => {
const som = criarSom();
await som.liberar();
for (let i = 0; i < 8; i++) som.tocar("pistola");
const fontes = (ContextoFalso.ultimo as ContextoFalso).fontes;
fontes[3].onended?.();
som.tocar("pistola");
expect(fontes.some((f) => f.stop.mock.calls.length > 0)).toBe(false);
});
it("variar muda a velocidade em até 6%", async () => {
const som = criarSom();
await som.liberar();
for (let i = 0; i < 20; i++) som.tocar("pistola", { variar: true });
for (const f of (ContextoFalso.ultimo as ContextoFalso).fontes) {
expect(f.playbackRate.value).toBeGreaterThanOrEqual(0.94);
expect(f.playbackRate.value).toBeLessThanOrEqual(1.06);
}
});
it("mudo zera o ganho geral, antes ou depois de liberar", async () => {
const som = criarSom();
som.definirMudo(true);
await som.liberar();
const geral = (ContextoFalso.ultimo as ContextoFalso).ganhos[0];
expect(geral.gain.value).toBe(0);
som.definirMudo(false);
expect(geral.gain.value).toBe(1);
som.definirMudo(true);
expect(geral.gain.value).toBe(0);
});
it("falha de download não lança erro e o som que faltou não toca", async () => {
const erro = jest.spyOn(console, "error").mockImplementation(() => {});
g.fetch = jest.fn((url: string) => {
if (url.includes("pistola")) return Promise.reject(new Error("rede"));
if (url.includes("faca")) return Promise.resolve({ ok: false });
return respostaOk();
});
const som = criarSom();
await expect(som.liberar()).resolves.toBeUndefined();
som.tocar("pistola");
som.tocar("faca");
som.tocar("golpe");
expect((ContextoFalso.ultimo as ContextoFalso).fontes).toHaveLength(1);
expect(erro).not.toHaveBeenCalled();
erro.mockRestore();
});
it("sem AudioContext tudo vira no-op", async () => {
delete g.AudioContext;
const som = criarSom();
await expect(som.liberar()).resolves.toBeUndefined();
expect(() => {
som.tocar("drone");
som.definirMudo(true);
som.descartar();
}).not.toThrow();
expect(g.fetch).not.toHaveBeenCalled();
});
it("descartar fecha o contexto e cala o resto", async () => {
const som = criarSom();
await som.liberar();
som.tocar("caixa");
som.descartar();
const ctx = ContextoFalso.ultimo as ContextoFalso;
expect(ctx.close).toHaveBeenCalled();
expect(ctx.fontes[0].stop).toHaveBeenCalled();
som.tocar("caixa");
expect(ctx.fontes).toHaveLength(1);
});