* 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
364 lines
13 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|