Files
galaxy-game/ui/frontend/tests/e2e/game-shell.spec.ts
T
Ilia Denisov 209f8508cd
Tests · UI / test (push) Successful in 2m53s
Tests · UI / test (pull_request) Successful in 3m0s
feat(ui): F8-11 — battles table under table submenu (#54)
Adds a sortable battles list as a new entity under the existing
`view → table` submenu (entity slug `battles`), replacing the
standalone top-level `battle log` shortcut which always opened a
"battle not found" placeholder. The single-battle viewer stays put
and is reached only by clicking a row (or a battle marker on the
map), identical to the existing `section-battles.svelte` flow.

Columns are planet (via the shared `planetLabel` helper) and shots
(the per-battle action count carried by `BattleSummary`), sortable
both ways with shots-desc default. No backend / FBS / map changes:
the wire payload is unchanged. Participants / observers / total
mass require the full BattleReport and were intentionally dropped
to avoid N round trips per menu open.

The top-level `battle log` item is removed from `header/view-menu`
and `sidebar/bottom-tabs` (and their stale comment blocks updated);
the now-orphan `game.view.battle` i18n key is dropped from both
locales.
2026-05-27 22:12:51 +02:00

223 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Phase 10 end-to-end coverage for the in-game shell. Every spec
// boots an authenticated session through `/__debug/store` (the
// in-game shell makes a handful of gateway calls — for the lobby
// record, the report, and the order read-back; we don't mock them
// here, the shell tolerates ECONNREFUSED), enters the game through
// the dev-only `window.__galaxyNav` affordance (the single-URL
// app-shell has no `/games/<id>/<view>` route — the address bar
// stays at the app base), and exercises one slice of the chrome:
// header navigation, sidebar tab preservation, mobile bottom-tabs,
// and the breakpoint switches at 768 / 1024 px.
import { expect, test, type Page } from "@playwright/test";
// `window.__galaxyDebug` is owned by `src/routes/__debug/store/+page.svelte`
// (auth bootstrap) and `window.__galaxyNav` by `src/routes/+page.svelte`
// (dev-only screen/view driver); both are typed by
// `tests/e2e/storage-keypair-persistence.spec.ts`.
const SESSION_ID = "phase-10-shell-session";
// GAME_ID has to be a real UUID — Phase 14's auto-sync calls
// `uuidToHiLo` on it for the FBS request envelope, and an
// arbitrary string would throw on every layout boot.
const GAME_ID = "10101010-1010-1010-1010-101010101010";
async function bootShell(page: Page): Promise<void> {
await page.goto("/__debug/store");
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
await page.evaluate(() => window.__galaxyDebug!.clearSession());
await page.evaluate(
(id) => window.__galaxyDebug!.setDeviceSessionId(id),
SESSION_ID,
);
// Load the app (seeded session → authenticated → lobby), then enter
// the game via the in-memory nav affordance.
await page.goto("/");
await page.waitForFunction(() => window.__galaxyNav !== undefined);
await page.evaluate(
(id) => window.__galaxyNav!.enterGame(id, "map", {}),
GAME_ID,
);
await expect(page.getByTestId("game-shell")).toBeVisible();
await expect(page.getByTestId("active-view-map")).toBeVisible();
}
test("shell mounts with header / sidebar / active-view chrome", async ({
page,
}) => {
await bootShell(page);
await expect(page.getByTestId("game-shell-header")).toBeVisible();
await expect(page.getByTestId("game-shell-headline")).toContainText(
"turn",
);
await expect(page.getByTestId("view-menu-trigger")).toBeVisible();
await expect(page.getByTestId("game-mode-theme-toggle")).toBeVisible();
});
test("header view-menu navigates to every active view", async ({ page }) => {
await bootShell(page);
// The address bar stays at the app base in the single-URL app-shell,
// so the visible active view is the only navigation signal to assert.
const destinations: Array<[string, string]> = [
["view-menu-item-report", "active-view-report"],
["view-menu-item-mail", "active-view-mail"],
["view-menu-item-designer-science", "active-view-designer-science"],
["view-menu-item-map", "active-view-map"],
];
for (const [trigger, viewTestId] of destinations) {
await page.getByTestId("view-menu-trigger").click();
await page.getByTestId(trigger).click();
await expect(page.getByTestId(viewTestId)).toBeVisible();
}
});
test("header view-menu Tables sub-list navigates to every entity", async ({
page,
}) => {
await bootShell(page);
const entities = [
"planets",
"ship-classes",
"ship-groups",
"fleets",
"sciences",
"races",
"battles",
];
for (const entity of entities) {
await page.getByTestId("view-menu-trigger").click();
await page
.getByTestId("view-menu-tables")
.locator("summary")
.click();
await page.getByTestId(`view-menu-item-table-${entity}`).click();
const view = page.getByTestId("active-view-table");
await expect(view).toBeVisible();
await expect(view).toHaveAttribute("data-entity", entity);
}
});
test("sidebar tab choice survives navigation between active views", async ({
page,
browserName,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("chromium-mobile") ||
testInfo.project.name === "webkit-desktop"
? false
: false,
"sidebar test runs on every project",
);
await bootShell(page);
// Skip on viewports below 1024 — sidebar is hidden by CSS there.
const viewport = page.viewportSize();
if (viewport === null || viewport.width < 1024) {
test.skip();
return;
}
void browserName;
await page.getByTestId("sidebar-tab-calculator").click();
await expect(page.getByTestId("sidebar-tool-calculator")).toBeVisible();
await page.getByTestId("view-menu-trigger").click();
await page.getByTestId("view-menu-item-report").click();
await expect(page.getByTestId("active-view-report")).toBeVisible();
// Sidebar still rendered; the calculator tool remains selected.
await expect(page.getByTestId("sidebar-tool-calculator")).toBeVisible();
await expect(page.getByTestId("sidebar")).toHaveAttribute(
"data-active-tab",
"calculator",
);
await page.getByTestId("view-menu-trigger").click();
await page.getByTestId("view-menu-item-map").click();
await expect(page.getByTestId("active-view-map")).toBeVisible();
await expect(page.getByTestId("sidebar")).toHaveAttribute(
"data-active-tab",
"calculator",
);
});
test("mobile bottom-tabs show on small viewports and toggle the tool overlay", async ({
page,
}, testInfo) => {
if (!testInfo.project.name.startsWith("chromium-mobile")) {
test.skip();
return;
}
await bootShell(page);
await expect(page.getByTestId("bottom-tabs")).toBeVisible();
await expect(page.getByTestId("sidebar")).not.toBeVisible();
await page.getByTestId("bottom-tab-calc").click();
await expect(page.getByTestId("sidebar-tool-calculator")).toBeVisible();
await page.getByTestId("bottom-tab-order").click();
await expect(page.getByTestId("sidebar-tool-order")).toBeVisible();
await page.getByTestId("bottom-tab-map").click();
await expect(page.getByTestId("active-view-map")).toBeVisible();
});
test("mobile More drawer navigates to every destination", async ({
page,
}, testInfo) => {
if (!testInfo.project.name.startsWith("chromium-mobile")) {
test.skip();
return;
}
await bootShell(page);
await page.getByTestId("bottom-tab-more").click();
await expect(page.getByTestId("bottom-tabs-more-drawer")).toBeVisible();
await page.getByTestId("bottom-tabs-more-mail").click();
await expect(page.getByTestId("active-view-mail")).toBeVisible();
await page.getByTestId("bottom-tab-more").click();
await page.getByTestId("bottom-tabs-more-report").click();
await expect(page.getByTestId("active-view-report")).toBeVisible();
});
test("breakpoint switches between desktop / tablet / mobile", async ({
page,
}, testInfo) => {
// Use a single chromium-desktop run to drive all three viewports in
// the same browser. Other projects skip — the viewport diff is the
// goal here, not browser-specific behaviour.
if (testInfo.project.name !== "chromium-desktop") {
test.skip();
return;
}
await bootShell(page);
// Desktop ≥ 1024: sidebar visible, bottom-tabs hidden, sidebar
// toggle hidden.
await page.setViewportSize({ width: 1280, height: 800 });
await expect(page.getByTestId("sidebar")).toBeVisible();
await expect(page.getByTestId("bottom-tabs")).not.toBeVisible();
await expect(page.getByTestId("sidebar-toggle")).not.toBeVisible();
// Tablet 7681024: sidebar hidden by default, sidebar toggle
// visible, bottom-tabs hidden. Click the toggle and the sidebar
// becomes visible again.
await page.setViewportSize({ width: 900, height: 800 });
await expect(page.getByTestId("sidebar")).not.toBeVisible();
await expect(page.getByTestId("sidebar-toggle")).toBeVisible();
await expect(page.getByTestId("bottom-tabs")).not.toBeVisible();
await page.getByTestId("sidebar-toggle").click();
await expect(page.getByTestId("sidebar")).toBeVisible();
// Mobile < 768: sidebar hidden entirely, bottom-tabs visible,
// sidebar toggle hidden again.
await page.setViewportSize({ width: 390, height: 800 });
await expect(page.getByTestId("bottom-tabs")).toBeVisible();
await expect(page.getByTestId("sidebar")).not.toBeVisible();
await expect(page.getByTestId("sidebar-toggle")).not.toBeVisible();
});