Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions frontend/src/RespondentLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<RespondentLayout>
<p>Survey content</p>
</RespondentLayout>,
);

expect(screen.getByText('Stele')).toBeInTheDocument();
expect(screen.getByText('Survey content')).toBeInTheDocument();
});
});
116 changes: 116 additions & 0 deletions frontend/src/admin/AdminApp.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <Outlet />,
}));

vi.mock('./AdminLayout', () => ({
AdminLayout: () => <Outlet />,
}));

vi.mock('./LoginView', () => ({
LoginView: () => <div>Login view</div>,
}));

vi.mock('./SurveyListView', () => ({
SurveyListView: () => <div>Survey list view</div>,
}));

vi.mock('./PiiReviewView', () => ({
PiiReviewView: () => <div>PII review view</div>,
}));

vi.mock('./MyDbAccessView', () => ({
MyDbAccessView: () => <div>My access view</div>,
}));

vi.mock('./SurveyEditorView', () => ({
SurveyEditorView: () => <div>Survey editor view</div>,
}));

vi.mock('./GdprView', () => ({
GdprView: () => <div>GDPR view</div>,
}));

vi.mock('./EtlView', () => ({
EtlView: () => <div>ETL view</div>,
}));

vi.mock('./UsersView', () => ({
UsersView: () => <div>Users view</div>,
}));

vi.mock('./DbCredentialsView', () => ({
DbCredentialsView: () => <div>DB credentials view</div>,
}));

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(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="/admin/*" element={<AdminApp />} />
</Routes>
</MemoryRouter>,
);
}

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();
});
});
53 changes: 53 additions & 0 deletions frontend/src/ui/Primitives.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Alert tone="error">Error</Alert>);
expect(screen.getByRole('alert')).toHaveTextContent('Error');

rerender(<Alert tone="success">Saved</Alert>);
expect(screen.getByRole('status')).toHaveTextContent('Saved');
});

it('renders card and card body content with provided attributes', () => {
render(
<Card data-testid="card" className="custom-card">
<CardBody className="custom-body">Body</CardBody>
</Card>,
);

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(
<Field id="email-field" label="Email" hint="Use your work email" placeholder="name@x.com" />,
);

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(
<PageHeader
title="Users"
subtitle="Manage accounts"
actions={<button type="button">Add user</button>}
/>,
);

expect(screen.getByRole('heading', { level: 1, name: 'Users' })).toBeInTheDocument();
expect(screen.getByText('Manage accounts')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add user' })).toBeInTheDocument();
});
});
Loading