feat(19-04): LoginPage (Surfaces 1-10) + App.tsx authModeQuery gate + /login route

- Create LoginPage with BrandSlot, username/password form, show/hide toggle
- Four error states: invalid credentials, rate-limit, locked, server (all per UI-SPEC)
- OIDC method divider + 'Login with OIDC' button rendered only when oidcEnabled
- Accessibility: role=main, h1 in BrandSlot, h2 Sign in, aria-live error banner, 44px targets
- Focus management: username autofocus, Enter navigates username→password→submit
- App.tsx: add authModeQuery (queryKey ['authMode'], staleTime 60s)
- App.tsx: add /login standalone route (sibling of /setup, no AppNav/BottomTabBar)
- App.tsx: login gate after setup gate — meQuery error + localEnabled → Navigate /login
- App.tsx: OidcRedirect helper for OIDC-only mode (meQuery error + !localEnabled + oidcEnabled)
- Fix App.test.tsx to include fetchAuthMode mock and hasLocalCredential in user fixture
This commit is contained in:
Lucas Berger
2026-06-17 17:14:18 -04:00
parent 869cdc26c8
commit 32d0408774
3 changed files with 522 additions and 2 deletions
+15 -1
View File
@@ -74,21 +74,33 @@ vi.mock('./routes/ListDetail.js', () => ({
ListDetail: () => <div data-testid="list-detail">ListDetail</div>,
}));
vi.mock('./routes/LoginPage.js', () => ({
LoginPage: () => <div data-testid="login-page">LoginPage</div>,
}));
// Mock the API client — this is the key mock for the gate
vi.mock('./api/client.js', () => ({
fetchSetupStatus: vi.fn(),
fetchMe: vi.fn(),
// Phase 19: fetchAuthMode is queried in App.tsx for the /login gate
fetchAuthMode: vi.fn().mockResolvedValue({ localEnabled: true, oidcEnabled: false }),
SetupAlreadyLockedError: class SetupAlreadyLockedError extends Error {
readonly name = 'SetupAlreadyLockedError';
},
SessionExpiredError: class SessionExpiredError extends Error {
readonly name = 'SessionExpiredError';
},
LoginError: class LoginError extends Error {
readonly name = 'LoginError';
constructor(public readonly code: string) {
super(`Login failed: ${code}`);
}
},
}));
// ── Imports (after mocks) ────────────────────────────────────────────────────
import { fetchSetupStatus, fetchMe } from './api/client.js';
import { fetchSetupStatus, fetchMe, fetchAuthMode } from './api/client.js';
import type { Mock } from 'vitest';
import App from './App.js';
@@ -113,6 +125,7 @@ function renderApp(queryClient: QueryClient) {
const mockFetchSetupStatus = fetchSetupStatus as Mock;
const mockFetchMe = fetchMe as Mock;
const _mockFetchAuthMode = fetchAuthMode as Mock; // eslint-disable-line @typescript-eslint/no-unused-vars
// ── Tests ─────────────────────────────────────────────────────────────────────
@@ -129,6 +142,7 @@ describe('App — setup-status gate', () => {
color: '#4a90d9',
isAdmin: false,
needsProviderSetup: false,
hasLocalCredential: false,
},
});
});