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.
226 lines
7.0 KiB
TypeScript
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);
|
|
});
|
|
});
|