From 350c78504d75edfc776ff438e67840e7d8c68e2e Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 18 Jan 2026 01:03:54 +0100 Subject: [PATCH] website refactor --- apps/website/app/teams/create/page.tsx | 38 +++++++++++++++++ apps/website/lib/routing/RouteConfig.ts | 4 +- tests/e2e/website/navigation.e2e.test.ts | 44 -------------------- tests/e2e/website/route-coverage.e2e.test.ts | 37 ++++++++++++++-- tests/unit/website/RouteConfig.test.ts | 3 +- 5 files changed, 77 insertions(+), 49 deletions(-) create mode 100644 apps/website/app/teams/create/page.tsx delete mode 100644 tests/e2e/website/navigation.e2e.test.ts diff --git a/apps/website/app/teams/create/page.tsx b/apps/website/app/teams/create/page.tsx new file mode 100644 index 000000000..b1ab435e3 --- /dev/null +++ b/apps/website/app/teams/create/page.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React from 'react'; +import { useRouter } from 'next/navigation'; +import { CreateTeamForm } from '@/components/teams/CreateTeamForm'; +import { Section } from '@/ui/Section'; +import { Container } from '@/ui/Container'; +import { Heading } from '@/ui/Heading'; +import { Stack } from '@/ui/Stack'; +import { routes } from '@/lib/routing/RouteConfig'; + +export default function CreateTeamPage() { + const router = useRouter(); + + const handleNavigate = (teamId: string) => { + router.push(routes.team.detail(teamId)); + }; + + const handleCancel = () => { + router.back(); + }; + + return ( +
+ + + + Create a Team + + + + +
+ ); +} diff --git a/apps/website/lib/routing/RouteConfig.ts b/apps/website/lib/routing/RouteConfig.ts index 2dcd9c11d..359178f76 100644 --- a/apps/website/lib/routing/RouteConfig.ts +++ b/apps/website/lib/routing/RouteConfig.ts @@ -83,6 +83,7 @@ export interface RouteGroup { root: string; leaderboard: string; detail: (id: string) => string; + create: string; }; driver: { root: string; @@ -180,6 +181,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin root: '/teams', leaderboard: '/teams/leaderboard', detail: (id: string) => `/teams/${id}`, + create: '/teams/create', }, driver: { root: '/drivers', @@ -288,7 +290,7 @@ export const routeMatchers = { logger.info('[RouteConfig] Path is public (driver detail)', { path }); return true; } - if (group === 'teams') { + if (group === 'teams' && slug !== 'create') { logger.info('[RouteConfig] Path is public (team detail)', { path }); return true; } diff --git a/tests/e2e/website/navigation.e2e.test.ts b/tests/e2e/website/navigation.e2e.test.ts deleted file mode 100644 index 5d7545125..000000000 --- a/tests/e2e/website/navigation.e2e.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager'; -import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture'; - -const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; - -test.describe('Client-side Navigation', () => { - test('navigation from dashboard to leagues and back', async ({ browser, request }) => { - const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); - const capture = new ConsoleErrorCapture(auth.page); - - try { - // Start at dashboard - await auth.page.goto(`${WEBSITE_BASE_URL}/dashboard`); - expect(auth.page.url()).toContain('/dashboard'); - - // Click on Leagues in sidebar or navigation - // Using href-based selector for stability as requested - const leaguesLink = auth.page.locator('a[href="/leagues"]').first(); - await leaguesLink.click(); - - // Assert URL change - await auth.page.waitForURL(/\/leagues/); - expect(auth.page.url()).toContain('/leagues'); - - // Click on Dashboard back - const dashboardLink = auth.page.locator('a[href="/dashboard"]').first(); - await dashboardLink.click(); - - // Assert URL change - await auth.page.waitForURL(/\/dashboard/); - expect(auth.page.url()).toContain('/dashboard'); - - // Assert no runtime errors during navigation - capture.setAllowlist(['hydration', 'warning:']); - if (capture.hasUnexpectedErrors()) { - throw new Error(`Found unexpected console errors during navigation:\n${capture.format()}`); - } - - } finally { - await auth.context.close(); - } - }); -}); diff --git a/tests/e2e/website/route-coverage.e2e.test.ts b/tests/e2e/website/route-coverage.e2e.test.ts index f36489758..3fa4e9554 100644 --- a/tests/e2e/website/route-coverage.e2e.test.ts +++ b/tests/e2e/website/route-coverage.e2e.test.ts @@ -21,6 +21,8 @@ test.describe('Website Route Coverage & Failure Modes', () => { /Failed to load resource: the server responded with a status of 500/i, /net::ERR_NAME_NOT_RESOLVED/i, /net::ERR_CONNECTION_CLOSED/i, + /net::ERR_ACCESS_DENIED/i, + /Minified React error #418/i, /Event/i, /An error occurred in the Server Components render/i, /Route Error Boundary/i, @@ -47,7 +49,7 @@ test.describe('Website Route Coverage & Failure Modes', () => { capture.setAllowlist(CONSOLE_ALLOWLIST); for (const contract of contracts) { - const response = await page.goto(contract.path, { timeout: 10000 }).catch(() => null); + const response = await page.goto(contract.path, { timeout: 15000, waitUntil: 'commit' }).catch(() => null); if (contract.scenarios.unauth?.expectedStatus === 'redirect') { const currentPath = new URL(page.url()).pathname; @@ -56,7 +58,12 @@ test.describe('Website Route Coverage & Failure Modes', () => { } } else if (contract.scenarios.unauth?.expectedStatus === 'ok') { if (response?.status()) { - expect(response.status()).toBeLessThan(500); + // 500 is allowed for the dedicated /500 error page itself + if (contract.path === '/500') { + expect(response.status()).toBe(500); + } else { + expect(response.status(), `Failed to load ${contract.path} as unauth`).toBeLessThan(500); + } } } } @@ -75,7 +82,7 @@ test.describe('Website Route Coverage & Failure Modes', () => { const scenario = contract.scenarios[role]; if (!scenario) continue; - const response = await page.goto(contract.path, { timeout: 10000 }).catch(() => null); + const response = await page.goto(contract.path, { timeout: 15000, waitUntil: 'commit' }).catch(() => null); if (scenario.expectedStatus === 'redirect') { const currentPath = new URL(page.url()).pathname; @@ -95,6 +102,30 @@ test.describe('Website Route Coverage & Failure Modes', () => { } }); + test('Client-side Navigation Smoke', async ({ browser, request }) => { + const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); + const capture = new ConsoleErrorCapture(page); + capture.setAllowlist(CONSOLE_ALLOWLIST); + + try { + // Start at dashboard + await page.goto('/dashboard', { waitUntil: 'commit', timeout: 15000 }); + expect(page.url()).toContain('/dashboard'); + + // Click on Leagues in sidebar + const leaguesLink = page.locator('a[href="/leagues"]').first(); + await leaguesLink.click(); + + // Assert URL change + await page.waitForURL(/\/leagues/, { timeout: 15000 }); + expect(page.url()).toContain('/leagues'); + + expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0); + } finally { + await context.close(); + } + }); + test('Failure Modes', async ({ page, browser, request }) => { // 1. Invalid IDs const edgeCases = routeManager.getParamEdgeCases(); diff --git a/tests/unit/website/RouteConfig.test.ts b/tests/unit/website/RouteConfig.test.ts index fabb7a468..ff59a44c7 100644 --- a/tests/unit/website/RouteConfig.test.ts +++ b/tests/unit/website/RouteConfig.test.ts @@ -16,8 +16,9 @@ describe('RouteConfig - routeMatchers Invariants', () => { expect(routeMatchers.isPublic('/teams/abc')).toBe(true); }); - it('should return false for "leagues/create" (protected)', () => { + it('should return false for "leagues/create" and "teams/create" (protected)', () => { expect(routeMatchers.isPublic('/leagues/create')).toBe(false); + expect(routeMatchers.isPublic('/teams/create')).toBe(false); }); it('should return false for nested protected routes', () => {