diff --git a/.gitignore b/.gitignore
index 67e08e0..688bf80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,6 @@ AGENTS.md
*storybook.log
storybook-static
+
+# Tests
+coverage/
diff --git a/package.json b/package.json
index 8d7b113..3710949 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,12 @@
"format": "dprint fmt",
"format:check": "dprint check",
"test:e2e": "playwright test",
- "test": "npm run test:e2e",
+ "test:unit": "vitest run",
+ "test:unit:watch": "vitest",
+ "test:unit:ui": "vitest --ui",
+ "test:unit:coverage": "vitest run --coverage",
+ "test:component": "vitest run --config vitest.config.component.ts",
+ "test": "npm run test:e2e && npm run test:unit",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
@@ -31,12 +36,16 @@
"@storybook/svelte-vite": "^10.1.11",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.18",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/svelte": "^5.3.1",
"@tsconfig/svelte": "^5.0.6",
+ "@types/jsdom": "^27",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"bits-ui": "^2.14.4",
"clsx": "^2.1.1",
"dprint": "^0.50.2",
+ "jsdom": "^27.4.0",
"lefthook": "^2.0.13",
"oxlint": "^1.35.0",
"playwright": "^1.57.0",
@@ -50,6 +59,7 @@
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^7.2.6",
- "vitest": "^4.0.16"
+ "vitest": "^4.0.16",
+ "vitest-browser-svelte": "^2.0.1"
}
}
diff --git a/src/features/SetupFont/ui/SetupFontMenu.svelte b/src/features/SetupFont/ui/SetupFontMenu.svelte
index 67a440c..383c928 100644
--- a/src/features/SetupFont/ui/SetupFontMenu.svelte
+++ b/src/features/SetupFont/ui/SetupFontMenu.svelte
@@ -12,48 +12,44 @@ const fontWeight = $derived($fontWeightStore);
const lineHeight = $derived($lineHeightStore);
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/shared/store/createControlStore.test.ts b/src/shared/store/createControlStore.test.ts
new file mode 100644
index 0000000..fb9ce03
--- /dev/null
+++ b/src/shared/store/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/store/createFilterStore.test.ts b/src/shared/store/createFilterStore.test.ts
new file mode 100644
index 0000000..3b94235
--- /dev/null
+++ b/src/shared/store/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/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts
new file mode 100644
index 0000000..615d23a
--- /dev/null
+++ b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts
@@ -0,0 +1,85 @@
+import type { Property } from '$shared/store/createFilterStore';
+import {
+ fireEvent,
+ render,
+ screen,
+} from '@testing-library/svelte';
+import {
+ beforeEach,
+ describe,
+ expect,
+ it,
+ vi,
+} from 'vitest';
+import CheckboxFilter from './CheckboxFilter.svelte';
+
+describe('CheckboxFilter', () => {
+ const mockProperties: Property[] = [
+ { id: '1', name: 'Sans-serif', selected: false },
+ { id: '2', name: 'Serif', selected: true },
+ { id: '3', name: 'Display', selected: false },
+ ];
+
+ const mockOnPropertyToggle = vi.fn();
+
+ beforeEach(() => {
+ mockOnPropertyToggle.mockClear();
+ });
+
+ it('renders with correct label', () => {
+ render(CheckboxFilter, {
+ displayedLabel: 'Font Categories',
+ properties: mockProperties,
+ onPropertyToggle: mockOnPropertyToggle,
+ });
+
+ expect(screen.getByText('Font Categories')).toBeInTheDocument();
+ });
+
+ it('displays all properties as checkboxes', () => {
+ render(CheckboxFilter, {
+ displayedLabel: 'Categories',
+ properties: mockProperties,
+ onPropertyToggle: mockOnPropertyToggle,
+ });
+
+ expect(screen.getByLabelText('Sans-serif')).toBeInTheDocument();
+ expect(screen.getByLabelText('Serif')).toBeInTheDocument();
+ expect(screen.getByLabelText('Display')).toBeInTheDocument();
+ });
+
+ it('shows selected count badge when items selected', () => {
+ render(CheckboxFilter, {
+ displayedLabel: 'Categories',
+ properties: mockProperties,
+ onPropertyToggle: mockOnPropertyToggle,
+ });
+
+ expect(screen.getByText('1')).toBeInTheDocument();
+ });
+
+ it('does not show badge when no items selected', () => {
+ const allUnselected = mockProperties.map(p => ({ ...p, selected: false }));
+
+ render(CheckboxFilter, {
+ displayedLabel: 'Categories',
+ properties: allUnselected,
+ onPropertyToggle: mockOnPropertyToggle,
+ });
+
+ expect(screen.queryByText('0')).not.toBeInTheDocument();
+ });
+
+ it('calls onPropertyToggle when checkbox clicked', async () => {
+ render(CheckboxFilter, {
+ displayedLabel: 'Categories',
+ properties: mockProperties,
+ onPropertyToggle: mockOnPropertyToggle,
+ });
+
+ const checkbox = screen.getByLabelText('Sans-serif');
+ await checkbox.click();
+
+ expect(mockOnPropertyToggle).toHaveBeenCalledWith('1');
+ });
+});
diff --git a/src/shared/ui/ComboControl/ComboControl.svelte.test.ts b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts
new file mode 100644
index 0000000..762db05
--- /dev/null
+++ b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts
@@ -0,0 +1,308 @@
+import {
+ fireEvent,
+ render,
+} from '@testing-library/svelte';
+import {
+ beforeEach,
+ describe,
+ expect,
+ it,
+ vi,
+} from 'vitest';
+import ComboControl from './ComboControl.svelte';
+
+describe('ComboControl', () => {
+ const onChangeMock = vi.fn() as (value: number) => void;
+ const onIncreaseMock = vi.fn() as () => void;
+ const onDecreaseMock = vi.fn() as () => void;
+
+ it('renders with default values', () => {
+ const { container } = render(ComboControl, {
+ value: 50,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ });
+
+ // Check that the control button displays the value
+ const controlButton = container.querySelector(
+ 'button[variant="outline"][size="icon"]:nth-child(2)',
+ );
+ expect(controlButton?.textContent).toBe('50');
+ });
+
+ it('renders with custom min/max/step', () => {
+ const { container } = render(ComboControl, {
+ value: 5,
+ minValue: 0,
+ maxValue: 10,
+ step: 0.5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ });
+
+ const controlButton = container.querySelector(
+ 'button[variant="outline"][size="icon"]:nth-child(2)',
+ );
+ expect(controlButton?.textContent).toBe('5');
+ });
+
+ it('calls onIncrease when increase button is clicked', async () => {
+ const { getByLabelText } = render(ComboControl, {
+ value: 5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ increaseLabel: 'Increase value',
+ });
+
+ const increaseButton = getByLabelText('Increase value');
+ await fireEvent.click(increaseButton);
+
+ expect(onIncreaseMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onDecrease when decrease button is clicked', async () => {
+ const { getByLabelText } = render(ComboControl, {
+ value: 5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ decreaseLabel: 'Decrease value',
+ });
+
+ const decreaseButton = getByLabelText('Decrease value');
+ await fireEvent.click(decreaseButton);
+
+ expect(onDecreaseMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('disables increase button when increaseDisabled is true', () => {
+ const { getByLabelText } = render(ComboControl, {
+ value: 100,
+ minValue: 0,
+ maxValue: 100,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ increaseDisabled: true,
+ increaseLabel: 'Increase value',
+ });
+
+ const increaseButton = getByLabelText('Increase value');
+ expect(increaseButton).toBeDisabled();
+ });
+
+ it('disables decrease button when decreaseDisabled is true', () => {
+ const { getByLabelText } = render(ComboControl, {
+ value: 0,
+ minValue: 0,
+ maxValue: 100,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ decreaseDisabled: true,
+ decreaseLabel: 'Decrease value',
+ });
+
+ const decreaseButton = getByLabelText('Decrease value');
+ expect(decreaseButton).toBeDisabled();
+ });
+
+ it('opens popover when control button is clicked', async () => {
+ const { getByLabelText, queryByRole } = render(ComboControl, {
+ value: 5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Initially, popover content should not be visible
+ expect(queryByRole('dialog')).not.toBeInTheDocument();
+
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // After clicking, popover content should be visible
+ expect(queryByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('updates value when slider changes', async () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 5,
+ minValue: 0,
+ maxValue: 10,
+ step: 1,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // Find slider - the Slider component should render an input with role slider
+ const slider = container.querySelector('[role="slider"]');
+ expect(slider).toBeInTheDocument();
+
+ // Simulate slider change
+ await fireEvent.input(slider!, { target: { value: '7' } });
+
+ expect(onChangeMock).toHaveBeenCalledWith(7);
+ });
+
+ it('updates value when number input changes', async () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 5,
+ minValue: 0,
+ maxValue: 10,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // Find number input
+ const input = container.querySelector('input[type="text"], input[type="number"]');
+ expect(input).toBeInTheDocument();
+
+ // Simulate input change
+ await fireEvent.change(input!, { target: { value: '8' } });
+
+ expect(onChangeMock).toHaveBeenCalledWith(8);
+ });
+
+ it('respects min and max values on input', () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 5,
+ minValue: 0,
+ maxValue: 10,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ fireEvent.click(controlButton);
+
+ // Find input
+ const input = container.querySelector('input[type="text"], input[type="number"]');
+
+ // Check min and max attributes
+ expect(input).toHaveAttribute('min', '0');
+ expect(input).toHaveAttribute('max', '10');
+ });
+
+ it('uses custom aria-labels', () => {
+ const { getByLabelText } = render(ComboControl, {
+ value: 5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ increaseLabel: 'Increase by step',
+ decreaseLabel: 'Decrease by step',
+ controlLabel: 'Change value',
+ });
+
+ expect(getByLabelText('Increase by step')).toBeInTheDocument();
+ expect(getByLabelText('Decrease by step')).toBeInTheDocument();
+ expect(getByLabelText('Change value')).toBeInTheDocument();
+ });
+
+ it('uses default min/max/step values when not provided', () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 50,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ fireEvent.click(controlButton);
+
+ // Find input
+ const input = container.querySelector('input[type="text"], input[type="number"]');
+
+ // Check default values (0, 100, 1)
+ expect(input).toHaveAttribute('min', '0');
+ expect(input).toHaveAttribute('max', '100');
+ });
+
+ it('does not call onChange when input value is invalid', async () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 5,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // Find input
+ const input = container.querySelector('input[type="text"], input[type="number"]');
+
+ // Simulate invalid input
+ await fireEvent.change(input!, { target: { value: 'invalid' } });
+
+ expect(onChangeMock).not.toHaveBeenCalled();
+ });
+
+ it('displays current value in input field', async () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 42,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // Find input
+ const input = container.querySelector('input[type="text"], input[type="number"]');
+
+ expect(input).toHaveValue('42');
+ });
+
+ it('handles step value for slider precision', async () => {
+ const { getByLabelText, container } = render(ComboControl, {
+ value: 5,
+ minValue: 0,
+ maxValue: 10,
+ step: 0.25,
+ onChange: onChangeMock,
+ onIncrease: onIncreaseMock,
+ onDecrease: onDecreaseMock,
+ controlLabel: 'Control value',
+ });
+
+ // Open popover
+ const controlButton = getByLabelText('Control value');
+ await fireEvent.click(controlButton);
+
+ // Find slider
+ const slider = container.querySelector('[role="slider"]');
+
+ // Simulate slider change
+ await fireEvent.input(slider!, { target: { value: '5.5' } });
+
+ expect(onChangeMock).toHaveBeenCalledWith(5.5);
+ });
+});
diff --git a/src/widgets/TypographySettings/ui/TypographyMenu.svelte b/src/widgets/TypographySettings/ui/TypographyMenu.svelte
index 4398c5c..60b4f2c 100644
--- a/src/widgets/TypographySettings/ui/TypographyMenu.svelte
+++ b/src/widgets/TypographySettings/ui/TypographyMenu.svelte
@@ -1,10 +1,12 @@
-
+
+
+
+
+
diff --git a/tsconfig.json b/tsconfig.json
index 6e6cdb3..1c223e3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
"moduleResolution": "bundler",
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
- "types": ["svelte"],
+ "types": ["svelte", "vitest/globals"],
/* Strictness & Safety */
"strict": true,
@@ -36,7 +36,8 @@
"src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte",
- "src/**/*.d.ts"
+ "src/**/*.d.ts",
+ "vitest.types.d.ts"
],
"exclude": [
"node_modules",
diff --git a/vitest.config.component.ts b/vitest.config.component.ts
new file mode 100644
index 0000000..0cfa05d
--- /dev/null
+++ b/vitest.config.component.ts
@@ -0,0 +1,37 @@
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import path from 'node:path';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [svelte()],
+
+ test: {
+ name: 'component',
+ environment: 'jsdom',
+ include: ['src/**/*.svelte.test.ts', 'src/**/*.svelte.test.js'],
+ exclude: [
+ 'node_modules',
+ 'dist',
+ 'e2e',
+ '.storybook',
+ 'src/shared/shadcn/**/*',
+ ],
+ testTimeout: 10000,
+ hookTimeout: 10000,
+ restoreMocks: true,
+ setupFiles: ['./vitest.setup.component.ts'],
+ globals: false,
+ },
+
+ resolve: {
+ alias: {
+ $lib: path.resolve(__dirname, './src/lib'),
+ $app: path.resolve(__dirname, './src/app'),
+ $shared: path.resolve(__dirname, './src/shared'),
+ $entities: path.resolve(__dirname, './src/entities'),
+ $features: path.resolve(__dirname, './src/features'),
+ $routes: path.resolve(__dirname, './src/routes'),
+ $widgets: path.resolve(__dirname, './src/widgets'),
+ },
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..96e7de8
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,66 @@
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import path from 'node:path';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [svelte()],
+
+ test: {
+ environment: 'node',
+ include: [
+ 'src/**/*.test.ts',
+ 'src/**/*.test.js',
+ 'src/**/*.spec.ts',
+ // Explicitly exclude component tests
+ '!src/**/*.svelte.test.ts',
+ '!src/**/*.svelte.test.js',
+ ],
+ exclude: [
+ 'node_modules',
+ 'dist',
+ 'e2e',
+ '.storybook',
+ 'src/shared/shadcn/**/*',
+ ],
+ restoreMocks: true,
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html', 'lcov'],
+ include: ['src/**/*.ts', 'src/**/*.svelte'],
+ exclude: [
+ 'node_modules',
+ 'dist',
+ 'e2e',
+ '.storybook',
+ '**/*.test.ts',
+ '**/*.test.js',
+ '**/*.svelte.test.ts',
+ '**/*.spec.ts',
+ '**/*.d.ts',
+ '**/*.stories.svelte',
+ 'src/shared/shadcn/**/*',
+ 'vitest.config.ts',
+ ],
+ thresholds: {
+ lines: 70,
+ functions: 70,
+ branches: 60,
+ statements: 70,
+ },
+ },
+ setupFiles: [],
+ globals: false,
+ },
+
+ resolve: {
+ alias: {
+ $lib: path.resolve(__dirname, './src/lib'),
+ $app: path.resolve(__dirname, './src/app'),
+ $shared: path.resolve(__dirname, './src/shared'),
+ $entities: path.resolve(__dirname, './src/entities'),
+ $features: path.resolve(__dirname, './src/features'),
+ $routes: path.resolve(__dirname, './src/routes'),
+ $widgets: path.resolve(__dirname, './src/widgets'),
+ },
+ },
+});
diff --git a/vitest.setup.component.ts b/vitest.setup.component.ts
new file mode 100644
index 0000000..4f4404a
--- /dev/null
+++ b/vitest.setup.component.ts
@@ -0,0 +1,12 @@
+import * as matchers from '@testing-library/jest-dom/matchers';
+import { cleanup } from '@testing-library/svelte';
+import {
+ afterEach,
+ expect,
+} from 'vitest';
+
+expect.extend(matchers);
+
+afterEach(() => {
+ cleanup();
+});
diff --git a/vitest.types.d.ts b/vitest.types.d.ts
new file mode 100644
index 0000000..0ee17a6
--- /dev/null
+++ b/vitest.types.d.ts
@@ -0,0 +1,8 @@
+import '@testing-library/jest-dom/vitest';
+import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
+
+declare global {
+ namespace Vi {
+ interface Matchers extends TestingLibraryMatchers {}
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index f2ce0de..21b3f3f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5,6 +5,13 @@ __metadata:
version: 8
cacheKey: 10c0
+"@acemir/cssom@npm:^0.9.28":
+ version: 0.9.30
+ resolution: "@acemir/cssom@npm:0.9.30"
+ checksum: 10c0/f90dd766826315904ab71361aa7b3e0f1ff01124c73b2abc4c6f676ccb036cc6d4fe7c834f89384302e3dfbbd1e819681816fd29da594bb94a5739c634e8282f
+ languageName: node
+ linkType: hard
+
"@adobe/css-tools@npm:^4.4.0":
version: 4.4.4
resolution: "@adobe/css-tools@npm:4.4.4"
@@ -22,6 +29,50 @@ __metadata:
languageName: node
linkType: hard
+"@asamuzakjp/css-color@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "@asamuzakjp/css-color@npm:4.1.1"
+ dependencies:
+ "@csstools/css-calc": "npm:^2.1.4"
+ "@csstools/css-color-parser": "npm:^3.1.0"
+ "@csstools/css-parser-algorithms": "npm:^3.0.5"
+ "@csstools/css-tokenizer": "npm:^3.0.4"
+ lru-cache: "npm:^11.2.4"
+ checksum: 10c0/2948ae9cd4c2f326ab5470d6ac7d415bb8062150ef254f830d774b6a77d6dccfbdb4b84ed4ef5c86c5643d42c52d77204b8d94d0d90f2e2cea9ec9b6cbb9c336
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/dom-selector@npm:^6.7.6":
+ version: 6.7.6
+ resolution: "@asamuzakjp/dom-selector@npm:6.7.6"
+ dependencies:
+ "@asamuzakjp/nwsapi": "npm:^2.3.9"
+ bidi-js: "npm:^1.0.3"
+ css-tree: "npm:^3.1.0"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ lru-cache: "npm:^11.2.4"
+ checksum: 10c0/1715faae0787f0c8430b3a0ff3db8576a5b9a4f964408d0808fc2060ab01e0c2f5d8e26409de54b8641433c891dab8b561b196e58798811146084c561a4954ce
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/nwsapi@npm:^2.3.9":
+ version: 2.3.9
+ resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
+ checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d
+ languageName: node
+ linkType: hard
+
+"@babel/code-frame@npm:^7.10.4":
+ version: 7.27.1
+ resolution: "@babel/code-frame@npm:7.27.1"
+ dependencies:
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ js-tokens: "npm:^4.0.0"
+ picocolors: "npm:^1.1.1"
+ checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00
+ languageName: node
+ linkType: hard
+
"@babel/helper-string-parser@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-string-parser@npm:7.27.1"
@@ -29,7 +80,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/helper-validator-identifier@npm:^7.28.5":
+"@babel/helper-validator-identifier@npm:^7.27.1, @babel/helper-validator-identifier@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/helper-validator-identifier@npm:7.28.5"
checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847
@@ -47,6 +98,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.12.5":
+ version: 7.28.4
+ resolution: "@babel/runtime@npm:7.28.4"
+ checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7
+ languageName: node
+ linkType: hard
+
"@babel/types@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
@@ -79,6 +137,59 @@ __metadata:
languageName: node
linkType: hard
+"@csstools/color-helpers@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "@csstools/color-helpers@npm:5.1.0"
+ checksum: 10c0/b7f99d2e455cf1c9b41a67a5327d5d02888cd5c8802a68b1887dffef537d9d4bc66b3c10c1e62b40bbed638b6c1d60b85a232f904ed7b39809c4029cb36567db
+ languageName: node
+ linkType: hard
+
+"@csstools/css-calc@npm:^2.1.4":
+ version: 2.1.4
+ resolution: "@csstools/css-calc@npm:2.1.4"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^3.0.5
+ "@csstools/css-tokenizer": ^3.0.4
+ checksum: 10c0/42ce5793e55ec4d772083808a11e9fb2dfe36db3ec168713069a276b4c3882205b3507c4680224c28a5d35fe0bc2d308c77f8f2c39c7c09aad8747708eb8ddd8
+ languageName: node
+ linkType: hard
+
+"@csstools/css-color-parser@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "@csstools/css-color-parser@npm:3.1.0"
+ dependencies:
+ "@csstools/color-helpers": "npm:^5.1.0"
+ "@csstools/css-calc": "npm:^2.1.4"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^3.0.5
+ "@csstools/css-tokenizer": ^3.0.4
+ checksum: 10c0/0e0c670ad54ec8ec4d9b07568b80defd83b9482191f5e8ca84ab546b7be6db5d7cc2ba7ac9fae54488b129a4be235d6183d3aab4416fec5e89351f73af4222c5
+ languageName: node
+ linkType: hard
+
+"@csstools/css-parser-algorithms@npm:^3.0.5":
+ version: 3.0.5
+ resolution: "@csstools/css-parser-algorithms@npm:3.0.5"
+ peerDependencies:
+ "@csstools/css-tokenizer": ^3.0.4
+ checksum: 10c0/d9a1c888bd43849ae3437ca39251d5c95d2c8fd6b5ccdb7c45491dfd2c1cbdc3075645e80901d120e4d2c1993db9a5b2d83793b779dbbabcfb132adb142eb7f7
+ languageName: node
+ linkType: hard
+
+"@csstools/css-syntax-patches-for-csstree@npm:^1.0.21":
+ version: 1.0.22
+ resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.22"
+ checksum: 10c0/5ec39dc28b30bb0d5e212e598979707786e152698be820502bdee4b0204983604170dd960a8684de8de89f28376cf4a151de81d9da166d97eeb35f869a44ae08
+ languageName: node
+ linkType: hard
+
+"@csstools/css-tokenizer@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@csstools/css-tokenizer@npm:3.0.4"
+ checksum: 10c0/3b589f8e9942075a642213b389bab75a2d50d05d203727fcdac6827648a5572674caff07907eff3f9a2389d86a4ee47308fafe4f8588f4a77b7167c588d2559f
+ languageName: node
+ linkType: hard
+
"@dprint/darwin-arm64@npm:0.50.2":
version: 0.50.2
resolution: "@dprint/darwin-arm64@npm:0.50.2"
@@ -377,6 +488,18 @@ __metadata:
languageName: node
linkType: hard
+"@exodus/bytes@npm:^1.6.0":
+ version: 1.8.0
+ resolution: "@exodus/bytes@npm:1.8.0"
+ peerDependencies:
+ "@exodus/crypto": ^1.0.0-rc.4
+ peerDependenciesMeta:
+ "@exodus/crypto":
+ optional: true
+ checksum: 10c0/1878868519230fa564b80d3d12fa7e2b559dfed143f4e48969eb5f98083caea431bbc70356d6b2f90ba80c8148295fe85af6a5ed8a746dd53baefe4f1ed086e8
+ languageName: node
+ linkType: hard
+
"@floating-ui/core@npm:^1.7.1, @floating-ui/core@npm:^1.7.3":
version: 1.7.3
resolution: "@floating-ui/core@npm:1.7.3"
@@ -1169,7 +1292,23 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/jest-dom@npm:^6.6.3":
+"@testing-library/dom@npm:9.x.x || 10.x.x":
+ version: 10.4.1
+ resolution: "@testing-library/dom@npm:10.4.1"
+ dependencies:
+ "@babel/code-frame": "npm:^7.10.4"
+ "@babel/runtime": "npm:^7.12.5"
+ "@types/aria-query": "npm:^5.0.1"
+ aria-query: "npm:5.3.0"
+ dom-accessibility-api: "npm:^0.5.9"
+ lz-string: "npm:^1.5.0"
+ picocolors: "npm:1.1.1"
+ pretty-format: "npm:^27.0.2"
+ checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1
+ languageName: node
+ linkType: hard
+
+"@testing-library/jest-dom@npm:^6.6.3, @testing-library/jest-dom@npm:^6.9.1":
version: 6.9.1
resolution: "@testing-library/jest-dom@npm:6.9.1"
dependencies:
@@ -1183,6 +1322,34 @@ __metadata:
languageName: node
linkType: hard
+"@testing-library/svelte-core@npm:1.0.0":
+ version: 1.0.0
+ resolution: "@testing-library/svelte-core@npm:1.0.0"
+ peerDependencies:
+ svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
+ checksum: 10c0/79942494df95b559ca6d4abc8398ca2d31a4709f836e42337f41e629e5191e0c0caed9d5fd905df93c2c2e4230e63e08da839e6bc304c21e6960a5777bc40f12
+ languageName: node
+ linkType: hard
+
+"@testing-library/svelte@npm:^5.3.1":
+ version: 5.3.1
+ resolution: "@testing-library/svelte@npm:5.3.1"
+ dependencies:
+ "@testing-library/dom": "npm:9.x.x || 10.x.x"
+ "@testing-library/svelte-core": "npm:1.0.0"
+ peerDependencies:
+ svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
+ vite: "*"
+ vitest: "*"
+ peerDependenciesMeta:
+ vite:
+ optional: true
+ vitest:
+ optional: true
+ checksum: 10c0/a5279b53514d94b0abbb34b87883886ae2b64ed18ac623c4bda981b05465be02213c778108671e47d56912ec9f4684645cfead852443e6b3810db2021d2823c7
+ languageName: node
+ linkType: hard
+
"@testing-library/user-event@npm:^14.6.1":
version: 14.6.1
resolution: "@testing-library/user-event@npm:14.6.1"
@@ -1208,6 +1375,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/aria-query@npm:^5.0.1":
+ version: 5.0.4
+ resolution: "@types/aria-query@npm:5.0.4"
+ checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08
+ languageName: node
+ linkType: hard
+
"@types/chai@npm:^5.2.2":
version: 5.2.3
resolution: "@types/chai@npm:5.2.3"
@@ -1232,6 +1406,17 @@ __metadata:
languageName: node
linkType: hard
+"@types/jsdom@npm:^27":
+ version: 27.0.0
+ resolution: "@types/jsdom@npm:27.0.0"
+ dependencies:
+ "@types/node": "npm:*"
+ "@types/tough-cookie": "npm:*"
+ parse5: "npm:^7.0.0"
+ checksum: 10c0/1ec7ff7177e1f7266e51279f07f3cd013e1713766b01eebceac783061675b31c672a47b0a508dcbaf040f7f22d90405858378c6c5358991989fbe8b95adde354
+ languageName: node
+ linkType: hard
+
"@types/mdx@npm:^2.0.0":
version: 2.0.13
resolution: "@types/mdx@npm:2.0.13"
@@ -1239,6 +1424,22 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:*":
+ version: 25.0.3
+ resolution: "@types/node@npm:25.0.3"
+ dependencies:
+ undici-types: "npm:~7.16.0"
+ checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835
+ languageName: node
+ linkType: hard
+
+"@types/tough-cookie@npm:*":
+ version: 4.0.5
+ resolution: "@types/tough-cookie@npm:4.0.5"
+ checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473
+ languageName: node
+ linkType: hard
+
"@vitest/browser-playwright@npm:^4.0.16":
version: 4.0.16
resolution: "@vitest/browser-playwright@npm:4.0.16"
@@ -1484,6 +1685,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-regex@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "ansi-regex@npm:5.0.1"
+ checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737
+ languageName: node
+ linkType: hard
+
"ansi-regex@npm:^6.0.1":
version: 6.2.2
resolution: "ansi-regex@npm:6.2.2"
@@ -1491,6 +1699,22 @@ __metadata:
languageName: node
linkType: hard
+"ansi-styles@npm:^5.0.0":
+ version: 5.2.0
+ resolution: "ansi-styles@npm:5.2.0"
+ checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
+ languageName: node
+ linkType: hard
+
+"aria-query@npm:5.3.0":
+ version: 5.3.0
+ resolution: "aria-query@npm:5.3.0"
+ dependencies:
+ dequal: "npm:^2.0.3"
+ checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469
+ languageName: node
+ linkType: hard
+
"aria-query@npm:^5.0.0, aria-query@npm:^5.3.0, aria-query@npm:^5.3.1":
version: 5.3.2
resolution: "aria-query@npm:5.3.2"
@@ -1539,6 +1763,15 @@ __metadata:
languageName: node
linkType: hard
+"bidi-js@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "bidi-js@npm:1.0.3"
+ dependencies:
+ require-from-string: "npm:^2.0.2"
+ checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
+ languageName: node
+ linkType: hard
+
"bits-ui@npm:^2.14.4":
version: 2.14.4
resolution: "bits-ui@npm:2.14.4"
@@ -1676,6 +1909,16 @@ __metadata:
languageName: node
linkType: hard
+"css-tree@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "css-tree@npm:3.1.0"
+ dependencies:
+ mdn-data: "npm:2.12.2"
+ source-map-js: "npm:^1.0.1"
+ checksum: 10c0/b5715852c2f397c715ca00d56ec53fc83ea596295ae112eb1ba6a1bda3b31086380e596b1d8c4b980fe6da09e7d0fc99c64d5bb7313030dd0fba9c1415f30979
+ languageName: node
+ linkType: hard
+
"css.escape@npm:^1.5.1":
version: 1.5.1
resolution: "css.escape@npm:1.5.1"
@@ -1683,6 +1926,28 @@ __metadata:
languageName: node
linkType: hard
+"cssstyle@npm:^5.3.4":
+ version: 5.3.7
+ resolution: "cssstyle@npm:5.3.7"
+ dependencies:
+ "@asamuzakjp/css-color": "npm:^4.1.1"
+ "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.21"
+ css-tree: "npm:^3.1.0"
+ lru-cache: "npm:^11.2.4"
+ checksum: 10c0/9330f014f4209df06305264b92b8e963dfef636fdc2ae7d13f24ea7da6468aba1dc5eb13082621258bdd22cbd7fb7cb291894e188a3cdf660e8b79cd2c5e5e0e
+ languageName: node
+ linkType: hard
+
+"data-urls@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "data-urls@npm:6.0.0"
+ dependencies:
+ whatwg-mimetype: "npm:^4.0.0"
+ whatwg-url: "npm:^15.0.0"
+ checksum: 10c0/952102a8e6282fea112f7120d79fac482a2f99e20c67f9cb069d661c00627305b042e1f7e3cef8e4bbc795b42c5d481bbc9c6effeff5bb1427f9acaf1722bd35
+ languageName: node
+ linkType: hard
+
"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.4.1":
version: 4.4.3
resolution: "debug@npm:4.4.3"
@@ -1695,6 +1960,13 @@ __metadata:
languageName: node
linkType: hard
+"decimal.js@npm:^10.6.0":
+ version: 10.6.0
+ resolution: "decimal.js@npm:10.6.0"
+ checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa
+ languageName: node
+ linkType: hard
+
"dedent-js@npm:^1.0.1":
version: 1.0.1
resolution: "dedent-js@npm:1.0.1"
@@ -1773,6 +2045,13 @@ __metadata:
languageName: node
linkType: hard
+"dom-accessibility-api@npm:^0.5.9":
+ version: 0.5.16
+ resolution: "dom-accessibility-api@npm:0.5.16"
+ checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053
+ languageName: node
+ linkType: hard
+
"dom-accessibility-api@npm:^0.6.3":
version: 0.6.3
resolution: "dom-accessibility-api@npm:0.6.3"
@@ -1847,6 +2126,13 @@ __metadata:
languageName: node
linkType: hard
+"entities@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "entities@npm:6.0.1"
+ checksum: 10c0/ed836ddac5acb34341094eb495185d527bd70e8632b6c0d59548cbfa23defdbae70b96f9a405c82904efa421230b5b3fd2283752447d737beffd3f3e6ee74414
+ languageName: node
+ linkType: hard
+
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -2143,12 +2429,16 @@ __metadata:
"@storybook/svelte-vite": "npm:^10.1.11"
"@sveltejs/vite-plugin-svelte": "npm:^6.2.1"
"@tailwindcss/vite": "npm:^4.1.18"
+ "@testing-library/jest-dom": "npm:^6.9.1"
+ "@testing-library/svelte": "npm:^5.3.1"
"@tsconfig/svelte": "npm:^5.0.6"
+ "@types/jsdom": "npm:^27"
"@vitest/browser-playwright": "npm:^4.0.16"
"@vitest/coverage-v8": "npm:^4.0.16"
bits-ui: "npm:^2.14.4"
clsx: "npm:^2.1.1"
dprint: "npm:^0.50.2"
+ jsdom: "npm:^27.4.0"
lefthook: "npm:^2.0.13"
oxlint: "npm:^1.35.0"
playwright: "npm:^1.57.0"
@@ -2163,6 +2453,7 @@ __metadata:
typescript: "npm:^5.9.3"
vite: "npm:^7.2.6"
vitest: "npm:^4.0.16"
+ vitest-browser-svelte: "npm:^2.0.1"
languageName: unknown
linkType: soft
@@ -2180,6 +2471,15 @@ __metadata:
languageName: node
linkType: hard
+"html-encoding-sniffer@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "html-encoding-sniffer@npm:6.0.0"
+ dependencies:
+ "@exodus/bytes": "npm:^1.6.0"
+ checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025
+ languageName: node
+ linkType: hard
+
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -2194,7 +2494,7 @@ __metadata:
languageName: node
linkType: hard
-"http-proxy-agent@npm:^7.0.0":
+"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
dependencies:
@@ -2204,7 +2504,7 @@ __metadata:
languageName: node
linkType: hard
-"https-proxy-agent@npm:^7.0.1":
+"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@@ -2271,6 +2571,13 @@ __metadata:
languageName: node
linkType: hard
+"is-potential-custom-element-name@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "is-potential-custom-element-name@npm:1.0.1"
+ checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
+ languageName: node
+ linkType: hard
+
"is-reference@npm:^3.0.0, is-reference@npm:^3.0.1, is-reference@npm:^3.0.3":
version: 3.0.3
resolution: "is-reference@npm:3.0.3"
@@ -2344,6 +2651,13 @@ __metadata:
languageName: node
linkType: hard
+"js-tokens@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "js-tokens@npm:4.0.0"
+ checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
+ languageName: node
+ linkType: hard
+
"js-tokens@npm:^9.0.1":
version: 9.0.1
resolution: "js-tokens@npm:9.0.1"
@@ -2351,6 +2665,39 @@ __metadata:
languageName: node
linkType: hard
+"jsdom@npm:^27.4.0":
+ version: 27.4.0
+ resolution: "jsdom@npm:27.4.0"
+ dependencies:
+ "@acemir/cssom": "npm:^0.9.28"
+ "@asamuzakjp/dom-selector": "npm:^6.7.6"
+ "@exodus/bytes": "npm:^1.6.0"
+ cssstyle: "npm:^5.3.4"
+ data-urls: "npm:^6.0.0"
+ decimal.js: "npm:^10.6.0"
+ html-encoding-sniffer: "npm:^6.0.0"
+ http-proxy-agent: "npm:^7.0.2"
+ https-proxy-agent: "npm:^7.0.6"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ parse5: "npm:^8.0.0"
+ saxes: "npm:^6.0.0"
+ symbol-tree: "npm:^3.2.4"
+ tough-cookie: "npm:^6.0.0"
+ w3c-xmlserializer: "npm:^5.0.0"
+ webidl-conversions: "npm:^8.0.0"
+ whatwg-mimetype: "npm:^4.0.0"
+ whatwg-url: "npm:^15.1.0"
+ ws: "npm:^8.18.3"
+ xml-name-validator: "npm:^5.0.0"
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+ checksum: 10c0/291bb71a611dbaed81ce516587b71a5ffd9d43337d65bbd0731e7924cd7018f5871cf66614facadfd0dffec2b23a0fc57b2ee36b5a39e20f0f569e2949b3418c
+ languageName: node
+ linkType: hard
+
"jsonc-parser@npm:^2.3.0":
version: 2.3.1
resolution: "jsonc-parser@npm:2.3.1"
@@ -2623,7 +2970,7 @@ __metadata:
languageName: node
linkType: hard
-"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1":
+"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.4":
version: 11.2.4
resolution: "lru-cache@npm:11.2.4"
checksum: 10c0/4a24f9b17537619f9144d7b8e42cd5a225efdfd7076ebe7b5e7dc02b860a818455201e67fbf000765233fe7e339d3c8229fc815e9b58ee6ede511e07608c19b2
@@ -2694,6 +3041,13 @@ __metadata:
languageName: node
linkType: hard
+"mdn-data@npm:2.12.2":
+ version: 2.12.2
+ resolution: "mdn-data@npm:2.12.2"
+ checksum: 10c0/b22443b71d70f72ccc3c6ba1608035431a8fc18c3c8fc53523f06d20e05c2ac10f9b53092759a2ca85cf02f0d37036f310b581ce03e7b99ac74d388ef8152ade
+ languageName: node
+ linkType: hard
+
"min-indent@npm:^1.0.0":
version: 1.0.1
resolution: "min-indent@npm:1.0.1"
@@ -2921,6 +3275,24 @@ __metadata:
languageName: node
linkType: hard
+"parse5@npm:^7.0.0":
+ version: 7.3.0
+ resolution: "parse5@npm:7.3.0"
+ dependencies:
+ entities: "npm:^6.0.0"
+ checksum: 10c0/7fd2e4e247e85241d6f2a464d0085eed599a26d7b0a5233790c49f53473232eb85350e8133344d9b3fd58b89339e7ad7270fe1f89d28abe50674ec97b87f80b5
+ languageName: node
+ linkType: hard
+
+"parse5@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "parse5@npm:8.0.0"
+ dependencies:
+ entities: "npm:^6.0.0"
+ checksum: 10c0/8279892dcd77b2f2229707f60eb039e303adf0288812b2a8fd5acf506a4d432da833c6c5d07a6554bef722c2367a81ef4a1f7e9336564379a7dba3e798bf16b3
+ languageName: node
+ linkType: hard
+
"path-scurry@npm:^2.0.0":
version: 2.0.1
resolution: "path-scurry@npm:2.0.1"
@@ -2956,7 +3328,7 @@ __metadata:
languageName: node
linkType: hard
-"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1":
+"picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
@@ -3042,6 +3414,17 @@ __metadata:
languageName: node
linkType: hard
+"pretty-format@npm:^27.0.2":
+ version: 27.5.1
+ resolution: "pretty-format@npm:27.5.1"
+ dependencies:
+ ansi-regex: "npm:^5.0.1"
+ ansi-styles: "npm:^5.0.0"
+ react-is: "npm:^17.0.1"
+ checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed
+ languageName: node
+ linkType: hard
+
"proc-log@npm:^6.0.0":
version: 6.1.0
resolution: "proc-log@npm:6.1.0"
@@ -3059,6 +3442,13 @@ __metadata:
languageName: node
linkType: hard
+"punycode@npm:^2.3.1":
+ version: 2.3.1
+ resolution: "punycode@npm:2.3.1"
+ checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
+ languageName: node
+ linkType: hard
+
"react-dom@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0":
version: 19.2.3
resolution: "react-dom@npm:19.2.3"
@@ -3070,6 +3460,13 @@ __metadata:
languageName: node
linkType: hard
+"react-is@npm:^17.0.1":
+ version: 17.0.2
+ resolution: "react-is@npm:17.0.2"
+ checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053
+ languageName: node
+ linkType: hard
+
"react@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0":
version: 19.2.3
resolution: "react@npm:19.2.3"
@@ -3107,6 +3504,13 @@ __metadata:
languageName: node
linkType: hard
+"require-from-string@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "require-from-string@npm:2.0.2"
+ checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
+ languageName: node
+ linkType: hard
+
"retry@npm:^0.12.0":
version: 0.12.0
resolution: "retry@npm:0.12.0"
@@ -3235,6 +3639,15 @@ __metadata:
languageName: node
linkType: hard
+"saxes@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "saxes@npm:6.0.0"
+ dependencies:
+ xmlchars: "npm:^2.2.0"
+ checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
+ languageName: node
+ linkType: hard
+
"scheduler@npm:^0.27.0":
version: 0.27.0
resolution: "scheduler@npm:0.27.0"
@@ -3534,6 +3947,13 @@ __metadata:
languageName: node
linkType: hard
+"symbol-tree@npm:^3.2.4":
+ version: 3.2.4
+ resolution: "symbol-tree@npm:3.2.4"
+ checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
+ languageName: node
+ linkType: hard
+
"tabbable@npm:^6.2.0":
version: 6.3.0
resolution: "tabbable@npm:6.3.0"
@@ -3640,6 +4060,24 @@ __metadata:
languageName: node
linkType: hard
+"tldts-core@npm:^7.0.19":
+ version: 7.0.19
+ resolution: "tldts-core@npm:7.0.19"
+ checksum: 10c0/8f9fa5838aa7b3adbe80a6588ad802019f21faef34e04aa1aeab3a20275bba5e22c60b66a6b3bdd830b0bd6a2d57b92e0605c3cdb2c6317f111e586fa2f37927
+ languageName: node
+ linkType: hard
+
+"tldts@npm:^7.0.5":
+ version: 7.0.19
+ resolution: "tldts@npm:7.0.19"
+ dependencies:
+ tldts-core: "npm:^7.0.19"
+ bin:
+ tldts: bin/cli.js
+ checksum: 10c0/d77d2fe6f8ec07e27248cd6647b91fc814dfc82e15dce104277f317d861576908409f6549ff46e21277677f823a037f57b7a748ada7d0fcdcb08535890f71050
+ languageName: node
+ linkType: hard
+
"totalist@npm:^3.0.0":
version: 3.0.1
resolution: "totalist@npm:3.0.1"
@@ -3647,6 +4085,24 @@ __metadata:
languageName: node
linkType: hard
+"tough-cookie@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "tough-cookie@npm:6.0.0"
+ dependencies:
+ tldts: "npm:^7.0.5"
+ checksum: 10c0/7b17a461e9c2ac0d0bea13ab57b93b4346d0b8c00db174c963af1e46e4ea8d04148d2a55f2358fc857db0c0c65208a98e319d0c60693e32e0c559a9d9cf20cb5
+ languageName: node
+ linkType: hard
+
+"tr46@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "tr46@npm:6.0.0"
+ dependencies:
+ punycode: "npm:^2.3.1"
+ checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e
+ languageName: node
+ linkType: hard
+
"ts-dedent@npm:^2.0.0":
version: 2.2.0
resolution: "ts-dedent@npm:2.2.0"
@@ -3704,6 +4160,13 @@ __metadata:
languageName: node
linkType: hard
+"undici-types@npm:~7.16.0":
+ version: 7.16.0
+ resolution: "undici-types@npm:7.16.0"
+ checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
+ languageName: node
+ linkType: hard
+
"unique-filename@npm:^5.0.0":
version: 5.0.0
resolution: "unique-filename@npm:5.0.0"
@@ -3817,6 +4280,16 @@ __metadata:
languageName: node
linkType: hard
+"vitest-browser-svelte@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "vitest-browser-svelte@npm:2.0.1"
+ peerDependencies:
+ svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
+ vitest: ^4.0.0
+ checksum: 10c0/47afa35683f0dd08f1b0d5204061cb294eb5a5e0f79cf5c1e7c82581549dd75291713a5ef9a80b2fcdb324a69e90e2f12d844a9d58a3db4626ec1ba52ffbb782
+ languageName: node
+ linkType: hard
+
"vitest@npm:^4.0.16":
version: 4.0.16
resolution: "vitest@npm:4.0.16"
@@ -3963,6 +4436,22 @@ __metadata:
languageName: node
linkType: hard
+"w3c-xmlserializer@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "w3c-xmlserializer@npm:5.0.0"
+ dependencies:
+ xml-name-validator: "npm:^5.0.0"
+ checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
+ languageName: node
+ linkType: hard
+
+"webidl-conversions@npm:^8.0.0":
+ version: 8.0.1
+ resolution: "webidl-conversions@npm:8.0.1"
+ checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46
+ languageName: node
+ linkType: hard
+
"webpack-virtual-modules@npm:^0.6.2":
version: 0.6.2
resolution: "webpack-virtual-modules@npm:0.6.2"
@@ -3970,6 +4459,23 @@ __metadata:
languageName: node
linkType: hard
+"whatwg-mimetype@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "whatwg-mimetype@npm:4.0.0"
+ checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
+ languageName: node
+ linkType: hard
+
+"whatwg-url@npm:^15.0.0, whatwg-url@npm:^15.1.0":
+ version: 15.1.0
+ resolution: "whatwg-url@npm:15.1.0"
+ dependencies:
+ tr46: "npm:^6.0.0"
+ webidl-conversions: "npm:^8.0.0"
+ checksum: 10c0/40c49b47044787c87486aaaa5b504da122820661c45ae20ab466c62595ed03c64be7c10c1d180d028949a393cd455db14144966a68359cd37fe6417e3426d128
+ languageName: node
+ linkType: hard
+
"which@npm:^6.0.0":
version: 6.0.0
resolution: "which@npm:6.0.0"
@@ -4017,6 +4523,20 @@ __metadata:
languageName: node
linkType: hard
+"xml-name-validator@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "xml-name-validator@npm:5.0.0"
+ checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
+ languageName: node
+ linkType: hard
+
+"xmlchars@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "xmlchars@npm:2.2.0"
+ checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
+ languageName: node
+ linkType: hard
+
"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"