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.
184 lines
5.3 KiB
TypeScript
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);
|
|
});
|