diff --git a/configs/tests/setup.ts b/configs/tests/setup.ts index 320a690d..f38c69b2 100644 --- a/configs/tests/setup.ts +++ b/configs/tests/setup.ts @@ -1,7 +1,8 @@ import { expect, afterEach, beforeAll, afterAll, vi } from 'vitest'; import { cleanup } from '@testing-library/react'; import * as matchers from '@testing-library/jest-dom/matchers'; -import { server } from '~/test-utils'; +import { matchMedia, MediaQueryListEvent } from 'mock-match-media'; +import { server, cleanup as cleanupMatchMedia } from '~/test-utils'; expect.extend(matchers); @@ -16,18 +17,22 @@ vi.mock('@farfetched/core', async (importOriginal) => { }; }); -afterEach(() => { - cleanup(); -}); - beforeAll(() => { server.listen(); }); afterEach(() => { + cleanup(); + cleanupMatchMedia(); server.resetHandlers(); }); afterAll(() => { server.close(); }); + +window.MediaQueryListEvent = MediaQueryListEvent; +window.matchMedia = (...args) => { + console.log(args); + return matchMedia(...args); +}; diff --git a/package-lock.json b/package-lock.json index 641880df..80650f6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "husky": "^8.0.3", "jsdom": "^24.1.0", "lint-staged": "^15.0.2", + "mock-match-media": "^0.4.3", "msw": "^2.3.1", "typed-css-modules": "^0.8.0", "typescript": "^5.2.2", @@ -5611,6 +5612,12 @@ "node": ">=8" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "dev": true + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -9079,6 +9086,15 @@ "ufo": "^1.5.3" } }, + "node_modules/mock-match-media": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/mock-match-media/-/mock-match-media-0.4.3.tgz", + "integrity": "sha512-8nqA2fh5mmgtZEN6sDPlEbLqlqEkF8kCIL17ozbWNHLxnLPF/cli0B6sLffOyfqSBBuVWprEKjYyN8tPXMYmVA==", + "dev": true, + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16498,6 +16514,12 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "dev": true + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -18953,6 +18975,15 @@ "ufo": "^1.5.3" } }, + "mock-match-media": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/mock-match-media/-/mock-match-media-0.4.3.tgz", + "integrity": "sha512-8nqA2fh5mmgtZEN6sDPlEbLqlqEkF8kCIL17ozbWNHLxnLPF/cli0B6sLffOyfqSBBuVWprEKjYyN8tPXMYmVA==", + "dev": true, + "requires": { + "css-mediaquery": "^0.1.2" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index a5b5beb8..0a247dc0 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "husky": "^8.0.3", "jsdom": "^24.1.0", "lint-staged": "^15.0.2", + "mock-match-media": "^0.4.3", "msw": "^2.3.1", "typed-css-modules": "^0.8.0", "typescript": "^5.2.2", diff --git a/src/shared/models/scheme.spec.ts b/src/shared/models/scheme.spec.ts new file mode 100644 index 00000000..b7f70760 --- /dev/null +++ b/src/shared/models/scheme.spec.ts @@ -0,0 +1,139 @@ +import { useColorScheme } from '@mui/material'; +import { ColorSchemeContextValue } from '@mui/system'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { started } from './app'; +import { + $biScheme, + $scheme, + colorSchemeChanged, + useSyncScheme +} from './scheme'; + +import { + RenderHookResult, + Scope, + act, + allSettled, + fork, + renderHook, + setMedia +} from '~/test-utils'; + +describe('shared/models/scheme', () => { + const schemes = ['light', 'system', 'dark'] as const; + let scope: Scope; + + beforeEach(async () => { + setMedia({ + 'prefers-color-scheme': 'light', + }); + + scope = fork(); + + await allSettled(started, { scope, }); + }); + + test('should has system value by default', async () => { + expect(scope.getState($scheme)).toBe('system'); + }); + + describe('change', () => { + test.each(schemes)( + 'should change color scheme on colorSchemeChanged call with %s', + async (scheme) => { + await allSettled(colorSchemeChanged, { scope, params: scheme, }); + + expect(scope.getState($scheme)).toBe(scheme); + } + ); + + test('should keep old value if colorSchemeChanged was called with empty one', async () => { + const currentScheme = scope.getState($scheme); + + await allSettled(colorSchemeChanged, { scope, params: null, }); + + expect(scope.getState($scheme)).toBe(currentScheme); + }); + }); + + describe('bivalue scheme', () => { + test('should convert light scheme to light in bivalue one', async () => { + await allSettled(colorSchemeChanged, { scope, params: 'light', }); + + expect(scope.getState($biScheme)).toBe('light'); + }); + + test('should convert light scheme to dark in bivalue one', async () => { + await allSettled(colorSchemeChanged, { scope, params: 'dark', }); + + expect(scope.getState($biScheme)).toBe('dark'); + }); + + test.skip.each(['dark', 'light'])( + 'should convert auto scheme to %s in bivalue one if prefers-ciolor-scheme=%s', + async (scheme) => { + setMedia({ + 'prefers-color-scheme': scheme, + }); + + await allSettled(colorSchemeChanged, { scope, params: 'system', }); + + expect(scope.getState($biScheme)).toBe(scheme); + } + ); + }); + + describe('persist', () => { + const key = 'abc-color-scheme'; + + test.each(schemes)( + 'should save current scheme into local storage', + async (scheme) => { + // To initiate reading from local storage + window.dispatchEvent(new StorageEvent('storage', { key, })); + + await allSettled(colorSchemeChanged, { scope, params: scheme, }); + + expect(localStorage.getItem(key)).toBe(JSON.stringify(scheme)); + } + ); + + test.each(schemes)( + 'should load saved %s scheme from local storage', + (scheme) => { + localStorage.setItem(key, JSON.stringify(scheme)); + + window.dispatchEvent(new StorageEvent('storage', { key, })); + + expect(scope.getState($scheme)).toBe(scheme); + } + ); + }); + + describe('useSyncScheme', () => { + let wrapper: RenderHookResult, void>; + + const createComponent = () => { + wrapper = renderHook( + () => { + useSyncScheme(); + return useColorScheme(); + }, + { scope, } + ); + }; + + beforeEach(async () => { + await act(() => createComponent()); + }); + + test('should sync biScheme with mui scheme', async () => { + expect(wrapper.result.current.mode).toBe(scope.getState($biScheme)); + + await allSettled(colorSchemeChanged, { scope, params: 'dark', }); + + expect(wrapper.result.current.mode).toBe(scope.getState($biScheme)); + }); + }); +}); diff --git a/src/shared/models/scheme.ts b/src/shared/models/scheme.ts index c99322cf..dafd786d 100644 --- a/src/shared/models/scheme.ts +++ b/src/shared/models/scheme.ts @@ -43,7 +43,7 @@ export const useSyncScheme = () => { persist({ store: $scheme, - key: 'bt-color-scheme', + key: 'abc-color-scheme', }); sample({ diff --git a/test-utils/index.ts b/test-utils/index.ts index 8ff39dab..2f818dc0 100644 --- a/test-utils/index.ts +++ b/test-utils/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-extraneous-dependencies */ export * from './fixtures'; export * from './mock-server'; export * from './utils'; +export { setMedia, MediaQueryListEvent } from 'mock-match-media'; diff --git a/test-utils/utils/render.tsx b/test-utils/utils/render.tsx index 905925f4..81abbfc7 100644 --- a/test-utils/utils/render.tsx +++ b/test-utils/utils/render.tsx @@ -1,4 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ +import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { @@ -42,9 +43,11 @@ const createAllProviders = ( return ( - - {children} - + + + {children} + + ); diff --git a/vitest.config.ts b/vitest.config.ts index fa5485aa..1e846e02 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', - setupFiles: ['./configs/tests/setup.ts'], + setupFiles: [path.resolve(__dirname, './configs/tests/setup.ts')], include: ['./src/**/*.spec.{ts,tsx}'], clearMocks: true, globals: true,