diff --git a/frontend/src/RespondentLayout.test.tsx b/frontend/src/RespondentLayout.test.tsx new file mode 100644 index 0000000..b186f5e --- /dev/null +++ b/frontend/src/RespondentLayout.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { RespondentLayout } from './RespondentLayout'; + +describe('RespondentLayout', () => { + it('renders the Stele header and page content', () => { + render( + +

Survey content

+
, + ); + + expect(screen.getByText('Stele')).toBeInTheDocument(); + expect(screen.getByText('Survey content')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/admin/AdminApp.test.tsx b/frontend/src/admin/AdminApp.test.tsx new file mode 100644 index 0000000..30565d1 --- /dev/null +++ b/frontend/src/admin/AdminApp.test.tsx @@ -0,0 +1,116 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter, Outlet, Route, Routes } from 'react-router-dom'; +import type { ReactNode } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { User } from '../api'; +import AdminApp from './AdminApp'; + +const authState = vi.hoisted(() => ({ + user: null as User | null, +})); + +vi.mock('../auth/AuthContext', () => ({ + AuthProvider: ({ children }: { children: ReactNode }) => <>{children}, + useAuth: () => ({ user: authState.user }), +})); + +vi.mock('../auth/RequireAuth', () => ({ + RequireAuth: () => , +})); + +vi.mock('./AdminLayout', () => ({ + AdminLayout: () => , +})); + +vi.mock('./LoginView', () => ({ + LoginView: () =>
Login view
, +})); + +vi.mock('./SurveyListView', () => ({ + SurveyListView: () =>
Survey list view
, +})); + +vi.mock('./PiiReviewView', () => ({ + PiiReviewView: () =>
PII review view
, +})); + +vi.mock('./MyDbAccessView', () => ({ + MyDbAccessView: () =>
My access view
, +})); + +vi.mock('./SurveyEditorView', () => ({ + SurveyEditorView: () =>
Survey editor view
, +})); + +vi.mock('./GdprView', () => ({ + GdprView: () =>
GDPR view
, +})); + +vi.mock('./EtlView', () => ({ + EtlView: () =>
ETL view
, +})); + +vi.mock('./UsersView', () => ({ + UsersView: () =>
Users view
, +})); + +vi.mock('./DbCredentialsView', () => ({ + DbCredentialsView: () =>
DB credentials view
, +})); + +afterEach(() => { + authState.user = null; + vi.clearAllMocks(); +}); + +function setRoles(roles: string[]) { + authState.user = { + id: 1, + email: 'user@example.com', + roles, + disabled: false, + created_at: 't', + }; +} + +function renderAt(path: string) { + return render( + + + } /> + + , + ); +} + +describe('AdminApp', () => { + it('renders the login route without requiring auth', async () => { + renderAt('/admin/login'); + expect(await screen.findByText('Login view')).toBeInTheDocument(); + }); + + it('lands authors on the survey list', async () => { + setRoles(['researcher']); + renderAt('/admin'); + expect(await screen.findByText('Survey list view')).toBeInTheDocument(); + }); + + it('redirects reviewers to the pii review queue', async () => { + setRoles(['reviewer']); + renderAt('/admin'); + expect(await screen.findByText('PII review view')).toBeInTheDocument(); + }); + + it('redirects analysts to my database access', async () => { + setRoles(['analyst']); + renderAt('/admin'); + expect(await screen.findByText('My access view')).toBeInTheDocument(); + }); + + it('falls back to survey list for unknown role sets', async () => { + setRoles(['viewer']); + renderAt('/admin'); + expect(await screen.findByText('Survey list view')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/ui/Primitives.test.tsx b/frontend/src/ui/Primitives.test.tsx new file mode 100644 index 0000000..a1655e9 --- /dev/null +++ b/frontend/src/ui/Primitives.test.tsx @@ -0,0 +1,53 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { Alert } from './Alert'; +import { Card, CardBody } from './Card'; +import { Field } from './Field'; +import { PageHeader } from './PageHeader'; + +describe('UI primitives', () => { + it('renders alert tones with the expected aria roles', () => { + const { rerender } = render(Error); + expect(screen.getByRole('alert')).toHaveTextContent('Error'); + + rerender(Saved); + expect(screen.getByRole('status')).toHaveTextContent('Saved'); + }); + + it('renders card and card body content with provided attributes', () => { + render( + + Body + , + ); + + const card = screen.getByTestId('card'); + expect(card).toHaveTextContent('Body'); + expect(card).toHaveClass('custom-card'); + expect(screen.getByText('Body')).toHaveClass('custom-body'); + }); + + it('associates field labels with inputs and renders hints', () => { + render( + , + ); + + expect(screen.getByLabelText('Email')).toHaveAttribute('id', 'email-field'); + expect(screen.getByText('Use your work email')).toBeInTheDocument(); + }); + + it('renders page headers with optional subtitle and actions', () => { + render( + Add user} + />, + ); + + expect(screen.getByRole('heading', { level: 1, name: 'Users' })).toBeInTheDocument(); + expect(screen.getByText('Manage accounts')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add user' })).toBeInTheDocument(); + }); +});