import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { WelcomeStep3 } from '../WelcomeStep3' import { useAuthStore } from '@/store/authStore' import { onboardingApi } from '@/api/onboarding' import { accountsApi } from '@/api/accounts' import type { Account, User } from '@/types' vi.mock('@/api/onboarding', async () => { const actual = await vi.importActual( '@/api/onboarding', ) return { ...actual, onboardingApi: { ...actual.onboardingApi, updateStep: vi.fn(), dismissRest: vi.fn(), }, } }) vi.mock('@/api/accounts', async () => { const actual = await vi.importActual( '@/api/accounts', ) return { ...actual, accountsApi: { ...actual.accountsApi, bulkInvite: vi.fn(), }, } }) vi.mock('@/lib/toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), promise: vi.fn(), }, })) function makeUser(overrides: Partial = {}): User { return { id: 'user-1', email: 'test@example.com', name: 'Test User', role: 'engineer', is_super_admin: false, is_active: true, must_change_password: false, account_id: 'acct-1', account_role: 'owner', team_id: null, created_at: '2026-05-01T00:00:00Z', last_login: null, phone: null, job_title: null, timezone: 'UTC', avatar_url: null, email_verified_at: null, onboarding_step_completed: 2, onboarding_dismissed: false, ...overrides, } } function makeAccount(overrides: Partial = {}): Account { return { id: 'acct-1', name: 'Acme MSP', display_code: 'ACME', owner_id: 'user-1', created_at: '2026-05-01T00:00:00Z', updated_at: '2026-05-01T00:00:00Z', ...overrides, } } function renderPage() { return render( } /> dashboard} /> , ) } describe('WelcomeStep3', () => { beforeEach(() => { useAuthStore.setState({ user: makeUser(), account: makeAccount(), subscription: null, token: null, isAuthenticated: true, fetchUser: vi.fn().mockResolvedValue(undefined), }) vi.mocked(onboardingApi.updateStep).mockResolvedValue({ onboarding_step_completed: 3, onboarding_dismissed: false, }) vi.mocked(onboardingApi.dismissRest).mockResolvedValue({ onboarding_step_completed: null, onboarding_dismissed: true, }) vi.mocked(accountsApi.bulkInvite).mockResolvedValue({ created: [], failed: [], }) }) afterEach(() => { vi.clearAllMocks() }) it('valid emails create invites and complete wizard', async () => { const user = userEvent.setup() vi.mocked(accountsApi.bulkInvite).mockResolvedValueOnce({ created: [ { id: 'inv-1', account_id: 'acct-1', email: 'a@example.com', role: 'engineer', code: 'c1', expires_at: null, used_at: null, created_at: '2026-05-06T00:00:00Z', }, { id: 'inv-2', account_id: 'acct-1', email: 'b@example.com', role: 'viewer', code: 'c2', expires_at: null, used_at: null, created_at: '2026-05-06T00:00:00Z', }, ], failed: [], }) renderPage() await user.type(screen.getByTestId('welcome-step-3-email-0'), 'a@example.com') await user.type(screen.getByTestId('welcome-step-3-email-1'), 'b@example.com') await user.selectOptions(screen.getByTestId('welcome-step-3-role-1'), 'viewer') await user.click(screen.getByTestId('welcome-step-3-send')) await waitFor(() => { expect(accountsApi.bulkInvite).toHaveBeenCalledWith([ { email: 'a@example.com', role: 'engineer' }, { email: 'b@example.com', role: 'viewer' }, ]) }) await waitFor(() => { expect(onboardingApi.updateStep).toHaveBeenCalledWith({ step: 3, action: 'complete', }) }) await waitFor(() => { expect(screen.getByText('dashboard')).toBeInTheDocument() }) }) it('partial-failure shows inline error per failed email', async () => { const user = userEvent.setup() vi.mocked(accountsApi.bulkInvite).mockResolvedValueOnce({ created: [ { id: 'inv-1', account_id: 'acct-1', email: 'good@example.com', role: 'engineer', code: 'c1', expires_at: null, used_at: null, created_at: '2026-05-06T00:00:00Z', }, ], failed: [ { email: 'bad@example.com', error: 'Email already invited' }, ], }) renderPage() await user.type(screen.getByTestId('welcome-step-3-email-0'), 'good@example.com') await user.type(screen.getByTestId('welcome-step-3-email-1'), 'bad@example.com') await user.click(screen.getByTestId('welcome-step-3-send')) await waitFor(() => { expect(accountsApi.bulkInvite).toHaveBeenCalled() }) // The bad-email row shows the error text. await waitFor(() => { expect(screen.getByTestId('welcome-step-3-row-error-1')).toHaveTextContent( /already invited/i, ) }) // Wizard did NOT auto-advance — onboarding-step is unchanged. expect(onboardingApi.updateStep).not.toHaveBeenCalled() expect(screen.queryByText('dashboard')).not.toBeInTheDocument() // "Continue anyway" is offered. expect(screen.getByTestId('welcome-step-3-continue-anyway')).toBeInTheDocument() }) it('empty + Skip advances without sending invites', async () => { const user = userEvent.setup() renderPage() await user.click(screen.getByTestId('welcome-step-3-skip')) await waitFor(() => { expect(onboardingApi.updateStep).toHaveBeenCalledWith({ step: 3, action: 'skip', }) }) // No bulk-invite call. expect(accountsApi.bulkInvite).not.toHaveBeenCalled() await waitFor(() => { expect(screen.getByText('dashboard')).toBeInTheDocument() }) }) it('empty + Send is a no-op bulk call but still completes the step', async () => { const user = userEvent.setup() renderPage() // All rows blank — Send should skip the bulk call entirely and just // mark the step complete. await user.click(screen.getByTestId('welcome-step-3-send')) await waitFor(() => { expect(onboardingApi.updateStep).toHaveBeenCalledWith({ step: 3, action: 'complete', }) }) expect(accountsApi.bulkInvite).not.toHaveBeenCalled() }) it('+ Add another adds a row, capped at 10', async () => { const user = userEvent.setup() renderPage() // Starts with 3 default rows. expect(screen.getByTestId('welcome-step-3-email-0')).toBeInTheDocument() expect(screen.getByTestId('welcome-step-3-email-1')).toBeInTheDocument() expect(screen.getByTestId('welcome-step-3-email-2')).toBeInTheDocument() expect(screen.queryByTestId('welcome-step-3-email-3')).not.toBeInTheDocument() const addBtn = screen.getByTestId('welcome-step-3-add-row') // Click 7 more times → 10 total. for (let i = 0; i < 7; i++) await user.click(addBtn) expect(screen.getByTestId('welcome-step-3-email-9')).toBeInTheDocument() // Capped — button disabled at 10. expect(addBtn).toBeDisabled() }) })