feat(tests): enhance test coverage with integration and utility tests

- Updated QueueCard tests to use getAllByText for better resilience against multiple occurrences.
- Modified useCodexLens tests to check for error existence instead of specific message.
- Added mock for ResizeObserver in test setup to support components using it.
- Introduced integration tests for appStore and hooks interactions, covering locale and theme flows.
- Created layout-utils tests to validate pane manipulation functions.
- Added queryKeys tests to ensure correct key generation for workspace queries.
- Implemented utils tests for class name merging and memory metadata parsing.
This commit is contained in:
catlog22
2026-02-17 13:06:13 +08:00
parent 8665ea73a4
commit d5c6f65599
25 changed files with 1437 additions and 2338 deletions

View File

@@ -0,0 +1,293 @@
// ========================================
// Layout Utilities Tests
// ========================================
// Tests for Allotment layout tree manipulation functions
import { describe, it, expect } from 'vitest';
import {
isPaneId,
findPaneInLayout,
removePaneFromLayout,
addPaneToLayout,
getAllPaneIds,
} from './layout-utils';
import type { AllotmentLayoutGroup, PaneId } from '@/stores/viewerStore';
describe('layout-utils', () => {
// Helper to create test layouts
const createSimpleLayout = (): AllotmentLayoutGroup => ({
direction: 'horizontal',
children: ['pane-1', 'pane-2', 'pane-3'],
sizes: [33, 33, 34],
});
const createNestedLayout = (): AllotmentLayoutGroup => ({
direction: 'horizontal',
children: [
'pane-1',
{
direction: 'vertical',
children: ['pane-2', 'pane-3'],
sizes: [50, 50],
},
'pane-4',
],
sizes: [25, 50, 25],
});
describe('isPaneId', () => {
it('should return true for string values (PaneId)', () => {
expect(isPaneId('pane-1')).toBe(true);
expect(isPaneId('any-string')).toBe(true);
});
it('should return false for group objects', () => {
const group: AllotmentLayoutGroup = {
direction: 'horizontal',
children: ['pane-1'],
};
expect(isPaneId(group)).toBe(false);
});
});
describe('findPaneInLayout', () => {
it('should find existing pane in simple layout', () => {
const layout = createSimpleLayout();
const result = findPaneInLayout(layout, 'pane-2');
expect(result.found).toBe(true);
expect(result.index).toBe(1);
expect(result.parent).toBe(layout);
});
it('should return not found for non-existing pane', () => {
const layout = createSimpleLayout();
const result = findPaneInLayout(layout, 'non-existing');
expect(result.found).toBe(false);
expect(result.index).toBe(-1);
expect(result.parent).toBeNull();
});
it('should find pane in nested layout', () => {
const layout = createNestedLayout();
const result = findPaneInLayout(layout, 'pane-3');
expect(result.found).toBe(true);
expect(result.index).toBe(1);
expect(result.parent).toEqual({
direction: 'vertical',
children: ['pane-2', 'pane-3'],
sizes: [50, 50],
});
});
it('should find pane at root level in nested layout', () => {
const layout = createNestedLayout();
const result = findPaneInLayout(layout, 'pane-1');
expect(result.found).toBe(true);
expect(result.index).toBe(0);
});
});
describe('removePaneFromLayout', () => {
it('should remove pane from simple layout', () => {
const layout = createSimpleLayout();
const result = removePaneFromLayout(layout, 'pane-2');
expect(result.children).toEqual(['pane-1', 'pane-3']);
expect(result.children).toHaveLength(2);
});
it('should update sizes after removal', () => {
const layout = createSimpleLayout();
const result = removePaneFromLayout(layout, 'pane-2');
expect(result.sizes).toBeDefined();
expect(result.sizes?.length).toBe(2);
// Sizes should be normalized to sum ~100
const sum = result.sizes?.reduce((a, b) => a + b, 0) ?? 0;
expect(Math.round(sum)).toBeCloseTo(100, 0);
});
it('should handle removal from empty layout', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: [],
};
const result = removePaneFromLayout(layout, 'pane-1');
expect(result.children).toEqual([]);
});
it('should remove pane from nested layout', () => {
const layout = createNestedLayout();
const result = removePaneFromLayout(layout, 'pane-3');
const allPanes = getAllPaneIds(result);
expect(allPanes).not.toContain('pane-3');
expect(allPanes).toContain('pane-1');
expect(allPanes).toContain('pane-2');
expect(allPanes).toContain('pane-4');
});
it('should handle removal of non-existing pane', () => {
const layout = createSimpleLayout();
const result = removePaneFromLayout(layout, 'non-existing');
expect(result.children).toEqual(['pane-1', 'pane-2', 'pane-3']);
});
it('should clean up empty groups after removal', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: [
{
direction: 'vertical',
children: ['only-pane'],
sizes: [100],
},
],
sizes: [100],
};
const result = removePaneFromLayout(layout, 'only-pane');
expect(result.children).toEqual([]);
});
});
describe('addPaneToLayout', () => {
it('should add pane to empty layout', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: [],
};
const result = addPaneToLayout(layout, 'new-pane');
expect(result.children).toEqual(['new-pane']);
expect(result.sizes).toEqual([100]);
});
it('should add pane to layout with same direction', () => {
const layout = createSimpleLayout();
const result = addPaneToLayout(layout, 'new-pane');
expect(result.children).toHaveLength(4);
expect(result.children).toContain('new-pane');
});
it('should add pane next to specific parent pane', () => {
const layout = createSimpleLayout();
const result = addPaneToLayout(layout, 'new-pane', 'pane-2', 'horizontal');
expect(result.children).toContain('new-pane');
// The new pane should be added relative to pane-2
});
it('should create nested group when direction differs', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: ['pane-1'],
sizes: [100],
};
const result = addPaneToLayout(layout, 'new-pane', undefined, 'vertical');
// Should create a vertical group containing the original layout and new pane
expect(result.direction).toBe('vertical');
});
it('should handle deeply nested layouts', () => {
const layout = createNestedLayout();
const result = addPaneToLayout(layout, 'new-pane', 'pane-3', 'horizontal');
const allPanes = getAllPaneIds(result);
expect(allPanes).toContain('new-pane');
expect(allPanes).toContain('pane-3');
});
it('should distribute sizes when adding to same direction', () => {
const layout = createSimpleLayout();
const result = addPaneToLayout(layout, 'new-pane');
// Should have 4 children with distributed sizes
expect(result.sizes).toHaveLength(4);
const sum = result.sizes?.reduce((a, b) => a + b, 0) ?? 0;
expect(Math.round(sum)).toBeCloseTo(100, 0);
});
});
describe('getAllPaneIds', () => {
it('should get all pane IDs from simple layout', () => {
const layout = createSimpleLayout();
const result = getAllPaneIds(layout);
expect(result).toEqual(['pane-1', 'pane-2', 'pane-3']);
});
it('should get all pane IDs from nested layout', () => {
const layout = createNestedLayout();
const result = getAllPaneIds(layout);
expect(result).toHaveLength(4);
expect(result).toContain('pane-1');
expect(result).toContain('pane-2');
expect(result).toContain('pane-3');
expect(result).toContain('pane-4');
});
it('should return empty array for empty layout', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: [],
};
const result = getAllPaneIds(layout);
expect(result).toEqual([]);
});
it('should handle deeply nested layouts', () => {
const layout: AllotmentLayoutGroup = {
direction: 'horizontal',
children: [
{
direction: 'vertical',
children: [
'pane-1',
{
direction: 'horizontal',
children: ['pane-2', 'pane-3'],
},
],
},
'pane-4',
],
};
const result = getAllPaneIds(layout);
expect(result).toHaveLength(4);
expect(result).toContain('pane-1');
expect(result).toContain('pane-2');
expect(result).toContain('pane-3');
expect(result).toContain('pane-4');
});
});
describe('integration: remove then add', () => {
it('should maintain layout integrity after remove and add', () => {
const layout = createNestedLayout();
// Remove a pane
const afterRemove = removePaneFromLayout(layout, 'pane-2');
expect(getAllPaneIds(afterRemove)).not.toContain('pane-2');
// Add a new pane
const afterAdd = addPaneToLayout(afterRemove, 'new-pane');
const allPanes = getAllPaneIds(afterAdd);
expect(allPanes).toContain('new-pane');
expect(allPanes).not.toContain('pane-2');
expect(allPanes).toContain('pane-3');
});
});
});

