Files
nimbus/resources/js/tests/composables/useKeyValueParameters.test.ts
Mazen Touati e1b844cee0 feat(history): add history viewer and rewind (#38)
* feat(ui): add `input group` base component

* feat(history): add history viewer and rewind

* test: update selector snapshot

* test: add PW base page

* style: apply TS style fixes

* chore(history): request history wiki

* chore(history): remove unwanted symbol

* chore: fix type

* style: apply TS style fixes
2026-01-17 20:50:00 +01:00

364 lines
13 KiB
TypeScript

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<ParameterContract[]>;
let onUpdateCallback: ReturnType<typeof vi.fn>;
let composable: ReturnType<typeof useKeyValueParameters>;
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');
});
});
});