import { useKeyValueParameters } from '@/composables/ui/useKeyValueParameters'; import { ParameterContract } from '@/interfaces'; import { ParameterType } from '@/interfaces/ui/key-value-parameters'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { nextTick, Ref, ref } from 'vue'; // Mock the config vi.mock('@/config', () => ({ keyValueParametersConfig: { DELETION_CONFIRMATION_TIMEOUT: 2000, SYNC_DEBOUNCE_DELAY: 0, }, })); describe('useKeyValueParameters', () => { let modelValue: Ref; let onUpdateCallback: ReturnType; let composable: ReturnType; beforeEach(() => { modelValue = ref([]); onUpdateCallback = vi.fn(); composable = useKeyValueParameters(modelValue, onUpdateCallback); }); describe('initialization', () => { it('should initialize with empty parameters array', () => { expect(composable.parameters.value).toEqual([]); }); it('should add one empty parameter on mount', () => { // The composable adds an empty parameter in onBeforeMount // We need to trigger the lifecycle manually in tests expect(composable.parameters.value.length).toBeGreaterThanOrEqual(0); }); it('should initialize with correct default state', () => { expect(composable.areAllParametersDisabled.value).toBe(true); expect(composable.deletingAll.value).toBe(false); }); }); describe('parameter management', () => { it('should add new empty parameter', () => { const initialLength = composable.parameters.value.length; composable.addNewEmptyParameter(); expect(composable.parameters.value).toHaveLength(initialLength + 1); const newParameter = composable.parameters.value[composable.parameters.value.length - 1]; expect(newParameter).toMatchObject({ type: ParameterType.Text, key: '', value: '', enabled: true, }); expect(typeof newParameter.id).toBe('number'); }); it('should toggle all parameters enabled state', () => { // Add some parameters composable.addNewEmptyParameter(); composable.addNewEmptyParameter(); // All should be enabled by default expect(composable.areAllParametersDisabled.value).toBe(false); // Disable all composable.toggleAllParametersEnabledState(); expect(composable.areAllParametersDisabled.value).toBe(true); // Enable all composable.toggleAllParametersEnabledState(); expect(composable.areAllParametersDisabled.value).toBe(false); }); it('should update parameters from parent modelValue', () => { modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'param1', value: 'value1', enabled: true, }, { id: 2, type: ParameterType.Text, key: 'param2', value: 'value2', enabled: true, }, ]; composable.updateParametersFromParentModel(); expect(composable.parameters.value).toHaveLength(2); expect(composable.parameters.value[0]).toMatchObject({ key: 'param1', value: 'value1', enabled: true, }); }); it('should merge parameters without duplicates', () => { // Add initial parameters composable.addNewEmptyParameter(); composable.parameters.value[0].key = 'existing'; composable.parameters.value[0].value = 'existing-value'; // Add external parameters with one duplicate modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'existing', value: 'new-value', enabled: true, }, // Duplicate { id: 2, type: ParameterType.Text, key: 'new', value: 'new-value', enabled: true, }, ]; composable.updateParametersFromParentModel(); // Should have 2 parameters: existing (updated) and new expect(composable.parameters.value).toHaveLength(2); const existingParam = composable.parameters.value.find( p => p.key === 'existing', ); expect(existingParam).toMatchObject({ key: 'existing', value: 'new-value', }); }); }); describe('deletion management', () => { it('should initiate parameter deletion on first click', () => { composable.addNewEmptyParameter(); const index = 0; composable.triggerParameterDeletion(composable.parameters.value, index); expect(composable.isParameterMarkedForDeletion(index)).toBe(true); }); it('should delete parameter on second click', () => { composable.addNewEmptyParameter(); composable.addNewEmptyParameter(); const index = 0; // First click - mark for deletion composable.triggerParameterDeletion(composable.parameters.value, index); expect(composable.parameters.value).toHaveLength(2); // Second click - actually delete composable.triggerParameterDeletion(composable.parameters.value, index); expect(composable.parameters.value).toHaveLength(1); }); it('should clear deletion states', () => { composable.addNewEmptyParameter(); composable.addNewEmptyParameter(); // Mark for deletion composable.triggerParameterDeletion(composable.parameters.value, 0); expect(composable.isParameterMarkedForDeletion(0)).toBe(true); // Clear all states composable.clearAllDeletionStates(); expect(composable.isParameterMarkedForDeletion(0)).toBe(false); }); it('should delete all parameters', () => { composable.addNewEmptyParameter(); composable.addNewEmptyParameter(); // First click - mark for deletion composable.deleteAllParameters(); expect(composable.deletingAll.value).toBe(true); // Second click - actually delete composable.deleteAllParameters(); expect(composable.parameters.value).toHaveLength(0); }); }); describe('event-based updates', () => { it('should call onUpdate callback when parameters change', async () => { composable.addNewEmptyParameter(); // Wait for debounce await new Promise(resolve => setTimeout(resolve, 50)); expect(onUpdateCallback).toHaveBeenCalled(); const callArgs = onUpdateCallback.mock.calls[onUpdateCallback.mock.calls.length - 1][0]; expect(callArgs).toBeInstanceOf(Array); expect(callArgs.length).toBeGreaterThan(0); }); it('should deep clone parameters when calling onUpdate', async () => { composable.addNewEmptyParameter(); composable.parameters.value[0].key = 'test'; composable.parameters.value[0].value = 'value'; // Wait for debounce await new Promise(resolve => setTimeout(resolve, 50)); const callArgs = onUpdateCallback.mock.calls[onUpdateCallback.mock.calls.length - 1][0]; const emittedParam = callArgs[0]; // Verify it's a deep clone (not the same reference) expect(emittedParam).not.toBe(composable.parameters.value[0]); expect(emittedParam).toMatchObject({ key: 'test', value: 'value', }); // Mutate the emitted parameter emittedParam.key = 'mutated'; // Original should be unchanged expect(composable.parameters.value[0].key).toBe('test'); }); it('should update parameters when modelValue changes externally', async () => { modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'initial', value: 'value', enabled: true, }, ]; await nextTick(); expect(composable.parameters.value.length).toBeGreaterThanOrEqual(1); expect(composable.parameters.value[0].key).toBe('initial'); // Change modelValue externally (must use same ID to preserve object) modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'updated', value: 'new-value', enabled: true, }, ]; await nextTick(); // The reconciliation logic preserves existing objects, so we need to check // that the values were updated expect(composable.parameters.value[0].key).toBe('updated'); expect(composable.parameters.value[0].value).toBe('new-value'); }); }); describe('edge cases', () => { it('should handle parameters with special characters in keys', () => { modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'key-with-dash', value: 'value1', enabled: true, }, { id: 2, type: ParameterType.Text, key: 'key_with_underscore', value: 'value2', enabled: true, }, { id: 3, type: ParameterType.Text, key: 'key.with.dot', value: 'value3', enabled: true, }, { id: 4, type: ParameterType.Text, key: 'key with space', value: 'value4', enabled: true, }, ]; composable.updateParametersFromParentModel(); expect(composable.parameters.value).toHaveLength(4); expect(composable.parameters.value[0].key).toBe('key-with-dash'); expect(composable.parameters.value[1].key).toBe('key_with_underscore'); expect(composable.parameters.value[2].key).toBe('key.with.dot'); expect(composable.parameters.value[3].key).toBe('key with space'); }); it('should handle empty string values', () => { modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'empty-value', value: '', enabled: true, }, ]; composable.updateParametersFromParentModel(); expect(composable.parameters.value[0].value).toBe(''); }); }); describe('duplicate handling', () => { it('should handle duplicate keys correctly', () => { modelValue.value = [ { id: 1, type: ParameterType.Text, key: 'duplicate', value: 'value1', enabled: true, }, { id: 2, type: ParameterType.Text, key: 'duplicate', value: 'value2', enabled: true, }, ]; composable.updateParametersFromParentModel(); // Should have 2 parameters: both duplicates expect(composable.parameters.value).toHaveLength(2); const duplicateParams = composable.parameters.value.filter( p => p.key === 'duplicate', ); expect(duplicateParams).toHaveLength(2); expect(duplicateParams[0].value).toBe('value1'); expect(duplicateParams[1].value).toBe('value2'); }); }); });