ae6db7e16f
Onboarding asks explicitly for one or two parents and shows one card per parent for balances; the balance form is per parent with inline validation; the partner can be filled from the FK deduction or added from it. Settings and Dashboard reuse the same form. CSS: bare buttons carry no colour any more; buttons, cards, choice cards, fields, alerts, badges, eyebrow and modal chrome are defined once in primitives.css. Media queries live only in responsive.css. A style guard test blocks hex, !important and stray media queries. Bilingual fixes: calendar selects size by content, header no longer wraps. Vitest is capped at four workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import { Auth } from './Auth'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
|
|
vi.mock('@/context/AuthContext')
|
|
vi.mock('@/context/WorkspaceContext')
|
|
|
|
const mockNavigate = vi.fn()
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
|
|
return {
|
|
...actual,
|
|
useNavigate: () => mockNavigate,
|
|
}
|
|
})
|
|
|
|
const mockUseAuth = vi.mocked(useAuth)
|
|
const mockUseWorkspace = vi.mocked(useWorkspace)
|
|
|
|
describe('Auth', () => {
|
|
const mockLogin = vi.fn()
|
|
const mockRegister = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseAuth.mockReturnValue({
|
|
accessToken: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
login: mockLogin,
|
|
register: mockRegister,
|
|
logout: vi.fn(),
|
|
})
|
|
mockUseWorkspace.mockReturnValue({
|
|
workspace: null,
|
|
isLoading: false,
|
|
error: null,
|
|
workspaceId: null,
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [],
|
|
selectedChildId: null,
|
|
selectChild: vi.fn(),
|
|
personalDataUuid: null,
|
|
parentName: null,
|
|
refreshContext: vi.fn(),
|
|
planVersion: 0,
|
|
bumpPlanVersion: vi.fn(),
|
|
})
|
|
})
|
|
|
|
it('renders the login form by default, with the login tab active', () => {
|
|
render(<Auth />)
|
|
expect(screen.getByRole('heading', { name: 'Logga in' })).toBeInTheDocument()
|
|
expect(screen.getByPlaceholderText('E-post')).toBeInTheDocument()
|
|
expect(screen.getByPlaceholderText('Lösenord')).toBeInTheDocument()
|
|
expect(screen.queryByPlaceholderText('Namn')).not.toBeInTheDocument()
|
|
// Both tabs' text is unique except "Logga in", which the login-mode
|
|
// submit button also renders — the tabs carry their own testids so
|
|
// these queries don't collide with it.
|
|
expect(screen.getByTestId('auth-tab-login')).toHaveAttribute('aria-pressed', 'true')
|
|
expect(screen.getByTestId('auth-tab-register')).toHaveAttribute('aria-pressed', 'false')
|
|
})
|
|
|
|
it('switches to the registration form via the tab', () => {
|
|
render(<Auth />)
|
|
fireEvent.click(screen.getByTestId('auth-tab-register'))
|
|
expect(screen.getByRole('heading', { name: 'Skapa konto' })).toBeInTheDocument()
|
|
expect(screen.getByPlaceholderText('Namn')).toBeInTheDocument()
|
|
expect(screen.getByTestId('auth-tab-register')).toHaveAttribute('aria-pressed', 'true')
|
|
expect(screen.getByTestId('auth-tab-login')).toHaveAttribute('aria-pressed', 'false')
|
|
})
|
|
|
|
it('switches back to the login form via the tab', () => {
|
|
render(<Auth />)
|
|
fireEvent.click(screen.getByTestId('auth-tab-register'))
|
|
fireEvent.click(screen.getByTestId('auth-tab-login'))
|
|
expect(screen.getByRole('heading', { name: 'Logga in' })).toBeInTheDocument()
|
|
expect(screen.queryByPlaceholderText('Namn')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('clears the error banner when switching between the Log in and Create account tabs', async () => {
|
|
mockLogin.mockRejectedValue(new Error('Fel användarnamn'))
|
|
render(<Auth />)
|
|
|
|
fireEvent.click(screen.getByTestId('auth-submit'))
|
|
expect(await screen.findByRole('alert')).toHaveTextContent('Fel användarnamn')
|
|
|
|
fireEvent.click(screen.getByTestId('auth-tab-register'))
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('logs in with the entered credentials and navigates home', async () => {
|
|
mockLogin.mockResolvedValue(undefined)
|
|
render(<Auth />)
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('E-post'), { target: { value: 'a@b.com' } })
|
|
fireEvent.change(screen.getByPlaceholderText('Lösenord'), { target: { value: 'secret' } })
|
|
fireEvent.click(screen.getByTestId('auth-submit'))
|
|
|
|
await waitFor(() => expect(mockLogin).toHaveBeenCalledWith({ email: 'a@b.com', password: 'secret' }))
|
|
expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true })
|
|
})
|
|
|
|
it('registers with the entered details and navigates home', async () => {
|
|
mockRegister.mockResolvedValue(undefined)
|
|
render(<Auth />)
|
|
|
|
fireEvent.click(screen.getByTestId('auth-tab-register'))
|
|
fireEvent.change(screen.getByPlaceholderText('Namn'), { target: { value: 'Alex' } })
|
|
fireEvent.change(screen.getByPlaceholderText('E-post'), { target: { value: 'a@b.com' } })
|
|
fireEvent.change(screen.getByPlaceholderText('Lösenord'), { target: { value: 'secret' } })
|
|
fireEvent.click(screen.getByTestId('auth-submit'))
|
|
|
|
await waitFor(() =>
|
|
expect(mockRegister).toHaveBeenCalledWith({ email: 'a@b.com', password: 'secret', displayName: 'Alex' }),
|
|
)
|
|
expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true })
|
|
})
|
|
|
|
it('shows the error message from a failed login', async () => {
|
|
mockLogin.mockRejectedValue(new Error('Fel användarnamn'))
|
|
render(<Auth />)
|
|
|
|
fireEvent.click(screen.getByTestId('auth-submit'))
|
|
|
|
expect(await screen.findByRole('alert')).toHaveTextContent('Fel användarnamn')
|
|
})
|
|
|
|
it('shows a generic error message when login rejects with a non-Error value', async () => {
|
|
mockLogin.mockRejectedValue('nope')
|
|
render(<Auth />)
|
|
|
|
fireEvent.click(screen.getByTestId('auth-submit'))
|
|
|
|
expect(await screen.findByRole('alert')).toHaveTextContent('Något gick fel')
|
|
})
|
|
|
|
it('redirects to the dashboard once authenticated and the workspace has loaded', () => {
|
|
mockUseAuth.mockReturnValue({
|
|
accessToken: 'token',
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
login: mockLogin,
|
|
register: mockRegister,
|
|
logout: vi.fn(),
|
|
})
|
|
render(<Auth />)
|
|
expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true })
|
|
})
|
|
|
|
it('does not redirect while the workspace is still loading', () => {
|
|
mockUseAuth.mockReturnValue({
|
|
accessToken: 'token',
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
login: mockLogin,
|
|
register: mockRegister,
|
|
logout: vi.fn(),
|
|
})
|
|
mockUseWorkspace.mockReturnValue({
|
|
workspace: null,
|
|
isLoading: true,
|
|
error: null,
|
|
workspaceId: null,
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [],
|
|
selectedChildId: null,
|
|
selectChild: vi.fn(),
|
|
personalDataUuid: null,
|
|
parentName: null,
|
|
refreshContext: vi.fn(),
|
|
planVersion: 0,
|
|
bumpPlanVersion: vi.fn(),
|
|
})
|
|
render(<Auth />)
|
|
expect(mockNavigate).not.toHaveBeenCalled()
|
|
})
|
|
})
|