View File

@@ -0,0 +1,246 @@
// ========================================
// Query Keys Tests
// ========================================
// Tests for workspace query keys factory
import { describe, it, expect } from 'vitest';
import { workspaceQueryKeys, apiSettingsKeys } from './queryKeys';
describe('queryKeys', () => {
const projectPath = '/test/project';
describe('workspaceQueryKeys', () => {
describe('base key', () => {
it('should create base key with projectPath', () => {
const result = workspaceQueryKeys.all(projectPath);
expect(result).toEqual(['workspace', projectPath]);
});
});
describe('sessions keys', () => {
it('should create sessions list key', () => {
const result = workspaceQueryKeys.sessionsList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'sessions', 'list']);
});
it('should create session detail key with sessionId', () => {
const sessionId = 'session-123';
const result = workspaceQueryKeys.sessionDetail(projectPath, sessionId);
expect(result).toEqual(['workspace', projectPath, 'sessions', 'detail', sessionId]);
});
});
describe('tasks keys', () => {
it('should create tasks list key with sessionId', () => {
const sessionId = 'session-456';
const result = workspaceQueryKeys.tasksList(projectPath, sessionId);
expect(result).toEqual(['workspace', projectPath, 'tasks', 'list', sessionId]);
});
it('should create task detail key with taskId', () => {
const taskId = 'task-789';
const result = workspaceQueryKeys.taskDetail(projectPath, taskId);
expect(result).toEqual(['workspace', projectPath, 'tasks', 'detail', taskId]);
});
});
describe('issues keys', () => {
it('should create issues list key', () => {
const result = workspaceQueryKeys.issuesList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'issues', 'list']);
});
it('should create issue queue key', () => {
const result = workspaceQueryKeys.issueQueue(projectPath);
expect(result).toEqual(['workspace', projectPath, 'issues', 'queue']);
});
it('should create issue queue by id key', () => {
const queueId = 'queue-123';
const result = workspaceQueryKeys.issueQueueById(projectPath, queueId);
expect(result).toEqual(['workspace', projectPath, 'issues', 'queueById', queueId]);
});
});
describe('memory keys', () => {
it('should create memory list key', () => {
const result = workspaceQueryKeys.memoryList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'memory', 'list']);
});
it('should create memory detail key with memoryId', () => {
const memoryId = 'memory-abc';
const result = workspaceQueryKeys.memoryDetail(projectPath, memoryId);
expect(result).toEqual(['workspace', projectPath, 'memory', 'detail', memoryId]);
});
});
describe('skills keys', () => {
it('should create skills list key', () => {
const result = workspaceQueryKeys.skillsList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'skills', 'list']);
});
it('should create codex skills list key', () => {
const result = workspaceQueryKeys.codexSkillsList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'codexSkills', 'list']);
});
});
describe('hooks keys', () => {
it('should create hooks list key', () => {
const result = workspaceQueryKeys.hooksList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'hooks', 'list']);
});
});
describe('mcp servers keys', () => {
it('should create mcp servers list key', () => {
const result = workspaceQueryKeys.mcpServersList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'mcpServers', 'list']);
});
});
describe('project overview keys', () => {
it('should create project overview key', () => {
const result = workspaceQueryKeys.projectOverview(projectPath);
expect(result).toEqual(['workspace', projectPath, 'projectOverview']);
});
});
describe('lite tasks keys', () => {
it('should create lite tasks list key without type', () => {
const result = workspaceQueryKeys.liteTasksList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'liteTasks', 'list', undefined]);
});
it('should create lite tasks list key with type', () => {
const result = workspaceQueryKeys.liteTasksList(projectPath, 'lite-plan');
expect(result).toEqual(['workspace', projectPath, 'liteTasks', 'list', 'lite-plan']);
});
});
describe('explorer keys', () => {
it('should create explorer tree key with rootPath', () => {
const rootPath = '/src';
const result = workspaceQueryKeys.explorerTree(projectPath, rootPath);
expect(result).toEqual(['workspace', projectPath, 'explorer', 'tree', rootPath]);
});
it('should create explorer file key with filePath', () => {
const filePath = '/src/index.ts';
const result = workspaceQueryKeys.explorerFile(projectPath, filePath);
expect(result).toEqual(['workspace', projectPath, 'explorer', 'file', filePath]);
});
});
describe('graph keys', () => {
it('should create graph dependencies key with options', () => {
const options = { maxDepth: 3 };
const result = workspaceQueryKeys.graphDependencies(projectPath, options);
expect(result).toEqual(['workspace', projectPath, 'graph', 'dependencies', options]);
});
it('should create graph impact key with nodeId', () => {
const nodeId = 'node-123';
const result = workspaceQueryKeys.graphImpact(projectPath, nodeId);
expect(result).toEqual(['workspace', projectPath, 'graph', 'impact', nodeId]);
});
});
describe('cli history keys', () => {
it('should create cli history list key', () => {
const result = workspaceQueryKeys.cliHistoryList(projectPath);
expect(result).toEqual(['workspace', projectPath, 'cliHistory', 'list']);
});
it('should create cli execution detail key', () => {
const executionId = 'exec-123';
const result = workspaceQueryKeys.cliExecutionDetail(projectPath, executionId);
expect(result).toEqual(['workspace', projectPath, 'cliHistory', 'detail', executionId]);
});
});
describe('unified memory keys', () => {
it('should create unified search key', () => {
const query = 'test query';
const result = workspaceQueryKeys.unifiedSearch(projectPath, query);
expect(result).toEqual(['workspace', projectPath, 'unifiedMemory', 'search', query, undefined]);
});
it('should create unified search key with categories', () => {
const query = 'test query';
const categories = 'core,workflow';
const result = workspaceQueryKeys.unifiedSearch(projectPath, query, categories);
expect(result).toEqual(['workspace', projectPath, 'unifiedMemory', 'search', query, categories]);
});
});
describe('key isolation', () => {
it('should produce different keys for different project paths', () => {
const path1 = '/project/one';
const path2 = '/project/two';
const key1 = workspaceQueryKeys.sessionsList(path1);
const key2 = workspaceQueryKeys.sessionsList(path2);
expect(key1).not.toEqual(key2);
});
});
});
describe('apiSettingsKeys', () => {
describe('base key', () => {
it('should create base key', () => {
const result = apiSettingsKeys.all;
expect(result).toEqual(['apiSettings']);
});
});
describe('providers keys', () => {
it('should create providers list key', () => {
const result = apiSettingsKeys.providers();
expect(result).toEqual(['apiSettings', 'providers']);
});
it('should create provider detail key with id', () => {
const id = 'provider-123';
const result = apiSettingsKeys.provider(id);
expect(result).toEqual(['apiSettings', 'providers', id]);
});
});
describe('endpoints keys', () => {
it('should create endpoints list key', () => {
const result = apiSettingsKeys.endpoints();
expect(result).toEqual(['apiSettings', 'endpoints']);
});
it('should create endpoint detail key with id', () => {
const id = 'endpoint-456';
const result = apiSettingsKeys.endpoint(id);
expect(result).toEqual(['apiSettings', 'endpoints', id]);
});
});
describe('model pools keys', () => {
it('should create model pools list key', () => {
const result = apiSettingsKeys.modelPools();
expect(result).toEqual(['apiSettings', 'modelPools']);
});
it('should create model pool detail key with id', () => {
const id = 'pool-789';
const result = apiSettingsKeys.modelPool(id);
expect(result).toEqual(['apiSettings', 'modelPools', id]);
});
});
describe('cache key', () => {
it('should create cache key', () => {
const result = apiSettingsKeys.cache();
expect(result).toEqual(['apiSettings', 'cache']);
});
});
});
});

