import { describe, expect, it } from 'vitest'; import { deepMerge } from './deep-merge'; describe('deepMerge', () => { describe('shallow merge', () => { it('should merge top-level properties', () => { const target = { a: 1, b: 2 }; const source = { c: 3, d: 4 }; const result = deepMerge(target, source); expect(result).toEqual({ a: 1, b: 2, c: 3, d: 4 }); }); it('should not mutate target', () => { const target = { a: 1 }; const source = { b: 2 }; deepMerge(target, source); expect(target).toEqual({ a: 1 }); }); it('should not mutate source', () => { const target = { a: 1 }; const source = { b: 2 }; deepMerge(target, source); expect(source).toEqual({ b: 2 }); }); }); describe('underwrite pattern (target wins)', () => { it('should prefer target values over source', () => { const target = { a: 1, b: 2 }; const source = { a: 999, c: 3 }; const result = deepMerge(target, source); expect(result).toEqual({ a: 1, // Target value wins b: 2, c: 3, }); }); it('should only use source for missing keys', () => { const target = { timeout: 5000 }; const source = { timeout: 3000, retries: 3 }; const result = deepMerge(target, source); expect(result).toEqual({ timeout: 5000, // User's timeout wins retries: 3, // Default retries used }); }); }); describe('deep (nested) merge', () => { it('should recursively merge nested objects', () => { const target = { api: { timeout: 5000 }, user: { name: 'Alice' }, }; const source = { api: { timeout: 3000, retries: 3, baseUrl: 'https://api.example.com' }, user: { name: 'Bob', email: 'bob@example.com' }, debug: false, }; const result = deepMerge(target, source); expect(result).toEqual({ api: { timeout: 5000, // Target wins retries: 3, // From source baseUrl: 'https://api.example.com', // From source }, user: { name: 'Alice', // Target wins email: 'bob@example.com', // From source }, debug: false, // From source }); }); it('should handle deeply nested objects', () => { const target = { a: { b: { c: { d: 'target', }, }, }, }; const source = { a: { b: { c: { d: 'source', e: 'new', }, f: 'another', }, }, }; const result = deepMerge(target, source); expect(result).toEqual({ a: { b: { c: { d: 'target', // Target wins e: 'new', }, f: 'another', }, }, }); }); it('should not mutate nested objects', () => { const target = { nested: { a: 1 } }; const source = { nested: { b: 2 } }; deepMerge(target, source); expect(target).toEqual({ nested: { a: 1 } }); expect(source).toEqual({ nested: { b: 2 } }); }); }); describe('array handling', () => { it('should treat arrays as values (not merge elements)', () => { const target = { list: [1, 2] }; const source = { list: [3, 4, 5] }; const result = deepMerge(target, source); expect(result.list).toEqual([1, 2]); // Target array wins completely }); it('should use source array if target is undefined', () => { const target = {}; const source = { list: [1, 2, 3] }; const result = deepMerge(target, source); expect(result.list).toEqual([1, 2, 3]); }); }); describe('null and undefined handling', () => { it('should use source value when target is undefined', () => { const target = { a: undefined }; const source = { a: 'value' }; const result = deepMerge(target, source); expect(result.a).toBe('value'); }); it('should preserve null in target', () => { const target = { a: null }; const source = { a: 'value' }; const result = deepMerge(target, source); expect(result.a).toBeNull(); // Target's null wins }); it('should handle source with null', () => { const target = {}; const source = { a: null }; const result = deepMerge(target, source); expect(result.a).toBeNull(); }); }); describe('special object types', () => { it('should not deep merge Date objects', () => { const targetDate = new Date('2024-01-01'); const sourceDate = new Date('2024-12-31'); const target = { date: targetDate }; const source = { date: sourceDate }; const result = deepMerge(target, source); expect(result.date).toBe(targetDate); // Target Date wins }); it('should not deep merge RegExp objects', () => { const target = { pattern: /foo/i }; const source = { pattern: /bar/g }; const result = deepMerge(target, source); expect(result.pattern).toEqual(/foo/i); // Target RegExp wins }); it('should handle mixed plain objects and special objects', () => { const target = { config: { timeout: 5000 }, date: new Date('2024-01-01'), }; const source = { config: { timeout: 3000, retries: 3 }, date: new Date('2024-12-31'), pattern: /test/, }; const result = deepMerge(target, source); expect(result).toEqual({ config: { timeout: 5000, retries: 3 }, date: target.date, pattern: /test/, }); }); }); describe('edge cases', () => { it('should handle empty objects', () => { const target = {}; const source = { a: 1 }; const result = deepMerge(target, source); expect(result).toEqual({ a: 1 }); }); it('should handle empty source', () => { const target = { a: 1 }; const source = {}; const result = deepMerge(target, source); expect(result).toEqual({ a: 1 }); }); it('should handle both empty', () => { const result = deepMerge({}, {}); expect(result).toEqual({}); }); it('should handle objects with prototype properties', () => { const source = Object.create({ inherited: 'value' }); source.own = 'own value'; const target = {}; const result = deepMerge(target, source); // Should only include own properties expect(result).toEqual({ own: 'own value' }); expect(result.inherited).toBeUndefined(); }); it('should handle numeric keys', () => { const target = { '0': 'a', '1': 'b' }; const source = { '1': 'x', '2': 'c' }; const result = deepMerge(target, source); expect(result).toEqual({ '0': 'a', '1': 'b', // Target wins '2': 'c', }); }); it('should handle boolean values', () => { const target = { enabled: true }; const source = { enabled: false, debug: true }; const result = deepMerge(target, source); expect(result).toEqual({ enabled: true, // Target wins debug: true, }); }); it('should handle number values including 0', () => { const target = { count: 0 }; const source = { count: 10, total: 100 }; const result = deepMerge(target, source); expect(result).toEqual({ count: 0, // Target wins (0 is truthy for our purposes) total: 100, }); }); it('should handle string values including empty string', () => { const target = { name: '' }; const source = { name: 'default', title: 'Mr.' }; const result = deepMerge(target, source); expect(result).toEqual({ name: '', // Target wins (empty string is a value) title: 'Mr.', }); }); }); describe('real-world config scenarios', () => { it('should handle SDK plugin config pattern', () => { const userConfig = { analytics: { tracking: { enabled: false, // User explicitly disabled }, }, }; const defaults = { analytics: { tracking: { enabled: true, sampleRate: 1.0, endpoint: '/api/track', }, reporting: { enabled: true, }, }, storage: { type: 'localStorage', prefix: 'app_', }, }; const result = deepMerge(userConfig, defaults); expect(result).toEqual({ analytics: { tracking: { enabled: false, // User's false wins over default true sampleRate: 1.0, endpoint: '/api/track', }, reporting: { enabled: true, }, }, storage: { type: 'localStorage', prefix: 'app_', }, }); }); it('should handle multiple levels of overrides', () => { const userConfig = { api: { timeout: 10000, }, }; const pluginDefaults = { api: { timeout: 5000, retries: 3, }, }; const systemDefaults = { api: { timeout: 3000, retries: 1, baseUrl: 'https://api.example.com', }, debug: false, }; // Apply defaults in order: system → plugin → user let config = deepMerge(systemDefaults, {}); config = deepMerge(pluginDefaults, config); config = deepMerge(userConfig, config); expect(config).toEqual({ api: { timeout: 10000, // User value retries: 3, // Plugin value baseUrl: 'https://api.example.com', // System value }, debug: false, }); }); }); });