Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions __tests__/components/build/KickoffQuestions-growth.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { act } from 'react'

beforeAll(() => {
;(globalThis as any).React = React
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
})

vi.mock('next-auth/react', () => ({ useSession: () => ({ status: 'unauthenticated' }), getSession: vi.fn(async () => null) }))
vi.mock('@/components/analytics/google-analytics', () => ({ trackEvent: vi.fn() }))
vi.mock('@/lib/build/value-moment', () => ({ decideLimitAction: () => 'continue' }))
vi.mock('@/lib/build/anon-draft-token', () => ({ getOrCreateAnonDraftToken: () => 'tok-growth' }))
const dispatchMock = vi.fn()
vi.mock('@/contexts/build-context', () => ({
useBuild: () => ({
state: { track: 'company', idea: 'x', appSub: 'acme', companyName: 'Acme', brandTagline: '', brandColor: '#000', answers: {}, growthIntent: true },
dispatch: dispatchMock,
}),
}))

import { KickoffQuestions } from '@/components/build/screens/KickoffQuestions'
import { COMPANY_GROW_BUSINESS_QUESTIONS } from '@/lib/build/kickoff-questions'

let host: HTMLElement
let root: Root
function render(node: React.ReactElement) {
host = document.createElement('div')
document.body.appendChild(host)
root = createRoot(host)
act(() => { root.render(node) })
}
afterEach(() => {
act(() => { root?.unmount() })
host?.remove()
vi.clearAllMocks()
})

describe('KickoffQuestions — growth variant reachable (#BLD-10)', () => {
it('shows the first COMPANY_GROW_BUSINESS_QUESTIONS question when state.growthIntent is true', () => {
render(React.createElement(KickoffQuestions))
expect(host.textContent).toContain(COMPANY_GROW_BUSINESS_QUESTIONS[0].text)
})
})
49 changes: 49 additions & 0 deletions __tests__/components/build/Start.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { act } from 'react'

beforeAll(() => {
;(globalThis as any).React = React
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
})

const dispatchMock = vi.fn()
vi.mock('@/contexts/build-context', () => ({ useBuild: () => ({ dispatch: dispatchMock }) }))

import { Start } from '@/components/build/screens/Start'

let host: HTMLElement
let root: Root
function render(node: React.ReactElement) {
host = document.createElement('div')
document.body.appendChild(host)
root = createRoot(host)
act(() => { root.render(node) })
}
afterEach(() => {
act(() => { root?.unmount() })
host?.remove()
vi.clearAllMocks()
})

describe('Start — "Grow my company" reaches kickoff (#BLD-10)', () => {
it('picking "Grow my company" then Continue dispatches GOTO_SCREEN build AND SET_GROWTH_INTENT true', async () => {
render(React.createElement(Start))
const growBtn = host.querySelector('[data-testid="start-grow"]') as HTMLButtonElement
const continueBtn = host.querySelector('[data-testid="start-continue"]') as HTMLButtonElement
await act(async () => { growBtn.click() })
await act(async () => { continueBtn.click() })
expect(dispatchMock).toHaveBeenCalledWith({ type: 'SET_GROWTH_INTENT', value: true })
expect(dispatchMock).toHaveBeenCalledWith({ type: 'GOTO_SCREEN', screen: 'build' })
})

it('picking "Create a new company" then Continue dispatches SET_GROWTH_INTENT false', async () => {
render(React.createElement(Start))
const continueBtn = host.querySelector('[data-testid="start-continue"]') as HTMLButtonElement
await act(async () => { continueBtn.click() })
expect(dispatchMock).toHaveBeenCalledWith({ type: 'SET_GROWTH_INTENT', value: false })
expect(dispatchMock).toHaveBeenCalledWith({ type: 'GOTO_SCREEN', screen: 'build' })
})
})
19 changes: 19 additions & 0 deletions __tests__/lib/build/state-growth-intent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest'
import { buildReducer, initialBuildState } from '@/lib/build/state'

