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();
});
});