/** * Tests for mergeWithDefaults utilities */ import { safeValue, isNumber, isString, isBoolean, isArray, isObject, deepMergeWithDefaults, createFormInitializer, } from '@/lib/forms/utils/mergeWithDefaults'; describe('Type Guards', () => { describe('isNumber', () => { it('returns true for valid numbers', () => { expect(isNumber(0)).toBe(true); expect(isNumber(42)).toBe(true); expect(isNumber(-10)).toBe(true); expect(isNumber(3.14)).toBe(true); }); it('returns false for NaN', () => { expect(isNumber(NaN)).toBe(false); }); it('returns false for non-numbers', () => { expect(isNumber('42')).toBe(false); expect(isNumber(null)).toBe(false); expect(isNumber(undefined)).toBe(false); expect(isNumber({})).toBe(false); }); }); describe('isString', () => { it('returns true for strings', () => { expect(isString('')).toBe(true); expect(isString('hello')).toBe(true); }); it('returns false for non-strings', () => { expect(isString(42)).toBe(false); expect(isString(null)).toBe(false); expect(isString(undefined)).toBe(false); }); }); describe('isBoolean', () => { it('returns true for booleans', () => { expect(isBoolean(true)).toBe(true); expect(isBoolean(false)).toBe(true); }); it('returns false for non-booleans', () => { expect(isBoolean(0)).toBe(false); expect(isBoolean(1)).toBe(false); expect(isBoolean('true')).toBe(false); }); }); describe('isArray', () => { it('returns true for arrays', () => { expect(isArray([])).toBe(true); expect(isArray([1, 2, 3])).toBe(true); }); it('returns false for non-arrays', () => { expect(isArray({})).toBe(false); expect(isArray('array')).toBe(false); expect(isArray(null)).toBe(false); }); it('validates item types when itemCheck provided', () => { expect(isArray([1, 2, 3], isNumber)).toBe(true); expect(isArray(['a', 'b'], isString)).toBe(true); expect(isArray([1, 'two', 3], isNumber)).toBe(false); }); }); describe('isObject', () => { it('returns true for plain objects', () => { expect(isObject({})).toBe(true); expect(isObject({ key: 'value' })).toBe(true); }); it('returns false for null', () => { expect(isObject(null)).toBe(false); }); it('returns false for arrays', () => { expect(isObject([])).toBe(false); }); it('returns false for primitives', () => { expect(isObject('string')).toBe(false); expect(isObject(42)).toBe(false); }); }); }); describe('safeValue', () => { it('returns value when type check passes', () => { expect(safeValue(42, 0, isNumber)).toBe(42); expect(safeValue('hello', '', isString)).toBe('hello'); }); it('returns default when type check fails', () => { expect(safeValue('not a number', 0, isNumber)).toBe(0); expect(safeValue(42, '', isString)).toBe(''); }); it('returns default for null/undefined', () => { expect(safeValue(null, 0, isNumber)).toBe(0); expect(safeValue(undefined, 'default', isString)).toBe('default'); }); }); describe('deepMergeWithDefaults', () => { it('returns defaults when source is null', () => { const defaults = { name: 'default', value: 10 }; expect(deepMergeWithDefaults(defaults, null)).toEqual(defaults); }); it('returns defaults when source is undefined', () => { const defaults = { name: 'default', value: 10 }; expect(deepMergeWithDefaults(defaults, undefined)).toEqual(defaults); }); it('merges source values over defaults', () => { const defaults = { name: 'default', value: 10 }; const source = { name: 'custom' }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ name: 'custom', value: 10, }); }); it('preserves default for missing source keys', () => { const defaults = { a: 1, b: 2, c: 3 }; const source = { a: 10 }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ a: 10, b: 2, c: 3, }); }); it('recursively merges nested objects', () => { const defaults = { config: { temperature: 0.7, max_tokens: 8192 }, }; // Source has partial nested config - deepMerge fills in missing fields const source = { config: { temperature: 0.5 }, } as unknown as Partial; expect(deepMergeWithDefaults(defaults, source)).toEqual({ config: { temperature: 0.5, max_tokens: 8192 }, }); }); it('only uses source values if types match', () => { const defaults = { value: 10, name: 'default' }; const source = { value: 'not a number' as unknown as number }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ value: 10, name: 'default', }); }); it('handles arrays - uses source array if types match', () => { const defaults = { items: ['a', 'b'] }; const source = { items: ['c', 'd', 'e'] }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ items: ['c', 'd', 'e'], }); }); it('skips undefined source values', () => { const defaults = { name: 'default' }; const source = { name: undefined }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ name: 'default', }); }); it('handles null values when defaults are null', () => { const defaults = { value: null }; const source = { value: null }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ value: null }); }); it('uses source value when default is null but source has value', () => { const defaults = { description: null as string | null, name: '' }; const source = { description: 'A real description', name: 'Test' }; expect(deepMergeWithDefaults(defaults, source)).toEqual({ description: 'A real description', name: 'Test', }); }); }); describe('createFormInitializer', () => { it('returns defaults when called with null', () => { const defaults = { name: '', age: 0 }; const initializer = createFormInitializer(defaults); expect(initializer(null)).toEqual(defaults); }); it('returns defaults when called with undefined', () => { const defaults = { name: '', age: 0 }; const initializer = createFormInitializer(defaults); expect(initializer(undefined)).toEqual(defaults); }); it('merges API data with defaults', () => { const defaults = { name: '', age: 0, active: false }; const initializer = createFormInitializer(defaults); const result = initializer({ name: 'John', age: 25 }); expect(result).toEqual({ name: 'John', age: 25, active: false }); }); it('uses custom transform function when provided', () => { interface Form { fullName: string; isActive: boolean; } interface Api { first_name: string; last_name: string; active: boolean; } const defaults: Form = { fullName: '', isActive: false }; const initializer = createFormInitializer(defaults, (apiData, defs) => ({ fullName: apiData ? `${apiData.first_name} ${apiData.last_name}` : defs.fullName, isActive: apiData?.active ?? defs.isActive, })); const result = initializer({ first_name: 'John', last_name: 'Doe', active: true, }); expect(result).toEqual({ fullName: 'John Doe', isActive: true }); }); it('transform receives defaults when apiData is null', () => { const defaults = { name: 'default' }; const initializer = createFormInitializer(defaults, (_, defs) => ({ name: defs.name.toUpperCase(), })); expect(initializer(null)).toEqual({ name: 'DEFAULT' }); }); });