Files
nimbus/resources/js/tests/composables/useKeyValueParameters.test.ts
Mazen Touati c2aa6895d6 feat: initial alpha release
This commit represents the complete foundational codebase for Nimbus Alpha, a Laravel package that provides an integrated, in-browser API client with automatic schema discovery from validation rules.

IMPORTANT: This is a squashed commit representing the culmination of extensive development, refactoring, and architectural iterations. All previous commit history has been intentionally removed to provide a clean foundation for the public alpha release.

The development of Nimbus involved:
- Multiple architectural refactorings
- Significant structural changes
- Experimental approaches that were later abandoned
- Learning iterations on the core concept
- Migration between different design patterns

This messy history would:
- Make git blame confusing and unhelpful
- Obscure the actual intent behind current implementation
- Create noise when reviewing changes
- Reference deleted or refactored code

If git blame brought you to this commit, it means you're looking at code that was part of the initial alpha release. Here's what to do:

1. Check Current Documentation
   - See `/wiki/contribution-guide/README.md` for architecture details
   - Review the specific module's README if available
   - Look for inline comments explaining the reasoning

2. Look for Related Code
   - Check other files in the same module
   - Look for tests that demonstrate intended behavior
   - Review interfaces and contracts

3. Context Matters
   - This code may have been updated since alpha
   - Check git log for subsequent changes to this file
   - Look for related issues or PRs on GitHub

---

This commit marks the beginning of Nimbus's public journey. All future
commits will build upon this foundation with clear, traceable history.

Thank you for using or contributing to Nimbus!
2025-10-23 00:16:28 +02:00

339 lines
12 KiB
TypeScript

