Files
portifolio-evandrodevbr/app/jogo/Jogo.test.tsx
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

226 lines
7.0 KiB
TypeScript

import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PROBLEMAS } from "@/lib/jogo/problemas";
import type { Cena, OpcoesDaCena } from "./cena";
import type { Resultado, Retrato } from "./contrato";
import Jogo from "./Jogo";
// a cena e o som são escritos em paralelo: virtual, para os dublês valerem mesmo antes de os arquivos existirem
jest.mock("./cena", () => ({ criarCena: jest.fn() }), { virtual: true });
jest.mock(
"./som",
() => ({
criarSom: jest.fn(() => ({
liberar: jest.fn(() => Promise.resolve()),
tocar: jest.fn(),
definirMudo: jest.fn(),
descartar: jest.fn(),
})),
}),
{ virtual: true },
);
const { criarCena } = jest.requireMock("./cena") as {
criarCena: jest.Mock<Promise<Cena>, [OpcoesDaCena]>;
};
const { criarSom } = jest.requireMock("./som") as { criarSom: jest.Mock };
const problema = PROBLEMAS[0];
const retrato: Retrato = {
fase: "horda",
horda: 0,
problemaAtual: problema.id,
restantes: 12,
vida: 100,
arma: 0,
municao: 0,
placar: { jogador: 3, drones: 1 },
feed: [],
drones: [],
pausado: false,
};
const resultado: Resultado = {
vitoria: true,
problemaId: problema.id,
dificuldade: "facil",
resolvidos: [problema.id],
placar: { jogador: 40, drones: 9 },
drones: [],
segundos: 300,
};
let cenas: { pausar: jest.Mock; continuar: jest.Mock; descartar: jest.Mock }[];
let midias: Record<string, boolean>;
function opcoes(i = 0) {
return criarCena.mock.calls[i][0];
}
function som() {
return criarSom.mock.results[0].value;
}
async function comecar() {
const usuario = userEvent.setup();
await usuario.click(screen.getByRole("radio", { name: problema.nome }));
await usuario.click(screen.getByRole("button", { name: "Começar" }));
await waitFor(() => expect(criarCena).toHaveBeenCalled());
return usuario;
}
beforeEach(() => {
cenas = [];
midias = {};
criarCena.mockReset().mockImplementation(async () => {
const cena = {
pausar: jest.fn(),
continuar: jest.fn(),
descartar: jest.fn(),
};
cenas.push(cena);
return cena;
});
criarSom.mockClear();
window.matchMedia = jest.fn((query: string) => ({
matches: midias[query] ?? false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
})) as unknown as typeof window.matchMedia;
jest
.spyOn(HTMLCanvasElement.prototype, "getContext")
.mockReturnValue({ getExtension: () => null } as never);
localStorage.clear();
window.history.replaceState(null, "", "/jogo");
});
afterEach(() => jest.restoreAllMocks());
describe("Jogo", () => {
it("começa pela tela inicial, sem carregar a cena", () => {
render(<Jogo />);
expect(
screen.getByRole("heading", { level: 1, name: "Expediente dos Mortos" }),
).toBeInTheDocument();
expect(criarCena).not.toHaveBeenCalled();
});
it("ao começar libera o som e cria a cena com a escolha", async () => {
render(<Jogo />);
await comecar();
expect(som().liberar).toHaveBeenCalled();
expect(opcoes()).toMatchObject({
escolha: { problemaId: problema.id, dificuldade: "facil" },
piloto: false,
movimentoReduzido: false,
});
expect(opcoes().canvas).toBeInstanceOf(HTMLCanvasElement);
expect(document.documentElement.style.overflow).toBe("hidden");
});
it("mostra no HUD o retrato que a cena manda", async () => {
render(<Jogo />);
await comecar();
act(() => opcoes().aoRetrato(retrato));
expect(await screen.findByText("Você 3 · Drones 1")).toBeInTheDocument();
});
it("ao terminar mostra o relatório, descarta a cena e destrava a rolagem", async () => {
render(<Jogo />);
await comecar();
await waitFor(() => expect(cenas).toHaveLength(1));
act(() => opcoes().aoTerminar(resultado));
expect(
screen.getByRole("heading", { level: 1, name: "Problemas resolvidos" }),
).toBeInTheDocument();
expect(cenas[0].descartar).toHaveBeenCalled();
expect(document.documentElement.style.overflow).toBe("");
});
it("jogar de novo cria outra cena com a mesma escolha e outra semente", async () => {
render(<Jogo />);
const usuario = await comecar();
await waitFor(() => expect(cenas).toHaveLength(1));
act(() => opcoes().aoTerminar(resultado));
await usuario.click(screen.getByRole("button", { name: "Jogar de novo" }));
await waitFor(() => expect(criarCena).toHaveBeenCalledTimes(2));
expect(opcoes(1).escolha).toEqual(opcoes(0).escolha);
expect(opcoes(1).semente).not.toBe(opcoes(0).semente);
});
it("ao desmontar descarta a cena e o som", async () => {
const { unmount } = render(<Jogo />);
await comecar();
await waitFor(() => expect(cenas).toHaveLength(1));
unmount();
expect(cenas[0].descartar).toHaveBeenCalled();
expect(som().descartar).toHaveBeenCalled();
expect(document.documentElement.style.overflow).toBe("");
});
it("se a cena falhar, volta ao início com a mensagem", async () => {
criarCena.mockRejectedValueOnce(new Error("sem webgl2"));
render(<Jogo />);
await comecar();
expect(await screen.findByRole("alert")).toHaveTextContent(
"Não deu para abrir o jogo neste navegador.",
);
expect(
screen.getByRole("heading", { level: 1, name: "Expediente dos Mortos" }),
).toBeInTheDocument();
});
it("com piloto=1 e um problema válido pula o início", async () => {
window.history.replaceState(
null,
"",
`/jogo?piloto=1&semente=7&problema=${problema.id}&dificuldade=normal&vel=8`,
);
render(<Jogo />);
await waitFor(() => expect(criarCena).toHaveBeenCalled());
expect(opcoes()).toMatchObject({
escolha: { problemaId: problema.id, dificuldade: "normal" },
semente: 7,
piloto: true,
velocidade: 8,
});
expect(
screen.queryByRole("heading", { name: "Expediente dos Mortos" }),
).not.toBeInTheDocument();
});
it("no toque, em retrato, pede para virar o aparelho e pausa", async () => {
midias["(pointer: coarse)"] = true;
midias["(orientation: portrait)"] = true;
render(<Jogo />);
await comecar();
expect(
await screen.findByText("Vire o aparelho para jogar"),
).toBeInTheDocument();
expect(cenas[0].pausar).toHaveBeenCalled();
});
it("continuar depois da pausa libera o som de novo, no mesmo clique", async () => {
render(<Jogo />);
const usuario = await comecar();
await waitFor(() => expect(cenas).toHaveLength(1));
act(() => opcoes().aoRetrato({ ...retrato, pausado: true }));
som().liberar.mockClear();
await usuario.click(
await screen.findByRole("button", { name: "Continuar" }),
);
expect(som().liberar).toHaveBeenCalledTimes(1);
expect(cenas[0].continuar).toHaveBeenCalled();
});
it("a cena sabe que foi cancelada quando o jogador sai durante a carga", async () => {
const { unmount } = render(<Jogo />);
await comecar();
expect(opcoes().cancelada()).toBe(false);
unmount();
expect(opcoes().cancelada()).toBe(true);
});
});