diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 70eb7ff..72f8f3d 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -64,9 +64,33 @@ jobs: echo "host=${HOST}" >> "$GITHUB_OUTPUT" echo "namespace=${NAMESPACE}" >> "$GITHUB_OUTPUT" - build: + test: runs-on: self-hosted needs: setup-env + timeout-minutes: 5 + steps: + - name: Actions checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install Yarn + run: npm install -g yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run tests + run: yarn test + env: + NODE_OPTIONS: "--max-old-space-size=2048" + + build: + runs-on: self-hosted + needs: [setup-env, test] steps: - name: Docker build & push run: | diff --git a/.github/workflows/prod.yaml b/.github/workflows/prod.yaml index 8d5c0d0..91e9baf 100644 --- a/.github/workflows/prod.yaml +++ b/.github/workflows/prod.yaml @@ -50,9 +50,33 @@ jobs: echo "namespace=${NAMESPACE}" >> "$GITHUB_OUTPUT" echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - build: + test: runs-on: self-hosted needs: setup-env + timeout-minutes: 5 + steps: + - name: Actions checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install Yarn + run: npm install -g yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run tests + run: yarn test + env: + NODE_OPTIONS: "--max-old-space-size=2048" + + build: + runs-on: self-hosted + needs: [setup-env, test] steps: - name: Docker build & push run: | diff --git a/.gitignore b/.gitignore index 0088726..7695829 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ node_modules .DS_Store -docker-compose.yml \ No newline at end of file +docker-compose.yml + +coverage \ No newline at end of file diff --git a/apps/core/package.json b/apps/core/package.json index 718afef..44b7a0b 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -14,6 +14,9 @@ "dev:localserver:host": "VITE_API_URL=https://localhost:8000/api vite --host", "build": "tsc -b && vite build", "preview": "yarn build && vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "stylelint": "stylelint \"src/**/*.scss\" --max-warnings 0", @@ -60,24 +63,34 @@ "devDependencies": { "@repo/eslint-config": "*", "@repo/models": "*", + "@repo/react-testing-library-config": "*", "@repo/stores": "*", "@repo/stylelint-config": "*", "@repo/types": "*", "@repo/typescript-config": "*", "@repo/ui": "*", "@repo/utils": "*", + "@repo/vitest-config": "*", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", "@types/node": "^24.0.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", + "@vitest/coverage-v8": "^2.1.8", "autoprefixer": "^10.4.13", "eslint": "^9.29.0", + "jsdom": "^25.0.1", "postcss": "^8.5.5", "prettier": "^3.5.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", "stylelint": "^16.20.0", "terser": "^5.43.1", "typescript": "^5.8.3", "vite": "^6.3.5", - "vite-plugin-compression2": "^2.2.0" + "vite-plugin-compression2": "^2.2.0", + "vitest": "^2.1.8" } } diff --git a/apps/core/src/components/calendar/sheets/CreateEventSheet/CreateEventSheet.tsx b/apps/core/src/components/calendar/sheets/CreateEventSheet/CreateEventSheet.tsx index 20aa554..d6904b2 100644 --- a/apps/core/src/components/calendar/sheets/CreateEventSheet/CreateEventSheet.tsx +++ b/apps/core/src/components/calendar/sheets/CreateEventSheet/CreateEventSheet.tsx @@ -1,7 +1,7 @@ import { Sheet } from '@gravity-ui/uikit'; import { EventFormFieldValues } from '@repo/types/calendar'; import { CalendarEventForm } from '@repo/ui'; -import { dateToNextNearestHalfHour, gravityDateToTemporal, temporalToGravityDate } from '@repo/utils'; +import { getDefaultEndTime, gravityDateToTemporal, temporalToGravityDate } from '@repo/utils'; import { observer } from 'mobx-react-lite'; import { useCallback, useMemo } from 'react'; @@ -20,10 +20,8 @@ const CreateEventSheet: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [createEvent.openState.isOpen]); - const nearestStartDateTime = dateToNextNearestHalfHour(selectedDateNowTime); - const startDate = temporalToGravityDate(selectedDateNowTime); - const endDate = temporalToGravityDate(nearestStartDateTime.add({ hours: 1 })); + const endDate = temporalToGravityDate(getDefaultEndTime(selectedDateNowTime)); const onSubmit = useCallback( async (data: EventFormFieldValues) => { diff --git a/apps/core/src/example.test.ts b/apps/core/src/example.test.ts new file mode 100644 index 0000000..452ac7e --- /dev/null +++ b/apps/core/src/example.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('Core App test', () => { + it('should work', () => { + expect(true).toBe(true); + }); +}); diff --git a/apps/core/tsconfig.json b/apps/core/tsconfig.json index ebf160d..0e557a8 100644 --- a/apps/core/tsconfig.json +++ b/apps/core/tsconfig.json @@ -22,6 +22,7 @@ }, "include": [ "src/**/*", - "vite.config.ts" + "vite.config.ts", + "vitest.config.ts" ] } diff --git a/apps/core/vitest.config.ts b/apps/core/vitest.config.ts new file mode 100644 index 0000000..47f7c6e --- /dev/null +++ b/apps/core/vitest.config.ts @@ -0,0 +1,24 @@ +/// +import { resolve } from 'path'; + +import { createReactVitestConfig } from '@repo/vitest-config'; +import react from '@vitejs/plugin-react'; + +export default createReactVitestConfig( + { + plugins: [react()], + }, + { + '@': resolve(__dirname, './src'), + '@repo/ui': resolve(__dirname, '../../packages/ui/src'), + '@repo/ui/*': resolve(__dirname, '../../packages/ui/src/*'), + '@repo/utils': resolve(__dirname, '../../packages/utils/src'), + '@repo/utils/*': resolve(__dirname, '../../packages/utils/src/*'), + '@repo/models': resolve(__dirname, '../../packages/models/src'), + '@repo/models/*': resolve(__dirname, '../../packages/models/src/*'), + '@repo/stores': resolve(__dirname, '../../packages/stores/src'), + '@repo/stores/*': resolve(__dirname, '../../packages/stores/src/*'), + '@repo/types': resolve(__dirname, '../../packages/types/src'), + '@repo/types/*': resolve(__dirname, '../../packages/types/src/*'), + }, +); diff --git a/config/eslint-config/index.js b/config/eslint-config/index.js index 9a352ef..c45aff5 100644 --- a/config/eslint-config/index.js +++ b/config/eslint-config/index.js @@ -118,4 +118,10 @@ export default [ 'prettier/prettier': 'error', }, }, + { + files: ['**/*.test.tsx', '**/*.spec.tsx', '**/*.test.jsx', '**/*.spec.jsx'], + rules: { + 'react/no-multi-comp': 'off', + }, + }, ]; diff --git a/config/react-testing-library-config/index.tsx b/config/react-testing-library-config/index.tsx new file mode 100644 index 0000000..1cf78fa --- /dev/null +++ b/config/react-testing-library-config/index.tsx @@ -0,0 +1,36 @@ +import { render, RenderOptions } from '@testing-library/react'; +import { ReactElement, ReactNode } from 'react'; + +export interface CustomRenderOptions extends Omit { + theme?: 'light' | 'dark'; + locale?: string; +} + +export function customRender( + ui: ReactElement, + options: CustomRenderOptions = {} +) { + const { + theme = 'light', + locale = 'ru', + ...renderOptions + } = options; + + const AllTheProviders = ({ children }: { children: ReactNode }) => { + return ( +
+ {children} +
+ ); + }; + + return render(ui, { + wrapper: AllTheProviders, + ...renderOptions, + }); +} + +export { customRender as render }; + +export * from '@testing-library/react'; +export { userEvent } from '@testing-library/user-event'; diff --git a/config/react-testing-library-config/package.json b/config/react-testing-library-config/package.json new file mode 100644 index 0000000..c1c3ebb --- /dev/null +++ b/config/react-testing-library-config/package.json @@ -0,0 +1,28 @@ +{ + "name": "@repo/react-testing-library-config", + "description": "DateData React Testing Library configuration and utilities", + "version": "0.0.0", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "main": "index.tsx", + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "react": "^19.1.0", + "react-dom": "^19.1.0" + } +} diff --git a/config/vitest-config/index.js b/config/vitest-config/index.js new file mode 100644 index 0000000..c98d216 --- /dev/null +++ b/config/vitest-config/index.js @@ -0,0 +1,102 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = resolve(__filename, '..'); + +export const baseConfig = { + test: { + globals: true, + setupFiles: [resolve(__dirname, './setup.ts')], + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*' + ], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.{js,cjs,mjs,ts,mts,cts,jsx,tsx}'], + exclude: [ + 'coverage/**', + 'dist/**', + '**/*.d.ts', + 'cypress/**', + 'test{,s}/**', + 'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}spec.{js,cjs,mjs,ts,tsx,jsx}', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/eslint.config.{js,ts,cjs,mjs}', + '**/.{eslint,mocha,prettier}rc.{js,cjs,yml,yaml,json}', + '**/.stylelintrc.{js,cjs,yml,yaml,json}', + '**/stylelint.config.{js,ts,cjs,mjs}', + '**/postcss.config.{js,ts,cjs,mjs}', + '**/tailwind.config.{js,ts,cjs,mjs}', + '**/*.{stories,story}.{js,cjs,mjs,ts,tsx,jsx}', + '**/.storybook/**', + '**/index.{js,cjs,mjs,ts,tsx,jsx}' + ] + } + } +}; + +export const reactConfig = { + ...baseConfig, + test: { + ...baseConfig.test, + environment: 'jsdom', + setupFiles: [resolve(__dirname, './setup-react.ts')] + } +}; + +export const nodeConfig = { + ...baseConfig, + test: { + ...baseConfig.test, + environment: 'node' + } +}; + +export function createVitestConfig(config = {}, aliases = {}) { + return defineConfig({ + ...config, + ...baseConfig, + resolve: { + alias: { + ...aliases + } + } + }); +} + +export function createReactVitestConfig(config = {}, aliases = {}) { + return defineConfig({ + ...config, + ...reactConfig, + resolve: { + alias: { + ...aliases + } + } + }); +} + +export function createNodeVitestConfig(config = {}, aliases = {}) { + return defineConfig({ + ...config, + ...nodeConfig, + resolve: { + alias: { + ...aliases + } + } + }); +} + +export default baseConfig; \ No newline at end of file diff --git a/config/vitest-config/package.json b/config/vitest-config/package.json new file mode 100644 index 0000000..4926102 --- /dev/null +++ b/config/vitest-config/package.json @@ -0,0 +1,22 @@ +{ + "name": "@repo/vitest-config", + "description": "DateData Vitest configuration", + "version": "0.0.0", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "main": "index.js", + "devDependencies": { + "@repo/react-testing-library-config": "*", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@vitejs/plugin-react": "^4.4.1", + "jsdom": "^25.0.1", + "vite": "^6.3.5", + "vitest": "^2.1.8" + } +} diff --git a/config/vitest-config/setup-react.ts b/config/vitest-config/setup-react.ts new file mode 100644 index 0000000..a7c8cf9 --- /dev/null +++ b/config/vitest-config/setup-react.ts @@ -0,0 +1,35 @@ +import '@testing-library/jest-dom'; +import { cleanup } from '@testing-library/react'; +import { afterEach, beforeAll, vi } from 'vitest'; + +afterEach(() => { + cleanup(); +}); + +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); + + global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); + + global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); +}); diff --git a/config/vitest-config/setup.ts b/config/vitest-config/setup.ts new file mode 100644 index 0000000..0ad0921 --- /dev/null +++ b/config/vitest-config/setup.ts @@ -0,0 +1,14 @@ +import { beforeAll } from 'vitest'; + +beforeAll(() => { + const originalWarn = console.warn; + console.warn = (...args) => { + if ( + typeof args[0] === 'string' && + args[0].includes('Warning: ReactDOM.render is no longer supported') + ) { + return; + } + originalWarn.call(console, ...args); + }; +}); diff --git a/package.json b/package.json index bd23d04..4ca1404 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "dev": "turbo run dev --filter=core", "build:packages": "turbo build --filter=@repo/ui --filter=@repo/models", "build": "turbo build", + "test": "turbo test", + "test:watch": "turbo test:watch", + "test:coverage": "turbo test:coverage", "prettier": "turbo prettier", "prettier:fix": "turbo prettier:fix", "eslint": "turbo eslint", diff --git a/packages/models/package.json b/packages/models/package.json index 61313bf..ffe62e3 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -23,6 +23,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --check \"**/*.{js,ts,tsx,scss}\"", @@ -36,11 +39,14 @@ "@repo/types": "*", "@repo/typescript-config": "*", "@repo/utils": "*", + "@repo/vitest-config": "*", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.29.0", "prettier": "^3.5.3", "stylelint": "^16.20.0", "tsup": "^8.0.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^2.1.8" }, "dependencies": { "temporal-polyfill": "^0.3.0" diff --git a/packages/models/src/api/Api.test.ts b/packages/models/src/api/Api.test.ts new file mode 100644 index 0000000..32363bd --- /dev/null +++ b/packages/models/src/api/Api.test.ts @@ -0,0 +1,513 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { Api } from './Api'; + +const BASE_URL = 'https://api.example.com'; +const CONTENT_TYPE = 'application/json'; +const REQUEST_ABORTED_ERROR = 'Request aborted'; + +const mockFetch = vi.fn(); +const mockAbort = vi.fn(); +const mockAddEventListener = vi.fn(); +const mockRemoveEventListener = vi.fn(); +const mockSetTimeout = vi.fn(); +const mockClearTimeout = vi.fn(); + +interface MockAbortSignal { + addEventListener: ReturnType; + removeEventListener: ReturnType; + aborted: boolean; + onabort: ((this: AbortSignal, ev: Event) => any) | null; + reason: any; + throwIfAborted(): void; + dispatchEvent(event: Event): boolean; +} + +const createMockResponse = (overrides: Partial = {}) => ({ + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + json: vi.fn().mockResolvedValue({}), + ...overrides, +}); + +const createMockAbortSignal = (overrides: Partial = {}): MockAbortSignal => ({ + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + aborted: false, + onabort: null, + reason: undefined, + throwIfAborted: vi.fn(), + dispatchEvent: vi.fn(), + ...overrides, +}); + +const createApiInstance = (config: { baseURL?: string; timeout?: number } = {}) => + new Api({ + baseURL: config.baseURL ?? BASE_URL, + timeout: config.timeout ?? 5000, + }); + +class MockAbortController implements AbortController { + signal: AbortSignal = { + aborted: false, + reason: undefined, + onabort: null, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + dispatchEvent: vi.fn(), + throwIfAborted: vi.fn(), + } as AbortSignal; + + abort = mockAbort; +} + +Object.assign(global, { + fetch: mockFetch, + AbortController: MockAbortController, + setTimeout: mockSetTimeout, + clearTimeout: mockClearTimeout, +}); + +describe('Api', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSetTimeout.mockImplementation((fn) => { + fn(); + + return 123; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with provided config', () => { + const config = { + baseURL: BASE_URL, + timeout: 5000, + }; + + const api = createApiInstance(config); + + expect(api).toBeInstanceOf(Api); + }); + + it('should use default timeout value of 20000 when timeout is undefined', () => { + const api = new Api({ + baseURL: BASE_URL, + timeout: undefined, + }); + + expect(api).toBeInstanceOf(Api); + }); + + it('should use default timeout when not provided', () => { + const api = createApiInstance({ baseURL: BASE_URL }); + + expect(api).toBeInstanceOf(Api); + }); + + it('should handle different baseURL formats', () => { + const testCases = [ + { baseURL: BASE_URL, description: 'HTTPS URL' }, + { baseURL: 'http://localhost:3000', description: 'HTTP localhost' }, + { baseURL: 'https://api.example.com/v1', description: 'URL with path' }, + ]; + + testCases.forEach(({ baseURL }) => { + const api = createApiInstance({ baseURL }); + + expect(api).toBeInstanceOf(Api); + }); + }); + }); + + describe('request method', () => { + let api: Api; + + beforeEach(() => { + api = createApiInstance(); + }); + + it('should make successful GET request', async () => { + const mockResponse = createMockResponse({ + headers: new Headers({ 'Content-Type': CONTENT_TYPE }), + json: vi.fn().mockResolvedValue({ message: 'success' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + const result = await api.get('/test'); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/test`, + expect.objectContaining({ + method: 'GET', + headers: { + 'Content-Type': CONTENT_TYPE, + }, + }), + ); + + expect(result).toEqual({ + data: { message: 'success' }, + status: 200, + statusText: 'OK', + headers: mockResponse.headers, + }); + }); + + it('should handle request timeout', async () => { + mockSetTimeout.mockImplementation(() => { + mockAbort(); + + return 123; + }); + + mockFetch.mockRejectedValue(new Error(REQUEST_ABORTED_ERROR)); + + await expect(api.get('/test')).rejects.toThrow(REQUEST_ABORTED_ERROR); + + expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 5000); + expect(mockAbort).toHaveBeenCalled(); + }); + + it('should handle HTTP error responses', async () => { + const mockResponse = createMockResponse({ + ok: false, + status: 404, + statusText: 'Not Found', + headers: new Headers({ 'Content-Type': CONTENT_TYPE }), + json: vi.fn().mockResolvedValue({ error: 'Not found' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + await expect(api.get('/test')).rejects.toThrow('HTTP 404: Not Found'); + + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should handle custom timeout', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.get('/test', { timeout: 10000 }); + + expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 10000); + }); + + it('should handle external abort signal', async () => { + const externalSignal = createMockAbortSignal(); + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.get('/test', { signal: externalSignal as AbortSignal }); + + expect(externalSignal.addEventListener).toHaveBeenCalledWith('abort', expect.any(Function), { once: true }); + }); + + it('should handle external abort signal triggering', async () => { + let abortCallback: (() => void) | null = null; + const externalSignal = createMockAbortSignal({ + addEventListener: vi.fn((event, callback) => { + if (event === 'abort') { + abortCallback = callback; + } + }), + }); + + mockFetch.mockImplementation(() => { + if (abortCallback) { + abortCallback(); + } + + return Promise.reject(new Error(REQUEST_ABORTED_ERROR)); + }); + + await expect(api.get('/test', { signal: externalSignal as AbortSignal })).rejects.toThrow(REQUEST_ABORTED_ERROR); + + expect(externalSignal.addEventListener).toHaveBeenCalledWith('abort', expect.any(Function), { once: true }); + expect(mockAbort).toHaveBeenCalled(); + }); + + it('should merge custom headers', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.get('/test', { + headers: { + Authorization: 'Bearer token', + 'X-Custom-Header': 'value', + }, + }); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/test`, + expect.objectContaining({ + headers: { + 'Content-Type': CONTENT_TYPE, + Authorization: 'Bearer token', + 'X-Custom-Header': 'value', + }, + }), + ); + }); + + it('should handle network errors', async () => { + const networkError = new Error('Network error'); + + mockFetch.mockRejectedValue(networkError); + + await expect(api.get('/test')).rejects.toThrow('Network error'); + + expect(mockClearTimeout).toHaveBeenCalled(); + }); + }); + + describe('HTTP methods', () => { + let api: Api; + + beforeEach(() => { + api = createApiInstance(); + }); + + describe('GET', () => { + it('should make GET request', async () => { + const mockResponse = createMockResponse({ + json: vi.fn().mockResolvedValue({ data: 'test' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + const result = await api.get('/users'); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users`, + expect.objectContaining({ + method: 'GET', + }), + ); + + expect(result.data).toEqual({ data: 'test' }); + }); + }); + + describe('POST', () => { + it('should make POST request with data', async () => { + const mockResponse = createMockResponse({ + status: 201, + statusText: 'Created', + json: vi.fn().mockResolvedValue({ id: 1 }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + const postData = { name: 'John', email: 'john@example.com' }; + const result = await api.post('/users', postData); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users`, + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(postData), + }), + ); + + expect(result.data).toEqual({ id: 1 }); + }); + + it('should make POST request without data', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.post('/users'); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users`, + expect.objectContaining({ + method: 'POST', + body: undefined, + }), + ); + }); + }); + + describe('PUT', () => { + it('should make PUT request with data', async () => { + const mockResponse = createMockResponse({ + json: vi.fn().mockResolvedValue({ updated: true }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + const putData = { name: 'Jane', email: 'jane@example.com' }; + const result = await api.put('/users/1', putData); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users/1`, + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify(putData), + }), + ); + + expect(result.data).toEqual({ updated: true }); + }); + + it('should make PUT request without data', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.put('/users/1'); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users/1`, + expect.objectContaining({ + method: 'PUT', + body: undefined, + }), + ); + }); + }); + + describe('DELETE', () => { + it('should make DELETE request', async () => { + const mockResponse = createMockResponse({ + status: 204, + statusText: 'No Content', + }); + + mockFetch.mockResolvedValue(mockResponse); + + const result = await api.delete('/users/1'); + + expect(mockFetch).toHaveBeenCalledWith( + `${BASE_URL}/users/1`, + expect.objectContaining({ + method: 'DELETE', + }), + ); + + expect(result.status).toBe(204); + }); + }); + }); + + describe('error handling', () => { + let api: Api; + + beforeEach(() => { + api = createApiInstance(); + }); + + it('should handle 400 Bad Request', async () => { + const mockResponse = createMockResponse({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: vi.fn().mockResolvedValue({ error: 'Invalid data' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + await expect(api.post('/users', {})).rejects.toThrow('HTTP 400: Bad Request'); + }); + + it('should handle 401 Unauthorized', async () => { + const mockResponse = createMockResponse({ + ok: false, + status: 401, + statusText: 'Unauthorized', + json: vi.fn().mockResolvedValue({ error: 'Unauthorized' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + await expect(api.get('/protected')).rejects.toThrow('HTTP 401: Unauthorized'); + }); + + it('should handle 500 Internal Server Error', async () => { + const mockResponse = createMockResponse({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: vi.fn().mockResolvedValue({ error: 'Server error' }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + await expect(api.get('/error')).rejects.toThrow('HTTP 500: Internal Server Error'); + }); + + it('should include response data in error', async () => { + const mockResponse = createMockResponse({ + ok: false, + status: 422, + statusText: 'Unprocessable Entity', + json: vi.fn().mockResolvedValue({ errors: ['Field is required'] }), + }); + + mockFetch.mockResolvedValue(mockResponse); + + try { + await api.post('/users', {}); + } catch (error: any) { + expect(error.status).toBe(422); + expect(error.statusText).toBe('Unprocessable Entity'); + expect(error.response).toBeDefined(); + expect(error.response.data).toEqual({ errors: ['Field is required'] }); + } + }); + }); + + describe('edge cases', () => { + let api: Api; + + beforeEach(() => { + api = createApiInstance(); + }); + + it('should handle empty endpoint', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.get(''); + + expect(mockFetch).toHaveBeenCalledWith(BASE_URL, expect.any(Object)); + }); + + it('should handle endpoint with query parameters', async () => { + const mockResponse = createMockResponse(); + + mockFetch.mockResolvedValue(mockResponse); + + await api.get('/users?page=1&limit=10'); + + expect(mockFetch).toHaveBeenCalledWith(`${BASE_URL}/users?page=1&limit=10`, expect.any(Object)); + }); + + it('should handle large JSON responses', async () => { + const largeData = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `User ${i}` })); + const mockResponse = createMockResponse({ + json: vi.fn().mockResolvedValue(largeData), + }); + + mockFetch.mockResolvedValue(mockResponse); + + const result = await api.get('/users'); + + expect(result.data).toEqual(largeData); + }); + }); +}); diff --git a/packages/models/src/calendar/calendar-entity/calendar-entity.test.ts b/packages/models/src/calendar/calendar-entity/calendar-entity.test.ts new file mode 100644 index 0000000..70254a7 --- /dev/null +++ b/packages/models/src/calendar/calendar-entity/calendar-entity.test.ts @@ -0,0 +1,102 @@ +import type { CalendarApi } from '@repo/types'; +import { describe, it, expect } from 'vitest'; + +import { Calendar } from './calendar-entity'; + +describe('Calendar', () => { + const DEFAULT_CALENDAR_ID = 'calendar-123'; + const DEFAULT_SUMMARY = 'Test Calendar'; + const DEFAULT_DESCRIPTION = 'Test calendar description'; + const DEFAULT_COLOR = '#FF5733'; + const DEFAULT_USER_ID = 'user-456'; + const DEFAULT_CREATED_AT = '2025-01-01T00:00:00Z'; + const DEFAULT_UPDATED_AT = '2025-01-02T00:00:00Z'; + + const createMockCalendarApi = (overrides: Partial = {}): CalendarApi => ({ + id: DEFAULT_CALENDAR_ID, + summary: DEFAULT_SUMMARY, + description: DEFAULT_DESCRIPTION, + color: DEFAULT_COLOR, + user_id: DEFAULT_USER_ID, + created_at: DEFAULT_CREATED_AT, + updated_at: DEFAULT_UPDATED_AT, + checked: true, + ...overrides, + }); + + describe('constructor', () => { + it.each([ + { + name: 'default calendar', + api: createMockCalendarApi(), + expected: { + id: DEFAULT_CALENDAR_ID, + summary: DEFAULT_SUMMARY, + description: DEFAULT_DESCRIPTION, + color: DEFAULT_COLOR, + userId: DEFAULT_USER_ID, + createdAt: DEFAULT_CREATED_AT, + updatedAt: DEFAULT_UPDATED_AT, + checked: true, + }, + }, + { + name: 'custom calendar', + api: createMockCalendarApi({ + id: 'unique-calendar-789', + summary: 'Work Calendar', + description: 'Calendar for work events', + color: '#3498DB', + user_id: 'user-999', + created_at: '2023-12-01T10:30:00Z', + updated_at: '2023-12-15T14:45:00Z', + checked: false, + }), + expected: { + id: 'unique-calendar-789', + summary: 'Work Calendar', + description: 'Calendar for work events', + color: '#3498DB', + userId: 'user-999', + createdAt: '2023-12-01T10:30:00Z', + updatedAt: '2023-12-15T14:45:00Z', + checked: false, + }, + }, + { + name: 'empty calendar', + api: createMockCalendarApi({ + id: '', + summary: '', + description: '', + color: '', + user_id: '', + created_at: '', + updated_at: '', + checked: false, + }), + expected: { + id: '', + summary: '', + description: '', + color: '', + userId: '', + createdAt: '', + updatedAt: '', + checked: false, + }, + }, + ])('should create Calendar instance and assign properties correctly: $name', ({ api, expected }) => { + const calendar = new Calendar(api); + + expect(calendar.id).toBe(expected.id); + expect(calendar.summary).toBe(expected.summary); + expect(calendar.description).toBe(expected.description); + expect(calendar.color).toBe(expected.color); + expect(calendar.userId).toBe(expected.userId); + expect(calendar.createdAt).toBe(expected.createdAt); + expect(calendar.updatedAt).toBe(expected.updatedAt); + expect(calendar.checked).toBe(expected.checked); + }); + }); +}); diff --git a/packages/models/src/calendar/calendar-event/calendar-event.test.ts b/packages/models/src/calendar/calendar-event/calendar-event.test.ts new file mode 100644 index 0000000..831cc44 --- /dev/null +++ b/packages/models/src/calendar/calendar-event/calendar-event.test.ts @@ -0,0 +1,177 @@ +import type { CalendarEventApi } from '@repo/types'; +import { utcStringToLocalDateTime } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarEvent } from './calendar-event'; + +vi.mock('@repo/utils', () => ({ + utcStringToLocalDateTime: vi.fn(), +})); + +const mockUtcStringToLocalDateTime = vi.mocked(utcStringToLocalDateTime); + +describe('CalendarEvent', () => { + const DEFAULT_EVENT_ID = 'event-123'; + const DEFAULT_CALENDAR_ID = 'calendar-456'; + const DEFAULT_START_DATE = '2025-01-15T10:30:00Z'; + const DEFAULT_END_DATE = '2025-01-15T12:00:00Z'; + const DEFAULT_TITLE = 'Test Event'; + const DEFAULT_DESCRIPTION = 'Test event description'; + + const createMockCalendarEventApi = (overrides: Partial = {}): CalendarEventApi => ({ + id: DEFAULT_EVENT_ID, + title: DEFAULT_TITLE, + description: DEFAULT_DESCRIPTION, + start_date: DEFAULT_START_DATE, + end_date: DEFAULT_END_DATE, + all_day: false, + calendar_id: DEFAULT_CALENDAR_ID, + ...overrides, + }); + + const mockStartDateTime = new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 0); + const mockEndDateTime = new Temporal.PlainDateTime(2025, 1, 15, 12, 0, 0); + + beforeEach(() => { + vi.clearAllMocks(); + mockUtcStringToLocalDateTime.mockImplementation((utcString: string) => { + if (utcString === DEFAULT_START_DATE) { + return mockStartDateTime; + } + if (utcString === DEFAULT_END_DATE) { + return mockEndDateTime; + } + + return new Temporal.PlainDateTime(2025, 1, 1, 0, 0, 0); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it.each([ + { + name: 'default event', + api: createMockCalendarEventApi(), + expected: { + id: DEFAULT_EVENT_ID, + title: DEFAULT_TITLE, + description: DEFAULT_DESCRIPTION, + start: mockStartDateTime, + end: mockEndDateTime, + allDay: false, + calendarId: DEFAULT_CALENDAR_ID, + }, + }, + { + name: 'all-day event', + api: createMockCalendarEventApi({ + all_day: true, + title: 'All Day Event', + description: 'All day event description', + }), + expected: { + id: DEFAULT_EVENT_ID, + title: 'All Day Event', + description: 'All day event description', + start: mockStartDateTime, + end: mockEndDateTime, + allDay: true, + calendarId: DEFAULT_CALENDAR_ID, + }, + }, + { + name: 'event with empty strings', + api: createMockCalendarEventApi({ + id: '', + title: '', + description: '', + calendar_id: '', + }), + expected: { + id: '', + title: '', + description: '', + start: mockStartDateTime, + end: mockEndDateTime, + allDay: false, + calendarId: '', + }, + }, + { + name: 'custom event with different dates', + api: createMockCalendarEventApi({ + id: 'custom-event-789', + title: 'Custom Event', + description: 'Custom event description', + start_date: '2024-12-25T09:00:00Z', + end_date: '2024-12-25T17:00:00Z', + calendar_id: 'custom-calendar-999', + }), + expected: { + id: 'custom-event-789', + title: 'Custom Event', + description: 'Custom event description', + start: new Temporal.PlainDateTime(2025, 1, 1, 0, 0, 0), + end: new Temporal.PlainDateTime(2025, 1, 1, 0, 0, 0), + allDay: false, + calendarId: 'custom-calendar-999', + }, + }, + ])('should create CalendarEvent instance correctly: $name', ({ api, expected }) => { + const calendarEvent = new CalendarEvent(api); + + expect(calendarEvent.id).toBe(expected.id); + expect(calendarEvent.title).toBe(expected.title); + expect(calendarEvent.description).toBe(expected.description); + expect(calendarEvent.start).toStrictEqual(expected.start); + expect(calendarEvent.end).toStrictEqual(expected.end); + expect(calendarEvent.allDay).toBe(expected.allDay); + expect(calendarEvent.calendarId).toBe(expected.calendarId); + }); + + it('should call utcStringToLocalDateTime with correct parameters', () => { + const eventApi = createMockCalendarEventApi(); + + new CalendarEvent(eventApi); + + expect(mockUtcStringToLocalDateTime).toHaveBeenCalledTimes(2); + expect(mockUtcStringToLocalDateTime).toHaveBeenCalledWith(DEFAULT_START_DATE); + expect(mockUtcStringToLocalDateTime).toHaveBeenCalledWith(DEFAULT_END_DATE); + }); + + it('should handle different date formats correctly', () => { + const customStartDate = '2024-06-15T14:30:00Z'; + const customEndDate = '2024-06-15T16:45:00Z'; + + const customStartDateTime = new Temporal.PlainDateTime(2024, 6, 15, 14, 30, 0); + const customEndDateTime = new Temporal.PlainDateTime(2024, 6, 15, 16, 45, 0); + + mockUtcStringToLocalDateTime.mockImplementation((utcString: string) => { + if (utcString === customStartDate) { + return customStartDateTime; + } + if (utcString === customEndDate) { + return customEndDateTime; + } + + return new Temporal.PlainDateTime(2025, 1, 1, 0, 0, 0); + }); + + const eventApi = createMockCalendarEventApi({ + start_date: customStartDate, + end_date: customEndDate, + }); + + const calendarEvent = new CalendarEvent(eventApi); + + expect(calendarEvent.start).toStrictEqual(customStartDateTime); + expect(calendarEvent.end).toStrictEqual(customEndDateTime); + expect(mockUtcStringToLocalDateTime).toHaveBeenCalledWith(customStartDate); + expect(mockUtcStringToLocalDateTime).toHaveBeenCalledWith(customEndDate); + }); + }); +}); diff --git a/packages/models/src/calendar/calendar-events-map/calendar-events-map.test.ts b/packages/models/src/calendar/calendar-events-map/calendar-events-map.test.ts new file mode 100644 index 0000000..c77f635 --- /dev/null +++ b/packages/models/src/calendar/calendar-events-map/calendar-events-map.test.ts @@ -0,0 +1,428 @@ +import type { CalendarEventApi } from '@repo/types'; +import { EventMode } from '@repo/types'; +import { getPlainDateIds, utcStringToLocalDateTime } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarEvent } from '../calendar-event'; + +import { CalendarEventsMap } from './calendar-events-map'; + +vi.mock('@repo/utils', () => ({ + getPlainDateIds: vi.fn(), + utcStringToLocalDateTime: vi.fn(), +})); + +vi.mock('../calendar-event', () => ({ + CalendarEvent: vi.fn(), +})); + +const mockGetPlainDateIds = vi.mocked(getPlainDateIds); +const mockUtcStringToLocalDateTime = vi.mocked(utcStringToLocalDateTime); +const MockCalendarEvent = vi.mocked(CalendarEvent); + +describe('CalendarEventsMap', () => { + let calendarEventsMap: CalendarEventsMap; + + const DEFAULT_EVENT_ID = 'event-123'; + const DEFAULT_CALENDAR_ID = 'calendar-456'; + const DEFAULT_START_DATE = '2025-01-15T10:30:00Z'; + const DEFAULT_END_DATE = '2025-01-15T12:00:00Z'; + const DEFAULT_TITLE = 'Test Event'; + const DEFAULT_DESCRIPTION = 'Test event description'; + const DEFAULT_DATE_STRING = '2025-01-15'; + const EVENT_ID_1 = 'event-1'; + const EVENT_ID_2 = 'event-2'; + const API_EVENT_ID_1 = 'api-event-1'; + const API_EVENT_ID_2 = 'api-event-2'; + const EXISTING_EVENT_ID = 'existing-event'; + const NON_EXISTENT_EVENT_ID = 'non-existent-event'; + const DATE_1 = '2025-01-15'; + const DATE_2 = '2025-01-16'; + const DATE_JAN_31 = '2025-01-31'; + const DATE_FEB_01 = '2025-02-01'; + const DATE_DEC_31 = '2024-12-31'; + const DATE_JAN_01 = '2025-01-01'; + const EVENT_DATE_ID = 'date-event'; + const EVENT_ALL_DAY_ID = 'all-day-event'; + const EVENT_EARLY_ID = 'early-event'; + const EVENT_LATE_ID = 'late-event'; + const EVENT_A_ID = 'event-a'; + const EVENT_B_ID = 'event-b'; + const CALENDAR_A_ID = 'calendar-a'; + const CALENDAR_B_ID = 'calendar-b'; + + const createMockCalendarEventApi = (overrides: Partial = {}): CalendarEventApi => ({ + id: DEFAULT_EVENT_ID, + title: DEFAULT_TITLE, + description: DEFAULT_DESCRIPTION, + start_date: DEFAULT_START_DATE, + end_date: DEFAULT_END_DATE, + all_day: false, + calendar_id: DEFAULT_CALENDAR_ID, + ...overrides, + }); + + const createMockCalendarEvent = (overrides: Partial = {}): CalendarEvent => { + const eventApi = createMockCalendarEventApi(overrides); + + const mockStartDateTime = mockUtcStringToLocalDateTime(eventApi.start_date); + const mockEndDateTime = mockUtcStringToLocalDateTime(eventApi.end_date); + + const mockEvent = { + id: eventApi.id, + title: eventApi.title, + description: eventApi.description, + start: mockStartDateTime, + end: mockEndDateTime, + allDay: eventApi.all_day, + calendarId: eventApi.calendar_id, + } as CalendarEvent; + + MockCalendarEvent.mockImplementation(() => mockEvent); + + return mockEvent; + }; + + const mockPlainDate = Temporal.PlainDate.from(DEFAULT_DATE_STRING); + + const createEventWithTime = (id: string, time: string) => + createMockCalendarEvent({ + id, + start_date: `${DEFAULT_DATE_STRING}T${time}Z`, + end_date: `${DEFAULT_DATE_STRING}T${time.replace(/(\d{2}):(\d{2})/, (_, h, m) => `${(parseInt(h) + 1).toString().padStart(2, '0')}:${m}`)}Z`, + }); + + const createEventWithDate = (id: string, date: string) => + createMockCalendarEvent({ + id, + start_date: `${date}T10:00:00Z`, + end_date: `${date}T11:00:00Z`, + }); + + const createAllDayEvent = (id: string, calendarId: string) => + createMockCalendarEvent({ + id, + calendar_id: calendarId, + all_day: true, + }); + + const expectEvents = (mode: EventMode, date: Temporal.PlainDate, expectedIds: string[]) => { + const events = calendarEventsMap.getEventsByPlainDate(mode, date); + + expect(events.map((e) => e.id)).toEqual(expectedIds); + }; + + const expectEmptyEvents = (mode: EventMode, date: Temporal.PlainDate) => { + expectEvents(mode, date, []); + }; + + beforeEach(() => { + vi.clearAllMocks(); + calendarEventsMap = new CalendarEventsMap(); + mockGetPlainDateIds.mockImplementation((date: Temporal.PlainDate) => { + const yearId = date.year.toString(); + const monthId = `${date.year}-${date.month.toString().padStart(2, '0')}`; + const dayId = `${date.year}-${date.month.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')}`; + + return [yearId, monthId, dayId] as [string, string, string]; + }); + + mockUtcStringToLocalDateTime.mockImplementation((utcString: string) => { + const dateTimeString = utcString.replace('Z', ''); + + return Temporal.PlainDateTime.from(dateTimeString); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarEventsMap instance with empty map', () => { + expect(calendarEventsMap).toBeInstanceOf(CalendarEventsMap); + expect(calendarEventsMap.map.size).toBe(0); + }); + }); + + describe('getEventsByPlainDate', () => { + it('should return empty array when no events exist', () => { + expectEmptyEvents(EventMode.date, mockPlainDate); + expect(mockGetPlainDateIds).toHaveBeenCalledWith(mockPlainDate); + }); + + it('should return events for the specified date and mode', () => { + const mockEvent = createMockCalendarEvent(); + + calendarEventsMap.add(mockEvent); + + expectEvents(EventMode.date, mockPlainDate, [mockEvent.id]); + }); + + it('should separate date and all-day events correctly', () => { + const dateEvent = createMockCalendarEvent({ id: EVENT_DATE_ID, all_day: false }); + const allDayEvent = createMockCalendarEvent({ id: EVENT_ALL_DAY_ID, all_day: true }); + + calendarEventsMap.add(dateEvent); + calendarEventsMap.add(allDayEvent); + + expectEvents(EventMode.date, mockPlainDate, [EVENT_DATE_ID]); + expectEvents(EventMode.allDay, mockPlainDate, [EVENT_ALL_DAY_ID]); + }); + }); + + describe('add', () => { + it('should sort date events by start time', () => { + const earlyEvent = createEventWithTime(EVENT_EARLY_ID, '09:00:00'); + const lateEvent = createEventWithTime(EVENT_LATE_ID, '11:00:00'); + + calendarEventsMap.add(lateEvent); + calendarEventsMap.add(earlyEvent); + + expectEvents(EventMode.date, mockPlainDate, [EVENT_EARLY_ID, EVENT_LATE_ID]); + }); + + it('should sort all-day events by calendar ID', () => { + const eventA = createAllDayEvent(EVENT_A_ID, CALENDAR_A_ID); + const eventB = createAllDayEvent(EVENT_B_ID, CALENDAR_B_ID); + + calendarEventsMap.add(eventB); + calendarEventsMap.add(eventA); + + expectEvents(EventMode.allDay, mockPlainDate, [EVENT_A_ID, EVENT_B_ID]); + }); + + it('should handle events with same start time correctly', () => { + const event1 = createEventWithTime(EVENT_ID_1, '10:00:00'); + const event2 = createEventWithTime(EVENT_ID_2, '10:00:00'); + + calendarEventsMap.add(event1); + calendarEventsMap.add(event2); + + const events = calendarEventsMap.getEventsByPlainDate(EventMode.date, mockPlainDate); + + expect(events).toHaveLength(2); + expect(events.map((e) => e.id)).toContain(EVENT_ID_1); + expect(events.map((e) => e.id)).toContain(EVENT_ID_2); + }); + + it.each([ + { id1: 'jan-event', d1: DATE_JAN_31, id2: 'feb-event', d2: DATE_FEB_01 }, + { id1: 'dec-event', d1: DATE_DEC_31, id2: 'jan-event', d2: DATE_JAN_01 }, + ])('should handle events across calendar boundaries: $d1 and $d2', ({ id1, d1, id2, d2 }) => { + const event1 = createEventWithDate(id1, d1); + const event2 = createEventWithDate(id2, d2); + + calendarEventsMap.add(event1); + calendarEventsMap.add(event2); + + expectEvents(EventMode.date, Temporal.PlainDate.from(d1), [id1]); + expectEvents(EventMode.date, Temporal.PlainDate.from(d2), [id2]); + }); + }); + + describe('delete', () => { + it('should delete event from the map', () => { + const mockEvent = createMockCalendarEvent(); + + calendarEventsMap.add(mockEvent); + calendarEventsMap.delete(mockEvent); + + expectEmptyEvents(EventMode.date, mockPlainDate); + }); + + it('should delete only the specified event when multiple events exist', () => { + const event1 = createMockCalendarEvent({ id: EVENT_ID_1 }); + const event2 = createMockCalendarEvent({ id: EVENT_ID_2 }); + + calendarEventsMap.add(event1); + calendarEventsMap.add(event2); + calendarEventsMap.delete(event1); + + expectEvents(EventMode.date, mockPlainDate, [EVENT_ID_2]); + }); + + it('should handle deleting non-existent events gracefully', () => { + const existingEvent = createMockCalendarEvent({ id: EXISTING_EVENT_ID }); + const nonExistentEvent = createMockCalendarEvent({ id: NON_EXISTENT_EVENT_ID }); + + calendarEventsMap.add(existingEvent); + + expect(() => { + calendarEventsMap.delete(nonExistentEvent); + }).not.toThrow(); + + expectEvents(EventMode.date, mockPlainDate, [EXISTING_EVENT_ID]); + }); + + it('should handle deleting events from different dates', () => { + const event1 = createEventWithDate(EVENT_ID_1, DATE_1); + const event2 = createEventWithDate(EVENT_ID_2, DATE_2); + + calendarEventsMap.add(event1); + calendarEventsMap.add(event2); + calendarEventsMap.delete(event2); + + expectEvents(EventMode.date, Temporal.PlainDate.from(DATE_1), [EVENT_ID_1]); + expectEmptyEvents(EventMode.date, Temporal.PlainDate.from(DATE_2)); + }); + + it('should leave day entry empty when deleting the last event of the day', () => { + const event = createMockCalendarEvent({ id: EXISTING_EVENT_ID }); + + calendarEventsMap.add(event); + expectEvents(EventMode.date, mockPlainDate, [EXISTING_EVENT_ID]); + + calendarEventsMap.delete(event); + + expectEmptyEvents(EventMode.date, mockPlainDate); + }); + + it('should delete all-day event correctly (cover allDay mode path)', () => { + const allDay = createAllDayEvent(EVENT_ALL_DAY_ID, CALENDAR_A_ID); + + calendarEventsMap.add(allDay); + expectEvents(EventMode.allDay, mockPlainDate, [EVENT_ALL_DAY_ID]); + + calendarEventsMap.delete(allDay); + + expectEmptyEvents(EventMode.allDay, mockPlainDate); + }); + + it('should handle delete for a day without an existing bucket (maps exist, day missing)', () => { + const eventOn15th = createEventWithDate(EVENT_ID_1, DATE_1); + + calendarEventsMap.add(eventOn15th); + + const eventOn16th = createEventWithDate('ghost-event', DATE_2); + + expect(() => { + calendarEventsMap.delete(eventOn16th); + }).not.toThrow(); + + expectEvents(EventMode.date, Temporal.PlainDate.from(DATE_1), [EVENT_ID_1]); + + expectEmptyEvents(EventMode.date, Temporal.PlainDate.from(DATE_2)); + }); + }); + + describe('update', () => { + it('should update event by deleting old and adding new', () => { + const oldEvent = createMockCalendarEvent({ id: 'old-event' }); + const newEvent = createMockCalendarEvent({ id: 'new-event', title: 'Updated Event' }); + + calendarEventsMap.add(oldEvent); + calendarEventsMap.update(oldEvent, newEvent); + + expectEvents(EventMode.date, mockPlainDate, ['new-event']); + }); + + it('should handle updating event with different date', () => { + const oldEvent = createEventWithDate('old-event', DATE_1); + const newEvent = createEventWithDate('new-event', DATE_2); + + calendarEventsMap.add(oldEvent); + calendarEventsMap.update(oldEvent, newEvent); + + expectEmptyEvents(EventMode.date, Temporal.PlainDate.from(DATE_1)); + expectEvents(EventMode.date, Temporal.PlainDate.from(DATE_2), ['new-event']); + }); + + it('should handle updating event from date to all-day', () => { + const dateEvent = createMockCalendarEvent({ id: 'date-event', all_day: false }); + const allDayEvent = createMockCalendarEvent({ id: EVENT_ALL_DAY_ID, all_day: true }); + + calendarEventsMap.add(dateEvent); + calendarEventsMap.update(dateEvent, allDayEvent); + + expectEmptyEvents(EventMode.date, mockPlainDate); + expectEvents(EventMode.allDay, mockPlainDate, [EVENT_ALL_DAY_ID]); + }); + }); + + describe('clear', () => { + it('should clear all events from the map', () => { + const mockEvent = createMockCalendarEvent(); + + calendarEventsMap.add(mockEvent); + calendarEventsMap.clear(); + + expect(calendarEventsMap.map.size).toBe(0); + }); + }); + + describe('addFromApi', () => { + it('should create CalendarEvent from API data and add it', () => { + const eventApi = createMockCalendarEventApi(); + const mockEvent = createMockCalendarEvent(); + + calendarEventsMap.addFromApi(eventApi); + + expect(MockCalendarEvent).toHaveBeenCalledWith(eventApi); + expectEvents(EventMode.date, mockPlainDate, [mockEvent.id]); + }); + }); + + describe('setListFromApi', () => { + it('should clear existing events and add new events from API', () => { + const existingEvent = createMockCalendarEvent({ id: EXISTING_EVENT_ID }); + + calendarEventsMap.add(existingEvent); + + const eventApi1 = createMockCalendarEventApi({ id: API_EVENT_ID_1 }); + const eventApi2 = createMockCalendarEventApi({ id: API_EVENT_ID_2 }); + + const mockEvent1 = { + id: API_EVENT_ID_1, + title: DEFAULT_TITLE, + description: DEFAULT_DESCRIPTION, + start: mockUtcStringToLocalDateTime(eventApi1.start_date), + end: mockUtcStringToLocalDateTime(eventApi1.end_date), + allDay: false, + calendarId: DEFAULT_CALENDAR_ID, + } as CalendarEvent; + + const mockEvent2 = { + id: API_EVENT_ID_2, + title: DEFAULT_TITLE, + description: DEFAULT_DESCRIPTION, + start: mockUtcStringToLocalDateTime(eventApi2.start_date), + end: mockUtcStringToLocalDateTime(eventApi2.end_date), + allDay: false, + calendarId: DEFAULT_CALENDAR_ID, + } as CalendarEvent; + + MockCalendarEvent.mockImplementation((api: CalendarEventApi) => { + if (api.id === API_EVENT_ID_1) { + return mockEvent1; + } + if (api.id === API_EVENT_ID_2) { + return mockEvent2; + } + + return mockEvent1; + }); + + calendarEventsMap.setListFromApi([eventApi1, eventApi2]); + + expect(MockCalendarEvent).toHaveBeenCalledWith(eventApi1); + expect(MockCalendarEvent).toHaveBeenCalledWith(eventApi2); + + const events = calendarEventsMap.getEventsByPlainDate(EventMode.date, mockPlainDate); + + expect(events).toHaveLength(2); + expect(events.map((e) => e.id)).toContain(API_EVENT_ID_1); + expect(events.map((e) => e.id)).toContain(API_EVENT_ID_2); + }); + + it('should handle empty API list', () => { + const existingEvent = createMockCalendarEvent(); + + calendarEventsMap.add(existingEvent); + calendarEventsMap.setListFromApi([]); + + expect(calendarEventsMap.map.size).toBe(0); + }); + }); +}); diff --git a/packages/models/src/calendar/calendar-events-map/calendar-events-map.ts b/packages/models/src/calendar/calendar-events-map/calendar-events-map.ts index c4399b7..4a55e51 100644 --- a/packages/models/src/calendar/calendar-events-map/calendar-events-map.ts +++ b/packages/models/src/calendar/calendar-events-map/calendar-events-map.ts @@ -5,16 +5,6 @@ import { Temporal } from 'temporal-polyfill'; import { CalendarEvent } from '../calendar-event'; -const sortDateEvents = (events: CalendarEvent[]): CalendarEvent[] => { - return events.sort( - (a, b) => a.start.toZonedDateTime('UTC').epochMilliseconds - b.start.toZonedDateTime('UTC').epochMilliseconds, - ); -}; - -const sortAllDayEvents = (events: CalendarEvent[]): CalendarEvent[] => { - return events.sort((a, b) => a.calendarId.localeCompare(b.calendarId)); -}; - export class CalendarEventsMap { map: ObservableMap>>>; @@ -66,9 +56,9 @@ export class CalendarEventsMap { const dayEvents = monthMap.get(dayId) || []; if (event.allDay) { - monthMap.set(dayId, sortAllDayEvents([...dayEvents, event])); + monthMap.set(dayId, this.sortAllDayEvents([...dayEvents, event])); } else { - monthMap.set(dayId, sortDateEvents([...dayEvents, event])); + monthMap.set(dayId, this.sortDateEvents([...dayEvents, event])); } }; @@ -107,4 +97,14 @@ export class CalendarEventsMap { this.add(event); } }; + + private sortDateEvents = (events: CalendarEvent[]): CalendarEvent[] => { + return events.sort( + (a, b) => a.start.toZonedDateTime('UTC').epochMilliseconds - b.start.toZonedDateTime('UTC').epochMilliseconds, + ); + }; + + private sortAllDayEvents = (events: CalendarEvent[]): CalendarEvent[] => { + return events.sort((a, b) => a.calendarId.localeCompare(b.calendarId)); + }; } diff --git a/packages/models/src/calendar/day.test.ts b/packages/models/src/calendar/day.test.ts new file mode 100644 index 0000000..d0ba0f7 --- /dev/null +++ b/packages/models/src/calendar/day.test.ts @@ -0,0 +1,73 @@ +import type { DayID, HourID } from '@repo/types'; +import { hourID, parseDayID } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarDay } from './day'; + +vi.mock('@repo/utils', () => ({ + hourID: vi.fn(), + parseDayID: vi.fn(), +})); + +const mockHourID = vi.mocked(hourID); +const mockParseDayID = vi.mocked(parseDayID); + +describe('CalendarDay', () => { + const DEFAULT_DAY_ID = '2025-01-15' as DayID; + const mockDate = new Temporal.PlainDate(2025, 1, 15); + + beforeEach(() => { + vi.clearAllMocks(); + mockParseDayID.mockReturnValue(mockDate); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarDay instance with correct properties', () => { + const calendarDay = new CalendarDay(DEFAULT_DAY_ID); + + expect(calendarDay.id).toBe(DEFAULT_DAY_ID); + expect(calendarDay.unit).toBe('day'); + expect(calendarDay.date).toBe(mockDate); + expect(mockParseDayID).toHaveBeenCalledTimes(1); + expect(mockParseDayID).toHaveBeenCalledWith(DEFAULT_DAY_ID); + }); + }); + + describe('getHoursKeys', () => { + it('should return array of 24 hour IDs for the day', () => { + const calendarDay = new CalendarDay(DEFAULT_DAY_ID); + const expectedHourIds: HourID[] = []; + + for (let hour = 0; hour < 24; hour++) { + const expectedHourId = `2025-01-15T${hour.toString().padStart(2, '0')}:00:00` as HourID; + + expectedHourIds.push(expectedHourId); + mockHourID.mockReturnValueOnce(expectedHourId); + } + + const result = calendarDay.getHoursKeys(); + + expect(result).toEqual(expectedHourIds); + expect(mockHourID).toHaveBeenCalledTimes(24); + expect(mockHourID).toHaveBeenCalledWith(new Temporal.PlainDateTime(2025, 1, 15, 0)); + expect(mockHourID).toHaveBeenCalledWith(new Temporal.PlainDateTime(2025, 1, 15, 23)); + }); + }); + + describe('getDayOfWeekName', () => { + it.each([ + { date: new Temporal.PlainDate(2025, 1, 15), id: '2025-01-15' as DayID, expected: 'Wednesday' }, + { date: new Temporal.PlainDate(2025, 1, 17), id: '2025-01-17' as DayID, expected: 'Friday' }, + ])('should return correct day name for $expected', ({ date, id, expected }) => { + mockParseDayID.mockReturnValue(date); + const calendarDay = new CalendarDay(id); + + expect(calendarDay.getDayOfWeekName()).toBe(expected); + }); + }); +}); diff --git a/packages/models/src/calendar/hour.test.ts b/packages/models/src/calendar/hour.test.ts new file mode 100644 index 0000000..4caf676 --- /dev/null +++ b/packages/models/src/calendar/hour.test.ts @@ -0,0 +1,50 @@ +import type { HourID } from '@repo/types'; +import { parseHourID } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarHour } from './hour'; + +vi.mock('@repo/utils', () => ({ + parseHourID: vi.fn(), +})); + +const mockParseHourID = vi.mocked(parseHourID); + +describe('CalendarHour', () => { + const DEFAULT_HOUR_ID = '2025-01-15T10:00:00' as HourID; + const mockDateTime = new Temporal.PlainDateTime(2025, 1, 15, 10, 0, 0); + + beforeEach(() => { + vi.clearAllMocks(); + mockParseHourID.mockReturnValue(mockDateTime); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarHour instance with correct properties', () => { + const calendarHour = new CalendarHour(DEFAULT_HOUR_ID); + + expect(calendarHour.id).toBe(DEFAULT_HOUR_ID); + expect(calendarHour.unit).toBe('hour'); + expect(calendarHour.dateTime).toBe(mockDateTime); + expect(mockParseHourID).toHaveBeenCalledTimes(1); + expect(mockParseHourID).toHaveBeenCalledWith(DEFAULT_HOUR_ID); + }); + }); + + describe('toString', () => { + it.each([ + { dt: new Temporal.PlainDateTime(2025, 1, 15, 0, 0, 0), id: '2025-01-15T00:00:00' as HourID, expected: '0:00' }, + { dt: new Temporal.PlainDateTime(2025, 1, 15, 23, 0, 0), id: '2025-01-15T23:00:00' as HourID, expected: '23:00' }, + ])('should return correct string format for $expected', ({ dt, id, expected }) => { + mockParseHourID.mockReturnValue(dt); + const calendarHour = new CalendarHour(id); + + expect(calendarHour.toString()).toBe(expected); + }); + }); +}); diff --git a/packages/models/src/calendar/month.test.ts b/packages/models/src/calendar/month.test.ts new file mode 100644 index 0000000..9d4b3da --- /dev/null +++ b/packages/models/src/calendar/month.test.ts @@ -0,0 +1,204 @@ +import type { DayID, MonthID } from '@repo/types'; +import { dayID, parseMonthID, weekStart } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarMonth } from './month'; + +vi.mock('@repo/utils', () => ({ + dayID: vi.fn(), + parseMonthID: vi.fn(), + weekStart: vi.fn(() => 1), +})); + +const mockDayID = vi.mocked(dayID); +const mockParseMonthID = vi.mocked(parseMonthID); +const mockWeekStart = vi.mocked(weekStart); + +describe('CalendarMonth', () => { + const DEFAULT_MONTH_ID = '2025-01' as MonthID; + const mockYearMonth = new Temporal.PlainYearMonth(2025, 1); + + beforeEach(() => { + vi.clearAllMocks(); + mockParseMonthID.mockReturnValue(mockYearMonth); + mockWeekStart.mockReturnValue(1); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarMonth instance with correct properties', () => { + const calendarMonth = new CalendarMonth(DEFAULT_MONTH_ID); + + expect(calendarMonth.id).toBe(DEFAULT_MONTH_ID); + expect(calendarMonth.unit).toBe('month'); + expect(calendarMonth.yearMonth).toStrictEqual(mockYearMonth); + expect(mockParseMonthID).toHaveBeenCalledTimes(1); + expect(mockParseMonthID).toHaveBeenCalledWith(DEFAULT_MONTH_ID); + }); + }); + + describe('getMonthName', () => { + it.each([ + { ym: new Temporal.PlainYearMonth(2025, 1), id: '2025-01' as MonthID, expected: 'January' }, + { ym: new Temporal.PlainYearMonth(2025, 12), id: '2025-12' as MonthID, expected: 'December' }, + ])('should return correct month name for $expected', ({ ym, id, expected }) => { + mockParseMonthID.mockReturnValue(ym); + const calendarMonth = new CalendarMonth(id); + + expect(calendarMonth.getMonthName()).toBe(expected); + }); + }); + + describe('getMonthNameShort', () => { + it('should return first 3 characters of month name', () => { + const januaryYearMonth = new Temporal.PlainYearMonth(2025, 1); + + mockParseMonthID.mockReturnValue(januaryYearMonth); + const calendarMonth = new CalendarMonth('2025-01' as MonthID); + + expect(calendarMonth.getMonthNameShort()).toBe('Jan'); + }); + + it('should return empty string when month name is missing', () => { + const marchYearMonth = new Temporal.PlainYearMonth(2025, 3); + + mockParseMonthID.mockReturnValue(marchYearMonth); + const calendarMonth = new CalendarMonth('2025-03' as MonthID); + + (calendarMonth as any).yearMonth = { month: 20 }; + + expect(calendarMonth.getMonthName()).toBe(''); + expect(calendarMonth.getMonthNameShort()).toBe(''); + }); + }); + + describe('getDaysKeys', () => { + it('should return array of day IDs for January (31 days)', () => { + const januaryYearMonth = new Temporal.PlainYearMonth(2025, 1); + + mockParseMonthID.mockReturnValue(januaryYearMonth); + const calendarMonth = new CalendarMonth('2025-01' as MonthID); + + const expectedDayIds: DayID[] = []; + + for (let day = 1; day <= 31; day++) { + const expectedDayId = `2025-01-${day.toString().padStart(2, '0')}` as DayID; + + expectedDayIds.push(expectedDayId); + mockDayID.mockReturnValueOnce(expectedDayId); + } + + const result = calendarMonth.getDaysKeys(); + + expect(result).toEqual(expectedDayIds); + expect(mockDayID).toHaveBeenCalledTimes(31); + }); + + it('should return array of day IDs for February (29 days in leap year)', () => { + const februaryYearMonth = new Temporal.PlainYearMonth(2024, 2); + + mockParseMonthID.mockReturnValue(februaryYearMonth); + const calendarMonth = new CalendarMonth('2024-02' as MonthID); + + const expectedDayIds: DayID[] = []; + + for (let day = 1; day <= 29; day++) { + const expectedDayId = `2024-02-${day.toString().padStart(2, '0')}` as DayID; + + expectedDayIds.push(expectedDayId); + mockDayID.mockReturnValueOnce(expectedDayId); + } + + const result = calendarMonth.getDaysKeys(); + + expect(result).toEqual(expectedDayIds); + expect(mockDayID).toHaveBeenCalledTimes(29); + }); + }); + + describe('getFirstWeekShift', () => { + it('should return correct shift for month starting on Monday', () => { + const septemberYearMonth = new Temporal.PlainYearMonth(2025, 9); + + mockParseMonthID.mockReturnValue(septemberYearMonth); + mockWeekStart.mockReturnValue(1); + const calendarMonth = new CalendarMonth('2025-09' as MonthID); + + expect(calendarMonth.getFirstWeekShift()).toBe(0); + }); + + it('should return correct shift for month starting on Sunday', () => { + const juneYearMonth = new Temporal.PlainYearMonth(2025, 6); + + mockParseMonthID.mockReturnValue(juneYearMonth); + mockWeekStart.mockReturnValue(1); + const calendarMonth = new CalendarMonth('2025-06' as MonthID); + + expect(calendarMonth.getFirstWeekShift()).toBe(6); + }); + }); + + describe('getPrevMonthDaysKeys', () => { + it('should return empty array for month starting on Monday', () => { + const septemberYearMonth = new Temporal.PlainYearMonth(2025, 9); + + mockParseMonthID.mockReturnValue(septemberYearMonth); + const calendarMonth = new CalendarMonth('2025-09' as MonthID); + + vi.spyOn(calendarMonth, 'getFirstWeekShift').mockReturnValue(0); + + expect(calendarMonth.getPrevMonthDaysKeys()).toHaveLength(0); + }); + + it('should return previous month days when needed', () => { + const marchYearMonth = new Temporal.PlainYearMonth(2025, 3); + + mockParseMonthID.mockReturnValue(marchYearMonth); + const calendarMonth = new CalendarMonth('2025-03' as MonthID); + + vi.spyOn(calendarMonth, 'getFirstWeekShift').mockReturnValue(5); + + expect(calendarMonth.getPrevMonthDaysKeys()).toHaveLength(5); + }); + }); + + describe('getNextMonthDaysKeys', () => { + it('should return empty array when grid is filled', () => { + const januaryYearMonth = new Temporal.PlainYearMonth(2025, 1); + + mockParseMonthID.mockReturnValue(januaryYearMonth); + const calendarMonth = new CalendarMonth('2025-01' as MonthID); + + vi.spyOn(calendarMonth, 'getPrevMonthDaysKeys').mockReturnValue( + Array.from({ length: 11 }, (_, i) => `2023-12-${(21 + i).toString().padStart(2, '0')}` as DayID), + ); + + vi.spyOn(calendarMonth, 'getDaysKeys').mockReturnValue( + Array.from({ length: 31 }, (_, i) => `2025-01-${(i + 1).toString().padStart(2, '0')}` as DayID), + ); + + expect(calendarMonth.getNextMonthDaysKeys()).toEqual([]); + }); + + it('should return next month days when needed', () => { + const februaryYearMonth = new Temporal.PlainYearMonth(2025, 2); + + mockParseMonthID.mockReturnValue(februaryYearMonth); + const calendarMonth = new CalendarMonth('2025-02' as MonthID); + + vi.spyOn(calendarMonth, 'getPrevMonthDaysKeys').mockReturnValue( + Array.from({ length: 4 }, (_, i) => `2025-01-${(28 + i + 1).toString().padStart(2, '0')}` as DayID), + ); + + vi.spyOn(calendarMonth, 'getDaysKeys').mockReturnValue( + Array.from({ length: 29 }, (_, i) => `2025-02-${(i + 1).toString().padStart(2, '0')}` as DayID), + ); + + expect(calendarMonth.getNextMonthDaysKeys()).toHaveLength(9); + }); + }); +}); diff --git a/packages/models/src/calendar/month.ts b/packages/models/src/calendar/month.ts index 251315a..1a70028 100644 --- a/packages/models/src/calendar/month.ts +++ b/packages/models/src/calendar/month.ts @@ -18,10 +18,7 @@ const MONTH_NAMES = [ 'December', ]; -const getMonthNames = () => { - return MONTH_NAMES; -}; - +// todo: refactor to CalendarMonthView const MONTH_GRID_COLS = 7; const MONTH_GRID_ROWS = 6; const MONTH_GRID_DAYS = MONTH_GRID_COLS * MONTH_GRID_ROWS; @@ -38,10 +35,14 @@ export class CalendarMonth implements ICalendarMonth { this.yearMonth = Temporal.PlainYearMonth.from(yearMonth); } + static getMonthNames(): string[] { + return MONTH_NAMES; + } + getMonthName(): string { const monthIndex = this.yearMonth.month - 1; - return getMonthNames()[monthIndex] || '?'; + return CalendarMonth.getMonthNames()[monthIndex] || ''; } getMonthNameShort(): string { diff --git a/packages/models/src/calendar/week.test.ts b/packages/models/src/calendar/week.test.ts new file mode 100644 index 0000000..24602c8 --- /dev/null +++ b/packages/models/src/calendar/week.test.ts @@ -0,0 +1,95 @@ +import type { DayID, WeekID } from '@repo/types'; +import { dayID, parseWeekID } from '@repo/utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarWeek } from './week'; + +vi.mock('@repo/utils', () => ({ + dayID: vi.fn(), + parseWeekID: vi.fn(), +})); + +const mockDayID = vi.mocked(dayID); +const mockParseWeekID = vi.mocked(parseWeekID); + +describe('CalendarWeek', () => { + const DEFAULT_WEEK_ID = '2025-01-13' as WeekID; + const mockFirstDate = new Temporal.PlainDate(2025, 1, 13); + + beforeEach(() => { + vi.clearAllMocks(); + mockParseWeekID.mockReturnValue(mockFirstDate); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarWeek instance with correct properties', () => { + const calendarWeek = new CalendarWeek(DEFAULT_WEEK_ID); + + expect(calendarWeek.id).toBe(DEFAULT_WEEK_ID); + expect(calendarWeek.unit).toBe('week'); + expect(calendarWeek.firstDate).toBe(mockFirstDate); + expect(mockParseWeekID).toHaveBeenCalledTimes(1); + expect(mockParseWeekID).toHaveBeenCalledWith(DEFAULT_WEEK_ID); + }); + }); + + describe('getDaysIds', () => { + it('should return array of 7 day IDs', () => { + const mondayDate = new Temporal.PlainDate(2025, 1, 13); + + mockParseWeekID.mockReturnValue(mondayDate); + const calendarWeek = new CalendarWeek('2025-01-13' as WeekID); + + const expectedDayIds: DayID[] = []; + + for (let day = 0; day < 7; day++) { + const expectedDayId = `2025-01-${(13 + day).toString().padStart(2, '0')}` as DayID; + + expectedDayIds.push(expectedDayId); + mockDayID.mockReturnValueOnce(expectedDayId); + } + + const result = calendarWeek.getDaysIds(); + + expect(result).toEqual(expectedDayIds); + expect(mockDayID).toHaveBeenCalledTimes(7); + expect(mockDayID).toHaveBeenCalledWith(mondayDate.add({ days: 0 })); + expect(mockDayID).toHaveBeenCalledWith(mondayDate.add({ days: 6 })); + }); + + it('should handle week crossing month boundary', () => { + const lastWeekOfMonthDate = new Temporal.PlainDate(2025, 1, 29); + + mockParseWeekID.mockReturnValue(lastWeekOfMonthDate); + const calendarWeek = new CalendarWeek('2025-01-29' as WeekID); + + const expectedDayIds: DayID[] = []; + const expectedDates = [ + '2025-01-29', + '2025-01-30', + '2025-01-31', + '2025-02-01', + '2025-02-02', + '2025-02-03', + '2025-02-04', + ]; + + for (let day = 0; day < 7; day++) { + const expectedDayId = expectedDates[day] as DayID; + + expectedDayIds.push(expectedDayId); + mockDayID.mockReturnValueOnce(expectedDayId); + } + + const result = calendarWeek.getDaysIds(); + + expect(result).toEqual(expectedDayIds); + expect(mockDayID).toHaveBeenCalledTimes(7); + }); + }); +}); diff --git a/packages/models/src/calendar/year.test.ts b/packages/models/src/calendar/year.test.ts new file mode 100644 index 0000000..2ee4527 --- /dev/null +++ b/packages/models/src/calendar/year.test.ts @@ -0,0 +1,93 @@ +import type { MonthID, YearID } from '@repo/types'; +import { monthID, parseYearID } from '@repo/utils'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { CalendarMonth } from './month'; +import { CalendarYear } from './year'; + +vi.mock('@repo/utils', () => ({ + monthID: vi.fn(), + parseYearID: vi.fn(), +})); + +vi.mock('./month', () => ({ + CalendarMonth: vi.fn(), +})); + +const mockMonthID = vi.mocked(monthID); +const mockParseYearID = vi.mocked(parseYearID); +const MockCalendarMonth = vi.mocked(CalendarMonth); + +describe('CalendarYear', () => { + const DEFAULT_YEAR_ID = '2025' as YearID; + const mockYear = 2025; + + const generateMonthId = (month: number): MonthID => `2025-${month.toString().padStart(2, '0')}` as MonthID; + + const setupMocksForConstructor = () => { + for (let month = 1; month <= 12; month++) { + const monthId = generateMonthId(month); + + mockMonthID.mockReturnValueOnce(monthId); + MockCalendarMonth.mockImplementationOnce(() => ({ id: monthId, unit: 'month' }) as CalendarMonth); + } + }; + + const setupMocksForMethods = () => { + for (let month = 1; month <= 12; month++) { + mockMonthID.mockReturnValueOnce(generateMonthId(month)); + } + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockParseYearID.mockReturnValue(mockYear); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create CalendarYear instance with correct properties', () => { + setupMocksForConstructor(); + + const calendarYear = new CalendarYear(DEFAULT_YEAR_ID); + + expect(calendarYear.id).toBe(DEFAULT_YEAR_ID); + expect(calendarYear.unit).toBe('year'); + expect(calendarYear.months).toHaveLength(12); + expect(mockParseYearID).toHaveBeenCalledTimes(12); + expect(MockCalendarMonth).toHaveBeenCalledTimes(12); + expect(mockMonthID).toHaveBeenCalledTimes(12); + }); + }); + + describe('getMonthsKeys', () => { + it('should return array of 12 month IDs', () => { + setupMocksForConstructor(); + setupMocksForMethods(); + + const calendarYear = new CalendarYear(DEFAULT_YEAR_ID); + + const expectedMonthIds = Array.from({ length: 12 }, (_, i) => generateMonthId(i + 1)); + + const result = calendarYear.getMonthsKeys(); + + expect(result).toEqual(expectedMonthIds); + }); + }); + + describe('getMonths', () => { + it('should return array of 12 CalendarMonth instances', () => { + setupMocksForConstructor(); + setupMocksForMethods(); + + const calendarYear = new CalendarYear(DEFAULT_YEAR_ID); + + const result = calendarYear.getMonths(); + + expect(result).toHaveLength(12); + }); + }); +}); diff --git a/packages/models/src/user/user.test.ts b/packages/models/src/user/user.test.ts new file mode 100644 index 0000000..e446810 --- /dev/null +++ b/packages/models/src/user/user.test.ts @@ -0,0 +1,69 @@ +import type { UserApi } from '@repo/types'; +import { describe, it, expect } from 'vitest'; + +import { UserModel } from './user'; + +describe('UserModel', () => { + const createMockUserApi = (overrides: Partial = {}): UserApi => ({ + id: 'user-123', + email: 'test@example.com', + name: 'Test User', + picture: 'https://example.com/avatar.jpg', + ...overrides, + }); + + describe('constructor', () => { + it('should create UserModel instance with valid UserApi data', () => { + const userApi = createMockUserApi(); + const userModel = new UserModel(userApi); + + expect(userModel).toBeInstanceOf(UserModel); + expect(userModel.id).toBe('user-123'); + expect(userModel.email).toBe('test@example.com'); + expect(userModel.name).toBe('Test User'); + expect(userModel.avatar).toBe('https://example.com/avatar.jpg'); + }); + + it('should assign all properties correctly from UserApi', () => { + const userApi = createMockUserApi({ + id: 'unique-id-456', + email: 'john.doe@company.com', + name: 'John Doe', + picture: 'https://cdn.example.com/profile.png', + }); + + const userModel = new UserModel(userApi); + + expect(userModel.id).toBe('unique-id-456'); + expect(userModel.email).toBe('john.doe@company.com'); + expect(userModel.name).toBe('John Doe'); + expect(userModel.avatar).toBe('https://cdn.example.com/profile.png'); + }); + + it('should set avatar to empty string when picture is undefined', () => { + const userApi = createMockUserApi({ + picture: undefined, + }); + + const userModel = new UserModel(userApi); + + expect(userModel.avatar).toBe(''); + }); + + it('should handle empty string values', () => { + const userApi = createMockUserApi({ + id: '', + email: '', + name: '', + picture: '', + }); + + const userModel = new UserModel(userApi); + + expect(userModel.id).toBe(''); + expect(userModel.email).toBe(''); + expect(userModel.name).toBe(''); + expect(userModel.avatar).toBe(''); + }); + }); +}); diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json index 89fe85b..dc72b8c 100644 --- a/packages/models/tsconfig.json +++ b/packages/models/tsconfig.json @@ -3,7 +3,8 @@ "extends": "@repo/typescript-config/base.json", "include": [ "src/**/*", - "tsup.config.ts" + "tsup.config.ts", + "vitest.config.ts" ], "exclude": [ "dist", diff --git a/packages/models/vitest.config.ts b/packages/models/vitest.config.ts new file mode 100644 index 0000000..ae10030 --- /dev/null +++ b/packages/models/vitest.config.ts @@ -0,0 +1,14 @@ +/// +import { resolve } from 'path'; + +import { createNodeVitestConfig } from '@repo/vitest-config'; + +export default createNodeVitestConfig( + {}, + { + '@repo/types': resolve(__dirname, '../types/src'), + '@repo/types/*': resolve(__dirname, '../types/src/*'), + '@repo/utils': resolve(__dirname, '../utils/src'), + '@repo/utils/*': resolve(__dirname, '../utils/src/*'), + }, +); diff --git a/packages/stores/package.json b/packages/stores/package.json index dc97a7b..a473e87 100644 --- a/packages/stores/package.json +++ b/packages/stores/package.json @@ -23,6 +23,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --check \"**/*.{js,ts,tsx,scss}\"", @@ -37,11 +40,14 @@ "@repo/types": "*", "@repo/typescript-config": "*", "@repo/utils": "*", + "@repo/vitest-config": "*", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.29.0", "prettier": "^3.5.3", "stylelint": "^16.20.0", "tsup": "^8.0.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^2.1.8" }, "dependencies": { "jwt-decode": "^4.0.0", diff --git a/packages/stores/src/example.test.ts b/packages/stores/src/example.test.ts new file mode 100644 index 0000000..8dd2f57 --- /dev/null +++ b/packages/stores/src/example.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('Stores package test', () => { + it('should work', () => { + expect(true).toBe(true); + }); +}); diff --git a/packages/stores/tsconfig.json b/packages/stores/tsconfig.json index 89fe85b..dc72b8c 100644 --- a/packages/stores/tsconfig.json +++ b/packages/stores/tsconfig.json @@ -3,7 +3,8 @@ "extends": "@repo/typescript-config/base.json", "include": [ "src/**/*", - "tsup.config.ts" + "tsup.config.ts", + "vitest.config.ts" ], "exclude": [ "dist", diff --git a/packages/stores/vitest.config.ts b/packages/stores/vitest.config.ts new file mode 100644 index 0000000..1516dbf --- /dev/null +++ b/packages/stores/vitest.config.ts @@ -0,0 +1,16 @@ +/// +import { resolve } from 'path'; + +import { createNodeVitestConfig } from '@repo/vitest-config'; + +export default createNodeVitestConfig( + {}, + { + '@repo/types': resolve(__dirname, '../types/src'), + '@repo/types/*': resolve(__dirname, '../types/src/*'), + '@repo/utils': resolve(__dirname, '../utils/src'), + '@repo/utils/*': resolve(__dirname, '../utils/src/*'), + '@repo/models': resolve(__dirname, '../models/src'), + '@repo/models/*': resolve(__dirname, '../models/src/*'), + }, +); diff --git a/packages/types/package.json b/packages/types/package.json index 535d47a..9ffa1c5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,6 +23,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --check \"**/*.{js,ts,tsx,scss}\"", @@ -35,11 +38,14 @@ "@gravity-ui/date-utils": "^2.5.6", "@repo/eslint-config": "*", "@repo/typescript-config": "*", + "@repo/vitest-config": "*", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.29.0", "prettier": "^3.5.3", "stylelint": "^16.20.0", "tsup": "^8.0.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^2.1.8" }, "dependencies": { "temporal-polyfill": "^0.3.0" diff --git a/packages/types/src/example.test.ts b/packages/types/src/example.test.ts new file mode 100644 index 0000000..e09c62d --- /dev/null +++ b/packages/types/src/example.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('Types package test', () => { + it('should work', () => { + expect(true).toBe(true); + }); +}); diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index 89fe85b..dc72b8c 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -3,7 +3,8 @@ "extends": "@repo/typescript-config/base.json", "include": [ "src/**/*", - "tsup.config.ts" + "tsup.config.ts", + "vitest.config.ts" ], "exclude": [ "dist", diff --git a/packages/types/vitest.config.ts b/packages/types/vitest.config.ts new file mode 100644 index 0000000..8a9e711 --- /dev/null +++ b/packages/types/vitest.config.ts @@ -0,0 +1,12 @@ +/// +import { resolve } from 'path'; + +import { createNodeVitestConfig } from '@repo/vitest-config'; + +export default createNodeVitestConfig( + {}, + { + '@repo/utils': resolve(__dirname, '../utils/src'), + '@repo/utils/*': resolve(__dirname, '../utils/src/*'), + }, +); diff --git a/packages/ui/package.json b/packages/ui/package.json index 05e5394..9b99889 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,6 +22,9 @@ "scripts": { "build": "vite build && tsc --project tsconfig.json", "dev": "vite build --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "stylelint": "stylelint \"src/**/*.scss\" --max-warnings 0", @@ -46,22 +49,32 @@ }, "devDependencies": { "@repo/eslint-config": "*", + "@repo/react-testing-library-config": "*", "@repo/stylelint-config": "*", "@repo/types": "*", "@repo/typescript-config": "*", "@repo/utils": "*", + "@repo/vitest-config": "*", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", "@types/node": "^24.0.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-v8": "^2.1.8", "autoprefixer": "^10.4.21", "eslint": "^9.29.0", + "jsdom": "^25.0.1", "postcss": "^8.5.5", "prettier": "^3.5.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", "stylelint": "^16.20.0", "typescript": "^5.8.3", "vite": "^6.3.5", - "vite-plugin-dts": "^4.5.4" + "vite-plugin-dts": "^4.5.4", + "vitest": "^2.1.8" }, "peerDependencies": { "react": "^19.1.0", diff --git a/packages/ui/src/example.test.tsx b/packages/ui/src/example.test.tsx new file mode 100644 index 0000000..28eb297 --- /dev/null +++ b/packages/ui/src/example.test.tsx @@ -0,0 +1,31 @@ +import { render, screen, userEvent } from '@repo/react-testing-library-config'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; + +const TestComponent = ({ onClick }: { onClick?: () => void }) => { + return ( +
+

Hello World

+ +
+ ); +}; + +describe('UI package test', () => { + it('should work', () => { + render(); + expect(screen.getByText('Hello World')).toBeInTheDocument(); + }); + + it('should handle user interactions', async () => { + const handleClick = vi.fn(); + + render(); + + const button = screen.getByRole('button', { name: 'Click me' }); + + await userEvent.click(button); + + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 685f72e..dc36e55 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -12,6 +12,7 @@ }, "include": [ "src/**/*", - "vite.config.ts" + "vite.config.ts", + "vitest.config.ts" ] } diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts new file mode 100644 index 0000000..b58628b --- /dev/null +++ b/packages/ui/vitest.config.ts @@ -0,0 +1,17 @@ +/// +import { resolve } from 'path'; + +import { createReactVitestConfig } from '@repo/vitest-config'; +import react from '@vitejs/plugin-react'; + +export default createReactVitestConfig( + { + plugins: [react()], + }, + { + '@repo/types': resolve(__dirname, '../types/src'), + '@repo/types/*': resolve(__dirname, '../types/src/*'), + '@repo/utils': resolve(__dirname, '../utils/src'), + '@repo/utils/*': resolve(__dirname, '../utils/src/*'), + }, +); diff --git a/packages/utils/package.json b/packages/utils/package.json index e6a04ca..7abba14 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -23,6 +23,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "eslint": "eslint \"**/*.js\" \"src/**/*.{ts,tsx}\" --max-warnings 0", "eslint:fix": "eslint --fix \"**/*.js\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --check \"**/*.{js,ts,tsx,scss}\"", @@ -35,11 +38,14 @@ "@repo/eslint-config": "*", "@repo/types": "*", "@repo/typescript-config": "*", + "@repo/vitest-config": "*", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.29.0", "prettier": "^3.5.3", "stylelint": "^16.20.0", "tsup": "^8.0.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^2.1.8" }, "dependencies": { "@gravity-ui/date-utils": "^2.5.6", diff --git a/packages/utils/src/adaptivity.ts b/packages/utils/src/adaptivity.ts index 0f6c9af..3bde775 100644 --- a/packages/utils/src/adaptivity.ts +++ b/packages/utils/src/adaptivity.ts @@ -1,3 +1,4 @@ +// TODO: implement breakpoints export const checkIsMobile = () => { return window.innerWidth < 768; }; diff --git a/packages/utils/src/calendar.test.ts b/packages/utils/src/calendar.test.ts new file mode 100644 index 0000000..6013db9 --- /dev/null +++ b/packages/utils/src/calendar.test.ts @@ -0,0 +1,278 @@ +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect } from 'vitest'; + +import { + hourID, + parseHourID, + dayID, + parseDayID, + weekID, + parseWeekID, + monthID, + parseMonthID, + yearID, + parseYearID, + getPlainDateIds, +} from './calendar'; + +describe('calendar', () => { + describe('hourID', () => { + it('should generate correct hour IDs for various cases', () => { + const testCases = [ + { + input: new Temporal.PlainDateTime(2025, 1, 5, 9), + expected: '2025_01_05_09', + description: 'hourID single digit values', + }, + { + input: new Temporal.PlainDateTime(2025, 12, 25, 23), + expected: '2025_12_25_23', + description: 'hourID double digit values', + }, + { + input: new Temporal.PlainDateTime(2000, 1, 1, 0), + expected: '2000_01_01_0', + description: 'leading zero hour', + }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(hourID(input)).toBe(expected); + }); + }); + }); + + describe('dayID', () => { + it('should generate correct day IDs for various cases', () => { + const testCases = [ + { input: new Temporal.PlainDate(2025, 1, 5), expected: '2025_01_05', description: 'dayID single digit values' }, + { + input: new Temporal.PlainDate(2025, 12, 25), + expected: '2025_12_25', + description: 'dayID double digit values', + }, + { input: new Temporal.PlainDate(2000, 1, 1), expected: '2000_01_01', description: 'leading zero day' }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(dayID(input)).toBe(expected); + }); + }); + }); + + describe('weekID', () => { + it('should generate correct week IDs for various cases', () => { + const testCases = [ + { input: new Temporal.PlainDate(2025, 1, 1), expected: '2024_12_30', description: 'Wednesday (start of week)' }, + { input: new Temporal.PlainDate(2025, 1, 7), expected: '2025_01_06', description: 'Tuesday (same week)' }, + { input: new Temporal.PlainDate(2025, 1, 3), expected: '2024_12_30', description: 'Friday (same week)' }, + { + input: new Temporal.PlainDate(2025, 1, 31), + expected: '2025_01_27', + description: 'week spanning across months', + }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(weekID(input)).toBe(expected); + }); + }); + }); + + describe('monthID', () => { + it('should generate correct month IDs for various cases', () => { + const testCases = [ + { + input: new Temporal.PlainYearMonth(2025, 1), + expected: '2025_01', + description: 'monthID single digit values', + }, + { + input: new Temporal.PlainYearMonth(2025, 12), + expected: '2025_12', + description: 'monthID double digit values', + }, + { input: new Temporal.PlainYearMonth(2000, 1), expected: '2000_01', description: 'leading zero month' }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(monthID(input)).toBe(expected); + }); + }); + }); + + describe('yearID', () => { + it('should generate correct year IDs for various cases', () => { + const testCases = [ + { input: '2025', expected: '2025', description: 'string year' }, + { input: '2000', expected: '2000', description: 'string year 2000' }, + { input: 2025, expected: '2025', description: 'number year' }, + { input: 2000, expected: '2000', description: 'number year 2000' }, + { input: 0, expected: '0', description: 'zero year' }, + { input: '0', expected: '0', description: 'zero string year' }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(yearID(input)).toBe(expected); + }); + }); + }); + + describe('getPlainDateIds', () => { + it('should return correct IDs for various dates', () => { + const testCases = [ + { + input: new Temporal.PlainDate(2025, 1, 5), + expected: ['2025', '2025_01', '2025_01_05'], + description: 'getPlainDateIds single digit values', + }, + { + input: new Temporal.PlainDate(2025, 12, 25), + expected: ['2025', '2025_12', '2025_12_25'], + description: 'getPlainDateIds double digit values', + }, + { + input: new Temporal.PlainDate(2000, 1, 1), + expected: ['2000', '2000_01', '2000_01_01'], + description: 'year 2000', + }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(getPlainDateIds(input)).toEqual(expected); + }); + }); + }); + + describe('parseHourID', () => { + it('should parse hour IDs correctly for various cases', () => { + const testCases = [ + { + input: '2025_01_05_09', + expected: { year: 2025, month: 1, day: 5, hour: 9 }, + description: 'parseHourID single digit values', + }, + { + input: '2025_12_25_23', + expected: { year: 2025, month: 12, day: 25, hour: 23 }, + description: 'parseHourID double digit values', + }, + { + input: '2000_01_01_00', + expected: { year: 2000, month: 1, day: 1, hour: 0 }, + description: 'leading zero hour', + }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = parseHourID(input); + + expect(result.year).toBe(expected.year); + expect(result.month).toBe(expected.month); + expect(result.day).toBe(expected.day); + expect(result.hour).toBe(expected.hour); + }); + }); + }); + + describe('parseDayID', () => { + it('should parse day IDs correctly for various cases', () => { + const testCases = [ + { input: '2025_01_05', expected: { year: 2025, month: 1, day: 5 }, description: 'single digit values' }, + { input: '2025_12_25', expected: { year: 2025, month: 12, day: 25 }, description: 'double digit values' }, + { input: '2000_01_01', expected: { year: 2000, month: 1, day: 1 }, description: 'leading zero day' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = parseDayID(input); + + expect(result.year).toBe(expected.year); + expect(result.month).toBe(expected.month); + expect(result.day).toBe(expected.day); + }); + }); + }); + + describe('parseWeekID', () => { + it('should parse week IDs correctly for various cases', () => { + const testCases = [ + { input: '2025_01_01', expected: { year: 2025, month: 1, day: 1 }, description: 'single digit values' }, + { input: '2025_12_25', expected: { year: 2025, month: 12, day: 25 }, description: 'double digit values' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = parseWeekID(input); + + expect(result.year).toBe(expected.year); + expect(result.month).toBe(expected.month); + expect(result.day).toBe(expected.day); + }); + }); + }); + + describe('parseMonthID', () => { + it('should parse month IDs correctly for various cases', () => { + const testCases = [ + { input: '2025_01', expected: { year: 2025, month: 1 }, description: 'single digit month' }, + { input: '2025_12', expected: { year: 2025, month: 12 }, description: 'double digit month' }, + { input: '2000_01', expected: { year: 2000, month: 1 }, description: 'leading zero month' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = parseMonthID(input); + + expect(result.year).toBe(expected.year); + expect(result.month).toBe(expected.month); + }); + }); + }); + + describe('parseYearID', () => { + it('should parse year IDs correctly for various cases', () => { + const testCases = [ + { input: '2025', expected: 2025, description: 'normal year' }, + { input: '2000', expected: 2000, description: 'year 2000' }, + { input: '0', expected: 0, description: 'zero year' }, + { input: '1', expected: 1, description: 'single digit year' }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(parseYearID(input)).toBe(expected); + }); + }); + }); + + describe('round-trip tests', () => { + it('should maintain consistency for hourID/parseHourID', () => { + const originalDateTime = new Temporal.PlainDateTime(2025, 1, 5, 9); + const hourId = hourID(originalDateTime); + const parsedDateTime = parseHourID(hourId); + + expect(parsedDateTime.equals(originalDateTime)).toBe(true); + }); + + it('should maintain consistency for dayID/parseDayID', () => { + const originalDate = new Temporal.PlainDate(2025, 1, 5); + const dayId = dayID(originalDate); + const parsedDate = parseDayID(dayId); + + expect(parsedDate.equals(originalDate)).toBe(true); + }); + + it('should maintain consistency for monthID/parseMonthID', () => { + const originalYearMonth = new Temporal.PlainYearMonth(2025, 1); + const monthId = monthID(originalYearMonth); + const parsedYearMonth = parseMonthID(monthId); + + expect(parsedYearMonth.equals(originalYearMonth)).toBe(true); + }); + + it('should maintain consistency for yearID/parseYearID', () => { + const originalYear = 2025; + const yearId = yearID(originalYear); + const parsedYear = parseYearID(yearId); + + expect(parsedYear).toBe(originalYear); + }); + }); +}); diff --git a/packages/utils/src/dates.test.ts b/packages/utils/src/dates.test.ts new file mode 100644 index 0000000..473e3aa --- /dev/null +++ b/packages/utils/src/dates.test.ts @@ -0,0 +1,142 @@ +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect } from 'vitest'; + +import { getDefaultEndTime } from './dates'; + +describe('dates', () => { + describe('getDefaultEndTime', () => { + it('should add 30 minutes by default', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 0); + const result = getDefaultEndTime(startDate); + + expect(result.hour).toBe(10); + expect(result.minute).toBe(30); + expect(result.day).toBe(15); + }); + + it('should add custom duration in minutes', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 0); + const result = getDefaultEndTime(startDate, 60); + + expect(result.hour).toBe(11); + expect(result.minute).toBe(0); + expect(result.day).toBe(15); + }); + + it('should handle hour transition', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 45); + const result = getDefaultEndTime(startDate, 30); + + expect(result.hour).toBe(11); + expect(result.minute).toBe(15); + expect(result.day).toBe(15); + }); + + it('should handle day transition', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 23, 45); + const result = getDefaultEndTime(startDate, 30); + + expect(result.hour).toBe(0); + expect(result.minute).toBe(15); + expect(result.day).toBe(16); + }); + + it('should handle month transition', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 31, 23, 45); + const result = getDefaultEndTime(startDate, 30); + + expect(result.hour).toBe(0); + expect(result.minute).toBe(15); + expect(result.day).toBe(1); + expect(result.month).toBe(2); + }); + + it('should handle year transition', () => { + const startDate = new Temporal.PlainDateTime(2023, 12, 31, 23, 45); + const result = getDefaultEndTime(startDate, 30); + + expect(result.hour).toBe(0); + expect(result.minute).toBe(15); + expect(result.day).toBe(1); + expect(result.month).toBe(1); + expect(result.year).toBe(2024); + }); + + it('should handle leap year transition', () => { + const startDate = new Temporal.PlainDateTime(2024, 2, 29, 23, 45); + const result = getDefaultEndTime(startDate, 30); + + expect(result.hour).toBe(0); + expect(result.minute).toBe(15); + expect(result.day).toBe(1); + expect(result.month).toBe(3); + }); + + it('should preserve other date components', () => { + const startDate = new Temporal.PlainDateTime(2025, 6, 15, 14, 25); + const result = getDefaultEndTime(startDate, 30); + + expect(result.year).toBe(2025); + expect(result.month).toBe(6); + expect(result.day).toBe(15); + expect(result.hour).toBe(14); + expect(result.minute).toBe(55); + expect(result.second).toBe(0); + expect(result.nanosecond).toBe(0); + }); + + it('should handle zero duration', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 30); + const result = getDefaultEndTime(startDate, 0); + + expect(result.equals(startDate)).toBe(true); + }); + + it('should handle negative duration', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 30); + const result = getDefaultEndTime(startDate, -30); + + expect(result.hour).toBe(10); + expect(result.minute).toBe(0); + expect(result.day).toBe(15); + }); + + it('should handle large duration', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 0); + const result = getDefaultEndTime(startDate, 150); + + expect(result.hour).toBe(12); + expect(result.minute).toBe(30); + expect(result.day).toBe(15); + }); + + it('should handle duration spanning multiple days', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 0); + const result = getDefaultEndTime(startDate, 1440); + + expect(result.hour).toBe(10); + expect(result.minute).toBe(0); + expect(result.day).toBe(16); + }); + + it('should handle various duration values', () => { + const startDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 0); + + const testCases = [ + { duration: 15, expectedHour: 10, expectedMinute: 15 }, + { duration: 45, expectedHour: 10, expectedMinute: 45 }, + { duration: 90, expectedHour: 11, expectedMinute: 30 }, + { duration: 120, expectedHour: 12, expectedMinute: 0 }, + { duration: 180, expectedHour: 13, expectedMinute: 0 }, + ]; + + testCases.forEach(({ duration, expectedHour, expectedMinute }) => { + const result = getDefaultEndTime(startDate, duration); + + expect(result.hour).toBe(expectedHour); + expect(result.minute).toBe(expectedMinute); + expect(result.day).toBe(15); + }); + }); + }); +}); diff --git a/packages/utils/src/dates.ts b/packages/utils/src/dates.ts index b4d3edb..8f238f4 100644 --- a/packages/utils/src/dates.ts +++ b/packages/utils/src/dates.ts @@ -1,16 +1,5 @@ import { Temporal } from 'temporal-polyfill'; -export const dateToNextNearestHalfHour = (date: Temporal.PlainDateTime, minutesRound = 30): Temporal.PlainDateTime => { - const hour = date.hour; - const minute = date.minute; - - if (hour > 23 && minute > minutesRound) { - return date.add({ days: 1 }).with({ hour: 0, minute: 0 }); - } - - if (minute > minutesRound) { - return date.with({ hour: date.hour + 1, minute: 0 }); - } - - return date.with({ minute: Math.ceil(minute / minutesRound) * minutesRound }); +export const getDefaultEndTime = (startDate: Temporal.PlainDateTime, duration = 30): Temporal.PlainDateTime => { + return startDate.add({ minutes: duration }); }; diff --git a/packages/utils/src/gravity-ui.test.ts b/packages/utils/src/gravity-ui.test.ts new file mode 100644 index 0000000..cdbece8 --- /dev/null +++ b/packages/utils/src/gravity-ui.test.ts @@ -0,0 +1,221 @@ +import { dateTime } from '@gravity-ui/date-utils'; +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect } from 'vitest'; + +import { temporalToGravityDate, gravityDateToTemporal } from './gravity-ui'; + +const TEST_DATE_STRING = '2025-01-15T10:30:45'; + +describe('gravity-ui', () => { + describe('temporalToGravityDate', () => { + it('should convert Temporal.PlainDateTime to Gravity DateTime', () => { + const temporalDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 45); + const result = temporalToGravityDate(temporalDate); + + expect(result).toBeInstanceOf(Object); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(0); // Gravity UI uses 0-based months + expect(result.date()).toBe(15); + expect(result.hour()).toBe(10); + expect(result.minute()).toBe(30); + expect(result.second()).toBe(45); + }); + + it('should handle single digit values', () => { + const temporalDate = new Temporal.PlainDateTime(2025, 1, 5, 9, 5, 5); + const result = temporalToGravityDate(temporalDate); + + expect(result.year()).toBe(2025); + expect(result.month()).toBe(0); // January = 0 + expect(result.date()).toBe(5); + expect(result.hour()).toBe(9); + expect(result.minute()).toBe(5); + expect(result.second()).toBe(5); + }); + + it('should handle double digit values', () => { + const temporalDate = new Temporal.PlainDateTime(2025, 12, 25, 23, 59, 59); + const result = temporalToGravityDate(temporalDate); + + expect(result.year()).toBe(2025); + expect(result.month()).toBe(11); // December = 11 + expect(result.date()).toBe(25); + expect(result.hour()).toBe(23); + expect(result.minute()).toBe(59); + expect(result.second()).toBe(59); + }); + + it('should handle edge cases', () => { + const temporalDate = new Temporal.PlainDateTime(2000, 1, 1, 0, 0, 0); + const result = temporalToGravityDate(temporalDate); + + expect(result.year()).toBe(2000); + expect(result.month()).toBe(0); // January = 0 + expect(result.date()).toBe(1); + expect(result.hour()).toBe(0); + expect(result.minute()).toBe(0); + expect(result.second()).toBe(0); + }); + + it('should handle leap year', () => { + const temporalDate = new Temporal.PlainDateTime(2024, 2, 29, 12, 0, 0); + const result = temporalToGravityDate(temporalDate); + + expect(result.year()).toBe(2024); + expect(result.month()).toBe(1); // February = 1 + expect(result.date()).toBe(29); + expect(result.hour()).toBe(12); + expect(result.minute()).toBe(0); + expect(result.second()).toBe(0); + }); + + it('should map date-time fields correctly', () => { + const temporalDate = new Temporal.PlainDateTime(2025, 6, 15, 14, 30, 0); + const result = temporalToGravityDate(temporalDate); + + expect(result.year()).toBe(2025); + expect(result.month()).toBe(5); // June = 5 + expect(result.date()).toBe(15); + expect(result.hour()).toBe(14); + expect(result.minute()).toBe(30); + expect(result.second()).toBe(0); + }); + }); + + describe('gravityDateToTemporal', () => { + it('should convert Gravity DateTime to Temporal.PlainDateTime', () => { + const gravityDate = dateTime({ input: TEST_DATE_STRING }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2025); + expect(result.month).toBe(1); + expect(result.day).toBe(15); + expect(result.hour).toBe(10); + expect(result.minute).toBe(30); + expect(result.second).toBe(45); + }); + + it('should handle single digit values', () => { + const gravityDate = dateTime({ input: '2025-01-05T09:05:05' }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2025); + expect(result.month).toBe(1); + expect(result.day).toBe(5); + expect(result.hour).toBe(9); + expect(result.minute).toBe(5); + expect(result.second).toBe(5); + }); + + it('should handle double digit values', () => { + const gravityDate = dateTime({ input: '2025-12-25T23:59:59' }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2025); + expect(result.month).toBe(12); + expect(result.day).toBe(25); + expect(result.hour).toBe(23); + expect(result.minute).toBe(59); + expect(result.second).toBe(59); + }); + + it('should handle edge cases', () => { + const gravityDate = dateTime({ input: '2000-01-01T00:00:00' }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2000); + expect(result.month).toBe(1); + expect(result.day).toBe(1); + expect(result.hour).toBe(0); + expect(result.minute).toBe(0); + expect(result.second).toBe(0); + }); + + it('should handle leap year', () => { + const gravityDate = dateTime({ input: '2024-02-29T12:00:00' }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2024); + expect(result.month).toBe(2); + expect(result.day).toBe(29); + expect(result.hour).toBe(12); + expect(result.minute).toBe(0); + expect(result.second).toBe(0); + }); + + it('should handle various date formats', () => { + const testCases = [TEST_DATE_STRING, '2025-01-15T10:30:45.000', '2025-01-15T10:30:45.123']; + + testCases.forEach((input) => { + const gravityDate = dateTime({ input }); + const result = gravityDateToTemporal(gravityDate); + + expect(result.year).toBe(2025); + expect(result.month).toBe(1); + expect(result.day).toBe(15); + expect(result.hour).toBe(10); + expect(result.minute).toBe(30); + expect(result.second).toBe(45); + }); + }); + }); + + describe('round-trip tests', () => { + it('should maintain consistency for temporalToGravityDate -> gravityDateToTemporal', () => { + const originalTemporal = new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 45); + const gravityDate = temporalToGravityDate(originalTemporal); + const convertedTemporal = gravityDateToTemporal(gravityDate); + + expect(convertedTemporal.equals(originalTemporal)).toBe(true); + }); + + it('should maintain consistency for gravityDateToTemporal -> temporalToGravityDate', () => { + const originalGravity = dateTime({ input: TEST_DATE_STRING }); + const temporalDate = gravityDateToTemporal(originalGravity); + const convertedGravity = temporalToGravityDate(temporalDate); + + expect(convertedGravity.year()).toBe(originalGravity.year()); + expect(convertedGravity.month()).toBe(originalGravity.month()); + expect(convertedGravity.date()).toBe(originalGravity.date()); + expect(convertedGravity.hour()).toBe(originalGravity.hour()); + expect(convertedGravity.minute()).toBe(originalGravity.minute()); + expect(convertedGravity.second()).toBe(originalGravity.second()); + }); + + it('should handle various date ranges', () => { + const testDates = [ + new Temporal.PlainDateTime(2000, 1, 1, 0, 0, 0), + new Temporal.PlainDateTime(2025, 6, 15, 12, 30, 45), + new Temporal.PlainDateTime(2030, 12, 31, 23, 59, 59), + ]; + + testDates.forEach((temporalDate) => { + const gravityDate = temporalToGravityDate(temporalDate); + const convertedTemporal = gravityDateToTemporal(gravityDate); + + expect(convertedTemporal.equals(temporalDate)).toBe(true); + }); + }); + + it('should handle leap year dates', () => { + const leapYearDate = new Temporal.PlainDateTime(2024, 2, 29, 12, 0, 0); + const gravityDate = temporalToGravityDate(leapYearDate); + const convertedTemporal = gravityDateToTemporal(gravityDate); + + expect(convertedTemporal.equals(leapYearDate)).toBe(true); + }); + + it('should handle edge cases with nanoseconds', () => { + const temporalDate = new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 45, 123, 456, 789); + const gravityDate = temporalToGravityDate(temporalDate); + const convertedTemporal = gravityDateToTemporal(gravityDate); + + expect(convertedTemporal.year).toBe(temporalDate.year); + expect(convertedTemporal.month).toBe(temporalDate.month); + expect(convertedTemporal.day).toBe(temporalDate.day); + expect(convertedTemporal.hour).toBe(temporalDate.hour); + expect(convertedTemporal.minute).toBe(temporalDate.minute); + expect(convertedTemporal.second).toBe(temporalDate.second); + }); + }); +}); diff --git a/packages/utils/src/leading-zero.test.ts b/packages/utils/src/leading-zero.test.ts new file mode 100644 index 0000000..9eaacf7 --- /dev/null +++ b/packages/utils/src/leading-zero.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; + +import { leadingZero } from './leading-zero'; + +describe('leadingZero', () => { + it('should add leading zero for single digit numbers', () => { + expect(leadingZero(1)).toBe('01'); + expect(leadingZero(2)).toBe('02'); + expect(leadingZero(3)).toBe('03'); + expect(leadingZero(4)).toBe('04'); + expect(leadingZero(5)).toBe('05'); + expect(leadingZero(6)).toBe('06'); + expect(leadingZero(7)).toBe('07'); + expect(leadingZero(8)).toBe('08'); + expect(leadingZero(9)).toBe('09'); + }); + + it('should not add leading zero for double digit numbers', () => { + expect(leadingZero(10)).toBe('10'); + expect(leadingZero(11)).toBe('11'); + expect(leadingZero(25)).toBe('25'); + expect(leadingZero(99)).toBe('99'); + expect(leadingZero(100)).toBe('100'); + }); + + it('should not add leading zero for negative numbers', () => { + expect(leadingZero(-1)).toBe('-1'); + expect(leadingZero(-5)).toBe('-5'); + expect(leadingZero(-10)).toBe('-10'); + }); + + it('should handle edge cases', () => { + expect(leadingZero(0)).toBe('0'); + expect(leadingZero(9)).toBe('09'); + expect(leadingZero(10)).toBe('10'); + }); +}); diff --git a/packages/utils/src/leading-zero.ts b/packages/utils/src/leading-zero.ts index 9ee7b8e..946477a 100644 --- a/packages/utils/src/leading-zero.ts +++ b/packages/utils/src/leading-zero.ts @@ -1,3 +1,7 @@ export const leadingZero = (value: number): string => { + if (value <= 0) { + return `${value}`; + } + return value < 10 ? `0${value}` : `${value}`; }; diff --git a/packages/utils/src/timezone.test.ts b/packages/utils/src/timezone.test.ts new file mode 100644 index 0000000..5afb373 --- /dev/null +++ b/packages/utils/src/timezone.test.ts @@ -0,0 +1,137 @@ +import { Temporal } from 'temporal-polyfill'; +import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; + +import { localDateTimeToUTC, utcStringToLocalDateTime } from './timezone'; + +const TEST_DATE_TIME = '2025-01-15T10:30:45Z'; +const TEST_DATE_TIME_MS = '2025-01-15T10:30:45.000Z'; +const TEST_DATE_TIME_NS = '2025-01-15T10:30:45.123Z'; +const TEST_DATETIME_2025_1_15 = new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 45); +const TIMEZONE_UTC = 'UTC'; +const TIMEZONE_MOSCOW = 'Europe/Moscow'; +const TIMEZONE_NEW_YORK = 'America/New_York'; +const TIMEZONE_TOKYO = 'Asia/Tokyo'; + +const mockResolvedOptions = vi.fn(); +const originalIntl = global.Intl; + +Object.defineProperty(global, 'Intl', { + value: { + DateTimeFormat: vi.fn(() => ({ + resolvedOptions: mockResolvedOptions, + })), + }, + writable: true, +}); + +describe('timezone', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockResolvedOptions.mockReturnValue({ timeZone: TIMEZONE_UTC }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + afterAll(() => { + Object.defineProperty(global, 'Intl', { value: originalIntl }); + }); + + describe('localDateTimeToUTC', () => { + it('should convert local datetime to UTC', () => { + const result = localDateTimeToUTC(TEST_DATETIME_2025_1_15); + + expect(result).toBeInstanceOf(Temporal.Instant); + }); + }); + + describe('utcStringToLocalDateTime', () => { + it('should convert UTC string to local datetime', () => { + const result = utcStringToLocalDateTime(TEST_DATE_TIME); + + expect(result).toBeInstanceOf(Temporal.PlainDateTime); + expect(result.year).toBe(2025); + expect(result.month).toBe(1); + expect(result.day).toBe(15); + }); + + it('should handle different timezones correctly', () => { + const timezoneTests = [ + { timezone: TIMEZONE_UTC, expectedHour: 10, description: 'UTC' }, + { timezone: TIMEZONE_MOSCOW, expectedHour: 13, description: 'Moscow (UTC+3)' }, + { timezone: TIMEZONE_NEW_YORK, expectedHour: 5, description: 'New York (UTC-5)' }, + { timezone: TIMEZONE_TOKYO, expectedHour: 19, description: 'Tokyo (UTC+9)' }, + ]; + + timezoneTests.forEach(({ timezone, expectedHour }) => { + mockResolvedOptions.mockReturnValue({ timeZone: timezone }); + + const result = utcStringToLocalDateTime(TEST_DATE_TIME); + + expect(result).toBeInstanceOf(Temporal.PlainDateTime); + expect(result.hour).toBe(expectedHour); + expect(result.minute).toBe(30); + expect(result.second).toBe(45); + }); + }); + + it('should handle various UTC string formats', () => { + const testCases = [TEST_DATE_TIME, TEST_DATE_TIME_MS, TEST_DATE_TIME_NS]; + + testCases.forEach((utcString) => { + const result = utcStringToLocalDateTime(utcString); + + expect(result).toBeInstanceOf(Temporal.PlainDateTime); + expect(result.year).toBe(2025); + expect(result.month).toBe(1); + expect(result.day).toBe(15); + expect(result.hour).toBe(10); + expect(result.minute).toBe(30); + expect(result.second).toBe(45); + }); + }); + + it('should handle edge cases and special dates', () => { + const edgeCases = [ + { input: '2000-01-01T00:00:00Z', year: 2000, month: 1, day: 1, description: 'Y2K' }, + { input: '2024-02-29T12:00:00Z', year: 2024, month: 2, day: 29, description: 'leap year' }, + { input: '2023-12-31T23:59:59Z', year: 2023, month: 12, day: 31, description: 'year transition' }, + ]; + + edgeCases.forEach(({ input, year, month, day }) => { + const result = utcStringToLocalDateTime(input); + + expect(result).toBeInstanceOf(Temporal.PlainDateTime); + expect(result.year).toBe(year); + expect(result.month).toBe(month); + expect(result.day).toBe(day); + }); + }); + }); + + describe('round-trip tests', () => { + it('should maintain consistency between conversions', () => { + const testCases = [ + { date: new Temporal.PlainDateTime(2025, 1, 15, 10, 30, 45), description: 'normal date' }, + { date: new Temporal.PlainDateTime(2000, 1, 1, 0, 0, 0), description: 'Y2K' }, + { date: new Temporal.PlainDateTime(2024, 2, 29, 12, 0, 0), description: 'leap year' }, + { date: new Temporal.PlainDateTime(2023, 12, 31, 23, 59, 59), description: 'year transition' }, + ]; + + const timezones = [TIMEZONE_UTC, TIMEZONE_MOSCOW, TIMEZONE_NEW_YORK, TIMEZONE_TOKYO]; + + testCases.forEach(({ date }) => { + timezones.forEach((timezone) => { + mockResolvedOptions.mockReturnValue({ timeZone: timezone }); + + const utcInstant = localDateTimeToUTC(date); + const utcString = utcInstant.toString(); + const convertedDateTime = utcStringToLocalDateTime(utcString); + + expect(convertedDateTime.equals(date)).toBe(true); + }); + }); + }); + }); +}); diff --git a/packages/utils/src/week-start.test.ts b/packages/utils/src/week-start.test.ts new file mode 100644 index 0000000..0092621 --- /dev/null +++ b/packages/utils/src/week-start.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; + +import { weekStart } from './week-start'; + +const mockWeekInfo = vi.fn(); +const originalIntl = global.Intl; + +Object.defineProperty(global, 'Intl', { + value: { + Locale: vi.fn(() => ({ + weekInfo: mockWeekInfo(), + })), + }, + writable: true, +}); + +describe('week-start', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + afterAll(() => { + Object.defineProperty(global, 'Intl', { value: originalIntl, writable: true }); + }); + + describe('weekStart', () => { + it('should return correct week start for Russian locale', () => { + mockWeekInfo.mockReturnValue({ firstDay: 1 }); + + const result = weekStart(); + + expect(result).toBe(1); + }); + + it('should return correct week start for different locales', () => { + const testCases = [ + { locale: 'en-US', expectedFirstDay: 0, description: 'US locale (Sunday)' }, + { locale: 'en-GB', expectedFirstDay: 1, description: 'UK locale (Monday)' }, + { locale: 'de-DE', expectedFirstDay: 1, description: 'German locale (Monday)' }, + { locale: 'fr-FR', expectedFirstDay: 1, description: 'French locale (Monday)' }, + { locale: 'ar-SA', expectedFirstDay: 0, description: 'Saudi locale (Sunday)' }, + ]; + + testCases.forEach(({ expectedFirstDay }) => { + mockWeekInfo.mockReturnValue({ firstDay: expectedFirstDay }); + + const result = weekStart(); + + expect(result).toBe(expectedFirstDay); + }); + }); + + it('should handle fallback cases when weekInfo is unavailable', () => { + const fallbackCases = [ + { weekInfo: undefined, description: 'missing weekInfo' }, + { weekInfo: null, description: 'null weekInfo' }, + { weekInfo: {}, description: 'empty weekInfo object' }, + { weekInfo: { firstDay: null }, description: 'null firstDay' }, + { weekInfo: { firstDay: undefined }, description: 'undefined firstDay' }, + ]; + + fallbackCases.forEach(({ weekInfo }) => { + mockWeekInfo.mockReturnValue(weekInfo); + + const result = weekStart(); + + expect(result).toBe(1); + }); + }); + + it('should handle various firstDay values', () => { + const testCases = [ + { firstDay: 0, description: 'Sunday' }, + { firstDay: 1, description: 'Monday' }, + { firstDay: 2, description: 'Tuesday' }, + { firstDay: 3, description: 'Wednesday' }, + { firstDay: 4, description: 'Thursday' }, + { firstDay: 5, description: 'Friday' }, + { firstDay: 6, description: 'Saturday' }, + ]; + + testCases.forEach(({ firstDay }) => { + mockWeekInfo.mockReturnValue({ firstDay }); + + const result = weekStart(); + + expect(result).toBe(firstDay); + }); + }); + + it('should handle edge cases and complex objects', () => { + const testCases = [ + { weekInfo: { firstDay: -1 }, expected: -1, description: 'negative firstDay' }, + { weekInfo: { firstDay: 7 }, expected: 7, description: 'firstDay > 6' }, + { weekInfo: { firstDay: 0.5 }, expected: 0.5, description: 'decimal firstDay' }, + { + weekInfo: { firstDay: 1, minimalDays: 4, weekend: [6, 0] }, + expected: 1, + description: 'complex weekInfo object', + }, + ]; + + testCases.forEach(({ weekInfo, expected }) => { + mockWeekInfo.mockReturnValue(weekInfo); + + const result = weekStart(); + + expect(result).toBe(expected); + }); + }); + }); +}); diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 89fe85b..dc72b8c 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -3,7 +3,8 @@ "extends": "@repo/typescript-config/base.json", "include": [ "src/**/*", - "tsup.config.ts" + "tsup.config.ts", + "vitest.config.ts" ], "exclude": [ "dist", diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts new file mode 100644 index 0000000..5dc6350 --- /dev/null +++ b/packages/utils/vitest.config.ts @@ -0,0 +1,12 @@ +/// +import { resolve } from 'path'; + +import { createNodeVitestConfig } from '@repo/vitest-config'; + +export default createNodeVitestConfig( + {}, + { + '@repo/types': resolve(__dirname, '../types/src'), + '@repo/types/*': resolve(__dirname, '../types/src/*'), + }, +); diff --git a/turbo.json b/turbo.json index a821983..c2956e7 100644 --- a/turbo.json +++ b/turbo.json @@ -55,6 +55,16 @@ "stylelint:fix", "^codestyle:fix" ] + }, + "test": { + "outputs": [] + }, + "test:watch": { + "cache": false, + "persistent": true + }, + "test:coverage": { + "outputs": ["coverage/**"] } } } diff --git a/yarn.lock b/yarn.lock index 2429f04..81510f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 8 cacheKey: 10c0 +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.4 + resolution: "@adobe/css-tools@npm:4.4.4" + checksum: 10c0/8f3e6cfaa5e6286e6f05de01d91d060425be2ebaef490881f5fe6da8bbdb336835c5d373ea337b0c3b0a1af4be048ba18780f0f6021d30809b4545922a7e13d9 + languageName: node + linkType: hard + "@adobe/css-tools@npm:~4.3.1": version: 4.3.3 resolution: "@adobe/css-tools@npm:4.3.3" @@ -12,7 +19,30 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.27.1": +"@ampproject/remapping@npm:^2.3.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed + languageName: node + linkType: hard + +"@asamuzakjp/css-color@npm:^3.2.0": + version: 3.2.0 + resolution: "@asamuzakjp/css-color@npm:3.2.0" + dependencies: + "@csstools/css-calc": "npm:^2.1.3" + "@csstools/css-color-parser": "npm:^3.0.9" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + lru-cache: "npm:^10.4.3" + checksum: 10c0/a4bf1c831751b1fae46b437e37e8a38c0b5bd58d23230157ae210bd1e905fe509b89b7c243e63d1522d852668a6292ed730a160e21342772b4e5b7b8ea14c092 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" dependencies: @@ -147,7 +177,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": version: 7.28.4 resolution: "@babel/parser@npm:7.28.4" dependencies: @@ -180,7 +210,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 @@ -213,7 +243,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4": version: 7.28.4 resolution: "@babel/types@npm:7.28.4" dependencies: @@ -223,6 +253,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + "@bem-react/classname@npm:^1.6.0": version: 1.7.0 resolution: "@bem-react/classname@npm:1.7.0" @@ -230,7 +267,37 @@ __metadata: languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^3.0.1, @csstools/css-parser-algorithms@npm:^3.0.5": +"@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.3, @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.0.9": + 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.1, @csstools/css-parser-algorithms@npm:^3.0.4, @csstools/css-parser-algorithms@npm:^3.0.5": version: 3.0.5 resolution: "@csstools/css-parser-algorithms@npm:3.0.5" peerDependencies: @@ -239,7 +306,7 @@ __metadata: languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.4": +"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.3, @csstools/css-tokenizer@npm:^3.0.4": version: 3.0.4 resolution: "@csstools/css-tokenizer@npm:3.0.4" checksum: 10c0/3b589f8e9942075a642213b389bab75a2d50d05d203727fcdac6827648a5572674caff07907eff3f9a2389d86a4ee47308fafe4f8588f4a77b7167c588d2559f @@ -310,6 +377,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/aix-ppc64@npm:0.25.9" @@ -317,6 +391,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-arm64@npm:0.25.9" @@ -324,6 +405,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-arm@npm:0.25.9" @@ -331,6 +419,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-x64@npm:0.25.9" @@ -338,6 +433,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/darwin-arm64@npm:0.25.9" @@ -345,6 +447,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/darwin-x64@npm:0.25.9" @@ -352,6 +461,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/freebsd-arm64@npm:0.25.9" @@ -359,6 +475,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/freebsd-x64@npm:0.25.9" @@ -366,6 +489,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-arm64@npm:0.25.9" @@ -373,6 +503,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-arm@npm:0.25.9" @@ -380,6 +517,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-ia32@npm:0.25.9" @@ -387,6 +531,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-loong64@npm:0.25.9" @@ -394,6 +545,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-mips64el@npm:0.25.9" @@ -401,6 +559,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-ppc64@npm:0.25.9" @@ -408,6 +573,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-riscv64@npm:0.25.9" @@ -415,6 +587,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-s390x@npm:0.25.9" @@ -422,6 +601,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-x64@npm:0.25.9" @@ -436,6 +622,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/netbsd-x64@npm:0.25.9" @@ -450,6 +643,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/openbsd-x64@npm:0.25.9" @@ -464,6 +664,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/sunos-x64@npm:0.25.9" @@ -471,6 +678,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-arm64@npm:0.25.9" @@ -478,6 +692,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-ia32@npm:0.25.9" @@ -485,6 +706,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-x64@npm:0.25.9" @@ -806,6 +1034,13 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -850,7 +1085,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -1277,12 +1512,36 @@ __metadata: "@repo/types": "npm:*" "@repo/typescript-config": "npm:*" "@repo/utils": "npm:*" + "@repo/vitest-config": "npm:*" + "@vitest/coverage-v8": "npm:^2.1.8" eslint: "npm:^9.29.0" prettier: "npm:^3.5.3" stylelint: "npm:^16.20.0" temporal-polyfill: "npm:^0.3.0" tsup: "npm:^8.0.0" typescript: "npm:^5.8.3" + vitest: "npm:^2.1.8" + languageName: unknown + linkType: soft + +"@repo/react-testing-library-config@npm:*, @repo/react-testing-library-config@workspace:config/react-testing-library-config": + version: 0.0.0-use.local + resolution: "@repo/react-testing-library-config@workspace:config/react-testing-library-config" + dependencies: + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.1.0" + "@testing-library/user-event": "npm:^14.5.2" + "@types/react": "npm:^19.1.2" + "@types/react-dom": "npm:^19.1.2" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" + typescript: "npm:^5.8.3" + peerDependencies: + "@testing-library/jest-dom": ^6.6.3 + "@testing-library/react": ^16.1.0 + "@testing-library/user-event": ^14.5.2 + react: ^19.1.0 + react-dom: ^19.1.0 languageName: unknown linkType: soft @@ -1295,6 +1554,8 @@ __metadata: "@repo/types": "npm:*" "@repo/typescript-config": "npm:*" "@repo/utils": "npm:*" + "@repo/vitest-config": "npm:*" + "@vitest/coverage-v8": "npm:^2.1.8" eslint: "npm:^9.29.0" jwt-decode: "npm:^4.0.0" mobx: "npm:^6.13.7" @@ -1302,6 +1563,7 @@ __metadata: stylelint: "npm:^16.20.0" tsup: "npm:^8.0.0" typescript: "npm:^5.8.3" + vitest: "npm:^2.1.8" languageName: unknown linkType: soft @@ -1326,12 +1588,15 @@ __metadata: "@gravity-ui/date-utils": "npm:^2.5.6" "@repo/eslint-config": "npm:*" "@repo/typescript-config": "npm:*" + "@repo/vitest-config": "npm:*" + "@vitest/coverage-v8": "npm:^2.1.8" eslint: "npm:^9.29.0" prettier: "npm:^3.5.3" stylelint: "npm:^16.20.0" temporal-polyfill: "npm:^0.3.0" tsup: "npm:^8.0.0" typescript: "npm:^5.8.3" + vitest: "npm:^2.1.8" languageName: unknown linkType: soft @@ -1350,24 +1615,34 @@ __metadata: "@gravity-ui/icons": "npm:^2.13.0" "@gravity-ui/uikit": "npm:^7.16.2" "@repo/eslint-config": "npm:*" + "@repo/react-testing-library-config": "npm:*" "@repo/stylelint-config": "npm:*" "@repo/types": "npm:*" "@repo/typescript-config": "npm:*" "@repo/utils": "npm:*" + "@repo/vitest-config": "npm:*" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.1.0" + "@testing-library/user-event": "npm:^14.5.2" "@types/node": "npm:^24.0.0" "@types/react": "npm:^19.1.2" "@types/react-dom": "npm:^19.1.2" "@vitejs/plugin-react": "npm:^4.0.0" + "@vitest/coverage-v8": "npm:^2.1.8" autoprefixer: "npm:^10.4.21" clsx: "npm:^2.1.1" eslint: "npm:^9.29.0" + jsdom: "npm:^25.0.1" postcss: "npm:^8.5.5" prettier: "npm:^3.5.3" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" react-hook-form: "npm:^7.62.0" stylelint: "npm:^16.20.0" typescript: "npm:^5.8.3" vite: "npm:^6.3.5" vite-plugin-dts: "npm:^4.5.4" + vitest: "npm:^2.1.8" peerDependencies: react: ^19.1.0 react-dom: ^19.1.0 @@ -1383,12 +1658,31 @@ __metadata: "@repo/eslint-config": "npm:*" "@repo/types": "npm:*" "@repo/typescript-config": "npm:*" + "@repo/vitest-config": "npm:*" + "@vitest/coverage-v8": "npm:^2.1.8" eslint: "npm:^9.29.0" prettier: "npm:^3.5.3" stylelint: "npm:^16.20.0" temporal-polyfill: "npm:^0.3.0" tsup: "npm:^8.0.0" typescript: "npm:^5.8.3" + vitest: "npm:^2.1.8" + languageName: unknown + linkType: soft + +"@repo/vitest-config@npm:*, @repo/vitest-config@workspace:config/vitest-config": + version: 0.0.0-use.local + resolution: "@repo/vitest-config@workspace:config/vitest-config" + dependencies: + "@repo/react-testing-library-config": "npm:*" + "@testing-library/dom": "npm:^10.4.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.1.0" + "@testing-library/user-event": "npm:^14.5.2" + "@vitejs/plugin-react": "npm:^4.4.1" + jsdom: "npm:^25.0.1" + vite: "npm:^6.3.5" + vitest: "npm:^2.1.8" languageName: unknown linkType: soft @@ -1422,6 +1716,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.52.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-android-arm64@npm:4.50.1" @@ -1429,6 +1730,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-android-arm64@npm:4.52.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-darwin-arm64@npm:4.50.1" @@ -1436,6 +1744,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.52.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-darwin-x64@npm:4.50.1" @@ -1443,6 +1758,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.52.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.1" @@ -1450,6 +1772,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.52.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-freebsd-x64@npm:4.50.1" @@ -1457,6 +1786,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.52.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1" @@ -1464,6 +1800,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1" @@ -1471,6 +1814,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.52.4" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.1" @@ -1478,6 +1828,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.52.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.1" @@ -1485,6 +1842,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.52.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.52.4" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1" @@ -1499,6 +1870,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.52.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1" @@ -1506,6 +1884,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.52.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1" @@ -1513,6 +1898,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.52.4" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.1" @@ -1520,6 +1912,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.52.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.1" @@ -1527,6 +1926,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.52.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.1" @@ -1534,6 +1940,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.52.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-openharmony-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1" @@ -1541,6 +1954,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-openharmony-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.52.4" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.1" @@ -1548,6 +1968,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.52.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.1" @@ -1555,6 +1982,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.52.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.52.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.1" @@ -1562,6 +2003,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.52.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -1664,6 +2112,65 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:^10.4.0": + 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": + version: 6.9.1 + resolution: "@testing-library/jest-dom@npm:6.9.1" + dependencies: + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + picocolors: "npm:^1.1.1" + redent: "npm:^3.0.0" + checksum: 10c0/4291ebd2f0f38d14cefac142c56c337941775a5807e2a3d6f1a14c2fbd6be76a18e498ed189e95bedc97d9e8cf1738049bc76c85b5bc5e23fae7c9e10f7b3a12 + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.1.0": + version: 16.3.0 + resolution: "@testing-library/react@npm:16.3.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/3a2cb1f87c9a67e1ebbbcfd99b94b01e496fc35147be8bc5d8bf07a699c7d523a09d57ef2f7b1d91afccd1a28e21eda3b00d80187fbb51b1de01e422592d845e + languageName: node + linkType: hard + +"@testing-library/user-event@npm:^14.5.2": + version: 14.6.1 + resolution: "@testing-library/user-event@npm:14.6.1" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10c0/75fea130a52bf320d35d46ed54f3eec77e71a56911b8b69a3fe29497b0b9947b2dc80d30f04054ad4ce7f577856ae3e5397ea7dff0ef14944d3909784c7a93fe + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.0": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -1680,6 +2187,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/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -2105,35 +2619,142 @@ __metadata: languageName: node linkType: hard -"@vis.gl/react-maplibre@npm:8.0.4": - version: 8.0.4 - resolution: "@vis.gl/react-maplibre@npm:8.0.4" +"@vis.gl/react-maplibre@npm:8.0.4": + version: 8.0.4 + resolution: "@vis.gl/react-maplibre@npm:8.0.4" + dependencies: + "@maplibre/maplibre-gl-style-spec": "npm:^19.2.1" + peerDependencies: + maplibre-gl: ">=4.0.0" + react: ">=16.3.0" + react-dom: ">=16.3.0" + peerDependenciesMeta: + maplibre-gl: + optional: true + checksum: 10c0/3a2059a30210b0bf27a07517495c3abd7803ab2700bc0f9cef851b19c18300f3e1225fdfe3d22e0cf39c8ba10e9f86ea47d0aca6ff637e266566ca04dfb089bb + languageName: node + linkType: hard + +"@vitejs/plugin-react@npm:^4.0.0, @vitejs/plugin-react@npm:^4.4.1": + version: 4.7.0 + resolution: "@vitejs/plugin-react@npm:4.7.0" + dependencies: + "@babel/core": "npm:^7.28.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-beta.27" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.17.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/692f23960972879485d647713663ec299c478222c96567d60285acf7c7dc5c178e71abfe9d2eefddef1eeb01514dacbc2ed68aad84628debf9c7116134734253 + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:^2.1.8": + version: 2.1.9 + resolution: "@vitest/coverage-v8@npm:2.1.9" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.7" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.12" + magicast: "npm:^0.3.5" + std-env: "npm:^3.8.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + "@vitest/browser": 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/ccf5871954a630453af9393e84ff40a0f8a4515e988ea32c7ebac5db7c79f17535a12c1c2567cbb78ea01a1eb99abdde94e297f6b6ccd5f7f7fc9b8b01c5963c + languageName: node + linkType: hard + +"@vitest/expect@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/expect@npm:2.1.9" + dependencies: + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/98d1cf02917316bebef9e4720723e38298a1c12b3c8f3a81f259bb822de4288edf594e69ff64f0b88afbda6d04d7a4f0c2f720f3fec16b4c45f5e2669f09fdbb + languageName: node + linkType: hard + +"@vitest/mocker@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/mocker@npm:2.1.9" dependencies: - "@maplibre/maplibre-gl-style-spec": "npm:^19.2.1" + "@vitest/spy": "npm:2.1.9" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.12" peerDependencies: - maplibre-gl: ">=4.0.0" - react: ">=16.3.0" - react-dom: ">=16.3.0" + msw: ^2.4.9 + vite: ^5.0.0 peerDependenciesMeta: - maplibre-gl: + msw: optional: true - checksum: 10c0/3a2059a30210b0bf27a07517495c3abd7803ab2700bc0f9cef851b19c18300f3e1225fdfe3d22e0cf39c8ba10e9f86ea47d0aca6ff637e266566ca04dfb089bb + vite: + optional: true + checksum: 10c0/f734490d8d1206a7f44dfdfca459282f5921d73efa72935bb1dc45307578defd38a4131b14853316373ec364cbe910dbc74594ed4137e0da35aa4d9bb716f190 languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.0.0, @vitejs/plugin-react@npm:^4.4.1": - version: 4.7.0 - resolution: "@vitejs/plugin-react@npm:4.7.0" +"@vitest/pretty-format@npm:2.1.9, @vitest/pretty-format@npm:^2.1.9": + version: 2.1.9 + resolution: "@vitest/pretty-format@npm:2.1.9" dependencies: - "@babel/core": "npm:^7.28.0" - "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" - "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" - "@rolldown/pluginutils": "npm:1.0.0-beta.27" - "@types/babel__core": "npm:^7.20.5" - react-refresh: "npm:^0.17.0" - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10c0/692f23960972879485d647713663ec299c478222c96567d60285acf7c7dc5c178e71abfe9d2eefddef1eeb01514dacbc2ed68aad84628debf9c7116134734253 + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/155f9ede5090eabed2a73361094bb35ed4ec6769ae3546d2a2af139166569aec41bb80e031c25ff2da22b71dd4ed51e5468e66a05e6aeda5f14b32e30bc18f00 + languageName: node + linkType: hard + +"@vitest/runner@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/runner@npm:2.1.9" + dependencies: + "@vitest/utils": "npm:2.1.9" + pathe: "npm:^1.1.2" + checksum: 10c0/e81f176badb12a815cbbd9bd97e19f7437a0b64e8934d680024b0f768d8670d59cad698ef0e3dada5241b6731d77a7bb3cd2c7cb29f751fd4dd35eb11c42963a + languageName: node + linkType: hard + +"@vitest/snapshot@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/snapshot@npm:2.1.9" + dependencies: + "@vitest/pretty-format": "npm:2.1.9" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + checksum: 10c0/394974b3a1fe96186a3c87f933b2f7f1f7b7cc42f9c781d80271dbb4c987809bf035fecd7398b8a3a2d54169e3ecb49655e38a0131d0e7fea5ce88960613b526 + languageName: node + linkType: hard + +"@vitest/spy@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/spy@npm:2.1.9" + dependencies: + tinyspy: "npm:^3.0.2" + checksum: 10c0/12a59b5095e20188b819a1d797e0a513d991b4e6a57db679927c43b362a3eff52d823b34e855a6dd9e73c9fa138dcc5ef52210841a93db5cbf047957a60ca83c + languageName: node + linkType: hard + +"@vitest/utils@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/utils@npm:2.1.9" + dependencies: + "@vitest/pretty-format": "npm:2.1.9" + loupe: "npm:^3.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/81a346cd72b47941f55411f5df4cc230e5f740d1e97e0d3f771b27f007266fc1f28d0438582f6409ea571bc0030ed37f684c64c58d1947d6298d770c21026fdf languageName: node linkType: hard @@ -2370,6 +2991,13 @@ __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 + "ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" @@ -2400,6 +3028,22 @@ __metadata: 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": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e + languageName: node + linkType: hard + "arr-union@npm:^3.1.0": version: 3.1.0 resolution: "arr-union@npm:3.1.0" @@ -2521,6 +3165,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "assign-symbols@npm:^1.0.0": version: 1.0.0 resolution: "assign-symbols@npm:1.0.0" @@ -2542,6 +3193,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + "autoprefixer@npm:^10.4.13, autoprefixer@npm:^10.4.21": version: 10.4.21 resolution: "autoprefixer@npm:10.4.21" @@ -2776,6 +3434,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.2": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/b360fd4d38861622e5010c2f709736988b05c7f31042305fa3f4e9911f6adb80ccfb4e302068bf8ed10e835c2e2520cba0f5edc13d878b886987e5aa62483f53 + languageName: node + linkType: hard + "chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -2793,6 +3464,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e + languageName: node + linkType: hard + "chokidar@npm:^4.0.0, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -2872,6 +3550,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + "commander@npm:^14.0.0": version: 14.0.1 resolution: "commander@npm:14.0.1" @@ -2962,20 +3649,27 @@ __metadata: "@gravity-ui/uikit": "npm:^7.16.2" "@repo/eslint-config": "npm:*" "@repo/models": "npm:*" + "@repo/react-testing-library-config": "npm:*" "@repo/stores": "npm:*" "@repo/stylelint-config": "npm:*" "@repo/types": "npm:*" "@repo/typescript-config": "npm:*" "@repo/ui": "npm:*" "@repo/utils": "npm:*" + "@repo/vitest-config": "npm:*" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.1.0" + "@testing-library/user-event": "npm:^14.5.2" "@types/node": "npm:^24.0.0" "@types/react": "npm:^19.1.2" "@types/react-dom": "npm:^19.1.2" "@vitejs/plugin-react": "npm:^4.4.1" + "@vitest/coverage-v8": "npm:^2.1.8" autoprefixer: "npm:^10.4.13" clsx: "npm:^2.1.1" date-fns: "npm:^4.1.0" eslint: "npm:^9.29.0" + jsdom: "npm:^25.0.1" maplibre-gl: "npm:^5.6.1" mobx: "npm:^6.13.7" mobx-react-lite: "npm:^4.1.0" @@ -2994,6 +3688,7 @@ __metadata: typescript: "npm:^5.8.3" vite: "npm:^6.3.5" vite-plugin-compression2: "npm:^2.2.0" + vitest: "npm:^2.1.8" zod: "npm:^4.0.5" languageName: unknown linkType: soft @@ -3052,6 +3747,13 @@ __metadata: languageName: node linkType: hard +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -3061,6 +3763,16 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^4.1.0": + version: 4.6.0 + resolution: "cssstyle@npm:4.6.0" + dependencies: + "@asamuzakjp/css-color": "npm:^3.2.0" + rrweb-cssom: "npm:^0.8.0" + checksum: 10c0/71add1b0ffafa1bedbef6855db6189b9523d3320e015a0bf3fbd504760efb9a81e1f1a225228d5fa892ee58e56d06994ca372e7f4e461cda7c4c9985fe075f65 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -3068,6 +3780,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -3154,6 +3876,32 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.1.1, debug@npm:^4.3.7": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + +"decimal.js@npm:^10.4.3": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -3183,6 +3931,20 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + +"dequal@npm:^2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 + languageName: node + linkType: hard + "detect-libc@npm:^1.0.3": version: 1.0.3 resolution: "detect-libc@npm:1.0.3" @@ -3210,6 +3972,20 @@ __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" + checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360 + languageName: node + linkType: hard + "dom-helpers@npm:^5.0.1": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" @@ -3296,6 +4072,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, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -3437,6 +4220,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.4": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": version: 1.1.1 resolution: "es-object-atoms@npm:1.1.1" @@ -3478,6 +4268,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de + languageName: node + linkType: hard + "esbuild@npm:^0.25.0": version: 0.25.9 resolution: "esbuild@npm:0.25.9" @@ -3875,6 +4745,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -3889,6 +4768,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.2.2 + resolution: "expect-type@npm:1.2.2" + checksum: 10c0/6019019566063bbc7a690d9281d920b1a91284a4a093c2d55d71ffade5ac890cf37a51e1da4602546c4b56569d2ad2fc175a2ccee77d1ae06cb3af91ef84f44b + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.2 resolution: "exponential-backoff@npm:3.1.2" @@ -4093,6 +4979,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695 + languageName: node + linkType: hard + "fraction.js@npm:^4.3.7": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -4289,7 +5188,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -4478,6 +5377,22 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "html-tags@npm:^3.3.1": version: 3.3.1 resolution: "html-tags@npm:3.3.1" @@ -4492,7 +5407,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: @@ -4502,7 +5417,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.5": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -4521,7 +5436,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -4593,6 +5508,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -4854,6 +5776,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-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -4973,6 +5902,45 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.7": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/d596317cfd9c22e1394f22a8d8ba0303d2074fe2e971887b32d870e4b33f8464b10f8ccbe6847808f7db485f084eba09e6c2ed706b3a978e4b52f07085b8f9bc + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -5032,6 +6000,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^25.0.1": + version: 25.0.1 + resolution: "jsdom@npm:25.0.1" + dependencies: + cssstyle: "npm:^4.1.0" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.4.3" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.5" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.12" + parse5: "npm:^7.1.2" + rrweb-cssom: "npm:^0.7.1" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^5.0.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + ws: "npm:^8.18.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/6bda32a6dfe4e37a30568bf51136bdb3ba9c0b72aadd6356280404275a34c9e097c8c25b5eb3c742e602623741e172da977ff456684befd77c9042ed9bf8c2b4 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -5388,7 +6390,14 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": +"loupe@npm:^3.1.0, loupe@npm:^3.1.2": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb @@ -5413,7 +6422,16 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.17": +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b + languageName: node + linkType: hard + +"magic-string@npm:^0.30.12, magic-string@npm:^0.30.17": version: 0.30.19 resolution: "magic-string@npm:0.30.19" dependencies: @@ -5422,6 +6440,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + source-map-js: "npm:^1.2.0" + checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64 + languageName: node + linkType: hard + "make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -5432,6 +6461,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -5540,6 +6578,22 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + "mime@npm:^1.4.1": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -5556,6 +6610,13 @@ __metadata: languageName: node linkType: hard +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c + languageName: node + linkType: hard + "minimatch@npm:10.0.3": version: 10.0.3 resolution: "minimatch@npm:10.0.3" @@ -5855,6 +6916,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.12": + version: 2.2.22 + resolution: "nwsapi@npm:2.2.22" + checksum: 10c0/b6a0e5ea6754aacfdfe551c8c0f1b374eaf94d48b0a4e7eac666f879ecbc1892ef1d7c457e9b02eefad3fa1323ea1faebcba533eeab6582e24c9c503411bf879 + languageName: node + linkType: hard + "object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -6040,6 +7108,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.1.2": + version: 7.3.0 + resolution: "parse5@npm:7.3.0" + dependencies: + entities: "npm:^6.0.0" + checksum: 10c0/7fd2e4e247e85241d6f2a464d0085eed599a26d7b0a5233790c49f53473232eb85350e8133344d9b3fd58b89339e7ad7270fe1f89d28abe50674ec97b87f80b5 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -6092,6 +7169,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 + languageName: node + linkType: hard + "pathe@npm:^2.0.1, pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" @@ -6099,6 +7183,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.1 + resolution: "pathval@npm:2.0.1" + checksum: 10c0/460f4709479fbf2c45903a65655fc8f0a5f6d808f989173aeef5fdea4ff4f303dc13f7870303999add60ec49d4c14733895c0a869392e9866f1091fa64fd7581 + languageName: node + linkType: hard + "pbf@npm:^4.0.1": version: 4.0.1 resolution: "pbf@npm:4.0.1" @@ -6110,7 +7201,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.1.1": +"picocolors@npm:1.1.1, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -6316,7 +7407,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.4.41, postcss@npm:^8.5.3, postcss@npm:^8.5.5, postcss@npm:^8.5.6": +"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.4.41, postcss@npm:^8.4.43, postcss@npm:^8.5.3, postcss@npm:^8.5.5, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -6359,6 +7450,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:^5.0.0": version: 5.0.0 resolution: "proc-log@npm:5.0.0" @@ -6401,7 +7503,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": +"punycode@npm:^2.1.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -6513,6 +7615,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-is@npm:^18.2.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -6634,6 +7743,16 @@ __metadata: languageName: node linkType: hard +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: "npm:^4.0.0" + strip-indent: "npm:^3.0.0" + checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae + languageName: node + linkType: hard + "redux@npm:^5.0.1": version: 5.0.1 resolution: "redux@npm:5.0.1" @@ -6817,6 +7936,87 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.52.4 + resolution: "rollup@npm:4.52.4" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.52.4" + "@rollup/rollup-android-arm64": "npm:4.52.4" + "@rollup/rollup-darwin-arm64": "npm:4.52.4" + "@rollup/rollup-darwin-x64": "npm:4.52.4" + "@rollup/rollup-freebsd-arm64": "npm:4.52.4" + "@rollup/rollup-freebsd-x64": "npm:4.52.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.52.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.52.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.52.4" + "@rollup/rollup-linux-loong64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-riscv64-musl": "npm:4.52.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.52.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-x64-musl": "npm:4.52.4" + "@rollup/rollup-openharmony-arm64": "npm:4.52.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.52.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.52.4" + "@rollup/rollup-win32-x64-gnu": "npm:4.52.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.52.4" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/aaec0f57e887d4fb37d152f93cf7133954eec79d11643e95de768ec9a377f08793b1745c648ca65a0dcc6c795c4d9ca398724d013e5745de270e88a543782aea + languageName: node + linkType: hard + "rollup@npm:^4.34.8, rollup@npm:^4.34.9": version: 4.50.1 resolution: "rollup@npm:4.50.1" @@ -6895,6 +8095,20 @@ __metadata: languageName: node linkType: hard +"rrweb-cssom@npm:^0.7.1": + version: 0.7.1 + resolution: "rrweb-cssom@npm:0.7.1" + checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e + languageName: node + linkType: hard + +"rrweb-cssom@npm:^0.8.0": + version: 0.8.0 + resolution: "rrweb-cssom@npm:0.8.0" + checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -6983,6 +8197,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.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" @@ -7028,6 +8251,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e + languageName: node + linkType: hard + "semver@npm:~7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -7159,6 +8391,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -7260,7 +8499,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -7332,6 +8571,20 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"std-env@npm:^3.8.0": + version: 3.9.0 + resolution: "std-env@npm:3.9.0" + checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -7476,6 +8729,15 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: "npm:^1.0.0" + checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679 + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -7724,6 +8986,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 + "synckit@npm:^0.11.7": version: 0.11.11 resolution: "synckit@npm:0.11.11" @@ -7804,6 +9073,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10c0/6d67b9af4336a2e12b26a68c83308c7863534c65f27ed4ff7068a56f5a58f7ac703e8fc80f698a19bb154fd8f705cdf7ec347d9512b2c522c737269507e7b263 + languageName: node + linkType: hard + "thenify-all@npm:^1.0.0": version: 1.6.0 resolution: "thenify-all@npm:1.6.0" @@ -7829,7 +9109,14 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.2": +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^0.3.1, tinyexec@npm:^0.3.2": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 @@ -7846,6 +9133,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.1": + version: 1.1.1 + resolution: "tinypool@npm:1.1.1" + checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b + languageName: node + linkType: hard + "tinyqueue@npm:^3.0.0": version: 3.0.0 resolution: "tinyqueue@npm:3.0.0" @@ -7853,6 +9147,38 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 10c0/7f78a4b997e5ba0f5ecb75e7ed786f30bab9063716e7dff24dd84013fb338802e43d176cb21ed12480561f5649a82184cf31efb296601a29d38145b1cdb4c192 + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.2": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10c0/55ffad24e346622b59292e097c2ee30a63919d5acb7ceca87fc0d1c223090089890587b426e20054733f97a58f20af2c349fb7cc193697203868ab7ba00bcea0 + languageName: node + linkType: hard + +"tldts-core@npm:^6.1.86": + version: 6.1.86 + resolution: "tldts-core@npm:6.1.86" + checksum: 10c0/8133c29375f3f99f88fce5f4d62f6ecb9532b106f31e5423b27c1eb1b6e711bd41875184a456819ceaed5c8b94f43911b1ad57e25c6eb86e1fc201228ff7e2af + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.86 + resolution: "tldts@npm:6.1.86" + dependencies: + tldts-core: "npm:^6.1.86" + bin: + tldts: bin/cli.js + checksum: 10c0/27ae7526d9d78cb97b2de3f4d102e0b4321d1ccff0648a7bb0e039ed54acbce86bacdcd9cd3c14310e519b457854e7bafbef1f529f58a1e217a737ced63f0940 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -7862,6 +9188,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^5.0.0": + version: 5.1.2 + resolution: "tough-cookie@npm:5.1.2" + dependencies: + tldts: "npm:^6.1.32" + checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626 + languageName: node + linkType: hard + "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -7871,6 +9206,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/ae270e194d52ec67ebd695c1a42876e0f19b96e4aca2ab464ab1d9d17dc3acd3e18764f5034c93897db73421563be27c70c98359c4501136a497e46deda5d5ec + languageName: node + linkType: hard + "tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" @@ -8370,6 +9714,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.9": + version: 2.1.9 + resolution: "vite-node@npm:2.1.9" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.7" + es-module-lexer: "npm:^1.5.4" + pathe: "npm:^1.1.2" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/0d3589f9f4e9cff696b5b49681fdb75d1638c75053728be52b4013f70792f38cb0120a9c15e3a4b22bdd6b795ad7c2da13bcaf47242d439f0906049e73bdd756 + languageName: node + linkType: hard + "vite-plugin-compression2@npm:^2.2.0": version: 2.2.1 resolution: "vite-plugin-compression2@npm:2.2.1" @@ -8403,6 +9762,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.4.20 + resolution: "vite@npm:5.4.20" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.43" + rollup: "npm:^4.20.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/391a1fdd7e05445d60aa3b15d6c1cffcdd92c5d154da375bf06b9cd5633c2387ebee0e8f2fceed3226a63dff36c8ef18fb497662dde8c135133c46670996c7a1 + languageName: node + linkType: hard + "vite@npm:^6.3.5": version: 6.3.6 resolution: "vite@npm:6.3.6" @@ -8458,6 +9860,56 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.1.8": + version: 2.1.9 + resolution: "vitest@npm:2.1.9" + dependencies: + "@vitest/expect": "npm:2.1.9" + "@vitest/mocker": "npm:2.1.9" + "@vitest/pretty-format": "npm:^2.1.9" + "@vitest/runner": "npm:2.1.9" + "@vitest/snapshot": "npm:2.1.9" + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + std-env: "npm:^3.8.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.1.9" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.9 + "@vitest/ui": 2.1.9 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/e339e16dccacf4589ff43cb1f38c7b4d14427956ae8ef48702af6820a9842347c2b6c77356aeddb040329759ca508a3cb2b104ddf78103ea5bc98ab8f2c3a54e + languageName: node + linkType: hard + "vscode-uri@npm:^3.0.8": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" @@ -8465,6 +9917,15 @@ __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:^4.0.2": version: 4.0.2 resolution: "webidl-conversions@npm:4.0.2" @@ -8472,6 +9933,39 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 + languageName: node + linkType: hard + +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e + 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:^14.0.0": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" + dependencies: + tr46: "npm:^5.1.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272 + languageName: node + linkType: hard + "whatwg-url@npm:^7.0.0": version: 7.1.0 resolution: "whatwg-url@npm:7.1.0" @@ -8577,6 +10071,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -8634,6 +10140,35 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.0": + version: 8.18.3 + resolution: "ws@npm:8.18.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + 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:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1"