Complete Step 16 by extracting and testing the useJsonFilter composable with comprehensive functionality and test coverage

This commit is contained in:
2026-01-29 03:47:32 +00:00
parent 78e5dd9217
commit 9507059ad9
3 changed files with 964 additions and 30 deletions

View File

@@ -0,0 +1,575 @@
import { describe, it, expect, beforeEach } from 'vitest';
import useJsonFilter from '../../../src/composables/useJsonFilter.js';
describe('useJsonFilter', () => {
let filter;
beforeEach(() => {
filter = useJsonFilter();
});
// Test data
const testData = [
{ id: 1, name: 'Pikachu', type: 'Electric', level: 25 },
{ id: 2, name: 'Charizard', type: 'Fire', level: 50 },
{ id: 3, name: 'Blastoise', type: 'Water', level: 50 },
{ id: 4, name: 'Venusaur', type: 'Grass', level: 50 },
{ id: 5, name: 'Pikachu', type: 'Electric', level: 30 },
];
const nestedData = [
{
id: 1,
name: 'Pokemon',
stats: { hp: 100, attack: 150, defense: 75 },
moves: ['Thunder', 'Thunderbolt']
},
{
id: 2,
name: 'Dragon',
stats: { hp: 120, attack: 180, defense: 95 },
moves: ['DragonClaw', 'DragonDance']
}
];
describe('Initialization', () => {
it('should initialize with empty state', () => {
expect(filter.filterProperty.value).toBe('');
expect(filter.filterValue.value).toBe('');
expect(filter.filterMode.value).toBe('equals');
expect(filter.availablePaths.value).toEqual([]);
expect(filter.filterError.value).toBeNull();
});
it('should initialize filter with data', () => {
filter.initializeFilter(testData);
expect(filter.availablePaths.value.length).toBeGreaterThan(0);
expect(filter.availablePaths.value[0]).toHaveProperty('path');
expect(filter.availablePaths.value[0]).toHaveProperty('breadcrumb');
});
it('should extract correct paths from simple data', () => {
filter.initializeFilter(testData);
const paths = filter.availablePaths.value.map(p => p.path);
expect(paths).toContain('id');
expect(paths).toContain('name');
expect(paths).toContain('type');
expect(paths).toContain('level');
});
it('should extract nested paths from complex data', () => {
filter.initializeFilter(nestedData);
const paths = filter.availablePaths.value.map(p => p.path);
expect(paths).toContain('stats.hp');
expect(paths).toContain('stats.attack');
expect(paths).toContain('stats.defense');
});
it('should handle empty data gracefully', () => {
filter.initializeFilter([]);
expect(filter.availablePaths.value).toEqual([]);
});
it('should handle non-array data', () => {
filter.initializeFilter(null);
expect(filter.availablePaths.value).toEqual([]);
});
});
describe('Path Extraction', () => {
it('should extract paths with breadcrumb formatting', () => {
filter.extractPathsFromData(nestedData);
const nestedPaths = filter.availablePaths.value.filter(p =>
p.path.includes('.')
);
expect(nestedPaths.length).toBeGreaterThan(0);
expect(nestedPaths[0].breadcrumb).toContain('');
});
it('should respect maxDepth parameter', () => {
const deepData = [
{
level1: {
level2: {
level3: {
level4: {
level5: {
value: 'deep'
}
}
}
}
}
}
];
filter.initializeFilter(deepData);
const paths = filter.availablePaths.value.map(p => p.path);
// maxDepth is 5, so level4 should be present but level5.value may not
expect(paths.some(p => p.includes('level1'))).toBeTruthy();
expect(paths.some(p => p.includes('level4'))).toBeTruthy();
});
it('should extract paths from sample size correctly', () => {
filter.extractPathsFromData(testData, 2); // Only first 2 items
expect(filter.availablePaths.value.length).toBeGreaterThan(0);
});
it('should sort paths alphabetically', () => {
filter.initializeFilter(testData);
const paths = filter.availablePaths.value.map(p => p.path);
const sortedPaths = [...paths].sort();
expect(paths).toEqual(sortedPaths);
});
});
describe('Filtering - Equals Mode', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should filter by exact string match', () => {
filter.setFilter('name', 'Pikachu', 'equals');
expect(filter.filteredData.value.length).toBe(2);
expect(filter.filteredData.value.every(item => item.name === 'Pikachu')).toBeTruthy();
});
it('should filter by exact number match', () => {
filter.setFilter('level', '50', 'equals');
expect(filter.filteredData.value.length).toBe(3);
expect(filter.filteredData.value.every(item => item.level === 50)).toBeTruthy();
});
it('should be case-insensitive', () => {
filter.setFilter('name', 'pikachu', 'equals');
expect(filter.filteredData.value.length).toBe(2);
});
it('should return all data with no filter', () => {
filter.clearFilters();
expect(filter.filteredData.value.length).toBe(testData.length);
});
it('should return empty with no matching filter', () => {
filter.setFilter('name', 'Nonexistent', 'equals');
expect(filter.filteredData.value.length).toBe(0);
});
});
describe('Filtering - Contains Mode', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should filter by substring match', () => {
filter.setFilter('name', 'izard', 'contains');
expect(filter.filteredData.value.length).toBe(1);
expect(filter.filteredData.value[0].name).toBe('Charizard');
});
it('should be case-insensitive', () => {
filter.setFilter('name', 'CHARIZARD', 'contains');
expect(filter.filteredData.value.length).toBe(1);
});
it('should match partial strings', () => {
filter.setFilter('type', 'Fire', 'contains');
expect(filter.filteredData.value.length).toBeGreaterThan(0);
});
it('should return all with empty filter value', () => {
filter.filterProperty.value = 'name';
filter.filterValue.value = '';
filter.filterMode.value = 'contains';
expect(filter.filteredData.value.length).toBe(testData.length);
});
});
describe('Filtering - Regex Mode', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should filter by regex pattern', () => {
filter.setFilter('name', '^P', 'regex');
const filtered = filter.filteredData.value;
expect(filtered.length).toBe(2); // Pikachu (x2)
expect(filtered.every(item => item.name.startsWith('P'))).toBeTruthy();
});
it('should handle complex regex patterns', () => {
filter.setFilter('name', '(Pikachu|Charizard)', 'regex');
const names = filter.filteredData.value.map(item => item.name);
expect(names.every(name =>
name === 'Pikachu' || name === 'Charizard'
)).toBeTruthy();
});
it('should be case-insensitive by default in regex', () => {
filter.setFilter('name', 'pikachu', 'regex');
expect(filter.filteredData.value.length).toBe(2);
});
it('should handle invalid regex gracefully', () => {
filter.setFilter('name', '[invalid', 'regex');
expect(filter.filterError.value).toBeTruthy();
});
it('should match numbers with regex', () => {
filter.setFilter('level', '\\d{2}', 'regex'); // 2-digit numbers
expect(filter.filteredData.value.length).toBeGreaterThan(0);
});
});
describe('Nested Property Filtering', () => {
beforeEach(() => {
filter.initializeFilter(nestedData);
});
it('should filter by nested property', () => {
filter.setFilter('stats.hp', '100', 'equals');
expect(filter.filteredData.value.length).toBe(1);
expect(filter.filteredData.value[0].id).toBe(1);
});
it('should filter nested properties with contains', () => {
filter.setFilter('stats.attack', '15', 'contains');
expect(filter.filteredData.value.length).toBeGreaterThan(0);
});
it('should handle missing nested properties', () => {
filter.setFilter('nonexistent.property', 'value', 'equals');
expect(filter.filteredData.value.length).toBe(0);
});
});
describe('matchesFilter Function', () => {
it('should match equals mode correctly', () => {
expect(filter.matchesFilter('test', 'test', 'equals')).toBeTruthy();
expect(filter.matchesFilter('test', 'TEST', 'equals')).toBeTruthy();
expect(filter.matchesFilter('test', 'no', 'equals')).toBeFalsy();
});
it('should match contains mode correctly', () => {
expect(filter.matchesFilter('testing', 'test', 'contains')).toBeTruthy();
expect(filter.matchesFilter('testing', 'TEST', 'contains')).toBeTruthy();
expect(filter.matchesFilter('testing', 'xyz', 'contains')).toBeFalsy();
});
it('should match regex mode correctly', () => {
expect(filter.matchesFilter('test123', '\\d+', 'regex')).toBeTruthy();
expect(filter.matchesFilter('test', '^test$', 'regex')).toBeTruthy();
expect(filter.matchesFilter('nodigits', '\\d+', 'regex')).toBeFalsy();
});
it('should handle empty filter value', () => {
expect(filter.matchesFilter('anything', '', 'equals')).toBeTruthy();
expect(filter.matchesFilter('anything', '', 'contains')).toBeTruthy();
});
it('should convert values to strings', () => {
expect(filter.matchesFilter(123, '123', 'equals')).toBeTruthy();
expect(filter.matchesFilter(true, 'true', 'equals')).toBeTruthy();
});
});
describe('getUniqueValues Function', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should return unique values for a property', () => {
const types = filter.getUniqueValues('type');
expect(types.length).toBe(4); // Electric, Fire, Water, Grass
expect(new Set(types).size).toBe(types.length); // All unique
});
it('should return sorted values', () => {
const types = filter.getUniqueValues('type');
const sorted = [...types].sort();
expect(types).toEqual(sorted);
});
it('should handle missing properties', () => {
const values = filter.getUniqueValues('nonexistent');
expect(values).toEqual([]);
});
it('should exclude null and undefined', () => {
const dataWithNull = [
{ prop: 'value1' },
{ prop: null },
{ prop: undefined },
{ prop: 'value2' }
];
filter.initializeFilter(dataWithNull);
const values = filter.getUniqueValues('prop');
expect(values).not.toContain('null');
expect(values).not.toContain('undefined');
});
it('should work with nested properties', () => {
// Re-initialize with nestedData for this test
filter.initializeFilter(nestedData);
const values = filter.getUniqueValues('stats.attack');
expect(values.length).toBeGreaterThan(0);
});
});
describe('Filter State Management', () => {
beforeEach(() => {
filter.initializeFilter(testData);
filter.setFilter('name', 'Pikachu', 'equals');
});
it('should set filter correctly', () => {
expect(filter.filterProperty.value).toBe('name');
expect(filter.filterValue.value).toBe('Pikachu');
expect(filter.filterMode.value).toBe('equals');
});
it('should clear filters', () => {
filter.clearFilters();
expect(filter.filterProperty.value).toBe('');
expect(filter.filterValue.value).toBe('');
expect(filter.filterMode.value).toBe('equals');
expect(filter.filterError.value).toBeNull();
});
it('should set hasActiveFilter correctly', () => {
expect(filter.hasActiveFilter.value).toBeTruthy();
filter.clearFilters();
expect(filter.hasActiveFilter.value).toBeFalsy();
});
it('should clear error on successful filter', () => {
filter.setFilter('name', '[invalid', 'regex');
expect(filter.filterError.value).toBeTruthy();
filter.setFilter('name', 'Pikachu', 'equals');
expect(filter.filterError.value).toBeNull();
});
});
describe('Filter Description', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should return default description with no filter', () => {
expect(filter.filterDescription.value).toBe('No filter applied');
});
it('should show description for equals filter', () => {
filter.setFilter('name', 'Pikachu', 'equals');
expect(filter.filterDescription.value).toContain('name');
expect(filter.filterDescription.value).toContain('Pikachu');
expect(filter.filterDescription.value).toContain('=');
});
it('should show description for contains filter', () => {
filter.setFilter('type', 'Fire', 'contains');
expect(filter.filterDescription.value).toContain('type');
expect(filter.filterDescription.value).toContain('Fire');
expect(filter.filterDescription.value).toContain('∋');
});
it('should show description for regex filter', () => {
filter.setFilter('name', '^P', 'regex');
expect(filter.filterDescription.value).toContain('name');
expect(filter.filterDescription.value).toContain('^P');
expect(filter.filterDescription.value).toContain('~');
});
});
describe('Filter Statistics', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should calculate stats with no filter', () => {
const stats = filter.filterStats.value;
expect(stats.total).toBe(testData.length);
expect(stats.matched).toBe(testData.length);
expect(stats.percentage).toBe(100);
});
it('should calculate stats with active filter', () => {
filter.setFilter('type', 'Electric', 'equals');
const stats = filter.filterStats.value;
expect(stats.total).toBe(testData.length);
expect(stats.matched).toBe(2);
expect(stats.percentage).toBe(40); // 2/5 = 40%
});
it('should calculate stats with no matches', () => {
filter.setFilter('name', 'Nonexistent', 'equals');
const stats = filter.filterStats.value;
expect(stats.total).toBe(testData.length);
expect(stats.matched).toBe(0);
expect(stats.percentage).toBe(0);
});
it('should handle empty data stats', () => {
filter.initializeFilter([]);
const stats = filter.filterStats.value;
expect(stats.total).toBe(0);
expect(stats.percentage).toBe(0);
});
});
describe('Lazy Path Extraction', () => {
it('should extract paths lazily from large datasets', (done) => {
const largeData = Array.from({ length: 300 }, (_, i) => ({
id: i,
name: `Item ${i}`,
category: i % 10,
metadata: { nested: true }
}));
filter.initializeFilter(largeData.slice(0, 100));
const initialPathCount = filter.availablePaths.value.length;
filter.extractPathsLazy(largeData, 100, (newPaths) => {
expect(filter.availablePaths.value.length).toBeGreaterThanOrEqual(
initialPathCount
);
done();
});
// Give async operation time to complete
setTimeout(() => {
done();
}, 200);
});
});
describe('Error Handling', () => {
beforeEach(() => {
filter.initializeFilter(testData);
});
it('should handle invalid regex gracefully', () => {
filter.setFilter('name', '[invalid(regex', 'regex');
expect(filter.filterError.value).toBeTruthy();
// With an error, it should return all data
expect(filter.filteredData.value.length).toBe(testData.length);
});
it('should handle missing property in filtering', () => {
filter.setFilter('missing.property.path', 'value', 'equals');
expect(filter.filteredData.value.length).toBe(0);
});
it('should handle null/undefined data in filtering', () => {
const dataWithNulls = [
{ id: 1, value: 'test' },
{ id: 2, value: null },
{ id: 3 } // missing value prop
];
filter.initializeFilter(dataWithNulls);
filter.setFilter('value', 'test', 'equals');
expect(filter.filteredData.value.length).toBe(1);
});
it('should recover from regex error when setting valid filter', () => {
filter.setFilter('name', '[invalid', 'regex');
expect(filter.filterError.value).toBeTruthy();
filter.setFilter('name', 'Pikachu', 'equals');
expect(filter.filterError.value).toBeNull();
});
});
describe('Edge Cases', () => {
it('should handle data with circular references', () => {
const circular = { id: 1, name: 'test' };
circular.self = circular; // Circular reference
// This should not crash
filter.extractPathsFromData([circular], 1);
expect(filter.availablePaths.value.length).toBeGreaterThan(0);
});
it('should handle arrays in data without recursing', () => {
const dataWithArrays = [
{ id: 1, tags: ['a', 'b', 'c'], name: 'test' }
];
filter.initializeFilter(dataWithArrays);
const paths = filter.availablePaths.value.map(p => p.path);
// Should have tags as a path, but not array indices
expect(paths).toContain('tags');
});
it('should handle numeric keys in objects', () => {
const dataWithNumericKeys = [
{ '1': 'numeric', '2': 'keys', name: 'test' }
];
filter.initializeFilter(dataWithNumericKeys);
const paths = filter.availablePaths.value.map(p => p.path);
expect(paths).toContain('1');
expect(paths).toContain('2');
expect(paths).toContain('name');
});
it('should handle unicode characters in paths', () => {
const unicodeData = [
{ '名前': 'Japanese', 'nome': 'Italian', name: 'English' }
];
filter.initializeFilter(unicodeData);
const paths = filter.availablePaths.value.map(p => p.path);
expect(paths).toContain('名前');
expect(paths).toContain('nome');
});
});
});