diff --git a/src/shared/lib/store/createControlStore/createControlStore.test.ts b/src/shared/lib/store/createControlStore/createControlStore.test.ts new file mode 100644 index 0000000..fb9ce03 --- /dev/null +++ b/src/shared/lib/store/createControlStore/createControlStore.test.ts @@ -0,0 +1,89 @@ +import { get } from 'svelte/store'; +import { + beforeEach, + describe, + expect, + it, +} from 'vitest'; +import { + type ControlModel, + createControlStore, +} from './createControlStore'; + +describe('createControlStore', () => { + let store: ReturnType>; + + beforeEach(() => { + const initialState: ControlModel = { + value: 10, + min: 0, + max: 100, + step: 5, + }; + store = createControlStore(initialState); + }); + + it('initializes with correct state', () => { + expect(get(store)).toEqual({ + value: 10, + min: 0, + max: 100, + step: 5, + }); + }); + + it('increases value by step', () => { + store.increase(); + expect(get(store).value).toBe(15); + }); + + it('decreases value by step', () => { + store.decrease(); + expect(get(store).value).toBe(5); + }); + + it('clamps value at maximum', () => { + store.setValue(200); + expect(get(store).value).toBe(100); + }); + + it('clamps value at minimum', () => { + store.setValue(-10); + expect(get(store).value).toBe(0); + }); + + it('rounds to step precision', () => { + store.setValue(12.34); + // With step=5, 12.34 is clamped and rounded to nearest integer (0 decimal places) + expect(get(store).value).toBe(12); + }); + + it('handles decimal steps correctly', () => { + const decimalStore = createControlStore({ + value: 1.0, + min: 0, + max: 2, + step: 0.05, + }); + decimalStore.increase(); + expect(get(decimalStore).value).toBe(1.05); + }); + + it('isAtMax returns true when at maximum', () => { + store.setValue(100); + expect(store.isAtMax()).toBe(true); + }); + + it('isAtMax returns false when not at maximum', () => { + expect(store.isAtMax()).toBe(false); + }); + + it('isAtMin returns true when at minimum', () => { + store.setValue(0); + expect(store.isAtMin()).toBe(true); + }); + + it('isAtMin returns false when not at minimum', () => { + expect(store.isAtMin()).toBe(false); + }); +}); diff --git a/src/shared/lib/store/createFilterStore/createFilterStore.test.ts b/src/shared/lib/store/createFilterStore/createFilterStore.test.ts new file mode 100644 index 0000000..3b94235 --- /dev/null +++ b/src/shared/lib/store/createFilterStore/createFilterStore.test.ts @@ -0,0 +1,136 @@ +import { get } from 'svelte/store'; +import { + beforeEach, + describe, + expect, + it, +} from 'vitest'; +import { + type FilterModel, + type Property, + createFilterStore, +} from './createFilterStore'; + +describe('createFilterStore', () => { + const mockProperties: Property[] = [ + { id: '1', name: 'Sans-serif', selected: false }, + { id: '2', name: 'Serif', selected: false }, + { id: '3', name: 'Display', selected: false }, + ]; + + let store: ReturnType; + + beforeEach(() => { + const initialState: FilterModel = { + searchQuery: '', + properties: mockProperties, + }; + store = createFilterStore(initialState); + }); + + it('initializes with correct state', () => { + const state = get(store); + expect(state).toEqual({ + searchQuery: '', + properties: mockProperties, + }); + }); + + it('sets search query', () => { + store.setSearchQuery('serif'); + const state = get(store); + expect(state.searchQuery).toBe('serif'); + }); + + it('clears search query', () => { + store.setSearchQuery('test'); + store.clearSearchQuery(); + const state = get(store); + expect(state.searchQuery).toBeUndefined(); + }); + + it('selects a property', () => { + store.selectProperty('1'); + const state = get(store); + const property = state.properties.find(p => p.id === '1'); + expect(property?.selected).toBe(true); + }); + + it('deselects a property', () => { + store.selectProperty('1'); + store.deselectProperty('1'); + const state = get(store); + const property = state.properties.find(p => p.id === '1'); + expect(property?.selected).toBe(false); + }); + + it('toggles property from unselected to selected', () => { + store.toggleProperty('1'); + const state = get(store); + const property = state.properties.find(p => p.id === '1'); + expect(property?.selected).toBe(true); + }); + + it('toggles property from selected to unselected', () => { + store.selectProperty('1'); + store.toggleProperty('1'); + const state = get(store); + const property = state.properties.find(p => p.id === '1'); + expect(property?.selected).toBe(false); + }); + + it('selects all properties', () => { + store.selectAllProperties(); + const state = get(store); + expect(state.properties.every(p => p.selected)).toBe(true); + }); + + it('deselects all properties', () => { + store.selectAllProperties(); + store.deselectAllProperties(); + const state = get(store); + expect(state.properties.every(p => !p.selected)).toBe(true); + }); + + it('gets all properties', () => { + const allProps = store.getAllProperties(); + const props = get(allProps); + expect(props).toEqual(mockProperties); + }); + + it('gets selected properties', () => { + store.selectProperty('1'); + store.selectProperty('3'); + const selectedProps = store.getSelectedProperties(); + const props = get(selectedProps); + expect(props).toHaveLength(2); + expect(props?.[0].id).toBe('1'); + expect(props?.[1].id).toBe('3'); + }); + + it('filters properties by search query', () => { + store.setSearchQuery('serif'); + const filteredProps = store.getFilteredProperties(); + const props = get(filteredProps); + // 'serif' is a substring of 'Sans-serif' (case-sensitive match) + expect(props).toHaveLength(1); + expect(props?.[0].id).toBe('1'); + }); + + it('filter is case-sensitive', () => { + store.setSearchQuery('San'); + const filteredProps = store.getFilteredProperties(); + const props = get(filteredProps); + // 'San' matches 'Sans-serif' exactly (case-sensitive) + expect(props).toHaveLength(1); + expect(props?.[0].id).toBe('1'); + }); + + it('filter returns all properties when query is empty', () => { + store.setSearchQuery(''); + const filteredProps = store.getFilteredProperties(); + let props: Property[] | undefined = undefined; + filteredProps.subscribe(p => (props = p))(); + expect(props).toHaveLength(3); + }); +}); diff --git a/src/shared/lib/store/index.ts b/src/shared/lib/store/index.ts new file mode 100644 index 0000000..f94a02a --- /dev/null +++ b/src/shared/lib/store/index.ts @@ -0,0 +1,18 @@ +/** + * Shared store exports + * + * Exports all store creators and types for Svelte 5 reactive state management + */ + +export { createFilterStore } from './createFilterStore/createFilterStore'; +export type { + FilterModel, + FilterStore, + Property, +} from './createFilterStore/createFilterStore'; + +export { createControlStore } from './createControlStore/createControlStore'; +export type { + ControlModel, + ControlStoreModel, +} from './createControlStore/createControlStore';