Phase 12: Initial Setup Wizard #22
@@ -25,8 +25,17 @@ vi.mock('./components/CalendarShell.js', () => ({
|
|||||||
CalendarShell: () => <div data-testid="calendar-shell">CalendarShell</div>,
|
CalendarShell: () => <div data-testid="calendar-shell">CalendarShell</div>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// SetupPage mock respects the `alreadyLocked` prop so the reverse-gate test (gap 5)
|
||||||
|
// can distinguish the active wizard (Step 1 "Welcome to FamilySync Setup") from the
|
||||||
|
// Surface 8 "Setup already complete" terminal surface. The real SetupPage renders
|
||||||
|
// these two surfaces based on this exact prop — see routes/SetupPage.tsx.
|
||||||
vi.mock('./routes/SetupPage.js', () => ({
|
vi.mock('./routes/SetupPage.js', () => ({
|
||||||
SetupPage: () => <div data-testid="setup-page">SetupPage</div>,
|
SetupPage: ({ alreadyLocked }: { alreadyLocked?: boolean }) =>
|
||||||
|
alreadyLocked ? (
|
||||||
|
<div data-testid="setup-page">Setup already complete</div>
|
||||||
|
) : (
|
||||||
|
<div data-testid="setup-page">Welcome to FamilySync Setup</div>
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./components/AppNav.js', () => ({
|
vi.mock('./components/AppNav.js', () => ({
|
||||||
@@ -177,6 +186,41 @@ describe('App — setup-status gate', () => {
|
|||||||
expect(screen.getByTestId('setup-page')).toBeInTheDocument();
|
expect(screen.getByTestId('setup-page')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// gap 5 (T-12-04): manually visiting /setup AFTER setup is complete must show the
|
||||||
|
// "Setup already complete" surface (alreadyLocked), NOT re-mount the active wizard.
|
||||||
|
it('renders the "already complete" surface (not the wizard) on /setup when setupComplete is true', async () => {
|
||||||
|
mockFetchSetupStatus.mockResolvedValue({ setupComplete: true });
|
||||||
|
// Navigate directly to /setup (URL-isolation pattern — beforeEach reset to /)
|
||||||
|
window.history.pushState({}, '', '/setup');
|
||||||
|
|
||||||
|
const queryClient = makeQueryClient();
|
||||||
|
renderApp(queryClient);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('setup-page')).toHaveTextContent('Setup already complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The active wizard's Step 1 heading must NOT render when setup is complete
|
||||||
|
expect(screen.queryByText('Welcome to FamilySync Setup')).toBeNull();
|
||||||
|
// Standalone wizard surface — no AppNav shell on /setup
|
||||||
|
expect(screen.queryByTestId('app-nav')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// gap 5 counterpart: /setup with setupComplete false still mounts the active wizard.
|
||||||
|
it('renders the active wizard on /setup when setupComplete is false', async () => {
|
||||||
|
mockFetchSetupStatus.mockResolvedValue({ setupComplete: false });
|
||||||
|
window.history.pushState({}, '', '/setup');
|
||||||
|
|
||||||
|
const queryClient = makeQueryClient();
|
||||||
|
renderApp(queryClient);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('setup-page')).toHaveTextContent('Welcome to FamilySync Setup');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('Setup already complete')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('App — setupStatus and route presence', () => {
|
describe('App — setupStatus and route presence', () => {
|
||||||
|
|||||||
+31
-3
@@ -81,11 +81,21 @@ export default function App() {
|
|||||||
// Fetch current user once at the app shell level so AppNav has member data on
|
// Fetch current user once at the app shell level so AppNav has member data on
|
||||||
// ALL routes. This is the same query key (['me']) used by CalendarShell, so
|
// ALL routes. This is the same query key (['me']) used by CalendarShell, so
|
||||||
// TanStack Query deduplicates the request — no double fetch.
|
// TanStack Query deduplicates the request — no double fetch.
|
||||||
|
//
|
||||||
|
// gap 6 (mechanism (ii) — ['me'] staleness, NOT a linking gap; see SUMMARY):
|
||||||
|
// the first-login claim in upsertUser (auth/user.ts) preserves the same users.id,
|
||||||
|
// so the wizard-stored CalDAV credential stays linked → needsProviderSetup is
|
||||||
|
// correctly FALSE in the DB after the operator authenticates post-wizard. The bug
|
||||||
|
// was purely client-cache: a ['me'] entry populated BEFORE the claim (e.g. an
|
||||||
|
// earlier pre-auth visit) served a stale needsProviderSetup=true for up to 5
|
||||||
|
// minutes, so the "Set up your calendar" banner kept showing. Set staleTime 0 on
|
||||||
|
// the boot ['me'] query so the authenticated app shell always refetches member
|
||||||
|
// status on entry — needsProviderSetup then reflects the just-claimed credential.
|
||||||
const meQuery = useQuery({
|
const meQuery = useQuery({
|
||||||
queryKey: ['me'],
|
queryKey: ['me'],
|
||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
retry: false,
|
retry: false,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// isAdmin from /api/me — used for UX gating only (D-03). Server enforces 403.
|
// isAdmin from /api/me — used for UX gating only (D-03). Server enforces 403.
|
||||||
@@ -135,8 +145,26 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* /setup route — standalone wizard, no AppNav/BottomTabBar shell (UI-SPEC §Routing) */}
|
{/* /setup route — standalone wizard, no AppNav/BottomTabBar shell (UI-SPEC §Routing).
|
||||||
<Route path="/setup" element={<SetupPage />} />
|
Reverse gate (gap 5, T-12-04): once setup is complete the wizard must NOT re-mount.
|
||||||
|
- While setupQuery is loading → render the no-flash placeholder (no wizard before
|
||||||
|
status resolves), mirroring the `*`-route loading gate below.
|
||||||
|
- setupComplete === true → render SetupPage with alreadyLocked → Surface 8
|
||||||
|
("Setup already complete"), keeping the operator on /setup with a terminal surface.
|
||||||
|
- setupComplete === false (or undefined post-load) → active wizard, as before.
|
||||||
|
The backend already 423s setup mutations; this is the matching frontend reverse-gate. */}
|
||||||
|
<Route
|
||||||
|
path="/setup"
|
||||||
|
element={
|
||||||
|
setupLoading ? (
|
||||||
|
<div aria-hidden="true" />
|
||||||
|
) : setupComplete === true ? (
|
||||||
|
<SetupPage alreadyLocked={true} />
|
||||||
|
) : (
|
||||||
|
<SetupPage />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* All other routes are gated on setup completion */}
|
{/* All other routes are gated on setup completion */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
Reference in New Issue
Block a user