Fix unit tests

This commit is contained in:
Kalle
2026-09-17 18:09:56 +03:00
parent 4f81f2f3ce
commit dad4a8234f
2 changed files with 135 additions and 87 deletions

View File

@@ -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: <TournamentAdminRegistrationPage /> }],
{ initialEntries: ["/"] },
[
{
id: ROUTE_ID,
path: "/",
element: (
<TournamentProvider tournament={tournament}>
<TournamentAdminRegistrationPage />
</TournamentProvider>
),
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(<RouterProvider router={router} />);
@@ -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();

View File

@@ -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: <TournamentAdminRegistrationPage />,
element: (
<TournamentProvider tournament={tournament}>
<TournamentAdminRegistrationPage />
</TournamentProvider>
),
loader: () => LOADER_DATA,
action: () => null,
},
{
@@ -68,7 +67,10 @@ function renderPage() {
},
},
],
{ initialEntries: ["/"] },
{
initialEntries: ["/"],
hydrationData: { loaderData: { [ROUTE_ID]: LOADER_DATA } },
},
);
return render(<RouterProvider router={router} />);