Files
supabase/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts
Joshen Lim bf54231c82 Reinstate source param in new connect sheet to select read replicas (#44018)
## Context

Adds a "source" param in the new Connect sheet to allow selecting read
replicas - this was present in the old Connect dialog

<img width="962" height="713" alt="image"
src="https://github.com/user-attachments/assets/3c805e41-e8df-431b-83d0-e5f227d68475"
/>

Selecting sources will update the connect steps at the bottom -
connection string, host etc
2026-03-23 13:22:34 +08:00

625 lines
19 KiB
TypeScript

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: 'supabasejs',
},
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: 'supabasejs',
},
mcpReadonly: {
id: 'mcpReadonly',
type: 'switch',
label: 'Readonly',
defaultValue: false,
},
},
steps: [],
}
const state = getDefaultState({ schema })
expect(state.framework).toBe('nextjs')
expect(state.library).toBe('supabasejs')
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')
})
})