import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter } from 'react-router-dom'; // ----------------------------------------------------------------------------- // UX-001 Phase 3 — CertificateStep inline team + owner creation contract // // The wizard has to satisfy C-001's six required certificate fields (name, // common_name, issuer_id, owner_id, team_id, renewal_policy_id). Before Phase // 3, a fresh install had no teams + no owners, so the two required `` auto- // selects the new team's id. // 3. "+ New owner" does the same for owners. // 4. Cancel closes the modal without firing the mutation — pins the // "nothing leaks on abort" guarantee. // // DashboardPage's outer wizard entry/exit contract is covered in // DashboardPage.test.tsx. Layout's sidebar re-entry button is covered in // Layout.test.tsx. // ----------------------------------------------------------------------------- // Mock the entire API client. vi.mock factories are hoisted above the imports // that follow, so these stubs are in effect when OnboardingWizard's module // graph resolves. vi.mock('../api/client', () => ({ getApiKey: vi.fn(() => 'test-api-key'), getIssuers: vi.fn(), getAgents: vi.fn(), getProfiles: vi.fn(), getOwners: vi.fn(), getTeams: vi.fn(), // G-1: wizard populates the renewal_policy_id dropdown from // getRenewalPolicies (rp-* ids), not getPolicies (which returns compliance // rules with pol-* ids and violates the FK). getRenewalPolicies: vi.fn(), createIssuer: vi.fn(), testIssuerConnection: vi.fn(), createCertificate: vi.fn(), triggerRenewal: vi.fn(), createTeam: vi.fn(), createOwner: vi.fn(), })); import OnboardingWizard from './OnboardingWizard'; import * as client from '../api/client'; function renderWizard() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0, staleTime: 0 }, }, }); return render( , ); } // Canonical "empty but well-formed" stubs for every query the wizard calls. // Returning data (rather than leaving queries pending) lets CertificateStep's // dropdowns render their placeholder options immediately. function stubAllQueriesEmpty() { vi.mocked(client.getIssuers).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 100, } as never); vi.mocked(client.getAgents).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 100, } as never); vi.mocked(client.getProfiles).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 100, } as never); vi.mocked(client.getOwners).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 500, } as never); vi.mocked(client.getTeams).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 500, } as never); // G-1: wizard populates renewal_policy_id from getRenewalPolicies, not // getPolicies. See comment on the mock factory above. vi.mocked(client.getRenewalPolicies).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 500, } as never); } // Drive through Skip to reach CertificateStep. Each step renders its own // "Skip this step" button in the WizardFooter; clicking it advances via the // parent goTo() state machine. "Skip setup" in the header (top-right) is a // different button tied to onDismiss and is intentionally not clicked here. async function advanceToCertificateStep() { // IssuerStep renders first. Wait for its heading before clicking skip so // we don't race ahead of the initial render. await waitFor(() => { expect( screen.getByRole('heading', { name: /Connect a Certificate Authority/i }), ).toBeInTheDocument(); }); fireEvent.click(screen.getByRole('button', { name: /Skip this step/i })); // AgentStep — wait for its heading, then skip. await waitFor(() => { expect( screen.getByRole('heading', { name: /Deploy a certctl Agent/i }), ).toBeInTheDocument(); }); fireEvent.click(screen.getByRole('button', { name: /Skip this step/i })); // CertificateStep — wait for its heading. Caller can now exercise the // inline modals. await waitFor(() => { expect( screen.getByRole('heading', { name: /Add a Certificate/i }), ).toBeInTheDocument(); }); } describe('OnboardingWizard — UX-001 inline team + owner creation in CertificateStep', () => { beforeEach(() => { vi.clearAllMocks(); cleanup(); stubAllQueriesEmpty(); }); it('skip-skip reaches CertificateStep with "+ New team" and "+ New owner" buttons', async () => { renderWizard(); await advanceToCertificateStep(); // The contract buttons are the whole point of UX-001 Phase 3 — if they // disappear, the dead-end is back. expect(screen.getByRole('button', { name: /\+ New team/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /\+ New owner/i })).toBeInTheDocument(); }); it('+ New team opens the inline modal, calls createTeam, invalidates the cache, and auto-selects the new team', async () => { // Drive getTeams from a closure variable so React Query's post-mutation // refetch observes the newly-created team. Without this, the parent // auto-selected the new team. Locate the select by // finding the new team's