diff --git a/packages/form-core/tests/mergeForm.spec.ts b/packages/form-core/tests/mergeForm.spec.ts index 972b6772f..6a572f3f5 100644 --- a/packages/form-core/tests/mergeForm.spec.ts +++ b/packages/form-core/tests/mergeForm.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import { mutateMergeDeep } from '../src/mergeForm' +import { FormApi } from '../src/FormApi' +import { mergeForm, mutateMergeDeep } from '../src/mergeForm' type TestObject = Record @@ -97,4 +98,163 @@ describe('mutateMergeDeep', () => { mutateMergeDeep(a, b) expect(a).toStrictEqual({ a: { a: 1, b: 2 } }) }) + + it('should return empty object when target is null', () => { + const result = mutateMergeDeep(null, { a: 1 }) + expect(result).toStrictEqual({}) + }) + + it('should return empty object when target is undefined', () => { + const result = mutateMergeDeep(undefined, { a: 1 }) + expect(result).toStrictEqual({}) + }) + + it('should return empty object when target is not an object', () => { + const result = mutateMergeDeep('string' as any, { a: 1 }) + expect(result).toStrictEqual({}) + }) + + it('should return target unchanged when source is null', () => { + const target = { a: 1 } + const result = mutateMergeDeep(target, null) + expect(result).toBe(target) + expect(result).toStrictEqual({ a: 1 }) + }) + + it('should return target unchanged when source is undefined', () => { + const target = { a: 1 } + const result = mutateMergeDeep(target, undefined) + expect(result).toBe(target) + expect(result).toStrictEqual({ a: 1 }) + }) + + it('should return target unchanged when source is not an object', () => { + const target = { a: 1 } + const result = mutateMergeDeep(target, 'string' as any) + expect(result).toBe(target) + expect(result).toStrictEqual({ a: 1 }) + }) + + it('should replace arrays completely without merging their elements', () => { + const target = { + users: [ + { id: 1, name: 'Old User', deleted: true }, + { id: 2, name: 'Another User' }, + ], + } + const source = { + users: [{ id: 1, name: 'New User' }], + } + + mutateMergeDeep(target, source) + + expect(target.users).toHaveLength(1) + expect(target.users[0]).toStrictEqual({ id: 1, name: 'New User' }) + expect(target.users[0]).not.toHaveProperty('deleted') + }) + + it('should handle circular references without stack overflow', () => { + const target: any = { data: { value: 1 } } + target.data.self = target.data + + const source = { data: { newValue: 2 } } + + expect(() => mutateMergeDeep(target, source)).not.toThrow() + expect(target.data.newValue).toBe(2) + expect(target.data.value).toBe(1) + expect(target.data.self).toBe(target.data) + }) + + it('should not merge symbol keys (Object.keys limitation)', () => { + const sym = Symbol('test') + const target = { [sym]: 'original', regular: 'value' } + const source = { [sym]: 'new', regular: 'updated' } + + mutateMergeDeep(target, source) + + expect(target[sym]).toBe('original') + + expect(target.regular).toBe('updated') + }) +}) + +describe('mergeForm', () => { + // `as any` required: FormApi has 12 generic params with TSubmitMeta defaulting to `never`, + // incompatible with mergeForm's `any` constraint. Type limitation only - runtime is correct. + // Production usage via useTransform receives AnyFormApi, avoiding this. + + it('should merge state into form using mutateMergeDeep', () => { + const form = new FormApi({ + defaultValues: { + name: '', + age: 0, + }, + }) + + form.mount() + + const newState = { + values: { + name: 'John', + age: 30, + }, + } + + const result = mergeForm(form as any, newState) + + expect(result).toBe(form) + + expect(form.state.values).toStrictEqual({ + name: 'John', + age: 30, + }) + }) + + it('should handle partial state updates', () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) + + form.mount() + + mergeForm(form as any, { + values: { + firstName: 'Jane', + }, + }) + + expect(form.state.values.firstName).toBe('Jane') + expect(form.state.values.lastName).toBe('') + }) + + it('should handle deeply nested state updates', () => { + const form = new FormApi({ + defaultValues: { + user: { + profile: { + name: '', + age: 0, + }, + }, + }, + }) + + form.mount() + + mergeForm(form as any, { + values: { + user: { + profile: { + name: 'Alice', + }, + }, + }, + }) + + expect(form.state.values.user.profile.name).toBe('Alice') + expect(form.state.values.user.profile.age).toBe(0) + }) })