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, [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; 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); await comecar(); expect(opcoes().cancelada()).toBe(false); unmount(); expect(opcoes().cancelada()).toBe(true); }); });