| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- import { describe, expect, test } from 'vitest'
- import {
- getActiveFields,
- getDefaultState,
- resetDependentFields,
- resolveConditional,
- resolveSteps,
- } from './connect.resolver'
- import type { ConditionalValue, ConnectSchema, ConnectState, StepTree } from './Connect.types'
- // ============================================================================
- // resolveConditional Tests
- // ============================================================================
- describe('connect.resolver:resolveConditional', () => {
- test('should return primitive values directly', () => {
- expect(resolveConditional('hello', { mode: 'framework' })).toBe('hello')
- expect(resolveConditional(42, { mode: 'framework' })).toBe(42)
- expect(resolveConditional(null, { mode: 'framework' })).toBe(null)
- expect(resolveConditional(true, { mode: 'framework' })).toBe(true)
- })
- test('should return arrays directly', () => {
- const arr = ['a', 'b', 'c']
- expect(resolveConditional(arr, { mode: 'framework' })).toBe(arr)
- })
- test('should resolve single-level conditional based on mode', () => {
- const conditional: ConditionalValue<string> = {
- framework: 'Framework Content',
- direct: 'Direct Content',
- DEFAULT: 'Default Content',
- }
- expect(resolveConditional(conditional, { mode: 'framework' })).toBe('Framework Content')
- expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content')
- expect(resolveConditional(conditional, { mode: 'orm' })).toBe('Default Content')
- })
- test('should resolve nested conditionals (mode -> framework)', () => {
- const conditional: ConditionalValue<string> = {
- framework: {
- nextjs: 'Next.js Content',
- react: 'React Content',
- DEFAULT: 'Generic Framework',
- },
- direct: 'Direct Content',
- }
- expect(resolveConditional(conditional, { mode: 'framework', framework: 'nextjs' })).toBe(
- 'Next.js Content'
- )
- expect(resolveConditional(conditional, { mode: 'framework', framework: 'react' })).toBe(
- 'React Content'
- )
- expect(resolveConditional(conditional, { mode: 'framework', framework: 'vue' })).toBe(
- 'Generic Framework'
- )
- expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content')
- })
- test('should resolve deeply nested conditionals (mode -> framework -> variant)', () => {
- const conditional: ConditionalValue<string> = {
- framework: {
- nextjs: {
- app: 'App Router Content',
- pages: 'Pages Router Content',
- DEFAULT: 'Generic Next.js',
- },
- DEFAULT: 'Generic Framework',
- },
- }
- expect(
- resolveConditional(conditional, {
- mode: 'framework',
- framework: 'nextjs',
- frameworkVariant: 'app',
- })
- ).toBe('App Router Content')
- expect(
- resolveConditional(conditional, {
- mode: 'framework',
- framework: 'nextjs',
- frameworkVariant: 'pages',
- })
- ).toBe('Pages Router Content')
- expect(
- resolveConditional(conditional, {
- mode: 'framework',
- framework: 'nextjs',
- frameworkVariant: 'unknown',
- })
- ).toBe('Generic Next.js')
- })
- test('should resolve MCP client-specific content', () => {
- const conditional: ConditionalValue<string> = {
- mcp: {
- cursor: 'Cursor Config',
- codex: 'Codex Config',
- 'claude-code': 'Claude Code Config',
- DEFAULT: 'Generic MCP',
- },
- }
- expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'cursor' })).toBe(
- 'Cursor Config'
- )
- expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'codex' })).toBe(
- 'Codex Config'
- )
- expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'claude-code' })).toBe(
- 'Claude Code Config'
- )
- })
- test('should return undefined when no match and no DEFAULT', () => {
- const conditional: ConditionalValue<string> = {
- framework: 'Framework',
- direct: 'Direct',
- }
- expect(resolveConditional(conditional, { mode: 'orm' })).toBe(undefined)
- })
- test('should handle frameworkUi boolean state (as string "true"/"false")', () => {
- const conditional: ConditionalValue<string> = {
- framework: {
- nextjs: {
- true: 'Shadcn Steps',
- DEFAULT: 'Regular Steps',
- },
- },
- }
- // When frameworkUi is true (boolean), it gets converted to string "true"
- expect(
- resolveConditional(conditional, { mode: 'framework', framework: 'nextjs', frameworkUi: true })
- ).toBe('Shadcn Steps')
- expect(
- resolveConditional(conditional, {
- mode: 'framework',
- framework: 'nextjs',
- frameworkUi: false,
- })
- ).toBe('Regular Steps')
- })
- })
- // ============================================================================
- // resolveSteps Tests
- // ============================================================================
- describe('connect.resolver:resolveSteps', () => {
- const createMockSchema = (steps: StepTree): ConnectSchema => ({
- modes: [
- { id: 'framework', label: 'Framework', description: '', fields: [] },
- { id: 'direct', label: 'Direct', description: '', fields: [] },
- ],
- fields: {},
- steps,
- })
- test('should resolve steps array for framework mode', () => {
- const schema = createMockSchema({
- mode: {
- framework: [
- { id: 'step1', title: 'Install', description: 'Install pkg', content: 'install-content' },
- { id: 'step2', title: 'Configure', description: 'Configure', content: 'config-content' },
- ],
- direct: [
- {
- id: 'connection',
- title: 'Connection',
- description: 'Connect',
- content: 'direct-content',
- },
- ],
- },
- })
- const steps = resolveSteps(schema, { mode: 'framework' })
- expect(steps).toHaveLength(2)
- expect(steps[0].id).toBe('step1')
- expect(steps[0].title).toBe('Install')
- expect(steps[1].id).toBe('step2')
- })
- test('should resolve steps for direct mode', () => {
- const schema = createMockSchema({
- mode: {
- framework: [{ id: 'step1', title: 'Install', description: 'Install', content: 'install' }],
- direct: [
- { id: 'connection', title: 'Connection', description: 'Connect', content: 'direct' },
- ],
- },
- })
- const steps = resolveSteps(schema, { mode: 'direct' })
- expect(steps).toHaveLength(1)
- expect(steps[0].id).toBe('connection')
- })
- test('should filter out steps with empty content', () => {
- const schema = createMockSchema({
- mode: {
- framework: [
- { id: 'step1', title: 'Valid', description: 'Valid step', content: 'valid-content' },
- { id: 'step2', title: 'Empty', description: 'Empty step', content: '' },
- { id: 'step3', title: 'Null', description: 'Null step', content: null },
- ],
- },
- })
- const steps = resolveSteps(schema, { mode: 'framework' })
- expect(steps).toHaveLength(1)
- expect(steps[0].id).toBe('step1')
- })
- test('should resolve conditional step content based on state', () => {
- const schema = createMockSchema({
- mode: {
- framework: [
- {
- id: 'configure',
- title: 'Configure',
- description: 'Configure',
- content: {
- nextjs: 'nextjs-content',
- react: 'react-content',
- DEFAULT: 'generic-content',
- },
- },
- ],
- },
- })
- const nextjsSteps = resolveSteps(schema, { mode: 'framework', framework: 'nextjs' })
- expect(nextjsSteps[0].content).toBe('nextjs-content')
- const reactSteps = resolveSteps(schema, { mode: 'framework', framework: 'react' })
- expect(reactSteps[0].content).toBe('react-content')
- const vueSteps = resolveSteps(schema, { mode: 'framework', framework: 'vue' })
- expect(vueSteps[0].content).toBe('generic-content')
- })
- test('should return empty array when no steps resolve', () => {
- const schema = createMockSchema({
- mode: {
- framework: [{ id: 'step1', title: 'Step', description: 'Desc', content: '' }],
- },
- })
- const steps = resolveSteps(schema, { mode: 'framework' })
- expect(steps).toEqual([])
- })
- test('should return empty array when steps is not an array', () => {
- const schema = createMockSchema({
- mode: {
- framework: {
- framework: {},
- },
- },
- } as any)
- const steps = resolveSteps(schema, { mode: 'framework' })
- expect(steps).toEqual([])
- })
- test('should append steps from multiple field conditions in order', () => {
- const schema = createMockSchema({
- mode: {
- framework: {
- framework: {
- nextjs: [{ id: 'base', title: 'Base', description: 'Base step', content: 'base' }],
- },
- frameworkUi: {
- true: [{ id: 'ui', title: 'UI', description: 'UI step', content: 'ui' }],
- },
- },
- },
- })
- const steps = resolveSteps(schema, {
- mode: 'framework',
- framework: 'nextjs',
- frameworkUi: true,
- })
- expect(steps.map((step) => step.id)).toEqual(['base', 'ui'])
- })
- })
- // ============================================================================
- // getActiveFields Tests
- // ============================================================================
- describe('connect.resolver:getActiveFields', () => {
- const createSchemaWithFields = (
- modes: ConnectSchema['modes'],
- fields: ConnectSchema['fields']
- ): ConnectSchema => ({
- modes,
- fields,
- steps: [],
- })
- test('should return fields for the current mode', () => {
- const schema = createSchemaWithFields(
- [
- { id: 'framework', label: 'Framework', description: '', fields: ['framework', 'library'] },
- { id: 'direct', label: 'Direct', description: '', fields: ['connectionType'] },
- ],
- {
- framework: {
- id: 'framework',
- type: 'radio-grid',
- label: 'Framework',
- defaultValue: 'nextjs',
- },
- library: {
- id: 'library',
- type: 'select',
- label: 'Library',
- defaultValue: 'brivenjs',
- },
- connectionType: {
- id: 'connectionType',
- type: 'select',
- label: 'Type',
- defaultValue: 'uri',
- },
- }
- )
- const frameworkFields = getActiveFields(schema, { mode: 'framework' })
- expect(frameworkFields).toHaveLength(2)
- expect(frameworkFields.map((f) => f.id)).toEqual(['framework', 'library'])
- const directFields = getActiveFields(schema, { mode: 'direct' })
- expect(directFields).toHaveLength(1)
- expect(directFields[0].id).toBe('connectionType')
- })
- test('should filter fields by dependsOn conditions', () => {
- const schema = createSchemaWithFields(
- [
- {
- id: 'framework',
- label: 'Framework',
- description: '',
- fields: ['framework', 'frameworkVariant', 'frameworkUi'],
- },
- ],
- {
- framework: {
- id: 'framework',
- type: 'radio-grid',
- label: 'Framework',
- defaultValue: 'nextjs',
- },
- frameworkVariant: {
- id: 'frameworkVariant',
- type: 'select',
- label: 'Variant',
- dependsOn: { framework: ['nextjs', 'react'] },
- },
- frameworkUi: {
- id: 'frameworkUi',
- type: 'switch',
- label: 'Shadcn',
- dependsOn: { framework: ['nextjs', 'react'] },
- },
- }
- )
- // With nextjs - should show all fields
- const nextjsFields = getActiveFields(schema, { mode: 'framework', framework: 'nextjs' })
- expect(nextjsFields).toHaveLength(3)
- // With vue - should hide frameworkVariant and frameworkUi
- const vueFields = getActiveFields(schema, { mode: 'framework', framework: 'vue' })
- expect(vueFields).toHaveLength(1)
- expect(vueFields[0].id).toBe('framework')
- })
- test('should handle multiple dependsOn conditions', () => {
- const schema = createSchemaWithFields(
- [
- {
- id: 'direct',
- label: 'Direct',
- description: '',
- fields: ['connectionMethod', 'useSharedPooler'],
- },
- ],
- {
- connectionMethod: {
- id: 'connectionMethod',
- type: 'radio-list',
- label: 'Method',
- defaultValue: 'direct',
- },
- useSharedPooler: {
- id: 'useSharedPooler',
- type: 'switch',
- label: 'Use Shared Pooler',
- dependsOn: { connectionMethod: ['transaction'] },
- },
- }
- )
- // Transaction mode - show shared pooler option
- const transactionFields = getActiveFields(schema, {
- mode: 'direct',
- connectionMethod: 'transaction',
- })
- expect(transactionFields).toHaveLength(2)
- // Direct mode - hide shared pooler option
- const directFields = getActiveFields(schema, { mode: 'direct', connectionMethod: 'direct' })
- expect(directFields).toHaveLength(1)
- expect(directFields[0].id).toBe('connectionMethod')
- })
- test('should return empty array for invalid mode', () => {
- const schema = createSchemaWithFields(
- [{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
- { framework: { id: 'framework', type: 'radio-grid', label: 'Framework' } }
- )
- const fields = getActiveFields(schema, { mode: 'invalid' as any })
- expect(fields).toEqual([])
- })
- test('should include resolvedOptions for each field', () => {
- const schema = createSchemaWithFields(
- [{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
- {
- framework: {
- id: 'framework',
- type: 'radio-grid',
- label: 'Framework',
- options: { source: 'frameworks' }, // Source reference - resolved elsewhere
- },
- }
- )
- const fields = getActiveFields(schema, { mode: 'framework' })
- expect(fields[0]).toHaveProperty('resolvedOptions')
- // Source options are resolved by the hook, not the resolver
- expect(fields[0].resolvedOptions).toEqual([])
- })
- })
- // ============================================================================
- // getDefaultState Tests
- // ============================================================================
- describe('connect.resolver:getDefaultState', () => {
- test('should return default state with first mode', () => {
- const schema: ConnectSchema = {
- modes: [
- { id: 'framework', label: 'Framework', description: '', fields: [] },
- { id: 'direct', label: 'Direct', description: '', fields: [] },
- ],
- fields: {},
- steps: [],
- }
- const state = getDefaultState({ schema })
- expect(state.mode).toBe('framework')
- })
- test('should include default values from fields', () => {
- const schema: ConnectSchema = {
- modes: [{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
- fields: {
- framework: {
- id: 'framework',
- type: 'radio-grid',
- label: 'Framework',
- defaultValue: 'nextjs',
- },
- library: {
- id: 'library',
- type: 'select',
- label: 'Library',
- defaultValue: 'brivenjs',
- },
- mcpReadonly: {
- id: 'mcpReadonly',
- type: 'switch',
- label: 'Readonly',
- defaultValue: false,
- },
- },
- steps: [],
- }
- const state = getDefaultState({ schema })
- expect(state.framework).toBe('nextjs')
- expect(state.library).toBe('brivenjs')
- expect(state.mcpReadonly).toBe(false)
- })
- test('should fallback to "direct" if no modes defined', () => {
- const schema: ConnectSchema = {
- modes: [],
- fields: {},
- steps: [],
- }
- const state = getDefaultState({ schema })
- expect(state.mode).toBe('direct')
- })
- })
- // ============================================================================
- // resetDependentFields Tests
- // ============================================================================
- describe('connect.resolver:resetDependentFields', () => {
- const createSchemaForReset = (): ConnectSchema => ({
- modes: [
- {
- id: 'framework',
- label: 'Framework',
- description: '',
- fields: ['framework', 'frameworkVariant', 'frameworkUi'],
- },
- { id: 'direct', label: 'Direct', description: '', fields: ['connectionMethod'] },
- ],
- fields: {
- framework: {
- id: 'framework',
- type: 'radio-grid',
- label: 'Framework',
- defaultValue: 'nextjs',
- },
- frameworkVariant: {
- id: 'frameworkVariant',
- type: 'select',
- label: 'Variant',
- dependsOn: { framework: ['nextjs', 'react'] },
- },
- frameworkUi: {
- id: 'frameworkUi',
- type: 'switch',
- label: 'Shadcn',
- dependsOn: { framework: ['nextjs', 'react'] },
- },
- connectionMethod: {
- id: 'connectionMethod',
- type: 'radio-list',
- label: 'Method',
- defaultValue: 'direct',
- },
- },
- steps: [],
- })
- test('should reset dependent fields when dependency no longer satisfied', () => {
- const schema = createSchemaForReset()
- const state: ConnectState = {
- mode: 'framework',
- framework: 'vue', // Changed from nextjs to vue
- frameworkVariant: 'app', // This should be reset
- frameworkUi: true, // This should be reset
- }
- const newState = resetDependentFields(state, 'framework', schema)
- expect(newState.frameworkVariant).toBeUndefined()
- expect(newState.frameworkUi).toBeUndefined()
- })
- test('should keep dependent fields when dependency still satisfied', () => {
- const schema = createSchemaForReset()
- const state: ConnectState = {
- mode: 'framework',
- framework: 'react', // Still in the allowed list
- frameworkVariant: 'vite',
- frameworkUi: true,
- }
- const newState = resetDependentFields(state, 'framework', schema)
- expect(newState.frameworkVariant).toBe('vite')
- expect(newState.frameworkUi).toBe(true)
- })
- test('should handle mode changes', () => {
- const schema = createSchemaForReset()
- const state: ConnectState = {
- mode: 'direct', // Changed mode
- framework: 'nextjs',
- frameworkVariant: 'app',
- }
- // Note: The current implementation of resetDependentFields for mode changes
- // looks for fields not in the current mode, but the logic compares against previous mode
- const newState = resetDependentFields(state, 'mode', schema)
- // Mode-specific field reset logic is handled
- expect(newState.mode).toBe('direct')
- })
- test('should not modify state for fields without dependencies', () => {
- const schema = createSchemaForReset()
- const state: ConnectState = {
- mode: 'framework',
- framework: 'nextjs',
- }
- const newState = resetDependentFields(state, 'framework', schema)
- expect(newState.mode).toBe('framework')
- expect(newState.framework).toBe('nextjs')
- })
- })
|