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:
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user