import { seedBadgeDefinitions, seedChallengeDefinitions, seedCollectionItems, seedMatches, seedStadiums, seedTeams, seedTriviaQuestions } from '@fan-passport/shared'; import type { ContentResponse, PassportState, UserProfile } from '@fan-passport/shared'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import App from '../App'; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); } function mockContent(): ContentResponse { return { version: '2026-demo-v1', teams: seedTeams, stadiums: seedStadiums, matches: seedMatches, triviaQuestions: seedTriviaQuestions, collectionItems: seedCollectionItems, badges: seedBadgeDefinitions, challenges: seedChallengeDefinitions }; } function mockPassport(user: UserProfile): PassportState { return { user, collections: [], triviaAnswers: [], predictions: [], memories: [], badges: [], challengeProgress: seedChallengeDefinitions.map((challenge) => ({ challengeId: challenge.id, current: 0, target: challenge.target, complete: false })), achievements: [], leaderboard: [ { userId: 'rival', displayName: 'Rival Fan', country: 'Global', points: 120, badges: 2, completedChallenges: 1, avatarEmoji: '⚽', rank: 1 }, { userId: user.id, displayName: user.displayName, country: user.country, points: user.points, badges: 0, completedChallenges: 0, avatarEmoji: '🎟️', rank: 2, isCurrentUser: true } ], stats: { totalCollections: 0, teamsCollected: 0, stadiumsCollected: 0, matchesLogged: 0, stickersCollected: 0, correctTriviaAnswers: 0, predictionsSubmitted: 0, giantKillingsPredicted: 0, memoriesCreated: 0, completedChallenges: 0, rank: 2 } }; } describe('App', () => { afterEach(() => { window.localStorage.clear(); vi.unstubAllGlobals(); }); it('renders onboarding and creates a passport through the API client', async () => { const user: UserProfile = { id: 'user-test', displayName: 'Test Fan', country: 'Global', createdAt: '2026-06-01T00:00:00.000Z', points: 0, level: 1, streakDays: 1, lastActiveDate: '2026-06-01' }; const passport = mockPassport(user); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.endsWith('/api/content')) { return jsonResponse(mockContent()); } if (url.endsWith('/api/users')) { return jsonResponse({ user, passport }, 201); } if (url.endsWith('/api/passport/user-test')) { return jsonResponse(passport); } return jsonResponse({ error: { code: 'NOT_FOUND', message: 'Not found' } }, 404); }); vi.stubGlobal('fetch', fetchMock); render(); expect(await screen.findByRole('heading', { name: /Start your passport/i })).toBeInTheDocument(); fireEvent.change(screen.getByLabelText(/Display name/i), { target: { value: 'Test Fan' } }); fireEvent.click(screen.getByRole('button', { name: /Create passport/i })); expect(await screen.findByRole('heading', { name: /Welcome, Test Fan/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^Collect England$/i })).toBeInTheDocument(); expect(screen.getByText(/No badges yet/i)).toBeInTheDocument(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/users'), expect.objectContaining({ method: 'POST' })); }); }); it('shows an empty-state recovery message when a saved user no longer exists', async () => { window.localStorage.setItem('fan-passport-demo-user-id', 'missing-user'); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.endsWith('/api/content')) { return jsonResponse(mockContent()); } if (url.endsWith('/api/passport/missing-user')) { return jsonResponse({ error: { code: 'USER_NOT_FOUND', message: 'Missing user' } }, 404); } return jsonResponse({ error: { code: 'NOT_FOUND', message: 'Not found' } }, 404); }); vi.stubGlobal('fetch', fetchMock); render(); expect(await screen.findByRole('alert')).toHaveTextContent(/saved demo passport no longer exists/i); expect(await screen.findByRole('heading', { name: /Start your passport/i })).toBeInTheDocument(); }); });