import { useKeyValueParameters } from '@/composables/ui/useKeyValueParameters';
import { ParametersExternalContract } from '@/interfaces';
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 model: Ref<ParametersExternalContract[]>;
let composable: ReturnType<typeof useKeyValueParameters>;
beforeEach(() => {
model = ref([]);
composable = useKeyValueParameters(model);
});
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: '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 model', () => {
model.value = [
{ key: 'param1', value: 'value1' },
{ key: 'param2', value: 'value2' },
];
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
model.value = [
{ key: 'existing', value: 'new-value' }, // Duplicate
{ key: 'new', value: 'new-value' }, // New
];
composable.updateParametersFromParentModel();
// Should have 2 parameters: the new one and the existing one (updated)
expect(composable.parameters.value).toHaveLength(2);
const existingParam = composable.parameters.value.find(
p => p.key === 'existing',
);
const newParam = composable.parameters.value.find(p => p.key === 'new');
expect(existingParam?.value).toBe('new-value'); // Updated
expect(newParam?.key).toBe('new');
});
});
describe('deletion management', () => {
it('should initiate parameter deletion on first click', () => {
composable.addNewEmptyParameter();
const parameter = composable.parameters.value[0];
composable.triggerParameterDeletion(composable.parameters.value, 0);
expect(composable.isParameterMarkedForDeletion(parameter.id)).toBe(true);
});
it('should delete parameter on second click', () => {
composable.addNewEmptyParameter();
const initialLength = composable.parameters.value.length;
// First click - mark for deletion
composable.triggerParameterDeletion(composable.parameters.value, 0);
// Second click - actually delete
composable.triggerParameterDeletion(composable.parameters.value, 0);
expect(composable.parameters.value).toHaveLength(initialLength - 1);
});
it('should handle bulk deletion confirmation', async () => {
composable.addNewEmptyParameter();
composable.addNewEmptyParameter();
// First click - mark for bulk deletion
composable.deleteAllParameters();
expect(composable.deletingAll.value).toBe(true);
expect(composable.parameters.value).toHaveLength(2);
// Second click - actually delete all
composable.deleteAllParameters();
expect(composable.parameters.value).toHaveLength(0);
expect(composable.deletingAll.value).toBe(false);
});
it('should clear deletion states', () => {
composable.addNewEmptyParameter();
const parameter = composable.parameters.value[0];
// Mark for deletion
composable.triggerParameterDeletion(composable.parameters.value, 0);
expect(composable.isParameterMarkedForDeletion(parameter.id)).toBe(true);
// Clear all states
composable.clearAllDeletionStates();
expect(composable.isParameterMarkedForDeletion(parameter.id)).toBe(false);
});
});
describe('computed properties', () => {
it('should correctly compute areAllParametersDisabled', () => {
// No parameters - should be true
expect(composable.areAllParametersDisabled.value).toBe(true);
// Add enabled parameter
composable.addNewEmptyParameter();
expect(composable.areAllParametersDisabled.value).toBe(false);
// Disable the parameter
composable.parameters.value[0].enabled = false;
expect(composable.areAllParametersDisabled.value).toBe(true);
});
it('should correctly compute deletingAll', () => {
expect(composable.deletingAll.value).toBe(false);
// Start bulk deletion
composable.deleteAllParameters();
expect(composable.deletingAll.value).toBe(true);
});
});
describe('parameter conversion', () => {
it('should convert external to internal format correctly', () => {
model.value = [
{ key: 'test', value: 'value' },
// @ts-expect-error asserting edge case.
{ key: 'number', value: 123 },
];
composable.updateParametersFromParentModel();
expect(composable.parameters.value[0]).toMatchObject({
key: 'test',
value: 'value',
type: 'text',
enabled: true,
});
expect(composable.parameters.value[1]).toMatchObject({
key: 'number',
value: '123', // Converted to string
type: 'text',
enabled: true,
});
});
it('should sync parameters back to model correctly', async () => {
composable.addNewEmptyParameter();
composable.parameters.value[0].key = 'test-key';
composable.parameters.value[0].value = 'test-value';
composable.parameters.value[0].enabled = true;
// Add another parameter but disabled
composable.addNewEmptyParameter();
composable.parameters.value[1].key = 'disabled-key';
composable.parameters.value[1].value = 'disabled-value';
composable.parameters.value[1].enabled = false;
// Add empty key parameter
composable.addNewEmptyParameter();
composable.parameters.value[2].key = '';
composable.parameters.value[2].value = 'empty-key-value';
composable.parameters.value[2].enabled = true;
// Trigger sync (this happens automatically with watchDebounced)
// We'll manually trigger it for testing
composable.parameters.value = [...composable.parameters.value];
await nextTick();
// Only enabled parameters with non-empty keys should be synced
expect(model.value).toEqual([
{
type: 'text', // <- added implicitly.
key: 'test-key',
value: 'test-value',
},
]);
});
});
describe('edge cases', () => {
it('should handle deletion of non-existent parameter', () => {
composable.addNewEmptyParameter();
const parameters = composable.parameters.value;
composable.triggerParameterDeletion(composable.parameters.value, 999);
expect(composable.isParameterMarkedForDeletion(parameters[0].id)).toBe(false);
});
it('should handle empty model value', () => {
model.value = [];
composable.updateParametersFromParentModel();
// Should not add any parameters from empty model
expect(composable.parameters.value.length).toBeGreaterThanOrEqual(0);
});
it('should handle null model value', () => {
// @ts-expect-error asserting edge case.
model.value = null;
composable.updateParametersFromParentModel();
// Should not crash and should not add parameters
expect(composable.parameters.value.length).toBeGreaterThanOrEqual(0);
});
it('should handle parameters with special characters in keys', () => {
model.value = [
{ key: 'key with spaces', value: 'value1' },
{ key: 'key-with-dashes', value: 'value2' },
{ key: 'key_with_underscores', value: 'value3' },
{ key: 'key.with.dots', value: 'value4' },
];
composable.updateParametersFromParentModel();
expect(composable.parameters.value).toHaveLength(4);
expect(composable.parameters.value[0].key).toBe('key with spaces');
expect(composable.parameters.value[1].key).toBe('key-with-dashes');
});
it('should handle very long parameter values', () => {
const longValue = 'a'.repeat(10000);
model.value = [{ key: 'long-value', value: longValue }];
composable.updateParametersFromParentModel();
expect(composable.parameters.value[0].value).toBe(longValue);
});
});
describe('duplicate handling', () => {
it('should handle duplicate keys correctly', () => {
// Add initial parameter
composable.addNewEmptyParameter();
composable.parameters.value[0].key = 'duplicate';
composable.parameters.value[0].value = 'original';
// Add external parameters with duplicate key
model.value = [
{ key: 'duplicate', value: 'updated' },
{ key: 'unique', value: 'new' },
];
composable.updateParametersFromParentModel();
// Should have 2 parameters: updated duplicate and new unique
expect(composable.parameters.value).toHaveLength(2);
const duplicateParam = composable.parameters.value.find(
p => p.key === 'duplicate',
);
const uniqueParam = composable.parameters.value.find(p => p.key === 'unique');
expect(duplicateParam?.value).toBe('updated');
expect(uniqueParam?.value).toBe('new');
});
});
});