diff --git a/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.browser.test.tsx b/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.browser.test.tsx index 79489da08..c6a649908 100644 --- a/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.browser.test.tsx +++ b/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.browser.test.tsx @@ -1,39 +1,12 @@ +import type { LoaderFunctionArgs } from "react-router"; import { createMemoryRouter, RouterProvider } from "react-router"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { userEvent } from "vitest/browser"; import { render } from "vitest-browser-react"; +import { TournamentProvider } from "~/features/tournament/tournament-context"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; import TournamentAdminRegistrationPage from "./to.$id.admin.registration.$tid"; -const { mockTournament, mockLoaderData, submitMock, loadMock } = vi.hoisted( - () => ({ - mockTournament: { - ctx: { id: 1, settings: { requireInGameNames: false } }, - canEditTournamentNames: (): boolean => false, - }, - mockLoaderData: { team: null as unknown }, - submitMock: vi.fn(), - loadMock: vi.fn(), - }), -); - -vi.mock("react-router", async () => { - const actual = await vi.importActual("react-router"); - return { - ...actual, - useLoaderData: () => mockLoaderData, - useFetcher: () => ({ - data: undefined, - state: "idle", - submit: submitMock, - load: loadMock, - }), - }; -}); - -vi.mock("~/features/tournament/tournament-context", () => ({ - useTournament: () => mockTournament, -})); - // stubbed so importing the route in a browser test doesn't pull in the database-backed action vi.mock( "~/features/tournament-admin/actions/to.$id.admin.registration.server", @@ -45,10 +18,77 @@ vi.mock( () => ({ loader: vi.fn() }), ); +const ROUTE_ID = "registration"; + +const tournament = { + ctx: { id: 1, settings: { requireInGameNames: false } }, + canEditTournamentNames: (): boolean => false, +} as unknown as Tournament; + +const SEARCHABLE_USERS = [ + { + type: "user" as const, + id: 1, + name: "sanu", + inGameName: null, + avatarUrl: null, + discordId: "1", + discordAvatar: null, + customUrl: null, + plusTier: null, + }, + { + type: "user" as const, + id: 2, + name: "Jolt", + inGameName: null, + avatarUrl: null, + discordId: "2", + discordAvatar: null, + customUrl: null, + plusTier: null, + }, +]; + +let loaderData: { team: unknown }; +let actionCallCount = 0; + function renderPage() { const router = createMemoryRouter( - [{ path: "/", element: }], - { initialEntries: ["/"] }, + [ + { + id: ROUTE_ID, + path: "/", + element: ( + + + + ), + loader: () => loaderData, + action: () => { + actionCallCount += 1; + return null; + }, + }, + { + path: "/search", + loader: ({ request }: LoaderFunctionArgs) => { + // biome-ignore lint/plugin: stub loader standing in for the real search route, reading the request the component built + const query = new URL(request.url).searchParams.get("q") ?? ""; + return { + query, + type: "users", + results: SEARCHABLE_USERS.filter( + (user) => String(user.id) === query || user.name === query, + ), + }; + }, + }, + ], + { + initialEntries: ["/"], + hydrationData: { loaderData: { [ROUTE_ID]: loaderData } }, + }, ); return render(); @@ -56,24 +96,25 @@ function renderPage() { const CAPTAIN_NOT_A_MEMBER_ERROR = "The captain must be one of the players"; -describe("tournament admin registration - captain field", () => { - beforeEach(() => { - submitMock.mockClear(); - loadMock.mockClear(); - }); +beforeEach(() => { + actionCallCount = 0; +}); +describe("tournament admin registration - captain field", () => { test("removing the captain's roster row does not leave a stale captain that fails validation", async () => { // captain (OWNER) is the first roster member - mockLoaderData.team = { - id: 10, - name: "low ink buddies", - team: undefined, - pickupAvatarUrl: null, - avatarImgId: null, - members: [ - { userId: 1, username: "sanu", inGameName: null, role: "OWNER" }, - { userId: 2, username: "Jolt", inGameName: null, role: "MEMBER" }, - ], + loaderData = { + team: { + id: 10, + name: "low ink buddies", + team: undefined, + pickupAvatarUrl: null, + avatarImgId: null, + members: [ + { userId: 1, username: "sanu", inGameName: null, role: "OWNER" }, + { userId: 2, username: "Jolt", inGameName: null, role: "MEMBER" }, + ], + }, }; const screen = await renderPage(); @@ -90,7 +131,7 @@ describe("tournament admin registration - captain field", () => { await screen.getByRole("button", { name: "Submit" }).click(); // the shown captain IS a current player, so "the captain must be one of the players" must not block the submit - await expect.poll(() => submitMock.mock.calls.length).toBe(1); + await expect.poll(() => actionCallCount).toBe(1); await expect .element(screen.getByText(CAPTAIN_NOT_A_MEMBER_ERROR)) .not.toBeInTheDocument(); @@ -99,36 +140,41 @@ describe("tournament admin registration - captain field", () => { describe("tournament admin registration - tournament name field", () => { beforeEach(() => { - mockLoaderData.team = { - id: 10, - name: "low ink buddies", - team: undefined, - pickupAvatarUrl: null, - avatarImgId: null, - members: [ - { - userId: 1, - username: "sanu", - inGameName: null, - tournamentName: "Sanu", - role: "OWNER", - }, - ], + loaderData = { + team: { + id: 10, + name: "low ink buddies", + team: undefined, + pickupAvatarUrl: null, + avatarImgId: null, + members: [ + { + userId: 1, + username: "sanu", + inGameName: null, + tournamentName: "Sanu", + role: "OWNER", + }, + ], + }, }; }); test("is not shown to organizers who can't edit tournament names", async () => { - mockTournament.canEditTournamentNames = () => false; + tournament.canEditTournamentNames = () => false; const screen = await renderPage(); + await expect + .element(screen.getByRole("button", { name: "Submit" })) + .toBeInTheDocument(); await expect .element(screen.getByLabelText("Tournament name")) .not.toBeInTheDocument(); }); test("shows the player's current tournament name", async () => { - mockTournament.canEditTournamentNames = () => true; + tournament.canEditTournamentNames = () => true; const screen = await renderPage(); diff --git a/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.captain-label.browser.test.tsx b/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.captain-label.browser.test.tsx index 535b8e963..a53386020 100644 --- a/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.captain-label.browser.test.tsx +++ b/app/features/tournament-admin/routes/to.$id.admin.registration.$tid.captain-label.browser.test.tsx @@ -3,28 +3,11 @@ import { createMemoryRouter, RouterProvider } from "react-router"; import { describe, expect, test, vi } from "vitest"; import { userEvent } from "vitest/browser"; import { render } from "vitest-browser-react"; +import { TournamentProvider } from "~/features/tournament/tournament-context"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; import TournamentAdminRegistrationPage from "./to.$id.admin.registration.$tid"; -const { mockTournament } = vi.hoisted(() => ({ - mockTournament: { - ctx: { id: 1, settings: { requireInGameNames: false } }, - canEditTournamentNames: () => false, - }, -})); - -vi.mock("react-router", async () => { - const actual = await vi.importActual("react-router"); - return { - ...actual, - // no team -> "add new team" flow, where the roster is built via user search - useLoaderData: () => ({ team: null }), - }; -}); - -vi.mock("~/features/tournament/tournament-context", () => ({ - useTournament: () => mockTournament, -})); - +// stubbed so importing the route in a browser test doesn't pull in the database-backed action vi.mock( "~/features/tournament-admin/actions/to.$id.admin.registration.server", () => ({ action: vi.fn() }), @@ -35,6 +18,16 @@ vi.mock( () => ({ loader: vi.fn() }), ); +const ROUTE_ID = "registration"; + +const tournament = { + ctx: { id: 1, settings: { requireInGameNames: false } }, + canEditTournamentNames: (): boolean => false, +} as unknown as Tournament; + +// no team -> "add new team" flow, where the roster is built via user search +const LOADER_DATA = { team: null }; + const GREY = { type: "user" as const, id: 5, @@ -51,8 +44,14 @@ function renderPage() { const router = createMemoryRouter( [ { + id: ROUTE_ID, path: "/", - element: , + element: ( + + + + ), + loader: () => LOADER_DATA, action: () => null, }, { @@ -68,7 +67,10 @@ function renderPage() { }, }, ], - { initialEntries: ["/"] }, + { + initialEntries: ["/"], + hydrationData: { loaderData: { [ROUTE_ID]: LOADER_DATA } }, + }, ); return render();