test(12-04): add failing tests for SetupPage wizard and setup API client (RED)
- Tests for all 7 setup client function exports (fetchSetupStatus, postSetupConfig, validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential, postSetupComplete) - Tests for SetupPage rendering: page title, Welcome step heading, 4-step indicator, Continue button, role=main, aria-live region - Tests for no AppNav/BottomTabBar in wizard output - Tests for Already Locked screen (alreadyLocked prop) and Sign-in link
This commit is contained in:
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* SetupPage — unit tests (TDD RED gate, Task 2, Plan 12-04)
|
||||||
|
*
|
||||||
|
* Tests cover:
|
||||||
|
* - API client functions exported from client.ts (fetchSetupStatus, postSetupConfig,
|
||||||
|
* validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential, postSetupComplete)
|
||||||
|
* - SetupPage renders wizard steps (Welcome, step indicator, step headings)
|
||||||
|
* - SetupPage renders "Already Locked" screen when status returns 423
|
||||||
|
* - SetupPage has role="main", aria-live
|
||||||
|
* - SetupPage does NOT import AppNav or BottomTabBar
|
||||||
|
* - SetupPage is standalone (no AppNav/BottomTabBar in render output)
|
||||||
|
*
|
||||||
|
* All API calls are mocked via vi.mock('../api/client.js').
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
|
// ── Module mocks ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
vi.mock('../api/client.js', () => ({
|
||||||
|
fetchSetupStatus: vi.fn(),
|
||||||
|
postSetupConfig: vi.fn(),
|
||||||
|
validateSetupDb: vi.fn(),
|
||||||
|
validateSetupOidc: vi.fn(),
|
||||||
|
validateSetupVapid: vi.fn(),
|
||||||
|
postSetupCredential: vi.fn(),
|
||||||
|
postSetupComplete: vi.fn(),
|
||||||
|
// Keep other exports the app uses
|
||||||
|
fetchMe: vi.fn(),
|
||||||
|
SessionExpiredError: class SessionExpiredError extends Error {
|
||||||
|
readonly name = 'SessionExpiredError';
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeQueryClient() {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSetupPage(queryClient: QueryClient) {
|
||||||
|
// Inline import to ensure mocks are set up first
|
||||||
|
const { SetupPage } = require('./SetupPage.js') as typeof import('./SetupPage.js');
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<SetupPage />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: API client exports ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Setup API client functions', () => {
|
||||||
|
it('client.ts exports fetchSetupStatus', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('fetchSetupStatus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports postSetupConfig', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('postSetupConfig');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports validateSetupDb', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('validateSetupDb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports validateSetupOidc', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('validateSetupOidc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports validateSetupVapid', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('validateSetupVapid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports postSetupCredential', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('postSetupCredential');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client.ts exports postSetupComplete', async () => {
|
||||||
|
const client = await import('../api/client.js');
|
||||||
|
expect(client).toHaveProperty('postSetupComplete');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Tests: SetupPage rendering ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('SetupPage — Welcome step', () => {
|
||||||
|
let queryClient: QueryClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queryClient = makeQueryClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the page title "FamilySync Setup"', async () => {
|
||||||
|
renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('FamilySync Setup')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the Welcome step heading', async () => {
|
||||||
|
renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Welcome to FamilySync Setup')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders step indicator with 4 steps', async () => {
|
||||||
|
renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
// Step labels: Welcome, Instance, Calendar, Complete
|
||||||
|
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Instance')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Calendar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Complete')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the Continue button on the Welcome step', async () => {
|
||||||
|
renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has role="main" on the wizard content', async () => {
|
||||||
|
renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('main')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has aria-live region for validation status', async () => {
|
||||||
|
const { container } = renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
const liveRegion = container.querySelector('[aria-live]');
|
||||||
|
expect(liveRegion).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT render AppNav', async () => {
|
||||||
|
const { container } = renderSetupPage(queryClient);
|
||||||
|
// AppNav has data-testid or we check for nav text unique to AppNav
|
||||||
|
// We just ensure the SetupPage module itself does not import AppNav
|
||||||
|
await waitFor(() => {
|
||||||
|
// The wizard should not contain nav links like "Calendar" or "Lists" from AppNav
|
||||||
|
// (those are nav items in AppNav/BottomTabBar)
|
||||||
|
const navEl = container.querySelector('nav');
|
||||||
|
// If there is no nav element, AppNav is not rendered
|
||||||
|
expect(navEl).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT render BottomTabBar', async () => {
|
||||||
|
const { container } = renderSetupPage(queryClient);
|
||||||
|
await waitFor(() => {
|
||||||
|
// BottomTabBar uses role="navigation" or a specific class
|
||||||
|
// The wizard page has no bottom tab bar
|
||||||
|
const tabs = container.querySelectorAll('[role="tablist"]');
|
||||||
|
expect(tabs.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Tests: SetupPage Already Locked screen ───────────────────────────────────
|
||||||
|
|
||||||
|
describe('SetupPage — Already Locked screen', () => {
|
||||||
|
it('renders "Setup already complete" when alreadyLocked state is true', async () => {
|
||||||
|
// SetupPage renders the locked screen when it encounters a 423 from the API
|
||||||
|
// We test this by using a special prop or by simulating the locked state
|
||||||
|
// The component should accept an optional prop for testing, or
|
||||||
|
// we render with a query that returns 423
|
||||||
|
// For now, we check the component can render the locked screen text
|
||||||
|
// We'll test this via the component's internal state management
|
||||||
|
const queryClient = makeQueryClient();
|
||||||
|
const { SetupPage } = require('./SetupPage.js') as typeof import('./SetupPage.js');
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MemoryRouter>
|
||||||
|
{/* Pass alreadyLocked prop to render locked screen directly */}
|
||||||
|
<SetupPage alreadyLocked />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Setup already complete')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('locked screen has "Sign in" link to /', async () => {
|
||||||
|
const queryClient = makeQueryClient();
|
||||||
|
const { SetupPage } = require('./SetupPage.js') as typeof import('./SetupPage.js');
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<SetupPage alreadyLocked />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
const link = screen.getByText('Sign in');
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user