Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import { NextStepCard, pickNextStep } from '../NextStepCard'
|
|
import { useBillingStore } from '@/store/billingStore'
|
|
import type { OnboardingStatus } from '@/api/onboarding'
|
|
|
|
vi.mock('@/api/onboarding', () => {
|
|
const mockGet = vi.fn()
|
|
const mockDismiss = vi.fn()
|
|
return {
|
|
getOnboardingStatus: mockGet,
|
|
dismissOnboarding: mockDismiss,
|
|
}
|
|
})
|
|
|
|
import {
|
|
getOnboardingStatus as _getOnboardingStatus,
|
|
} from '@/api/onboarding'
|
|
|
|
const getOnboardingStatus = _getOnboardingStatus as unknown as ReturnType<typeof vi.fn>
|
|
|
|
function makeStatus(overrides: Partial<OnboardingStatus> = {}): OnboardingStatus {
|
|
return {
|
|
created_flow: false,
|
|
ran_session: false,
|
|
exported_session: false,
|
|
tried_ai_assistant: false,
|
|
invited_teammate: false,
|
|
connected_psa: false,
|
|
is_team_user: false,
|
|
dismissed: false,
|
|
email_verified: false,
|
|
shop_setup_done: false,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function renderWithRouter(ui: React.ReactElement) {
|
|
return render(<BrowserRouter>{ui}</BrowserRouter>)
|
|
}
|
|
|
|
function setBillingComplimentary() {
|
|
// 'complimentary' status -> stage 'complimentary' (not in plan-gate set), so the
|
|
// "Pick a plan" item stays hidden — perfect default for unrelated tests.
|
|
useBillingStore.setState({
|
|
subscription: {
|
|
status: 'complimentary',
|
|
plan: 'pro',
|
|
current_period_start: '2026-05-01T00:00:00Z',
|
|
current_period_end: null,
|
|
cancel_at_period_end: false,
|
|
seat_limit: null,
|
|
has_pro_entitlement: true,
|
|
is_paid: true,
|
|
},
|
|
planBilling: null,
|
|
planLimits: {},
|
|
enabledFeatures: {},
|
|
isLoading: false,
|
|
error: null,
|
|
})
|
|
}
|
|
|
|
describe('NextStepCard', () => {
|
|
beforeEach(() => {
|
|
getOnboardingStatus.mockReset()
|
|
setBillingComplimentary()
|
|
})
|
|
|
|
it('renders Verify your email when email unverified', async () => {
|
|
getOnboardingStatus.mockResolvedValue(makeStatus({ email_verified: false }))
|
|
renderWithRouter(<NextStepCard />)
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('next-step-card')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByRole('heading', { name: /Verify your email/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders Set up your shop after email verified', async () => {
|
|
getOnboardingStatus.mockResolvedValue(
|
|
makeStatus({ email_verified: true, shop_setup_done: false }),
|
|
)
|
|
renderWithRouter(<NextStepCard />)
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /Set up your shop/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders Run your first FlowPilot session after shop setup', async () => {
|
|
getOnboardingStatus.mockResolvedValue(
|
|
makeStatus({
|
|
email_verified: true,
|
|
shop_setup_done: true,
|
|
ran_session: false,
|
|
}),
|
|
)
|
|
renderWithRouter(<NextStepCard />)
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('heading', { name: /Run your first FlowPilot session/i }),
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('hidden when all items done', async () => {
|
|
getOnboardingStatus.mockResolvedValue(
|
|
makeStatus({
|
|
email_verified: true,
|
|
shop_setup_done: true,
|
|
ran_session: true,
|
|
connected_psa: true,
|
|
invited_teammate: true,
|
|
}),
|
|
)
|
|
const { container } = renderWithRouter(<NextStepCard />)
|
|
// Resolve the awaited promise.
|
|
await waitFor(() => expect(getOnboardingStatus).toHaveBeenCalled())
|
|
expect(container.querySelector('[data-testid="next-step-card"]')).toBeNull()
|
|
})
|
|
|
|
it('hidden when onboarding_dismissed', async () => {
|
|
getOnboardingStatus.mockResolvedValue(makeStatus({ dismissed: true }))
|
|
const { container } = renderWithRouter(<NextStepCard />)
|
|
await waitFor(() => expect(getOnboardingStatus).toHaveBeenCalled())
|
|
expect(container.querySelector('[data-testid="next-step-card"]')).toBeNull()
|
|
})
|
|
|
|
it('Pick a plan item appears when trial stage is warning or later', () => {
|
|
// Direct unit-test on the pure picker — easier than coordinating both the
|
|
// billing store + the network mock + a fake clock for stage='warning'.
|
|
const allDoneExceptPlan = makeStatus({
|
|
email_verified: true,
|
|
shop_setup_done: true,
|
|
ran_session: true,
|
|
connected_psa: true,
|
|
invited_teammate: true,
|
|
})
|
|
|
|
expect(pickNextStep(allDoneExceptPlan, 'pristine')).toBeNull()
|
|
expect(pickNextStep(allDoneExceptPlan, 'paid')).toBeNull()
|
|
expect(pickNextStep(allDoneExceptPlan, 'complimentary')).toBeNull()
|
|
|
|
expect(pickNextStep(allDoneExceptPlan, 'warning')?.key).toBe('pick_plan')
|
|
expect(pickNextStep(allDoneExceptPlan, 'urgent')?.key).toBe('pick_plan')
|
|
expect(pickNextStep(allDoneExceptPlan, 'expired')?.key).toBe('pick_plan')
|
|
})
|
|
})
|