1
0
Fork 0
polaris-task-force/tests/e2e/opord-recall.e2e.spec.ts
Z8MB1E 95663924ea feat(minigames): anti-cheat pass for OPORD Recall
- briefing is now self-paced (player clicks Begin recall; no memo countdown)
- recall quiz has a hard server-validated time band per tier (45/60/75s limits, minimum plausible human time, 2.5s grace); scripted instant or over-limit submissions are rejected
- briefing is rendered on canvas: no selectable text in the DOM, copy/cut/context-menu/ctrl-a-c-x-s-p blocked; screenshots remain possible but burn quiz time
- ?seed=<n> URL param pins a deterministic scenario (no server meaning; used by the e2e spec to know the grid)
- e2e spec updated for the new flow and asserts no grid text leaks into the DOM
2026-09-08 11:46:53 -04:00

66 lines
2.7 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { formatOpordGrid, generateOpordScenario } from "../../src/lib/opord-recall";
const FIXED_SEED = 424242;
const QUIZ_OPTION_BUTTON = "button.font-mono";
async function login(page: Page): Promise<void> {
await page.goto("/login");
await page.fill("#username", "dev");
await page.fill("#password", "Test123");
await page.locator('button[type="submit"]').click();
await page.waitForURL((url) => !url.pathname.startsWith("/login"), { timeout: 30_000 });
}
async function playOpordRound(page: Page): Promise<void> {
await page.goto(`/opord?seed=${FIXED_SEED}`);
await expect(page.getByRole("heading", { name: "OPORD Recall" })).toBeVisible();
await page.getByRole("button", { name: "Receive briefing" }).click();
await expect(page.getByText("Study at your own pace")).toBeVisible();
// The briefing is drawn on canvas, so derive the expected grid from the lib.
const scenario = generateOpordScenario(FIXED_SEED);
const grid = formatOpordGrid(scenario.grid);
await page.getByRole("button", { name: "Begin recall" }).click();
const questionCounter = page.getByText(/Question \d+ of \d+/);
await expect(questionCounter).toBeVisible();
await expect(questionCounter).toContainText("Question 1 of 4");
// Question 1 is always the objective grid: answer it correctly.
await page.locator(QUIZ_OPTION_BUTTON).filter({ hasText: grid }).first().click();
for (let index = 1; index < 4; index += 1) {
await expect(questionCounter).toContainText(`Question ${index + 1} of 4`, {
timeout: 10_000,
});
await page.locator(QUIZ_OPTION_BUTTON).first().click();
}
await expect(page.getByRole("heading", { name: "Debrief" })).toBeVisible({ timeout: 15_000 });
}
test.describe("OPORD Recall", () => {
test("plays a full round and records the score", async ({ page }) => {
await login(page);
await playOpordRound(page);
// The grid answer alone scores 100; the XP metre shows a positive bonus.
await expect(page.locator("body")).toContainText(/Recalled/);
await expect(page.getByText(/^\+\d+$/).first()).toBeVisible();
// The briefing must not expose selectable text to copy.
const bodyText = await page.locator("body").innerText();
expect(bodyText).not.toContain(formatOpordGrid(generateOpordScenario(FIXED_SEED).grid));
// The leaderboard refetches after the run and shows the recorded run for this user.
const leaderboard = page.getByRole("table");
await expect(leaderboard).toBeVisible({ timeout: 15_000 });
const myRow = leaderboard.locator("tbody tr").filter({ hasText: "(you)" }).first();
await expect(myRow).toBeVisible({ timeout: 15_000 });
await expect(myRow.locator("td").last()).toContainText(/\d{3}/);
});
});