View File

@@ -0,0 +1,147 @@
// ========================================
// Utils Tests
// ========================================
// Tests for utility functions in utils.ts
import { describe, it, expect } from 'vitest';
import { cn, parseMemoryMetadata } from './utils';
describe('utils', () => {
describe('cn', () => {
it('should merge class names correctly', () => {
const result = cn('px-2', 'py-1');
expect(result).toContain('px-2');
expect(result).toContain('py-1');
});
it('should handle conflicting Tailwind classes by keeping the last one', () => {
const result = cn('px-2', 'px-4');
expect(result).toBe('px-4');
});
it('should handle conditional classes with undefined values', () => {
const condition = false;
const result = cn('base-class', condition && 'conditional-class');
expect(result).toBe('base-class');
});
it('should handle conditional classes with truthy values', () => {
const condition = true;
const result = cn('base-class', condition && 'conditional-class');
expect(result).toContain('base-class');
expect(result).toContain('conditional-class');
});
it('should handle empty input', () => {
const result = cn();
expect(result).toBe('');
});
it('should handle null and undefined inputs', () => {
const result = cn('valid-class', null, undefined, 'another-class');
expect(result).toContain('valid-class');
expect(result).toContain('another-class');
});
it('should handle object-style classes', () => {
const result = cn({ 'active': true, 'disabled': false });
expect(result).toBe('active');
});
it('should handle array of classes', () => {
const result = cn(['class-a', 'class-b']);
expect(result).toContain('class-a');
expect(result).toContain('class-b');
});
it('should merge multiple types of inputs', () => {
const result = cn(
'string-class',
['array-class'],
{ 'object-class': true },
true && 'conditional-class'
);
expect(result).toContain('string-class');
expect(result).toContain('array-class');
expect(result).toContain('object-class');
expect(result).toContain('conditional-class');
});
it('should deduplicate identical classes', () => {
const result = cn('duplicate', 'duplicate');
// clsx may or may not deduplicate, but tailwind-merge handles conflicts
expect(typeof result).toBe('string');
});
});
describe('parseMemoryMetadata', () => {
it('should return empty object for undefined input', () => {
const result = parseMemoryMetadata(undefined);
expect(result).toEqual({});
});
it('should return empty object for null input', () => {
const result = parseMemoryMetadata(null);
expect(result).toEqual({});
});
it('should return empty object for empty string', () => {
const result = parseMemoryMetadata('');
expect(result).toEqual({});
});
it('should return the object as-is when input is already an object', () => {
const input = { key: 'value', nested: { prop: 123 } };
const result = parseMemoryMetadata(input);
expect(result).toEqual(input);
});
it('should parse valid JSON string', () => {
const input = '{"key": "value", "number": 42}';
const result = parseMemoryMetadata(input);
expect(result).toEqual({ key: 'value', number: 42 });
});
it('should return empty object for invalid JSON string', () => {
const input = 'not a valid json';
const result = parseMemoryMetadata(input);
expect(result).toEqual({});
});
it('should handle complex nested object', () => {
const input = {
level1: {
level2: {
level3: 'deep value'
}
},
array: [1, 2, 3]
};
const result = parseMemoryMetadata(input);
expect(result).toEqual(input);
});
it('should parse JSON string with nested objects', () => {
const input = '{"outer": {"inner": "value"}}';
const result = parseMemoryMetadata(input);
expect(result).toEqual({ outer: { inner: 'value' } });
});
it('should handle JSON string with arrays', () => {
const input = '{"items": [1, 2, 3]}';
const result = parseMemoryMetadata(input);
expect(result).toEqual({ items: [1, 2, 3] });
});
it('should handle empty object string', () => {
const result = parseMemoryMetadata('{}');
expect(result).toEqual({});
});
it('should preserve array in object input', () => {
const input = { tags: ['a', 'b', 'c'] };
const result = parseMemoryMetadata(input);
expect(result.tags).toEqual(['a', 'b', 'c']);
});
});
});