From eeadb6cf586d65117694084e13b763fae975a430 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Sun, 26 Apr 2026 07:42:45 -0400 Subject: [PATCH] fix(lobby): mark past journeys with solo/MP mode icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The past-journeys list showed solo and multiplayer saves with no visual distinction — a misclick risk in MP context (sq-playtest 2026-04-26 [BUG-LOW]). Adds a per-row mode badge: ◈ solo, ⚑ multiplayer, ◇ unknown (legacy entries pre 2026-04-24 that lack the mode field). The unknown glyph is intentional rather than defaulting to solo, so an unknown row visually differs from a known solo row instead of silently misleading. Helps Alex (slow reader, MP context) see at-a-glance which session a row belongs to before clicking. Aria-label + title give screen-reader users the same signal. modeBadge() lives in its own file to keep the React Fast Refresh boundary component-only (react-refresh/only-export-components). Tests: - JourneyHistory.test.tsx: unit test for the per-row badge + a direct test of the modeBadge mapping function - past-journeys-mode-icon-wiring.test.tsx: wiring test that mounts the full ConnectScreen and asserts all three badge variants survive the production render path --- .../past-journeys-mode-icon-wiring.test.tsx | 106 ++++++++++++++++++ src/screens/lobby/JourneyHistory.tsx | 15 +++ .../lobby/__tests__/JourneyHistory.test.tsx | 71 ++++++++++++ src/screens/lobby/modeBadge.ts | 22 ++++ 4 files changed, 214 insertions(+) create mode 100644 src/screens/__tests__/past-journeys-mode-icon-wiring.test.tsx create mode 100644 src/screens/lobby/modeBadge.ts diff --git a/src/screens/__tests__/past-journeys-mode-icon-wiring.test.tsx b/src/screens/__tests__/past-journeys-mode-icon-wiring.test.tsx new file mode 100644 index 0000000..d22cb73 --- /dev/null +++ b/src/screens/__tests__/past-journeys-mode-icon-wiring.test.tsx @@ -0,0 +1,106 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { MemoryRouter } from "react-router-dom"; +import { ConnectScreen } from "@/screens/ConnectScreen"; +import { appendHistory } from "@/screens/lobby/historyStore"; +import type { GenresResponse } from "@/types/genres"; + +/** + * Wiring test for the [BUG-LOW] Past Journeys mode-icon fix + * (sq-playtest-pingpong.md, 2026-04-26). + * + * Per CLAUDE.md "Every Test Suite Needs a Wiring Test" — the unit tests + * in `lobby/__tests__/JourneyHistory.test.tsx` prove the icon renders + * when JourneyHistory is mounted directly. That's not enough: we also + * need to prove the lobby (`ConnectScreen`) actually mounts JourneyHistory + * AND that the mode badge survives the full production render path + * (provider chain, prop wiring, conditional rendering). + * + * If someone deletes `` from ConnectScreen or stops + * passing the entry's mode field through, this test fails. + */ +const GENRES: GenresResponse = { + victoria: { + name: "Victorian London", + description: "Gaslight and intrigue.", + worlds: [ + { + slug: "albion", + name: "Albion", + description: "Foggy streets.", + era: null, + setting: null, + inspirations: [], + axis_snapshot: {}, + hero_image: null, + }, + ], + }, +}; + +describe("past-journeys mode icon — lobby wiring", () => { + beforeEach(() => { + localStorage.clear(); + // ConnectScreen polls /api/sessions on mount via useSessions. + globalThis.fetch = vi.fn().mockImplementation((url: string) => { + if (typeof url === "string" && url.startsWith("/api/sessions")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ sessions: [] }), + }); + } + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + }) as unknown as typeof fetch; + }); + + it("renders solo, multiplayer, and unknown-mode icons in the lobby past-journeys list", () => { + // Seed three entries — one of each mode flavor — exactly the way the + // production code seeds them via appendHistory after a successful start. + appendHistory({ + player_name: "SoloRider", + genre: "victoria", + world: "albion", + mode: "solo", + }); + appendHistory({ + player_name: "MPRider", + genre: "victoria", + world: "albion", + mode: "multiplayer", + }); + appendHistory({ + player_name: "LegacyRider", + genre: "victoria", + world: "albion", + // No `mode` — pre-2026-04-24 entry shape. + }); + + render( + + + , + ); + + // The lobby must surface all three rows with distinguishable mode badges. + // Locate each row by its player-name text, then assert its row contains + // a mode-tagged span. data-mode is the contract; the literal glyph is + // an implementation detail re-asserted below for the visual spec. + const soloRow = screen.getByText("SoloRider").closest("button")!; + const mpRow = screen.getByText("MPRider").closest("button")!; + const legacyRow = screen.getByText("LegacyRider").closest("button")!; + + const soloBadge = soloRow.querySelector('[data-mode="solo"]'); + const mpBadge = mpRow.querySelector('[data-mode="multiplayer"]'); + const legacyBadge = legacyRow.querySelector('[data-mode="unknown"]'); + + expect(soloBadge).not.toBeNull(); + expect(mpBadge).not.toBeNull(); + expect(legacyBadge).not.toBeNull(); + + // Visual spec — locks the chosen glyphs to the playtest report's spec + // (◈ solo, ⚑ MP) plus the legacy ◇ choice documented in modeBadge(). + expect(soloBadge!.textContent).toBe("◈"); + expect(mpBadge!.textContent).toBe("⚑"); + expect(legacyBadge!.textContent).toBe("◇"); + }); +}); diff --git a/src/screens/lobby/JourneyHistory.tsx b/src/screens/lobby/JourneyHistory.tsx index ed32e6e..c911bd1 100644 --- a/src/screens/lobby/JourneyHistory.tsx +++ b/src/screens/lobby/JourneyHistory.tsx @@ -5,6 +5,7 @@ import { formatRelativeTime, type JourneyEntry, } from "./historyStore"; +import { modeBadge } from "./modeBadge"; export interface JourneyHistoryProps { /** @@ -70,6 +71,12 @@ export function JourneyHistory({ {entries.map((entry) => { const genreName = prettyGenre(entry.genre); const worldName = prettyWorld(entry.genre, entry.world); + // Mode icon: helps Alex (slow reader, MP context) see at-a-glance + // whether a row is a solo or multiplayer save before clicking. Pre + // 2026-04-24 entries lack `mode` — render a hollow diamond rather + // than guessing, so an unknown row visually differs from a known + // solo/MP row instead of silently looking like one. + const { glyph: modeGlyph, label: modeLabel } = modeBadge(entry.mode); return (