/** * SetupBanner.test.tsx — regression tests for the gap-6 ['me'] freshness fix * (Phase 12 Plan 07 Task 2) plus the success-only dismissal contract. * * gap 6 mechanism (ii) — ['me'] staleness, NOT a backend linking gap: * The first-login claim in upsertUser (apps/api/src/auth/user.ts) preserves the * same users.id, so the wizard-stored CalDAV credential stays linked and the DB * correctly reports needsProviderSetup=false after the operator authenticates. * The bug was purely client-cache: a ['me'] entry populated BEFORE the claim * (e.g. a pre-auth visit) served a stale needsProviderSetup=true for up to the * 5-minute staleTime, so the "Set up your calendar" banner kept showing. * Fix: the banner's ['me'] query uses staleTime 0, so a stale cache entry is * refetched on mount and the banner reflects the just-claimed credential. * * Tests cover: * - needsProviderSetup=false → banner absent * - needsProviderSetup=true → banner present (no dismiss/X button) * - staleTime 0: a pre-seeded stale ['me'] (needsProviderSetup=true) is refetched * on mount; once fetchMe resolves needsProviderSetup=false, the banner hides * (mirrors the post-wizard claim where the DB now reports false) */ 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'; // CredentialSheet is rendered (closed) by SetupBanner — mock it to avoid the // real sheet's DOM/focus-trap complexity. We're testing banner visibility only. vi.mock('./CredentialSheet.js', () => ({ CredentialSheet: () => null, })); vi.mock('../api/client.js', () => ({ fetchMe: vi.fn(), })); import { fetchMe } from '../api/client.js'; import type { Mock } from 'vitest'; import { SetupBanner } from './SetupBanner.js'; const mockFetchMe = fetchMe as Mock; function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); } function renderBanner(queryClient: QueryClient) { return render( , ); } function meUser(needsProviderSetup: boolean) { return { user: { id: 1, displayName: 'Test User', color: '#4a90d9', isAdmin: false, needsProviderSetup, }, }; } describe('SetupBanner', () => { beforeEach(() => { vi.clearAllMocks(); }); it('does not render when needsProviderSetup is false', async () => { mockFetchMe.mockResolvedValue(meUser(false)); const queryClient = makeQueryClient(); renderBanner(queryClient); // Give the query a tick to resolve, then confirm the banner stays absent await new Promise((r) => setTimeout(r, 50)); expect(screen.queryByText('Set up your calendar')).toBeNull(); }); it('renders the banner (no dismiss button) when needsProviderSetup is true', async () => { mockFetchMe.mockResolvedValue(meUser(true)); const queryClient = makeQueryClient(); renderBanner(queryClient); await waitFor(() => { expect(screen.getByText('Set up your calendar')).toBeInTheDocument(); }); // The ONLY action is "Set up now" — there is NO dismiss/X button (T-05-24). expect(screen.getByRole('button', { name: 'Set up now' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /dismiss/i })).toBeNull(); expect(screen.queryByRole('button', { name: /close/i })).toBeNull(); }); // gap 6 regression: a stale ['me'] cache entry (needsProviderSetup=true, as it // would be from a pre-claim visit) must be refetched on mount because the query // uses staleTime 0. After the fresh fetch resolves needsProviderSetup=false (the // post-wizard-claim DB truth), the banner must hide. it('refetches a stale ["me"] on mount (staleTime 0) and hides the banner once needsProviderSetup is false', async () => { const queryClient = makeQueryClient(); // Seed a STALE cache entry as if populated before the wizard claim. queryClient.setQueryData(['me'], meUser(true)); // The server now reports the claimed credential → needsProviderSetup=false. mockFetchMe.mockResolvedValue(meUser(false)); renderBanner(queryClient); // staleTime 0 → the seeded entry is stale → refetch fires on mount. await waitFor(() => { expect(mockFetchMe).toHaveBeenCalled(); }); // After the refetch resolves false, the banner must disappear. await waitFor(() => { expect(screen.queryByText('Set up your calendar')).toBeNull(); }); }); });