import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import Login from './Login'; import App from '../App'; import { AuthProvider } from '../auth/AuthContext'; import { clearToken, getToken } from '../lib/auth-storage'; // Keep the real signIn/apiFetch flow (drive it through a stubbed global fetch); only silence the // authed shell's data fetches so the lifecycle test's screens render without real requests. vi.mock('../lib/api', async () => { const actual = await vi.importActual('../lib/api'); return { ...actual, apiFetch: vi.fn().mockResolvedValue([]) }; }); // A 200 response carrying the bearer token the way better-auth returns it. function tokenResponse() { return new Response(null, { status: 200, headers: { 'set-auth-token': 'tok' } }); } function renderLogin() { return render( , ); } describe('Login', () => { beforeEach(() => { clearToken(); window.history.replaceState(null, '', '/'); }); afterEach(() => { vi.restoreAllMocks(); clearToken(); }); it('renders the Dutch login form', () => { renderLogin(); expect(screen.getByText('E-mailadres')).toBeInTheDocument(); expect(screen.getByText('Wachtwoord')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Inloggen' })).toBeInTheDocument(); }); it('submits the typed credentials to the sign-in endpoint and stores the token', async () => { const fetchMock = vi.fn().mockResolvedValue(tokenResponse()); vi.stubGlobal('fetch', fetchMock); const user = userEvent.setup(); renderLogin(); await user.type(screen.getByLabelText('E-mailadres'), 'worker@example.com'); await user.type(screen.getByLabelText('Wachtwoord'), 'geheim123'); await user.click(screen.getByRole('button', { name: 'Inloggen' })); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); const [url, init] = fetchMock.mock.calls[0]; expect(url).toContain('/api/auth/sign-in/email'); expect(JSON.parse(init.body as string)).toEqual({ email: 'worker@example.com', password: 'geheim123', }); expect(getToken()).toBe('tok'); }); it('shows an error message and stores no token when sign-in fails', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 }))); const user = userEvent.setup(); renderLogin(); await user.type(screen.getByLabelText('E-mailadres'), 'x@y.z'); await user.type(screen.getByLabelText('Wachtwoord'), 'wrong'); await user.click(screen.getByRole('button', { name: 'Inloggen' })); expect(await screen.findByText('Inloggen mislukt')).toBeInTheDocument(); expect(getToken()).toBeNull(); }); it('disables the submit button while the request is pending', async () => { let release: (() => void) | undefined; const pending = new Promise((resolve) => { release = () => resolve(tokenResponse()); }); vi.stubGlobal('fetch', vi.fn().mockReturnValue(pending)); const user = userEvent.setup(); renderLogin(); const button = screen.getByRole('button', { name: 'Inloggen' }); await user.type(screen.getByLabelText('E-mailadres'), 'a@b.c'); await user.type(screen.getByLabelText('Wachtwoord'), 'pw'); await user.click(button); await waitFor(() => expect(button).toBeDisabled()); release?.(); await waitFor(() => expect(button).not.toBeDisabled()); }); it('lifecycle: a successful login flips the app to the authenticated shell', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(tokenResponse())); const user = userEvent.setup(); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); // Unauthenticated: the login form is shown first. await user.type(screen.getByLabelText('E-mailadres'), 'worker@example.com'); await user.type(screen.getByLabelText('Wachtwoord'), 'geheim123'); await user.click(screen.getByRole('button', { name: 'Inloggen' })); // Context flips to authenticated → the tabbed shell renders. const nav = await screen.findByRole('navigation'); expect(within(nav).getByText('Stopwatch')).toBeInTheDocument(); expect(getToken()).toBe('tok'); }); });