feat: self-serve signup Phase 2 (frontend cutover) (#162)
Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
This commit was merged in pull request #162.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user