>({});
return (
@@ -623,17 +623,25 @@ export function ScoringPatternSection({
)}
{/* Info button */}
-
+
{/* Preset Info Flyout */}
@@ -923,7 +931,7 @@ export function ChampionshipsSection({
const [showChampFlyout, setShowChampFlyout] = useState(false);
const [activeChampFlyout, setActiveChampFlyout] = useState(null);
const champInfoRef = useRef(null);
- const champItemRefs = useRef>({});
+ const champItemRefs = useRef>({});
const updateChampionship = (key: keyof LeagueConfigFormModel['championships'], value: boolean) => {
if (!onChange) return;
@@ -1073,17 +1081,25 @@ export function ChampionshipsSection({
{/* Info button */}
-
+
{/* Championship Item Info Flyout */}
diff --git a/apps/website/lib/leagueWizardService.ts b/apps/website/lib/leagueWizardService.ts
new file mode 100644
index 000000000..4ad826f08
--- /dev/null
+++ b/apps/website/lib/leagueWizardService.ts
@@ -0,0 +1,272 @@
+import type {
+ LeagueConfigFormModel,
+} from '@gridpilot/racing/application';
+import type {
+ CreateLeagueWithSeasonAndScoringCommand,
+ CreateLeagueWithSeasonAndScoringResultDTO,
+} from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
+import {
+ getDriverRepository,
+ getCreateLeagueWithSeasonAndScoringUseCase,
+} from '@/lib/di-container';
+
+export type WizardStep = 1 | 2 | 3 | 4 | 5;
+
+export interface WizardErrors {
+ basics?: {
+ name?: string;
+ visibility?: string;
+ };
+ structure?: {
+ maxDrivers?: string;
+ maxTeams?: string;
+ driversPerTeam?: string;
+ };
+ timings?: {
+ qualifyingMinutes?: string;
+ mainRaceMinutes?: string;
+ roundsPlanned?: string;
+ };
+ scoring?: {
+ patternId?: string;
+ };
+ submit?: string;
+}
+
+/**
+ * Step-scoped validation extracted from the React wizard.
+ * Returns a fresh error bag for the given step based on the provided form model.
+ */
+export function validateLeagueWizardStep(
+ form: LeagueConfigFormModel,
+ step: WizardStep,
+): WizardErrors {
+ const errors: WizardErrors = {};
+
+ if (step === 1) {
+ const basicsErrors: NonNullable = {};
+ if (!form.basics.name.trim()) {
+ basicsErrors.name = 'Name is required';
+ }
+ if (!form.basics.visibility) {
+ basicsErrors.visibility = 'Visibility is required';
+ }
+ if (Object.keys(basicsErrors).length > 0) {
+ errors.basics = basicsErrors;
+ }
+ }
+
+ if (step === 2) {
+ const structureErrors: NonNullable = {};
+ if (form.structure.mode === 'solo') {
+ if (!form.structure.maxDrivers || form.structure.maxDrivers <= 0) {
+ structureErrors.maxDrivers =
+ 'Max drivers must be greater than 0 for solo leagues';
+ }
+ } else if (form.structure.mode === 'fixedTeams') {
+ if (!form.structure.maxTeams || form.structure.maxTeams <= 0) {
+ structureErrors.maxTeams =
+ 'Max teams must be greater than 0 for team leagues';
+ }
+ if (!form.structure.driversPerTeam || form.structure.driversPerTeam <= 0) {
+ structureErrors.driversPerTeam =
+ 'Drivers per team must be greater than 0';
+ }
+ }
+ if (Object.keys(structureErrors).length > 0) {
+ errors.structure = structureErrors;
+ }
+ }
+
+ if (step === 3) {
+ const timingsErrors: NonNullable = {};
+ if (!form.timings.qualifyingMinutes || form.timings.qualifyingMinutes <= 0) {
+ timingsErrors.qualifyingMinutes =
+ 'Qualifying duration must be greater than 0 minutes';
+ }
+ if (!form.timings.mainRaceMinutes || form.timings.mainRaceMinutes <= 0) {
+ timingsErrors.mainRaceMinutes =
+ 'Main race duration must be greater than 0 minutes';
+ }
+ if (Object.keys(timingsErrors).length > 0) {
+ errors.timings = timingsErrors;
+ }
+ }
+
+ if (step === 4) {
+ const scoringErrors: NonNullable = {};
+ if (!form.scoring.patternId && !form.scoring.customScoringEnabled) {
+ scoringErrors.patternId =
+ 'Select a scoring preset or enable custom scoring';
+ }
+ if (Object.keys(scoringErrors).length > 0) {
+ errors.scoring = scoringErrors;
+ }
+ }
+
+ return errors;
+}
+
+/**
+ * Helper to validate all steps (1-4) and merge errors into a single bag.
+ */
+export function validateAllLeagueWizardSteps(
+ form: LeagueConfigFormModel,
+): WizardErrors {
+ const aggregate: WizardErrors = {};
+
+ const merge = (next: WizardErrors) => {
+ if (next.basics) {
+ aggregate.basics = { ...aggregate.basics, ...next.basics };
+ }
+ if (next.structure) {
+ aggregate.structure = { ...aggregate.structure, ...next.structure };
+ }
+ if (next.timings) {
+ aggregate.timings = { ...aggregate.timings, ...next.timings };
+ }
+ if (next.scoring) {
+ aggregate.scoring = { ...aggregate.scoring, ...next.scoring };
+ }
+ if (next.submit) {
+ aggregate.submit = next.submit;
+ }
+ };
+
+ merge(validateLeagueWizardStep(form, 1));
+ merge(validateLeagueWizardStep(form, 2));
+ merge(validateLeagueWizardStep(form, 3));
+ merge(validateLeagueWizardStep(form, 4));
+
+ return aggregate;
+}
+
+export function hasWizardErrors(errors: WizardErrors): boolean {
+ return Object.keys(errors).some((key) => {
+ const value = (errors as any)[key];
+ if (!value) return false;
+ if (typeof value === 'string') return true;
+ return Object.keys(value).length > 0;
+ });
+}
+
+/**
+ * Pure mapping from LeagueConfigFormModel to the creation command.
+ * Driver ownership is handled by the caller.
+ */
+export function buildCreateLeagueCommandFromConfig(
+ form: LeagueConfigFormModel,
+ ownerId: string,
+): CreateLeagueWithSeasonAndScoringCommand {
+ const structure = form.structure;
+ let maxDrivers: number | undefined;
+ let maxTeams: number | undefined;
+
+ if (structure.mode === 'solo') {
+ maxDrivers =
+ typeof structure.maxDrivers === 'number' ? structure.maxDrivers : undefined;
+ maxTeams = undefined;
+ } else {
+ const teams =
+ typeof structure.maxTeams === 'number' ? structure.maxTeams : 0;
+ const perTeam =
+ typeof structure.driversPerTeam === 'number'
+ ? structure.driversPerTeam
+ : 0;
+ maxTeams = teams > 0 ? teams : undefined;
+ maxDrivers = teams > 0 && perTeam > 0 ? teams * perTeam : undefined;
+ }
+
+ return {
+ name: form.basics.name.trim(),
+ description: form.basics.description?.trim() || undefined,
+ visibility: form.basics.visibility,
+ ownerId,
+ gameId: form.basics.gameId,
+ maxDrivers,
+ maxTeams,
+ enableDriverChampionship: form.championships.enableDriverChampionship,
+ enableTeamChampionship: form.championships.enableTeamChampionship,
+ enableNationsChampionship: form.championships.enableNationsChampionship,
+ enableTrophyChampionship: form.championships.enableTrophyChampionship,
+ scoringPresetId: form.scoring.patternId || undefined,
+ };
+}
+
+/**
+ * Thin application-level facade that:
+ * - pulls the current driver via repository
+ * - builds the creation command
+ * - delegates to the create-league use case
+ */
+export async function createLeagueFromConfig(
+ form: LeagueConfigFormModel,
+): Promise {
+ const driverRepo = getDriverRepository();
+ const drivers = await driverRepo.findAll();
+ const currentDriver = drivers[0];
+
+ if (!currentDriver) {
+ const error = new Error(
+ 'No driver profile found. Please create a driver profile first.',
+ );
+ (error as any).code = 'NO_DRIVER';
+ throw error;
+ }
+
+ const useCase = getCreateLeagueWithSeasonAndScoringUseCase();
+ const command = buildCreateLeagueCommandFromConfig(form, currentDriver.id);
+ return useCase.execute(command);
+}
+
+/**
+ * Apply scoring preset selection and derive timings, returning a new form model.
+ * This mirrors the previous React handler but keeps it in testable, non-UI logic.
+ */
+export function applyScoringPresetToConfig(
+ form: LeagueConfigFormModel,
+ patternId: string,
+): LeagueConfigFormModel {
+ const lowerPresetId = patternId.toLowerCase();
+ const timings = form.timings ?? ({} as LeagueConfigFormModel['timings']);
+ let updatedTimings = { ...timings };
+
+ if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) {
+ updatedTimings = {
+ ...updatedTimings,
+ practiceMinutes: 15,
+ qualifyingMinutes: 20,
+ sprintRaceMinutes: 20,
+ mainRaceMinutes: 35,
+ sessionCount: 2,
+ };
+ } else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) {
+ updatedTimings = {
+ ...updatedTimings,
+ practiceMinutes: 30,
+ qualifyingMinutes: 30,
+ sprintRaceMinutes: undefined,
+ mainRaceMinutes: 90,
+ sessionCount: 1,
+ };
+ } else {
+ updatedTimings = {
+ ...updatedTimings,
+ practiceMinutes: 20,
+ qualifyingMinutes: 30,
+ sprintRaceMinutes: undefined,
+ mainRaceMinutes: 40,
+ sessionCount: 1,
+ };
+ }
+
+ return {
+ ...form,
+ scoring: {
+ ...form.scoring,
+ patternId,
+ customScoringEnabled: false,
+ },
+ timings: updatedTimings,
+ };
+}
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/1.svg b/apps/website/public/images/elements/strokes/1.svg
new file mode 100644
index 000000000..d730f3888
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/1.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/10.svg b/apps/website/public/images/elements/strokes/10.svg
new file mode 100644
index 000000000..a4baf71a0
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/10.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/2.svg b/apps/website/public/images/elements/strokes/2.svg
new file mode 100644
index 000000000..6cf25ac99
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/2.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/3.svg b/apps/website/public/images/elements/strokes/3.svg
new file mode 100644
index 000000000..3d3ff96e4
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/3.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/4.svg b/apps/website/public/images/elements/strokes/4.svg
new file mode 100644
index 000000000..8e6695e59
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/4.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/5.svg b/apps/website/public/images/elements/strokes/5.svg
new file mode 100644
index 000000000..ac87cf7e9
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/5.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/6.svg b/apps/website/public/images/elements/strokes/6.svg
new file mode 100644
index 000000000..256c4a0ca
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/6.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/7.svg b/apps/website/public/images/elements/strokes/7.svg
new file mode 100644
index 000000000..dab2be1a5
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/7.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/8.svg b/apps/website/public/images/elements/strokes/8.svg
new file mode 100644
index 000000000..afcd0dfa4
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/8.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/strokes/9.svg b/apps/website/public/images/elements/strokes/9.svg
new file mode 100644
index 000000000..33ea486a3
--- /dev/null
+++ b/apps/website/public/images/elements/strokes/9.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/1.svg b/apps/website/public/images/elements/tire-marks/1.svg
new file mode 100644
index 000000000..ee669aae6
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/10.svg b/apps/website/public/images/elements/tire-marks/10.svg
new file mode 100644
index 000000000..a9f35fd9d
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/10.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/2.svg b/apps/website/public/images/elements/tire-marks/2.svg
new file mode 100644
index 000000000..0c6d72f4e
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/3.svg b/apps/website/public/images/elements/tire-marks/3.svg
new file mode 100644
index 000000000..0b97c3867
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/4.svg b/apps/website/public/images/elements/tire-marks/4.svg
new file mode 100644
index 000000000..028c9e7a2
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/4.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/5.svg b/apps/website/public/images/elements/tire-marks/5.svg
new file mode 100644
index 000000000..e5b97f208
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/5.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/6.svg b/apps/website/public/images/elements/tire-marks/6.svg
new file mode 100644
index 000000000..19bca3301
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/6.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/7.svg b/apps/website/public/images/elements/tire-marks/7.svg
new file mode 100644
index 000000000..15747bce0
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/7.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/8.svg b/apps/website/public/images/elements/tire-marks/8.svg
new file mode 100644
index 000000000..f3a1828ae
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/8.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/public/images/elements/tire-marks/9.svg b/apps/website/public/images/elements/tire-marks/9.svg
new file mode 100644
index 000000000..20b55efcd
--- /dev/null
+++ b/apps/website/public/images/elements/tire-marks/9.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/logo.afdesign b/resources/logo/logo.afdesign
new file mode 100644
index 000000000..85182c367
Binary files /dev/null and b/resources/logo/logo.afdesign differ
diff --git a/resources/logo/png/Rectangle Wordmark Dark.png b/resources/logo/png/Rectangle Wordmark Dark.png
new file mode 100644
index 000000000..066567fd3
Binary files /dev/null and b/resources/logo/png/Rectangle Wordmark Dark.png differ
diff --git a/resources/logo/png/Rectangle Wordmark Dark@2x.png b/resources/logo/png/Rectangle Wordmark Dark@2x.png
new file mode 100644
index 000000000..66c80fa0b
Binary files /dev/null and b/resources/logo/png/Rectangle Wordmark Dark@2x.png differ
diff --git a/resources/logo/png/Rectangle Wordmark.png b/resources/logo/png/Rectangle Wordmark.png
new file mode 100644
index 000000000..788e23a46
Binary files /dev/null and b/resources/logo/png/Rectangle Wordmark.png differ
diff --git a/resources/logo/png/Rectangle Wordmark@2x.png b/resources/logo/png/Rectangle Wordmark@2x.png
new file mode 100644
index 000000000..38c4a6a86
Binary files /dev/null and b/resources/logo/png/Rectangle Wordmark@2x.png differ
diff --git a/resources/logo/png/Square Icon Dark.png b/resources/logo/png/Square Icon Dark.png
new file mode 100644
index 000000000..b92245992
Binary files /dev/null and b/resources/logo/png/Square Icon Dark.png differ
diff --git a/resources/logo/png/Square Icon Dark@2x.png b/resources/logo/png/Square Icon Dark@2x.png
new file mode 100644
index 000000000..d64829cbe
Binary files /dev/null and b/resources/logo/png/Square Icon Dark@2x.png differ
diff --git a/resources/logo/png/Square Icon.png b/resources/logo/png/Square Icon.png
new file mode 100644
index 000000000..cce4c71c0
Binary files /dev/null and b/resources/logo/png/Square Icon.png differ
diff --git a/resources/logo/png/Square Icon@2x.png b/resources/logo/png/Square Icon@2x.png
new file mode 100644
index 000000000..6add659ea
Binary files /dev/null and b/resources/logo/png/Square Icon@2x.png differ
diff --git a/resources/logo/png/Square Logo Dark.png b/resources/logo/png/Square Logo Dark.png
new file mode 100644
index 000000000..554a01ff2
Binary files /dev/null and b/resources/logo/png/Square Logo Dark.png differ
diff --git a/resources/logo/png/Square Logo Dark@2x.png b/resources/logo/png/Square Logo Dark@2x.png
new file mode 100644
index 000000000..793ad1ca5
Binary files /dev/null and b/resources/logo/png/Square Logo Dark@2x.png differ
diff --git a/resources/logo/png/Square Logo.png b/resources/logo/png/Square Logo.png
new file mode 100644
index 000000000..517cd2263
Binary files /dev/null and b/resources/logo/png/Square Logo.png differ
diff --git a/resources/logo/png/Square Logo@2x.png b/resources/logo/png/Square Logo@2x.png
new file mode 100644
index 000000000..e716d0bcd
Binary files /dev/null and b/resources/logo/png/Square Logo@2x.png differ
diff --git a/resources/logo/png/Square Wordmark Dark.png b/resources/logo/png/Square Wordmark Dark.png
new file mode 100644
index 000000000..7d4d65a68
Binary files /dev/null and b/resources/logo/png/Square Wordmark Dark.png differ
diff --git a/resources/logo/png/Square Wordmark Dark@2x.png b/resources/logo/png/Square Wordmark Dark@2x.png
new file mode 100644
index 000000000..2cc9ee8a5
Binary files /dev/null and b/resources/logo/png/Square Wordmark Dark@2x.png differ
diff --git a/resources/logo/png/Square Wordmark.png b/resources/logo/png/Square Wordmark.png
new file mode 100644
index 000000000..4365e1108
Binary files /dev/null and b/resources/logo/png/Square Wordmark.png differ
diff --git a/resources/logo/png/Square Wordmark@2x.png b/resources/logo/png/Square Wordmark@2x.png
new file mode 100644
index 000000000..00bf9a827
Binary files /dev/null and b/resources/logo/png/Square Wordmark@2x.png differ
diff --git a/resources/logo/svg/Rectangle Wordmark Dark.svg b/resources/logo/svg/Rectangle Wordmark Dark.svg
new file mode 100644
index 000000000..4f7c24551
--- /dev/null
+++ b/resources/logo/svg/Rectangle Wordmark Dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Rectangle Wordmark.svg b/resources/logo/svg/Rectangle Wordmark.svg
new file mode 100644
index 000000000..9cf4df611
--- /dev/null
+++ b/resources/logo/svg/Rectangle Wordmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Icon Dark.svg b/resources/logo/svg/Square Icon Dark.svg
new file mode 100644
index 000000000..3a9c68747
--- /dev/null
+++ b/resources/logo/svg/Square Icon Dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Icon.svg b/resources/logo/svg/Square Icon.svg
new file mode 100644
index 000000000..6b9e60396
--- /dev/null
+++ b/resources/logo/svg/Square Icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Logo Dark.svg b/resources/logo/svg/Square Logo Dark.svg
new file mode 100644
index 000000000..8ad4e99ab
--- /dev/null
+++ b/resources/logo/svg/Square Logo Dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Logo.svg b/resources/logo/svg/Square Logo.svg
new file mode 100644
index 000000000..3271f6544
--- /dev/null
+++ b/resources/logo/svg/Square Logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Wordmark Dark.svg b/resources/logo/svg/Square Wordmark Dark.svg
new file mode 100644
index 000000000..9d774aa65
--- /dev/null
+++ b/resources/logo/svg/Square Wordmark Dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/logo/svg/Square Wordmark.svg b/resources/logo/svg/Square Wordmark.svg
new file mode 100644
index 000000000..ee6b831a7
--- /dev/null
+++ b/resources/logo/svg/Square Wordmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx b/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx
new file mode 100644
index 000000000..e481d5c47
--- /dev/null
+++ b/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+
+// --- Mocks for Next.js navigation ---
+
+const useSearchParamsMock = vi.fn();
+const useRouterMock = vi.fn();
+
+const routerInstance = {
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+};
+
+vi.mock('next/navigation', () => {
+ return {
+ useSearchParams: () => useSearchParamsMock(),
+ useRouter: () => {
+ return useRouterMock() ?? routerInstance;
+ },
+ };
+});
+
+// Minimal next/link mock to keep existing patterns consistent
+vi.mock('next/link', () => {
+ const ActualLink = ({ href, children, ...rest }: any) => (
+
+ {children}
+
+ );
+ return { default: ActualLink };
+});
+
+import CreateLeaguePage from '../../../../apps/website/app/leagues/create/page';
+
+// Helper to build a searchParams-like object
+function createSearchParams(stepValue: string | null) {
+ return {
+ get: (key: string) => {
+ if (key === 'step') {
+ return stepValue;
+ }
+ return null;
+ },
+ } as any;
+}
+
+describe('CreateLeaguePage - URL-bound wizard steps', () => {
+ beforeEach(() => {
+ useSearchParamsMock.mockReset();
+ useRouterMock.mockReset();
+ routerInstance.push.mockReset();
+ routerInstance.replace.mockReset();
+ });
+
+ it('defaults to basics step when step param is missing', () => {
+ useSearchParamsMock.mockReturnValue(createSearchParams(null));
+
+ render();
+
+ // Basics step title from the wizard
+ expect(screen.getByText('Name your league')).toBeInTheDocument();
+ });
+
+ it('treats invalid step value as basics', () => {
+ useSearchParamsMock.mockReturnValue(createSearchParams('invalid-step'));
+
+ render();
+
+ expect(screen.getByText('Name your league')).toBeInTheDocument();
+ });
+
+ it('mounts directly on scoring step when step=scoring', () => {
+ useSearchParamsMock.mockReturnValue(createSearchParams('scoring'));
+
+ render();
+
+ // Step 4 title in the wizard
+ expect(screen.getByText('Scoring & championships')).toBeInTheDocument();
+ });
+
+ it('clicking Continue from basics navigates to step=structure via router', () => {
+ useSearchParamsMock.mockReturnValue(createSearchParams(null));
+ useRouterMock.mockReturnValue(routerInstance);
+
+ render();
+
+ const continueButton = screen.getByRole('button', { name: /continue/i });
+ fireEvent.click(continueButton);
+
+ expect(routerInstance.push).toHaveBeenCalledTimes(1);
+ const callArg = routerInstance.push.mock.calls[0][0] as string;
+ expect(callArg).toContain('/leagues/create');
+ expect(callArg).toContain('step=structure');
+ });
+
+ it('clicking Back from schedule navigates to step=structure via router', () => {
+ useSearchParamsMock.mockReturnValue(createSearchParams('schedule'));
+ useRouterMock.mockReturnValue(routerInstance);
+
+ render();
+
+ const backButton = screen.getByRole('button', { name: /back/i });
+ fireEvent.click(backButton);
+
+ expect(routerInstance.push).toHaveBeenCalledTimes(1);
+ const callArg = routerInstance.push.mock.calls[0][0] as string;
+ expect(callArg).toContain('/leagues/create');
+ expect(callArg).toContain('step=structure');
+ });
+
+ it('derives current step solely from URL so a "reload" keeps the same step', () => {
+ useSearchParamsMock.mockReturnValueOnce(createSearchParams('scoring'));
+ useSearchParamsMock.mockReturnValueOnce(createSearchParams('scoring'));
+
+ render();
+ expect(screen.getByText('Scoring & championships')).toBeInTheDocument();
+
+ // Simulate a logical reload by re-rendering with the same URL state
+ render();
+ expect(screen.getByText('Scoring & championships')).toBeInTheDocument();
+ });
+});
\ No newline at end of file