| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- import { act, renderHook } from '@testing-library/react'
- import { describe, expect, test, vi } from 'vitest'
- import { useConnectState } from './useConnectState'
- vi.mock('common', () => ({
- useParams: () => ({ ref: 'test-ref' }),
- }))
- vi.mock('@/data/read-replicas/replicas-query', () => ({
- useReadReplicasQuery: () => ({ data: [] }),
- }))
- vi.mock('@/hooks/misc/useCheckEntitlements', () => ({
- useCheckEntitlements: vi.fn().mockImplementation(() => ({ hasAccess: true })),
- }))
- vi.mock('@/hooks/misc/useSelectedProject', () => ({
- useIsHighAvailability: vi.fn().mockImplementation(() => false),
- }))
- describe('useConnectState', () => {
- // ============================================================================
- // Initial State Tests
- // ============================================================================
- describe('initial state', () => {
- test('should initialize with framework mode by default', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.state.mode).toBe('framework')
- })
- test('should initialize with nextjs as default framework', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.state.framework).toBe('nextjs')
- })
- test('should initialize with app variant for nextjs', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.state.frameworkVariant).toBe('app')
- })
- test('should initialize with brivenjs library', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.state.library).toBe('brivenjs')
- })
- test('should accept initial state override', () => {
- const { result } = renderHook(() =>
- useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
- )
- expect(result.current.state.mode).toBe('direct')
- expect(result.current.state.connectionMethod).toBe('transaction')
- })
- test('should merge initial state with defaults', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'react' }))
- expect(result.current.state.mode).toBe('framework')
- expect(result.current.state.framework).toBe('react')
- })
- })
- // ============================================================================
- // Mode Switching Tests
- // ============================================================================
- describe('setMode', () => {
- test('should switch to direct mode', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.setMode('direct')
- })
- expect(result.current.state.mode).toBe('direct')
- })
- test('should initialize direct mode defaults when switching', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.setMode('direct')
- })
- expect(result.current.state.connectionMethod).toBeDefined()
- expect(result.current.state.connectionType).toBeDefined()
- })
- test('should switch to orm mode and initialize defaults', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.setMode('orm')
- })
- expect(result.current.state.mode).toBe('orm')
- expect(result.current.state.orm).toBe('prisma')
- })
- test('should switch to mcp mode and initialize defaults', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.setMode('mcp')
- })
- expect(result.current.state.mode).toBe('mcp')
- expect(result.current.state.mcpClient).toBeDefined()
- })
- test('should preserve framework state when switching back to framework mode', () => {
- const { result } = renderHook(() => useConnectState())
- // Change framework
- act(() => {
- result.current.updateField('framework', 'react')
- })
- // Switch to direct
- act(() => {
- result.current.setMode('direct')
- })
- // Switch back to framework
- act(() => {
- result.current.setMode('framework')
- })
- expect(result.current.state.framework).toBe('react')
- })
- })
- // ============================================================================
- // Field Update Tests
- // ============================================================================
- describe('updateField', () => {
- test('should update framework selection', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.updateField('framework', 'react')
- })
- expect(result.current.state.framework).toBe('react')
- })
- test('should cascade variant reset when changing framework', () => {
- const { result } = renderHook(() => useConnectState())
- // Start with nextjs which has variants
- expect(result.current.state.frameworkVariant).toBe('app')
- // Switch to a framework with multiple variants
- act(() => {
- result.current.updateField('framework', 'react')
- })
- // Should have the first variant of react
- expect(result.current.state.frameworkVariant).toBeDefined()
- })
- test('should remove variant when switching to framework without variants', () => {
- const { result } = renderHook(() => useConnectState())
- // Start with nextjs which has variants
- expect(result.current.state.frameworkVariant).toBe('app')
- // Switch to remix which has no variants
- act(() => {
- result.current.updateField('framework', 'remix')
- })
- expect(result.current.state.frameworkVariant).toBeUndefined()
- })
- test('should update library when variant changes', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.updateField('frameworkVariant', 'pages')
- })
- expect(result.current.state.library).toBe('brivenjs')
- })
- test('should update connection method', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- act(() => {
- result.current.updateField('connectionMethod', 'transaction')
- })
- expect(result.current.state.connectionMethod).toBe('transaction')
- })
- test('should clear useSharedPooler when connectionMethod changes to direct', () => {
- const { result } = renderHook(() =>
- useConnectState({
- mode: 'direct',
- connectionMethod: 'transaction',
- useSharedPooler: true,
- })
- )
- act(() => {
- result.current.updateField('connectionMethod', 'direct')
- })
- // useSharedPooler is cleared because it depends on connectionMethod: ['transaction']
- // When the dependency is not satisfied, the field is removed from state
- expect(result.current.state.useSharedPooler).toBeUndefined()
- })
- test('should update MCP client', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
- act(() => {
- result.current.updateField('mcpClient', 'codex')
- })
- expect(result.current.state.mcpClient).toBe('codex')
- })
- test('should update boolean fields', () => {
- const { result } = renderHook(() => useConnectState())
- act(() => {
- result.current.updateField('frameworkUi', true)
- })
- expect(result.current.state.frameworkUi).toBe(true)
- })
- test('should update ORM selection', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
- act(() => {
- result.current.updateField('orm', 'drizzle')
- })
- expect(result.current.state.orm).toBe('drizzle')
- })
- })
- // ============================================================================
- // Active Fields Tests
- // ============================================================================
- describe('activeFields', () => {
- test('should return framework mode fields', () => {
- const { result } = renderHook(() => useConnectState())
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('framework')
- })
- test('should include variant field for nextjs', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('frameworkVariant')
- })
- test('should include frameworkUi field for nextjs', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('frameworkUi')
- })
- test('should not include frameworkUi for non-nextjs/react frameworks', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'remix' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).not.toContain('frameworkUi')
- })
- test('should return direct mode fields', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('connectionMethod')
- expect(fieldIds).toContain('connectionType')
- })
- test('should show useSharedPooler only for transaction connection method when user has dedicated_pooler entitlement', async () => {
- const { useCheckEntitlements } = await import('@/hooks/misc/useCheckEntitlements')
- vi.mocked(useCheckEntitlements).mockReturnValue({ hasAccess: true } as any)
- const { result } = renderHook(() =>
- useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
- )
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('useSharedPooler')
- })
- test('should hide useSharedPooler even if using transaction method when user lacks dedicated_pooler entitlement', async () => {
- const { useCheckEntitlements } = await import('@/hooks/misc/useCheckEntitlements')
- vi.mocked(useCheckEntitlements).mockReturnValue({ hasAccess: false } as any)
- const { result } = renderHook(() =>
- useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
- )
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).not.toContain('useSharedPooler')
- })
- test('should hide useSharedPooler for direct connection method', () => {
- const { result } = renderHook(() =>
- useConnectState({ mode: 'direct', connectionMethod: 'direct' })
- )
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).not.toContain('useSharedPooler')
- })
- test('should return orm mode fields', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('orm')
- })
- test('should return mcp mode fields', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('mcpClient')
- expect(fieldIds).toContain('mcpReadonly')
- })
- })
- // ============================================================================
- // Resolved Steps Tests
- // ============================================================================
- describe('resolvedSteps', () => {
- test('should resolve steps for framework mode', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
- })
- test('should have install step for framework mode', () => {
- const { result } = renderHook(() => useConnectState())
- const stepIds = result.current.resolvedSteps.map((s) => s.id)
- expect(stepIds).toContain('install')
- })
- test('should resolve different steps for mcp mode', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
- const stepIds = result.current.resolvedSteps.map((s) => s.id)
- // MCP mode (defaults to claude-code) should have claude-add-server step
- expect(stepIds.some((id) => id.includes('claude') || id.includes('mcp'))).toBe(true)
- })
- test('should resolve different steps for different mcp clients', () => {
- const { result: cursorResult } = renderHook(() =>
- useConnectState({ mode: 'mcp', mcpClient: 'cursor' })
- )
- const { result: codexResult } = renderHook(() =>
- useConnectState({ mode: 'mcp', mcpClient: 'codex' })
- )
- // Codex has more steps than cursor
- expect(codexResult.current.resolvedSteps.length).toBeGreaterThanOrEqual(
- cursorResult.current.resolvedSteps.length
- )
- })
- test('should include skills install step', () => {
- const { result } = renderHook(() => useConnectState())
- const stepIds = result.current.resolvedSteps.map((s) => s.id)
- expect(stepIds).toContain('install-skills')
- })
- test('should resolve steps for direct mode', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
- })
- test('should resolve steps for orm mode', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
- expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
- const stepIds = result.current.resolvedSteps.map((s) => s.id)
- expect(stepIds).toContain('install')
- expect(stepIds).toContain('configure')
- })
- test('should resolve shadcn steps when frameworkUi is true', () => {
- const { result } = renderHook(() =>
- useConnectState({ framework: 'nextjs', frameworkUi: true })
- )
- const stepIds = result.current.resolvedSteps.map((s) => s.id)
- expect(stepIds).toContain('shadcn-add')
- expect(stepIds).toContain('shadcn-env')
- })
- })
- // ============================================================================
- // Field Options Tests
- // ============================================================================
- describe('getFieldOptions', () => {
- test('should return framework options', () => {
- const { result } = renderHook(() => useConnectState())
- const options = result.current.getFieldOptions('framework')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'nextjs')).toBe(true)
- expect(options.some((o) => o.value === 'react')).toBe(true)
- })
- test('should return variant options for nextjs', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
- const options = result.current.getFieldOptions('frameworkVariant')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'app')).toBe(true)
- expect(options.some((o) => o.value === 'pages')).toBe(true)
- })
- test('should return empty variant options for frameworks without variants', () => {
- const { result } = renderHook(() => useConnectState({ framework: 'remix' }))
- const options = result.current.getFieldOptions('frameworkVariant')
- expect(options).toEqual([])
- })
- test('should return connection method options', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const options = result.current.getFieldOptions('connectionMethod')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'direct')).toBe(true)
- expect(options.some((o) => o.value === 'transaction')).toBe(true)
- })
- test('should return connection type options', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const options = result.current.getFieldOptions('connectionType')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'uri')).toBe(true)
- expect(options.some((o) => o.value === 'psql')).toBe(true)
- })
- test('should return ORM options', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
- const options = result.current.getFieldOptions('orm')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'prisma')).toBe(true)
- expect(options.some((o) => o.value === 'drizzle')).toBe(true)
- })
- test('should return MCP client options', () => {
- const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
- const options = result.current.getFieldOptions('mcpClient')
- expect(options.length).toBeGreaterThan(0)
- expect(options.some((o) => o.value === 'cursor')).toBe(true)
- })
- test('should return empty array for unknown field', () => {
- const { result } = renderHook(() => useConnectState())
- const options = result.current.getFieldOptions('unknownField')
- expect(options).toEqual([])
- })
- test('should return library options for selected framework', () => {
- const { result } = renderHook(() =>
- useConnectState({ framework: 'nextjs', frameworkVariant: 'app' })
- )
- const options = result.current.getFieldOptions('library')
- expect(options.length).toBeGreaterThan(0)
- })
- })
- // ============================================================================
- // High Availability Tests
- // ============================================================================
- describe('high availability projects', () => {
- test('should hide connectionMethod field for HA projects', async () => {
- const { useIsHighAvailability } = await import('@/hooks/misc/useSelectedProject')
- vi.mocked(useIsHighAvailability).mockReturnValue(true)
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).not.toContain('connectionMethod')
- })
- test('should hide useSharedPooler field for HA projects', async () => {
- const { useIsHighAvailability } = await import('@/hooks/misc/useSelectedProject')
- vi.mocked(useIsHighAvailability).mockReturnValue(true)
- const { result } = renderHook(() =>
- useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
- )
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).not.toContain('useSharedPooler')
- })
- test('should rename connectionType label to "Connection Type" for HA projects', async () => {
- const { useIsHighAvailability } = await import('@/hooks/misc/useSelectedProject')
- vi.mocked(useIsHighAvailability).mockReturnValue(true)
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const connectionTypeField = result.current.activeFields.find((f) => f.id === 'connectionType')
- expect(connectionTypeField?.label).toBe('Connection Type')
- })
- test('should not affect non-HA projects', async () => {
- const { useIsHighAvailability } = await import('@/hooks/misc/useSelectedProject')
- vi.mocked(useIsHighAvailability).mockReturnValue(false)
- const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
- const fieldIds = result.current.activeFields.map((f) => f.id)
- expect(fieldIds).toContain('connectionMethod')
- expect(fieldIds).toContain('connectionType')
- const connectionTypeField = result.current.activeFields.find((f) => f.id === 'connectionType')
- expect(connectionTypeField?.label).toBe('Type')
- })
- })
- // ============================================================================
- // Schema Access Tests
- // ============================================================================
- describe('schema', () => {
- test('should expose the connect schema', () => {
- const { result } = renderHook(() => useConnectState())
- expect(result.current.schema).toBeDefined()
- expect(result.current.schema.modes).toBeDefined()
- expect(result.current.schema.fields).toBeDefined()
- expect(result.current.schema.steps).toBeDefined()
- })
- test('should have all expected modes in schema', () => {
- const { result } = renderHook(() => useConnectState())
- const modeIds = result.current.schema.modes.map((m) => m.id)
- expect(modeIds).toContain('framework')
- expect(modeIds).toContain('direct')
- expect(modeIds).toContain('orm')
- expect(modeIds).toContain('mcp')
- })
- })
- })
|