describe('growthIntent (#BLD-10)', () => {
it('defaults to false', () => {
expect(initialBuildState.growthIntent).toBe(false)
})

it('SET_GROWTH_INTENT sets the flag', () => {
const next = buildReducer(initialBuildState, { type: 'SET_GROWTH_INTENT', value: true })
expect(next.growthIntent).toBe(true)
})

it('SET_GROWTH_INTENT can clear the flag back to false', () => {
const grown = buildReducer(initialBuildState, { type: 'SET_GROWTH_INTENT', value: true })
const cleared = buildReducer(grown, { type: 'SET_GROWTH_INTENT', value: false })
expect(cleared.growthIntent).toBe(false)
})
})
2 changes: 1 addition & 1 deletion components/build/screens/KickoffQuestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function KickoffQuestions() {
const [index, setIndex] = useState(0)
const [freeText, setFreeText] = useState('')
const [proceeding, setProceeding] = useState(false)
const questions = getKickoffQuestions(state.track, false)
const questions = getKickoffQuestions(state.track, state.growthIntent ?? false)
const question = questions[index]

const answer = (value: string) => {
Expand Down
13 changes: 8 additions & 5 deletions components/build/screens/Start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
* Start — funnel step 1 (Claude Design handoff). "Let's get started." The visitor
* picks whether they're creating a new company or growing an existing one.
*
* Create a new company → step 2 (Build: "Let's build something")
* Grow my company → straight to auth (they already have a business)
* #BLD-10 — both paths now reach the same build/kickoff flow; growthIntent
* (set here) selects which kickoff question variant KickoffQuestions.tsx
* shows. The auth-wall decision is untouched — it stays in
* KickoffQuestions.tsx's proceed(), same as the "create" path already used.
*
* Selection is a two-card single-choice; Continue commits it. Matches the
* prototype's create/grow branch (Landing & Signup.dc.html).
Expand Down Expand Up @@ -35,9 +37,10 @@ export function Start() {

const goBuild = () => {
window.scrollTo(0, 0)
// "Grow my company" already has a business → go to auth. "Create" continues
// into the build-idea step.
dispatch({ type: 'GOTO_SCREEN', screen: path === 'create' ? 'build' : 'login' })
// #BLD-10 — both paths reach the same idea-entry/kickoff flow now;
// growthIntent tells KickoffQuestions.tsx which question variant to show.
dispatch({ type: 'SET_GROWTH_INTENT', value: path === 'grow' })
dispatch({ type: 'GOTO_SCREEN', screen: 'build' })
}

return (
Expand Down
5 changes: 5 additions & 0 deletions lib/build/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface BuildState {
conflictResolved: boolean
conflictView: string // the upstream artifact whose edit triggered the conflict ('' = none)
answers: { privacy?: PrivacyAnswer; [k: string]: string | undefined }
growthIntent: boolean // #BLD-10 — true when the visitor picked "Grow my company" on Start.tsx
companyName: string
appSub: string // staging subdomain, e.g. {appSub}.ainative.studio
// A ?company= deep link whose slug turned out not to resolve to any real,
Expand Down Expand Up @@ -198,6 +199,7 @@ export const initialBuildState: BuildState = {
conflictResolved: false,
conflictView: '',
answers: {},
growthIntent: false,
companyName: '',
appSub: '',
deepLinkNotFound: null,
Expand Down Expand Up @@ -240,6 +242,7 @@ export type BuildAction =
// before kickoff's own 3rd question has been answered. Pure data, no
// side effects — mirrors SET_IDEA's own shape.
| { type: 'SET_BRAND_DRAFT'; appSub: string; companyName: string; brandTagline: string; brandColor: string }
| { type: 'SET_GROWTH_INTENT'; value: boolean }
| { type: 'GEN_DONE'; view: string; content: unknown }
| { type: 'GEN_FAIL'; view: string; error: string }
/** Inline artifact edit (GR-16 #329): replace a view's generated content with
Expand Down Expand Up @@ -407,6 +410,8 @@ export function buildReducer(state: BuildState, action: BuildAction): BuildState
brandTagline: action.brandTagline,
brandColor: action.brandColor,
}
case 'SET_GROWTH_INTENT':
return { ...state, growthIntent: action.value }
case 'GEN_DONE':
return {
...state,
Expand Down
Loading