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 { WelcomeStep1 } from '../WelcomeStep1' import { useAuthStore } from '@/store/authStore' import { onboardingApi } from '@/api/onboarding' 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(), }, } }) 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: null, 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( } /> step-2} /> dashboard} /> , ) } describe('WelcomeStep1', () => { beforeEach(() => { useAuthStore.setState({ user: makeUser(), account: makeAccount(), subscription: null, token: null, isAuthenticated: true, // Stub fetchUser so it doesn't try to hit the network in jsdom. fetchUser: vi.fn().mockResolvedValue(undefined), }) vi.mocked(onboardingApi.updateStep).mockResolvedValue({ onboarding_step_completed: 1, onboarding_dismissed: false, }) vi.mocked(onboardingApi.dismissRest).mockResolvedValue({ onboarding_step_completed: null, onboarding_dismissed: true, }) }) afterEach(() => { vi.clearAllMocks() }) it('pre-fills the company name from the auth store account', () => { renderPage() const input = screen.getByTestId('welcome-step-1-company-name') as HTMLInputElement expect(input.value).toBe('Acme MSP') }) it('Continue persists data and navigates to /welcome/step-2', async () => { const user = userEvent.setup() renderPage() const teamSize = screen.getByTestId('welcome-step-1-team-size') as HTMLSelectElement await user.selectOptions(teamSize, '3-5') const role = screen.getByTestId('welcome-step-1-role') as HTMLSelectElement await user.selectOptions(role, 'owner') await user.click(screen.getByTestId('welcome-step-1-continue')) await waitFor(() => { expect(onboardingApi.updateStep).toHaveBeenCalledWith({ step: 1, action: 'complete', data: { company_name: 'Acme MSP', team_size_bucket: '3-5', role_at_signup: 'owner', }, }) }) await waitFor(() => { expect(screen.getByText('step-2')).toBeInTheDocument() }) }) it('Skip this step calls updateStep with action=skip and navigates to /welcome/step-2', async () => { const user = userEvent.setup() renderPage() await user.click(screen.getByTestId('welcome-step-1-skip')) await waitFor(() => { expect(onboardingApi.updateStep).toHaveBeenCalledWith({ step: 1, action: 'skip', }) }) await waitFor(() => { expect(screen.getByText('step-2')).toBeInTheDocument() }) }) it('Skip-the-rest dismisses and navigates to /home', async () => { const user = userEvent.setup() renderPage() const dismiss = screen.getByTestId('welcome-step-1-dismiss-rest') // Sanity check: it's a quiet text link, not a primary button. expect(dismiss.className).toMatch(/text-muted-foreground/) expect(dismiss.className).toMatch(/hover:underline/) expect(dismiss.className).toMatch(/text-xs/) expect(dismiss.className).not.toMatch(/bg-primary/) await user.click(dismiss) await waitFor(() => { expect(onboardingApi.dismissRest).toHaveBeenCalled() }) await waitFor(() => { expect(screen.getByText('dashboard')).toBeInTheDocument() }) }) it('shows an error when the persist call fails and stays on the page', async () => { vi.mocked(onboardingApi.updateStep).mockRejectedValueOnce( new Error('boom'), ) const user = userEvent.setup() renderPage() await user.click(screen.getByTestId('welcome-step-1-continue')) await waitFor(() => { expect(screen.getByTestId('welcome-step-1-error')).toBeInTheDocument() }) // Should not have navigated. expect(screen.queryByText('step-2')).not.toBeInTheDocument() }) })