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.
This commit is contained in:
1 parent
c7d09d63d4
commit
67b32fcab3
118 files changed
+16730
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
type Browser,
|
||||
chromium,
|
||||
expect,
|
||||
type Page,
|
||||
test,
|
||||
} from "@playwright/test";
|
||||
import type { ResultadoDaBancada } from "../app/jogo/Bancada";
|
||||
import { DESKTOP } from "./helpers";
|
||||
|
||||
/** Abre a bancada, espera `window.__bancada` aparecer e o devolve. */
|
||||
async function medir(page: Page, busca: string, espera: number) {
|
||||
const resposta = await page.goto(`/jogo/bancada?${busca}`);
|
||||
expect(resposta?.status()).toBe(200);
|
||||
await page.waitForFunction(() => window.__bancada !== undefined, null, {
|
||||
timeout: espera,
|
||||
});
|
||||
const resultado = (await page.evaluate(
|
||||
() => window.__bancada,
|
||||
)) as ResultadoDaBancada;
|
||||
console.log(`bancada ${busca}: ${JSON.stringify(resultado)}`);
|
||||
return resultado;
|
||||
}
|
||||
|
||||
test("a bancada abre sem indexação, mede e publica o resultado sem erro no console", async ({
|
||||
page,
|
||||
}) => {
|
||||
const erros: string[] = [];
|
||||
page.on("console", (mensagem) => {
|
||||
if (mensagem.type() === "error") erros.push(mensagem.text());
|
||||
});
|
||||
page.on("pageerror", (erro) => erros.push(erro.message));
|
||||
|
||||
const resultado = await medir(page, "zumbis=4&seg=2", 60_000);
|
||||
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveAttribute(
|
||||
"content",
|
||||
"noindex, nofollow",
|
||||
);
|
||||
if ("erro" in resultado) expect(resultado.erro).toBe("sem-webgl");
|
||||
else {
|
||||
expect(resultado.zumbis).toBe(4);
|
||||
expect(resultado.quadros).toBeGreaterThan(0);
|
||||
expect(resultado.p95).toBeGreaterThan(0);
|
||||
expect(typeof resultado.trabalhoP95).toBe("number");
|
||||
}
|
||||
expect(erros).toEqual([]);
|
||||
});
|
||||
|
||||
test.describe("com GPU (BANCADA_GPU=1)", () => {
|
||||
test.skip(!process.env.BANCADA_GPU, "precisa de GPU: BANCADA_GPU=1");
|
||||
// O Playwright recusa `test.use({ launchOptions })` dentro de um describe (exigiria outro worker): o navegador com
|
||||
// os argumentos de GPU abre aqui, só para este bloco, com o mesmo `PW_CHROMIUM` da configuração.
|
||||
let navegador: Browser;
|
||||
test.beforeAll(async () => {
|
||||
navegador = await chromium.launch({
|
||||
executablePath: process.env.PW_CHROMIUM || undefined,
|
||||
args: [
|
||||
"--enable-gpu",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--use-angle=vulkan",
|
||||
"--enable-features=Vulkan",
|
||||
],
|
||||
});
|
||||
});
|
||||
test.afterAll(() => navegador?.close());
|
||||
|
||||
test("16 zumbis em 1440×900: p95 até 16,7 ms", async ({ baseURL }) => {
|
||||
const contexto = await navegador.newContext({ viewport: DESKTOP, baseURL });
|
||||
const resultado = await medir(
|
||||
await contexto.newPage(),
|
||||
"zumbis=16&seg=20",
|
||||
90_000,
|
||||
);
|
||||
if ("erro" in resultado) throw new Error("sem WebGL com GPU");
|
||||
expect(resultado.p95).toBeLessThanOrEqual(16.7);
|
||||
});
|
||||
|
||||
test("16 zumbis com a CPU 4× mais lenta: p95 até 33 ms", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
const contexto = await navegador.newContext({ viewport: DESKTOP, baseURL });
|
||||
const page = await contexto.newPage();
|
||||
const cdp = await contexto.newCDPSession(page);
|
||||
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 });
|
||||
const resultado = await medir(page, "zumbis=16&seg=20", 110_000);
|
||||
if ("erro" in resultado) throw new Error("sem WebGL com GPU");
|
||||
expect(resultado.p95).toBeLessThanOrEqual(33);
|
||||
});
|
||||
|
||||
test("memória (CA9): 3ª medição com as mesmas geometrias e texturas da 1ª", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
const contexto = await navegador.newContext({ viewport: DESKTOP, baseURL });
|
||||
const page = await contexto.newPage();
|
||||
const primeira = await medir(page, "zumbis=16&seg=3", 90_000);
|
||||
if ("erro" in primeira) throw new Error("sem WebGL com GPU");
|
||||
let ultima: ResultadoDaBancada = primeira;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
const botao = page.getByRole("button", { name: "Medir de novo" });
|
||||
await botao.click();
|
||||
await botao.waitFor({ state: "detached", timeout: 30_000 });
|
||||
await page.waitForFunction(() => window.__bancada !== undefined, null, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
ultima = (await page.evaluate(
|
||||
() => window.__bancada,
|
||||
)) as ResultadoDaBancada;
|
||||
console.log(`bancada medição ${i + 2}: ${JSON.stringify(ultima)}`);
|
||||
}
|
||||
if ("erro" in ultima) throw new Error("sem WebGL com GPU");
|
||||
expect(ultima.geometrias).toBe(primeira.geometrias);
|
||||
expect(ultima.texturas).toBe(primeira.texturas);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,579 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import {
|
||||
type Browser,
|
||||
type BrowserContext,
|
||||
type BrowserContextOptions,
|
||||
type CDPSession,
|
||||
chromium,
|
||||
expect,
|
||||
type Locator,
|
||||
type Page,
|
||||
test,
|
||||
} from "@playwright/test";
|
||||
import { PROBLEMAS, problemaPorId } from "../lib/jogo/problemas";
|
||||
import { DESKTOP, isDevServer, saveConsent } from "./helpers";
|
||||
|
||||
/**
|
||||
* Ponta a ponta do Expediente dos Mortos (/jogo): CA1, CA4, CA8, CA9, CA11 e CA13 da spec
|
||||
* specs/2026-11-10-jogo-zumbis-etapa-1.md.
|
||||
*
|
||||
* Os casos que desenham 3D abrem um Chromium próprio com os argumentos de GPU da bancada. Sem WebGL (CI sem GPU)
|
||||
* eles conferem só o aviso "Este navegador não desenha 3D." e anotam que rodaram sem 3D.
|
||||
* Falha num desses casos deixa a captura `falha-N.png` na pasta do teste em test-results/.
|
||||
*/
|
||||
|
||||
const GPU = [
|
||||
"--enable-gpu",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--use-angle=vulkan",
|
||||
"--enable-features=Vulkan",
|
||||
];
|
||||
// CA9: `gc()` na página e `performance.memory` sem arredondamento
|
||||
const MEMORIA = ["--js-flags=--expose-gc", "--enable-precise-memory-info"];
|
||||
const SEM_3D = "Este navegador não desenha 3D.";
|
||||
// mesma exceção de e2e/routes.spec.ts: aviso que só o React de desenvolvimento emite
|
||||
const AVISO_DO_DEV = /Each child in a list should have a unique "key" prop/;
|
||||
const FIM = /^(Problemas resolvidos|Os problemas venceram desta vez)$/;
|
||||
|
||||
let navegador: Browser;
|
||||
const contextos: BrowserContext[] = [];
|
||||
|
||||
test.beforeAll(async () => {
|
||||
navegador = await chromium.launch({
|
||||
executablePath: process.env.PW_CHROMIUM || undefined,
|
||||
args: [...GPU, ...MEMORIA],
|
||||
});
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await navegador?.close();
|
||||
});
|
||||
test.afterEach(async () => {
|
||||
const info = test.info();
|
||||
const falhou = info.status !== info.expectedStatus;
|
||||
const paginas = contextos.flatMap((c) => c.pages());
|
||||
for (const [i, pagina] of paginas.entries())
|
||||
if (falhou)
|
||||
await pagina
|
||||
.screenshot({ path: info.outputPath(`falha-${i}.png`) })
|
||||
.catch(() => {});
|
||||
for (const contexto of contextos.splice(0)) await contexto.close();
|
||||
});
|
||||
|
||||
/** Página no Chromium com GPU, com o consentimento de cookies já salvo para a barra não cobrir o jogo. */
|
||||
async function abrir(
|
||||
opcoes: BrowserContextOptions,
|
||||
baseURL: string | undefined,
|
||||
): Promise<Page> {
|
||||
const contexto = await navegador.newContext({ baseURL, ...opcoes });
|
||||
contextos.push(contexto);
|
||||
const page = await contexto.newPage();
|
||||
await saveConsent(page);
|
||||
return page;
|
||||
}
|
||||
|
||||
/** Sem WebGL: confere o aviso, anota e devolve true para o teste terminar ali. */
|
||||
async function sem3d(page: Page): Promise<boolean> {
|
||||
const tem = await page.evaluate(() => {
|
||||
const contexto = document.createElement("canvas").getContext("webgl2");
|
||||
contexto?.getExtension("WEBGL_lose_context")?.loseContext();
|
||||
return contexto !== null;
|
||||
});
|
||||
if (tem) return false;
|
||||
await expect(page.getByText(SEM_3D)).toBeVisible();
|
||||
test.info().annotations.push({
|
||||
type: "sem 3D",
|
||||
description: "WebGL indisponível: conferido só o aviso da página",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function vigiarConsole(page: Page, dev: boolean): string[] {
|
||||
const erros: string[] = [];
|
||||
page.on("console", (mensagem) => {
|
||||
if (mensagem.type() !== "error") return;
|
||||
if (dev && AVISO_DO_DEV.test(mensagem.text())) return;
|
||||
erros.push(mensagem.text());
|
||||
});
|
||||
page.on("pageerror", (erro) => erros.push(erro.message));
|
||||
return erros;
|
||||
}
|
||||
|
||||
const estado = (page: Page) => page.evaluate(() => window.__jogo);
|
||||
|
||||
async function comecarPartida(page: Page, nome: string) {
|
||||
await page
|
||||
.locator("label", {
|
||||
has: page.getByRole("radio", { name: nome, exact: true }),
|
||||
})
|
||||
.click();
|
||||
await page
|
||||
.getByRole("button", { name: /^(Começar|Jogar mesmo assim)$/ })
|
||||
.click();
|
||||
await page.waitForFunction(() => window.__jogo !== undefined, null, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function dentroDaTela(
|
||||
alvo: Locator,
|
||||
tela: { width: number; height: number },
|
||||
) {
|
||||
const caixa = await alvo.boundingBox();
|
||||
expect(caixa, "elemento sem caixa").not.toBeNull();
|
||||
if (!caixa) return;
|
||||
const fora = `${JSON.stringify(caixa)} fora de ${tela.width}x${tela.height}`;
|
||||
expect(caixa.x, fora).toBeGreaterThanOrEqual(0);
|
||||
expect(caixa.y, fora).toBeGreaterThanOrEqual(0);
|
||||
expect(caixa.x + caixa.width, fora).toBeLessThanOrEqual(tela.width + 0.5);
|
||||
expect(caixa.y + caixa.height, fora).toBeLessThanOrEqual(tela.height + 0.5);
|
||||
}
|
||||
|
||||
type Dedo = { id: number; x: number; y: number };
|
||||
const tocar = (
|
||||
cdp: CDPSession,
|
||||
type: "touchStart" | "touchMove" | "touchEnd",
|
||||
touchPoints: Dedo[],
|
||||
) => cdp.send("Input.dispatchTouchEvent", { type, touchPoints });
|
||||
|
||||
/** O que passou pela tela durante a partida: cartões "Resolvido:" e linhas do feed. Roda dentro da página. */
|
||||
type Vistos = { resolvidos: string[]; feed: string[] };
|
||||
declare global {
|
||||
interface Window {
|
||||
__vistos?: Vistos;
|
||||
}
|
||||
}
|
||||
function anotarTextos() {
|
||||
const vistos: Vistos = { resolvidos: [], feed: [] };
|
||||
window.__vistos = vistos;
|
||||
const olhar = () => {
|
||||
for (const p of document.querySelectorAll("p")) {
|
||||
const texto = p.textContent ?? "";
|
||||
const nome = texto.startsWith("Resolvido: ") ? texto.slice(11) : "";
|
||||
if (nome && !vistos.resolvidos.includes(nome))
|
||||
vistos.resolvidos.push(nome);
|
||||
}
|
||||
for (const li of document.querySelectorAll(
|
||||
'[aria-label="Últimas eliminações"] li',
|
||||
)) {
|
||||
const texto = li.textContent ?? "";
|
||||
if (!vistos.feed.includes(texto)) vistos.feed.push(texto);
|
||||
}
|
||||
};
|
||||
new MutationObserver(olhar).observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function axeSerio(page: Page) {
|
||||
const { violations } = await new AxeBuilder({ page }).analyze();
|
||||
return violations
|
||||
.filter(({ impact }) => impact === "serious" || impact === "critical")
|
||||
.map(
|
||||
({ id, impact, nodes }) =>
|
||||
`${id} (${impact}): ${nodes.length} elemento(s), por exemplo ${nodes[0]?.target.join(" ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
test("CA1: o piloto joga a partida inteira e o relatório liga cada problema ao serviço", async ({
|
||||
baseURL,
|
||||
request,
|
||||
}) => {
|
||||
test.setTimeout(300_000);
|
||||
const dev = await isDevServer(request);
|
||||
const page = await abrir({ viewport: { width: 960, height: 540 } }, baseURL);
|
||||
const erros = vigiarConsole(page, dev);
|
||||
await page.addInitScript(anotarTextos);
|
||||
await page.goto(
|
||||
"/jogo?piloto=1&semente=1&problema=site-lento&dificuldade=facil&vel=8",
|
||||
);
|
||||
if (await sem3d(page)) return;
|
||||
|
||||
const titulo = page.getByRole("heading", { level: 1, name: FIM });
|
||||
await expect(titulo).toBeVisible({ timeout: 240_000 });
|
||||
const vistos = await page.evaluate(() => window.__vistos);
|
||||
if (!vistos) throw new Error("o observador de textos não rodou");
|
||||
console.log(`CA1 vistos: ${JSON.stringify(vistos)}`);
|
||||
expect(vistos.resolvidos.length, "cartão Resolvido:").toBeGreaterThan(0);
|
||||
expect(
|
||||
vistos.feed.filter((linha) => linha.includes("eliminou")).length,
|
||||
"linha do feed com eliminou",
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
await expect(page.getByText(/^Você \d+ · Drones \d+$/)).toBeVisible();
|
||||
|
||||
// o nome acessível junta o texto e o trecho só para leitor de tela, com ou sem espaço antes dos dois-pontos
|
||||
const links = page.getByRole("link", { name: /^Ver como eu resolvo isso/ });
|
||||
const nomes: string[] = [];
|
||||
for (const link of await links.all()) {
|
||||
const nome = (await link.textContent())?.split(": ")[1] ?? "";
|
||||
const problema = PROBLEMAS.find((p) => p.nome === nome);
|
||||
expect(problema, `problema do link "${nome}"`).toBeDefined();
|
||||
await expect(link).toHaveAttribute("href", problema?.destino ?? "");
|
||||
nomes.push(nome);
|
||||
}
|
||||
console.log(`CA1 relatório: ${await titulo.textContent()} · ${nomes}`);
|
||||
expect(new Set(nomes).size, "um link por problema").toBe(nomes.length);
|
||||
for (const nome of vistos.resolvidos) expect(nomes).toContain(nome);
|
||||
const chefao = problemaPorId("site-lento");
|
||||
if ((await titulo.textContent()) === "Problemas resolvidos")
|
||||
expect(nomes).toContain(chefao?.nome);
|
||||
|
||||
await expect(
|
||||
// o rodapé do site tem outro "Chamar no WhatsApp", com a mensagem genérica
|
||||
page
|
||||
.getByRole("main")
|
||||
.getByRole("link", { name: "Chamar no WhatsApp", exact: true }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
new RegExp(encodeURIComponent(chefao?.nome ?? "?")),
|
||||
);
|
||||
expect(erros).toEqual([]);
|
||||
});
|
||||
|
||||
for (const tela of [
|
||||
{ width: 915, height: 412 },
|
||||
{ width: 640, height: 360 },
|
||||
]) {
|
||||
test(`CA4: toque em ${tela.width}x${tela.height}: controles na tela, andar, mirar e acertar`, async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
test.setTimeout(200_000);
|
||||
const page = await abrir(
|
||||
{ viewport: tela, hasTouch: true, isMobile: true },
|
||||
baseURL,
|
||||
);
|
||||
await page.goto("/jogo");
|
||||
if (await sem3d(page)) return;
|
||||
await comecarPartida(page, "Site lento");
|
||||
const atirar = page.getByRole("button", { name: "Atirar", exact: true });
|
||||
await expect(atirar).toBeVisible({ timeout: 30_000 });
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
const W = tela.width;
|
||||
const H = tela.height;
|
||||
|
||||
// direcional na metade esquerda; empurrado para a frente, anda
|
||||
const mover: Dedo = {
|
||||
id: 1,
|
||||
x: Math.round(W * 0.2),
|
||||
y: Math.round(H * 0.6),
|
||||
};
|
||||
const parado = await estado(page);
|
||||
await tocar(cdp, "touchStart", [mover]);
|
||||
await expect(page.getByTestId("direcional")).toBeVisible();
|
||||
await dentroDaTela(page.getByTestId("direcional"), tela);
|
||||
await tocar(cdp, "touchMove", [{ ...mover, y: mover.y - 50 }]);
|
||||
await page.waitForTimeout(1500);
|
||||
const andou = await estado(page);
|
||||
await tocar(cdp, "touchEnd", []);
|
||||
expect(
|
||||
Math.hypot(
|
||||
(andou?.x ?? 0) - (parado?.x ?? 0),
|
||||
(andou?.z ?? 0) - (parado?.z ?? 0),
|
||||
),
|
||||
`de ${JSON.stringify(parado)} para ${JSON.stringify(andou)}`,
|
||||
).toBeGreaterThan(0.01);
|
||||
|
||||
await dentroDaTela(atirar, tela);
|
||||
for (const arma of [/^Pistola$/, /^Automática/, /^Faca$/])
|
||||
await dentroDaTela(page.getByRole("button", { name: arma }), tela);
|
||||
|
||||
// arrastar na metade direita gira
|
||||
const mirar: Dedo = {
|
||||
id: 2,
|
||||
x: Math.round(W * 0.6),
|
||||
y: Math.round(H * 0.3),
|
||||
};
|
||||
const rumo = (await estado(page))?.rumo;
|
||||
await tocar(cdp, "touchStart", [mirar]);
|
||||
for (let dx = 8; dx <= 80; dx += 8)
|
||||
await tocar(cdp, "touchMove", [{ ...mirar, x: mirar.x + dx }]);
|
||||
await tocar(cdp, "touchEnd", []);
|
||||
await page.waitForTimeout(300);
|
||||
expect((await estado(page))?.rumo, "rumo").not.toBe(rumo);
|
||||
|
||||
// segura Atirar e varre a mira em voltas
|
||||
const caixa = await atirar.boundingBox();
|
||||
if (!caixa) throw new Error("Atirar sem caixa");
|
||||
const tiro: Dedo = {
|
||||
id: 3,
|
||||
x: Math.round(caixa.x + caixa.width / 2),
|
||||
y: Math.round(caixa.y + caixa.height / 2),
|
||||
};
|
||||
const limite = Date.now() + 90_000;
|
||||
let acertos = 0;
|
||||
while (acertos < 1 && Date.now() < limite) {
|
||||
const varrer: Dedo = { id: 4, x: Math.round(W * 0.55), y: mirar.y };
|
||||
await tocar(cdp, "touchStart", [tiro]);
|
||||
await tocar(cdp, "touchStart", [tiro, varrer]);
|
||||
for (let dx = 6; dx <= W * 0.35 && acertos < 1; dx += 6) {
|
||||
await tocar(cdp, "touchMove", [tiro, { ...varrer, x: varrer.x + dx }]);
|
||||
await page.waitForTimeout(40);
|
||||
acertos = (await estado(page))?.placar.jogador ?? 0;
|
||||
}
|
||||
await tocar(cdp, "touchEnd", []);
|
||||
}
|
||||
const fim = await estado(page);
|
||||
expect(
|
||||
fim?.placar.jogador,
|
||||
`estado: ${JSON.stringify(fim)}`,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
}
|
||||
|
||||
test("CA4: em retrato, pede para virar o aparelho", async ({ baseURL }) => {
|
||||
test.setTimeout(120_000);
|
||||
const page = await abrir(
|
||||
{ viewport: { width: 412, height: 915 }, hasTouch: true, isMobile: true },
|
||||
baseURL,
|
||||
);
|
||||
await page.goto("/jogo");
|
||||
if (await sem3d(page)) return;
|
||||
await comecarPartida(page, "Site lento");
|
||||
await expect(page.getByText("Vire o aparelho para jogar")).toBeVisible();
|
||||
});
|
||||
|
||||
test("CA8: a home, rolada até o fim, não pede nada do jogo", async ({
|
||||
browser,
|
||||
baseURL,
|
||||
request,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
/** Caminhos dos scripts que a página pede até a rede parar. */
|
||||
async function scripts(caminho: string) {
|
||||
const contexto = await browser.newContext({ baseURL });
|
||||
contextos.push(contexto);
|
||||
const page = await contexto.newPage();
|
||||
const pedidos = new Set<string>();
|
||||
page.on("request", (pedido) => {
|
||||
if (pedido.resourceType() === "script")
|
||||
pedidos.add(new URL(pedido.url()).pathname);
|
||||
});
|
||||
await page.goto(caminho);
|
||||
await page.waitForLoadState("networkidle");
|
||||
return pedidos;
|
||||
}
|
||||
// Blocos da cena: dos scripts que a partida automática pede a mais que a tela inicial parada, os que trazem o
|
||||
// código da cena (ela publica `window.__jogo`). O three em si fica de fora: o fundo 3D da home
|
||||
// (HeroField → BlocosScene) já o carrega sob demanda, e é o mesmo arquivo.
|
||||
const parada = await scripts("/jogo");
|
||||
const novos = [
|
||||
...(await scripts("/jogo?piloto=1&problema=site-lento")),
|
||||
].filter((caminho) => !parada.has(caminho));
|
||||
const blocos: string[] = [];
|
||||
for (const caminho of novos)
|
||||
if ((await (await request.get(caminho)).text()).includes("__jogo"))
|
||||
blocos.push(caminho);
|
||||
console.log(`CA8 pedidos da partida: ${novos.join(" ")}`);
|
||||
console.log(`CA8 blocos da cena: ${blocos.join(" ")}`);
|
||||
expect(blocos.length, "blocos da cena achados").toBeGreaterThan(0);
|
||||
|
||||
const contexto = await browser.newContext({ baseURL });
|
||||
contextos.push(contexto);
|
||||
const page = await contexto.newPage();
|
||||
const pedidos: string[] = [];
|
||||
page.on("request", (pedido) => {
|
||||
pedidos.push(pedido.url());
|
||||
});
|
||||
await page.goto("/");
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
const noFim = await page.evaluate(() => {
|
||||
window.scrollBy(0, window.innerHeight * 0.8);
|
||||
return (
|
||||
window.scrollY + window.innerHeight >=
|
||||
document.documentElement.scrollHeight - 2
|
||||
);
|
||||
});
|
||||
await page.waitForTimeout(150);
|
||||
if (noFim) break;
|
||||
}
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(1500);
|
||||
const doJogo = pedidos.filter(
|
||||
(url) => url.includes("/jogo/") || blocos.includes(new URL(url).pathname),
|
||||
);
|
||||
console.log(
|
||||
`CA8 da partida também pedidos pela home: ${novos.filter((caminho) => pedidos.some((url) => new URL(url).pathname === caminho))}`,
|
||||
);
|
||||
expect(doJogo).toEqual([]);
|
||||
});
|
||||
|
||||
test("CA9: três partidas seguidas sem acumular geometria, textura nem memória", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
test.setTimeout(900_000);
|
||||
const page = await abrir({ viewport: DESKTOP }, baseURL);
|
||||
// semente fixa na 1ª partida; "Jogar de novo" sorteia outra
|
||||
await page.goto("/jogo?piloto=1&semente=1&problema=site-lento&vel=16");
|
||||
if (await sem3d(page)) return;
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
const jogarDeNovo = page.getByRole("button", { name: "Jogar de novo" });
|
||||
const medidas: {
|
||||
geometrias?: number;
|
||||
texturas?: number;
|
||||
heap: number | null;
|
||||
sobras?: { geometrias: number; texturas: number };
|
||||
}[] = [];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (i > 0) await jogarDeNovo.click();
|
||||
const pronto = await page.waitForFunction(
|
||||
() => {
|
||||
const j = window.__jogo;
|
||||
return j?.horda === 1 && j.fase === "horda"
|
||||
? { geometrias: j.geometrias, texturas: j.texturas }
|
||||
: null;
|
||||
},
|
||||
null,
|
||||
{ timeout: 180_000, polling: 50 },
|
||||
);
|
||||
const contagem = await pronto.jsonValue();
|
||||
// coleta completa pelo protocolo do Chromium antes de medir
|
||||
await cdp.send("HeapProfiler.collectGarbage");
|
||||
const heap = await page.evaluate(() => {
|
||||
// `performance.memory` só existe no Chromium (preciso com as flags de MEMORIA); os tipos do DOM não o trazem
|
||||
const desempenho = performance as Performance & {
|
||||
memory?: { usedJSHeapSize: number };
|
||||
};
|
||||
return desempenho.memory?.usedJSHeapSize ?? null;
|
||||
});
|
||||
// no fim da partida a cena é descartada e publica o que o renderer ainda contava
|
||||
await expect(jogarDeNovo).toBeVisible({ timeout: 300_000 });
|
||||
const sobras = await page.evaluate(() => window.__jogo?.sobras);
|
||||
medidas.push({ ...contagem, heap, sobras });
|
||||
console.log(`CA9 partida ${i + 1}: ${JSON.stringify(medidas[i])}`);
|
||||
}
|
||||
const [primeira, , terceira] = medidas;
|
||||
expect(terceira.geometrias, "geometrias").toBe(primeira.geometrias);
|
||||
expect(terceira.texturas, "texturas").toBe(primeira.texturas);
|
||||
for (const [i, { sobras }] of medidas.entries())
|
||||
expect(sobras, `sobras da partida ${i + 1}`).toEqual({
|
||||
geometrias: 0,
|
||||
texturas: 0,
|
||||
});
|
||||
if (primeira.heap !== null && terceira.heap !== null)
|
||||
expect(terceira.heap, "heap JS").toBeLessThanOrEqual(primeira.heap * 1.1);
|
||||
});
|
||||
|
||||
test.describe("CA11: acesso", () => {
|
||||
test("sem JavaScript, mostra os 27 problemas com link", async ({
|
||||
browser,
|
||||
baseURL,
|
||||
}) => {
|
||||
const contexto = await browser.newContext({
|
||||
baseURL,
|
||||
javaScriptEnabled: false,
|
||||
});
|
||||
contextos.push(contexto);
|
||||
const page = await contexto.newPage();
|
||||
await page.goto("/jogo");
|
||||
const lista = page.locator('section[aria-labelledby="lista-de-problemas"]');
|
||||
await expect(lista.getByRole("link")).toHaveCount(27);
|
||||
for (const problema of PROBLEMAS) {
|
||||
const item = lista
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: problema.nome });
|
||||
await expect(item.getByRole("link")).toHaveAttribute(
|
||||
"href",
|
||||
problema.destino,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("com movimento reduzido, oferece Jogar mesmo assim, e o aviso de sangue vem antes", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
const page = await abrir(
|
||||
{ viewport: DESKTOP, reducedMotion: "reduce" },
|
||||
baseURL,
|
||||
);
|
||||
await page.goto("/jogo");
|
||||
await expect(
|
||||
page.getByText("Tem sangue e sustos. É um jogo de tiro."),
|
||||
).toBeVisible();
|
||||
if (await sem3d(page)) return;
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Jogar mesmo assim" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("no computador, o mudo alterna e perder a trava do mouse pausa", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const page = await abrir({ viewport: DESKTOP }, baseURL);
|
||||
await page.goto("/jogo");
|
||||
if (await sem3d(page)) return;
|
||||
await comecarPartida(page, "Site lento");
|
||||
|
||||
const desligar = page.getByRole("button", { name: "Desligar o som" });
|
||||
const ligar = page.getByRole("button", { name: "Ligar o som" });
|
||||
await desligar.click();
|
||||
await expect(ligar).toBeVisible();
|
||||
await ligar.click();
|
||||
await expect(desligar).toBeVisible();
|
||||
|
||||
const pausado = page.getByRole("heading", { name: "Pausado" });
|
||||
await page.mouse.click(DESKTOP.width / 2, DESKTOP.height / 2);
|
||||
const travou = await page
|
||||
.waitForFunction(() => document.pointerLockElement !== null, null, {
|
||||
timeout: 5_000,
|
||||
})
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
test.info().annotations.push({
|
||||
type: "trava do mouse",
|
||||
description: travou ? "obtida ao clicar" : "não obtida no clique",
|
||||
});
|
||||
console.log(`CA11 trava do mouse obtida: ${travou}`);
|
||||
await page.keyboard.press("Escape");
|
||||
const pausouComEsc = await pausado.waitFor({ timeout: 3_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
console.log(`CA11 Esc pausou: ${pausouComEsc}`);
|
||||
if (!pausouComEsc) {
|
||||
test.info().annotations.push({
|
||||
type: "pausa",
|
||||
description: "Esc não pausou; conferido saindo da trava do mouse",
|
||||
});
|
||||
await page.evaluate(() => document.exitPointerLock());
|
||||
}
|
||||
await expect(pausado).toBeVisible();
|
||||
});
|
||||
|
||||
test("axe sem violação serious ou critical na tela inicial", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
const page = await abrir({ viewport: DESKTOP }, baseURL);
|
||||
await page.goto("/jogo", { waitUntil: "load" });
|
||||
await page.waitForTimeout(800);
|
||||
expect(await axeSerio(page)).toEqual([]);
|
||||
});
|
||||
|
||||
test("axe sem violação serious ou critical no relatório", async ({
|
||||
baseURL,
|
||||
}) => {
|
||||
test.setTimeout(300_000);
|
||||
const page = await abrir({ viewport: DESKTOP }, baseURL);
|
||||
await page.goto("/jogo?piloto=1&semente=1&problema=site-lento&vel=16");
|
||||
if (await sem3d(page)) return;
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: FIM }),
|
||||
).toBeVisible({ timeout: 240_000 });
|
||||
await page.waitForTimeout(800);
|
||||
expect(await axeSerio(page)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test("CA13: /jogo e /jogo/bancada trazem noindex", async ({ page }) => {
|
||||
for (const caminho of ["/jogo", "/jogo/bancada"]) {
|
||||
await page.goto(caminho);
|
||||
await expect(page.locator('meta[name="robots"]'), caminho).toHaveAttribute(
|
||||
"content",
|
||||
/noindex/,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user