mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 00:19:41 +02:00
Generic factory that eliminates ~110 LOC boilerplate per module for view mode, sort, filters, and saved filter presets with localStorage persistence. Includes 23 tests covering all store operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
// Must dynamically import to get fresh $state per test
|
|
async function freshStore() {
|
|
vi.resetModules();
|
|
const mod = await import('./view.svelte');
|
|
return mod.createViewStore;
|
|
}
|
|
|
|
type TestViewMode = 'list' | 'grid' | 'table';
|
|
|
|
interface TestFilters {
|
|
search?: string;
|
|
tagIds?: string[];
|
|
status?: string[];
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
describe('createViewStore', () => {
|
|
describe('defaults', () => {
|
|
it('initializes with default view mode', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
expect(store.viewMode).toBe('list');
|
|
});
|
|
|
|
it('initializes with default sort', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
expect(store.sort).toEqual({ field: 'name', direction: 'asc' });
|
|
});
|
|
|
|
it('starts with empty filters', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
expect(store.activeFilters).toEqual({});
|
|
});
|
|
|
|
it('starts with empty saved filters', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
expect(store.savedFilters).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('viewMode', () => {
|
|
it('updates view mode', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setViewMode('grid');
|
|
expect(store.viewMode).toBe('grid');
|
|
});
|
|
|
|
it('persists view mode to localStorage', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setViewMode('table');
|
|
expect(JSON.parse(localStorage.getItem('test_view_mode')!)).toBe('table');
|
|
});
|
|
});
|
|
|
|
describe('sort', () => {
|
|
it('updates sort', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setSort({ field: 'date', direction: 'desc' });
|
|
expect(store.sort).toEqual({ field: 'date', direction: 'desc' });
|
|
});
|
|
|
|
it('persists sort to localStorage', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setSort({ field: 'date', direction: 'desc' });
|
|
expect(JSON.parse(localStorage.getItem('test_sort')!)).toEqual({
|
|
field: 'date',
|
|
direction: 'desc',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('filters', () => {
|
|
it('sets filters', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello', tagIds: ['t1'] });
|
|
expect(store.activeFilters).toEqual({ search: 'hello', tagIds: ['t1'] });
|
|
});
|
|
|
|
it('updates a single filter key', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
store.updateFilter('tagIds', ['t1', 't2']);
|
|
expect(store.activeFilters).toEqual({ search: 'hello', tagIds: ['t1', 't2'] });
|
|
});
|
|
|
|
it('clears all filters', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello', tagIds: ['t1'] });
|
|
store.clearFilters();
|
|
expect(store.activeFilters).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('hasActiveFilters', () => {
|
|
it('returns false when no filters are set', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
expect(store.hasActiveFilters).toBe(false);
|
|
});
|
|
|
|
it('uses default heuristic when no custom fn provided', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
expect(store.hasActiveFilters).toBe(true);
|
|
});
|
|
|
|
it('ignores empty arrays in default heuristic', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ tagIds: [] });
|
|
expect(store.hasActiveFilters).toBe(false);
|
|
});
|
|
|
|
it('uses custom hasActiveFilters function', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
hasActiveFilters: (f) => !!f.search,
|
|
});
|
|
store.setFilters({ tagIds: ['t1'] });
|
|
expect(store.hasActiveFilters).toBe(false);
|
|
store.setFilters({ search: 'x' });
|
|
expect(store.hasActiveFilters).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('saved filters', () => {
|
|
it('saves a named filter preset', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
store.saveFilter('My Filter');
|
|
expect(store.savedFilters).toHaveLength(1);
|
|
expect(store.savedFilters[0].name).toBe('My Filter');
|
|
expect(store.savedFilters[0].criteria).toEqual({ search: 'hello' });
|
|
});
|
|
|
|
it('persists saved filters to localStorage', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
store.saveFilter('My Filter');
|
|
const stored = JSON.parse(localStorage.getItem('test_saved_filters')!);
|
|
expect(stored).toHaveLength(1);
|
|
expect(stored[0].name).toBe('My Filter');
|
|
});
|
|
|
|
it('loads a saved filter', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
store.saveFilter('My Filter');
|
|
store.clearFilters();
|
|
expect(store.activeFilters).toEqual({});
|
|
store.loadFilter(store.savedFilters[0].id);
|
|
expect(store.activeFilters).toEqual({ search: 'hello' });
|
|
});
|
|
|
|
it('deletes a saved filter', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.setFilters({ search: 'hello' });
|
|
store.saveFilter('Filter 1');
|
|
store.saveFilter('Filter 2');
|
|
expect(store.savedFilters).toHaveLength(2);
|
|
store.deleteSavedFilter(store.savedFilters[0].id);
|
|
expect(store.savedFilters).toHaveLength(1);
|
|
expect(store.savedFilters[0].name).toBe('Filter 2');
|
|
});
|
|
});
|
|
|
|
describe('initialize', () => {
|
|
it('loads persisted view mode from localStorage', async () => {
|
|
localStorage.setItem('test_view_mode', JSON.stringify('grid'));
|
|
localStorage.setItem('test_sort', JSON.stringify({ field: 'date', direction: 'desc' }));
|
|
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.initialize();
|
|
expect(store.viewMode).toBe('grid');
|
|
expect(store.sort).toEqual({ field: 'date', direction: 'desc' });
|
|
});
|
|
|
|
it('only initializes once', async () => {
|
|
localStorage.setItem('test_view_mode', JSON.stringify('grid'));
|
|
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.initialize();
|
|
expect(store.viewMode).toBe('grid');
|
|
|
|
// Change localStorage directly — second initialize should be no-op
|
|
localStorage.setItem('test_view_mode', JSON.stringify('table'));
|
|
store.initialize();
|
|
expect(store.viewMode).toBe('grid');
|
|
});
|
|
|
|
it('loads persisted saved filters', async () => {
|
|
localStorage.setItem(
|
|
'test_saved_filters',
|
|
JSON.stringify([
|
|
{ id: 'f1', name: 'Preset', criteria: { search: 'test' }, createdAt: '2024-01-01' },
|
|
])
|
|
);
|
|
|
|
const createViewStore = await freshStore();
|
|
const store = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'test',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
store.initialize();
|
|
expect(store.savedFilters).toHaveLength(1);
|
|
expect(store.savedFilters[0].name).toBe('Preset');
|
|
});
|
|
});
|
|
|
|
describe('storage prefix isolation', () => {
|
|
it('uses different localStorage keys per prefix', async () => {
|
|
const createViewStore = await freshStore();
|
|
const store1 = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'app1',
|
|
defaultViewMode: 'list',
|
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
});
|
|
const _store2 = createViewStore<TestViewMode, TestFilters>({
|
|
storagePrefix: 'app2',
|
|
defaultViewMode: 'grid',
|
|
defaultSort: { field: 'date', direction: 'desc' },
|
|
});
|
|
|
|
store1.setViewMode('table');
|
|
expect(JSON.parse(localStorage.getItem('app1_view_mode')!)).toBe('table');
|
|
expect(localStorage.getItem('app2_view_mode')).toBeNull();
|
|
});
|
|
});
|
|
});
|