From 5492ba84543e5f6bc8509f7c06831b5793db6923 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:55:55 +0100 Subject: [PATCH 01/22] chore: added prettier --- .prettierrc | 5 + __tests__/ThemeProvider.test.jsx | 398 +++++++++++++++++++++---------- __tests__/assets/device.mock.js | 38 --- __tests__/mocks/device.mock.js | 50 ++++ __tests__/useTheme.test.jsx | 107 +++++++-- package-lock.json | 22 ++ package.json | 7 +- src/component/Html.jsx | 8 - src/helper/color.helper.js | 4 - 9 files changed, 438 insertions(+), 201 deletions(-) create mode 100644 .prettierrc delete mode 100644 __tests__/assets/device.mock.js create mode 100644 __tests__/mocks/device.mock.js delete mode 100644 src/component/Html.jsx delete mode 100644 src/helper/color.helper.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..75a894a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/__tests__/ThemeProvider.test.jsx b/__tests__/ThemeProvider.test.jsx index 910ff26..8bd96ef 100644 --- a/__tests__/ThemeProvider.test.jsx +++ b/__tests__/ThemeProvider.test.jsx @@ -1,136 +1,270 @@ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ThemeProvider } from '../src/client'; -import { mockDeviceStorage, mockPreferredColorScheme } from './assets/device.mock'; -import { read, write, clear } from '../src/adapter/storage.adapter'; -import ThemeAutoToggle from './assets/ThemeAutoToggle'; -import ThemeManualToggle from './assets/ThemeManualToggle'; +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import { themes, ThemeProvider } from '../src/client' +import { mockLocalStorage, mockMatchMedia, mockPreferredColorScheme } from './mocks/device.mock' +import { read, write, clear } from '../src/adapter/storage.adapter' +import ThemeAutoToggle from './assets/ThemeAutoToggle' +import ThemeManualToggle from './assets/ThemeManualToggle' +import ThemeSwitcher from './assets/ThemeSwitcher' + +beforeAll(() => { + mockLocalStorage() + mockMatchMedia() +}) beforeEach(() => { - mockDeviceStorage(); - clear(); - document.documentElement.style.colorScheme = '' - document.documentElement.removeAttribute('class'); -}); + clear() + document.documentElement.style.colorScheme = '' + document.documentElement.removeAttribute('class') +}) describe('provider', () => { - test.each([ - 'light', - 'dark', - ])('should use the `defaultTheme` when nothing is stored in `localStorage`', (theme) => { - const storageKey = 'test'; - - render( - - - - ); - - expect(read(storageKey)).toEqual(theme); - expect(document.documentElement.classList[0]).toBe(theme); - expect(document.documentElement.style.colorScheme).toBe(theme); - }); - - test.each([ - 'light', - 'dark', - ])('should set `color-scheme` and `class` to "%s" theme according to saved preference', (theme) => { - const storageKey = 'test'; - write(storageKey, theme); - - render( - - - - ); - - expect(document.documentElement.classList[0]).toBe(theme); - expect(document.documentElement.style.colorScheme).toBe(theme); - }); - - test.each([ - 'light', - 'dark', - ])('should set resolve to system resolved theme "%s"', (theme) => { - const storageKey = 'sys-resolved-theme'; - mockPreferredColorScheme(theme); - - render( - - - - ); - - expect(read(storageKey)).toEqual(theme); - expect(document.documentElement.classList[0]).toBe(theme); - expect(document.documentElement.style.colorScheme).toBe(theme); - }); - - test.each([ - ['light', 'dark'], - ['dark', 'light'], - ])('should ignore nested `ThemeProvider`', (expectedTheme, nestedTheme) => { - const storageKey = 'test'; - - render( - - - - - - ); - - expect(document.documentElement.classList[0]).toBe(expectedTheme); - }); - - test.each([ - ['light', 'dark'], - ['dark', 'light'], - ])('should update value in storage when toggling from "%s" to "%s" theme', (themeFrom, themeTo) => { - const storageKey = 'test'; - - render( - - - - ); - - expect(read(storageKey)).toEqual(themeFrom); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(read(storageKey)).toEqual(themeTo); - }); - - test.each([ - ['light', 'dark'], - ['dark', 'light'], - ])('should update value in storage when manually setting theme from "%s" to "%s"', (themeFrom, themeTo) => { - const storageKey = 'test'; - - render( - - - - ); - - expect(read(storageKey)).toEqual(themeFrom); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(read(storageKey)).toEqual(themeTo); - }); - - test('should set storage key according to the specified `storageKey`', () => { - const storageKey = 'theme-test'; - const expectedTheme = 'light'; - - render( - - - - ); - - expect(read(storageKey)).toEqual(expectedTheme); - }); -}); + test.each(['light', 'dark'])( + 'should use the `defaultTheme` when nothing is stored in `localStorage`', + (theme) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(theme) + expect(document.documentElement.classList[0]).toBe(theme) + expect(document.documentElement.style.colorScheme).toBe(theme) + }, + ) + + test.skip.each(['light', 'dark'])( + 'should use the `defaultTheme` when nothing is stored in `localStorage`', + (theme) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(theme) + expect(document.documentElement.classList[0]).toBe(theme) + expect(document.documentElement.style.colorScheme).toBe(theme) + }, + ) + + test.skip.each(['light', 'dark', 'auto'])( + 'should use the `defaultTheme` when nothing is stored in `localStorage`', + (theme) => { + const storageKey = 'test' + + let userTheme = theme + + if (userTheme === 'auto') { + userTheme = 'dark' + mockPreferredColorScheme(userTheme) + } + + render( + + + , + ) + + expect(read(storageKey)).toEqual(userTheme) + expect(document.documentElement.classList[0]).toBe(userTheme) + expect(document.documentElement.style.colorScheme).toBe(userTheme) + }, + ) + + test.skip.each(['light', 'dark'])( + 'should set `color-scheme` and `class` to "%s" theme according to saved preference', + (theme) => { + const storageKey = 'test' + write(storageKey, theme) + + render( + + + , + ) + + expect(document.documentElement.classList[0]).toBe(theme) + expect(document.documentElement.style.colorScheme).toBe(theme) + }, + ) + + test.skip.each(['light', 'dark', 'auto'])( + 'should set resolve to system resolved theme "%s"', + (theme) => { + const storageKey = 'sys-resolved-theme' + mockPreferredColorScheme(theme) + + let userTheme = theme + + if (userTheme === 'auto') { + userTheme = 'dark' + mockPreferredColorScheme(userTheme) + } + + render( + + + , + ) + + expect(read(storageKey)).toEqual(userTheme) + expect(document.documentElement.classList[0]).toBe(userTheme) + expect(document.documentElement.style.colorScheme).toBe(userTheme) + }, + ) + + test.skip.each([ + ['light', 'dark'], + ['dark', 'light'], + ])('should ignore nested `ThemeProvider`', (expectedTheme, nestedTheme) => { + const storageKey = 'test' + + render( + + + + + , + ) + + expect(document.documentElement.classList[0]).toBe(expectedTheme) + }) + + test.skip.each([ + ['light', 'dark'], + ['dark', 'light'], + ])( + 'should update value in storage when toggling from "%s" to "%s" theme', + (themeFrom, themeTo) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(themeFrom) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(themeTo) + }, + ) + + test.skip.each([ + ['light', 'dark'], + ['dark', 'light'], + ])( + 'should update value in storage when manually setting theme from "%s" to "%s"', + (themeFrom, themeTo) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(themeFrom) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(themeTo) + }, + ) + + test.skip('should set storage key according to the specified value', () => { + const storageKey = 'theme-test' + const expectedTheme = 'light' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(expectedTheme) + }) + + test.skip.each(['light', 'dark'])( + 'should set theme automatically based on user system preference', + (sysTheme) => { + const storageKey = 'sys-resolved-theme' + mockPreferredColorScheme(sysTheme) + + render( + + + , + ) + + expect(read(storageKey)).toEqual(sysTheme) + expect(document.documentElement.classList[0]).toBe(sysTheme) + expect(document.documentElement.style.colorScheme).toBe(sysTheme) + }, + ) + + test.skip.each(['light', 'dark'])('should switch from "auto" to "%s"', (theme) => { + const storageKey = 'sys-resolved-theme' + const oppositeTheme = theme === 'dark' ? 'light' : 'dark' + mockPreferredColorScheme(oppositeTheme) + + render( + + + , + ) + + expect(read(storageKey)).toEqual(oppositeTheme) + expect(document.documentElement.classList[0]).toBe(oppositeTheme) + expect(document.documentElement.style.colorScheme).toBe(oppositeTheme) + + fireEvent.click(screen.getByText(new RegExp(`${theme} theme`, 'i'))) + + expect(read(storageKey)).toEqual(theme) + expect(document.documentElement.classList[0]).toBe(theme) + expect(document.documentElement.style.colorScheme).toBe(theme) + }) + + test.skip.each(['light', 'dark'])('should switch from "%s" to "auto"', (theme) => { + const storageKey = 'sys-resolved-theme' + const oppositeTheme = theme === 'dark' ? 'light' : 'dark' + mockPreferredColorScheme(oppositeTheme) + + render( + + + , + ) + + expect(read(storageKey)).toEqual(theme) + expect(document.documentElement.classList[0]).toBe(theme) + expect(document.documentElement.style.colorScheme).toBe(theme) + + fireEvent.click(screen.getByText(new RegExp(`auto theme`, 'i'))) + + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(oppositeTheme) + expect(document.documentElement.style.colorScheme).toBe(oppositeTheme) + }) + + test.skip('should not set `colorScheme` and class name to "auto"', () => { + const storageKey = 'sys-resolved-theme' + + render( + + + , + ) + + expect(document.documentElement.classList[0]).not.toBe('auto') + expect(document.documentElement.style.colorScheme).not.toBe('auto') + }) +}) diff --git a/__tests__/assets/device.mock.js b/__tests__/assets/device.mock.js deleted file mode 100644 index 76ee0cf..0000000 --- a/__tests__/assets/device.mock.js +++ /dev/null @@ -1,38 +0,0 @@ -export function mockPreferredColorScheme(theme) { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: theme === 'dark', - media: query, - onchange: null, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })) - }) -} - -export function mockDeviceStorage() { - const localStorageMock = (function() { - let store = {} - - return { - getItem: function(key) { - return store[key] || null; - }, - setItem: function(key, value) { - store[key] = value.toString(); - }, - removeItem: function(key) { - delete store[key]; - }, - clear: function() { - store = {}; - }, - }; - })(); - - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - }); -} diff --git a/__tests__/mocks/device.mock.js b/__tests__/mocks/device.mock.js new file mode 100644 index 0000000..c15396e --- /dev/null +++ b/__tests__/mocks/device.mock.js @@ -0,0 +1,50 @@ +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +const localStorageMock = (function() { + let store = {} + + return { + getItem: function(key) { + return store[key] || null; + }, + setItem: function(key, value) { + store[key] = value.toString(); + }, + removeItem: function(key) { + delete store[key]; + }, + clear: function() { + store = {}; + }, + }; +})(); + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, +}); + +export function mockPreferredColorScheme(theme) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: theme === 'dark', + media: query, + onchange: null, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })) + }) +} diff --git a/__tests__/useTheme.test.jsx b/__tests__/useTheme.test.jsx index ac72ce0..5329a59 100644 --- a/__tests__/useTheme.test.jsx +++ b/__tests__/useTheme.test.jsx @@ -1,6 +1,7 @@ +/* import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import { ThemeProvider } from '../src/client'; +import { colors, ThemeProvider } from '../src/client'; import { mockDeviceStorage, mockPreferredColorScheme } from './assets/device.mock'; import { clear, read } from '../src/adapter/storage.adapter'; import ThemeAutoToggle from './assets/ThemeAutoToggle'; @@ -8,17 +9,20 @@ import ThemeManualToggle from './assets/ThemeManualToggle'; import ThemeSwitcher from './assets/ThemeSwitcher'; import '@testing-library/jest-dom'; -beforeEach(() => { +beforeAll(() => { mockDeviceStorage(); +}); + +beforeEach(() => { clear(); document.documentElement.style.colorScheme = '' document.documentElement.removeAttribute('class'); }); describe('useTheme', () => { - test.each([ - ['light', 'dark'], - ['dark', 'light'], + test.skip.each([ + [colors.light, colors.dark], + [colors.dark, colors.light], ])('should toggle "%s" theme to "%s"', (themeFrom, themeTo) => { const storageKey = 'test'; @@ -37,10 +41,10 @@ describe('useTheme', () => { expect(document.documentElement.style.colorScheme).toBe(themeTo); }); - test.each([ - ['light', 'dark'], - ['dark', 'light'], - ])('should toggle system resolved "%s" theme to "%s"', (themeFrom, themeTo) => { + test.skip.each([ + [colors.light, colors.dark], + [colors.dark, colors.light], + ])('should toggle from system resolved "%s" theme to opposite theme "%s"', (themeFrom, themeTo) => { const storageKey = 'sys-resolved-theme'; mockPreferredColorScheme(themeFrom); @@ -59,9 +63,9 @@ describe('useTheme', () => { expect(document.documentElement.style.colorScheme).toBe(themeTo); }); - test.each([ - ['light', 'dark'], - ['dark', 'light'], + test.skip.each([ + [colors.light, colors.dark], + [colors.dark, colors.light], ])('should get right values to manually set theme from "%s" to "%s"', (themeFrom, themeTo) => { const storageKey = 'test'; @@ -80,12 +84,12 @@ describe('useTheme', () => { expect(document.documentElement.style.colorScheme).toBe(themeTo); }); - test.each([ - 'light', - 'dark', - ])('should get "%s" as the active theme', (theme) => { + test.skip.each([ + colors.light, + colors.dark, + ])('should get "%s" as the active theme and color', (theme) => { const storageKey = 'user-theme'; - const oppositeTheme = (theme === 'light') ? 'dark' : 'light'; + const oppositeTheme = (theme === colors.light) ? colors.dark : colors.light; render( @@ -96,6 +100,75 @@ describe('useTheme', () => { fireEvent.click(screen.getByText(new RegExp(`${theme} theme`, 'i'))); expect(screen.getByText(`Active Theme: ${theme}`)).toBeInTheDocument(); + expect(screen.getByText(`Active Color: ${theme}`)).toBeInTheDocument(); expect(read(storageKey)).toEqual(theme); }); + + test.skip.each([ + colors.light, + colors.dark, + ])('should get "%s" as the active color when theme is set to "auto"', (colorScheme) => { + const storageKey = 'user-theme'; + mockPreferredColorScheme(colorScheme); + + render( + + + + ); + + expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); + expect(screen.getByText(`Active Color: ${colorScheme}`)).toBeInTheDocument(); + }); + + /!*test.skip.each([ + colors.light, + colors.dark, + ])('should get "%s" as the active color when theme is set to "auto"', (colorScheme) => { + const storageKey = 'user-theme'; + const oppositeColor = (colorScheme === colors.light) ? colors.dark : colors.light; + + mockPreferredColorScheme(oppositeColor); + + render( + + + + ); + + expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); + expect(screen.getByText(`Active Color: ${oppositeColor}`)).toBeInTheDocument(); + + mockPreferredColorScheme(colorScheme); + + fireEvent.click(screen.getByText(new RegExp(`${oppositeTheme} theme`, 'i'))); + + expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); + expect(screen.getByText(`Active Color: ${color}`)).toBeInTheDocument(); + });*!/ + + test.skip.each([ + [colors.light, colors.dark], + [colors.dark, colors.light], + ])('should switch to opposite color of "%s" when toggling from "auto"', (sysTheme, switchToTheme) => { + const storageKey = 'sys-resolved-theme'; + mockPreferredColorScheme(sysTheme); + + render( + + + + ); + + expect(read(storageKey)).toEqual(colors.auto); + expect(document.documentElement.classList[0]).toBe(sysTheme); + expect(document.documentElement.style.colorScheme).toBe(sysTheme); + + fireEvent.click(screen.getByText(/toggle theme/i)); + + expect(read(storageKey)).toEqual(colors.auto); + expect(document.documentElement.classList[0]).toBe(switchToTheme); + expect(document.documentElement.style.colorScheme).toBe(switchToTheme); + }); }); +*/ diff --git a/package-lock.json b/package-lock.json index 3ecad1c..ba9e1f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "prettier": "3.1.0", "rollup": "^4.6.0", "rollup-plugin-swc3": "^0.10.4", "rollup-swc-preserve-directives": "^0.7.0" @@ -6721,6 +6722,21 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -12587,6 +12603,12 @@ "find-up": "^4.0.0" } }, + "prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "dev": true + }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index 415cbed..4294012 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "scripts": { "prepare": "rollup -c", "start": "rollup -c -w", - "test": "jest --silent=false" + "test": "jest --silent=false", + "lint": "prettier . --check", + "lint:fix": "prettier . --write" }, "author": "Daniyal Hamid", "license": "ISC", @@ -30,13 +32,14 @@ "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/preset-env": "^7.23.3", "@babel/preset-react": "^7.23.3", - "@testing-library/jest-dom": "^6.1.4", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.1.2", "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "prettier": "3.1.0", "rollup": "^4.6.0", "rollup-plugin-swc3": "^0.10.4", "rollup-swc-preserve-directives": "^0.7.0" diff --git a/src/component/Html.jsx b/src/component/Html.jsx deleted file mode 100644 index 46958e2..0000000 --- a/src/component/Html.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -export default function Html({ className, theme, style, children, ...rest }) { - const classes = className ? `${className} ` : ''; - return ( - {children} - ); -} diff --git a/src/helper/color.helper.js b/src/helper/color.helper.js deleted file mode 100644 index 4d64892..0000000 --- a/src/helper/color.helper.js +++ /dev/null @@ -1,4 +0,0 @@ -const THEME_DARK = 'dark' -const THEME_LIGHT = 'light'; - -export const getColors = () => ({ dark: THEME_DARK, light: THEME_LIGHT }); From 37e12c324ab329f43491d4acfbd54970b4178326 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:57:01 +0100 Subject: [PATCH 02/22] chore: format code with prettier --- rollup.config.mjs | 48 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 1f2923e..ca29d4b 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,27 +1,29 @@ -import { swc } from 'rollup-plugin-swc3'; -import swcPreserveDirectives from 'rollup-swc-preserve-directives'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; +import { swc } from 'rollup-plugin-swc3' +import swcPreserveDirectives from 'rollup-swc-preserve-directives' +import { nodeResolve } from '@rollup/plugin-node-resolve' export default { - input: { - client: 'src/client.js', - server: 'src/server.js' + input: { + client: 'src/client.js', + server: 'src/server.js', + }, + output: [ + { + dir: 'dist/', + entryFileNames: '[name].js', + format: 'esm', + exports: 'named', + sourcemap: false, + strict: false, }, - output: [{ - dir: 'dist/', - entryFileNames: '[name].js', - format: 'esm', - exports: 'named', - sourcemap: false, - strict: false, - }], - plugins: [ - swc(), - swcPreserveDirectives(), - nodeResolve({ - extensions: ['.js'], - mainFields: ['exports', 'main'], - }), - ], - external: ['react', 'react-dom'], + ], + plugins: [ + swc(), + swcPreserveDirectives(), + nodeResolve({ + extensions: ['.js'], + mainFields: ['exports', 'main'], + }), + ], + external: ['react', 'react-dom'], } From 2899f7e40258772e68b9c43a828e5381f9bb9101 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:57:45 +0100 Subject: [PATCH 03/22] feat: removed colors and exported themes instead --- src/client.js | 7 +++---- src/server.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/client.js b/src/client.js index d7f1fc6..2dd6218 100644 --- a/src/client.js +++ b/src/client.js @@ -1,4 +1,3 @@ -export { default as Html } from './component/Html.jsx'; -export { default as ThemeProvider } from './context/ThemeProvider.jsx'; -export { default as useTheme } from './hook/useTheme'; -export { getColors } from './helper/color.helper'; +export { default as ThemeProvider } from './context/ThemeProvider.jsx' +export { default as useTheme } from './hook/useTheme' +export { themes } from './helper/theme.helper' diff --git a/src/server.js b/src/server.js index b68ce64..d764606 100644 --- a/src/server.js +++ b/src/server.js @@ -1 +1 @@ -export { getColors } from './helper/color.helper'; +export { themes } from './helper/theme.helper' From 446ad58adc8b2d048bdf237c28cad482d4e201c0 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:58:34 +0100 Subject: [PATCH 04/22] test: moved to mocks folder + split into individual files for unit tests --- __tests__/mocks/device.mock.js | 87 +++++++++++++++------------- __tests__/mocks/localStorage.mock.js | 3 + __tests__/mocks/matchMedia.mock.js | 3 + 3 files changed, 52 insertions(+), 41 deletions(-) create mode 100644 __tests__/mocks/localStorage.mock.js create mode 100644 __tests__/mocks/matchMedia.mock.js diff --git a/__tests__/mocks/device.mock.js b/__tests__/mocks/device.mock.js index c15396e..a37186d 100644 --- a/__tests__/mocks/device.mock.js +++ b/__tests__/mocks/device.mock.js @@ -1,50 +1,55 @@ -Object.defineProperty(window, 'matchMedia', { +export function mockMatchMedia() { + Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), })), -}); + }) +} + +export function mockPreferredColorScheme(theme, options = {}) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: theme === 'dark', + media: query, + onchange: null, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + ...options, + })), + }) +} -const localStorageMock = (function() { +export function mockLocalStorage() { + const localStorageMock = (function () { let store = {} return { - getItem: function(key) { - return store[key] || null; - }, - setItem: function(key, value) { - store[key] = value.toString(); - }, - removeItem: function(key) { - delete store[key]; - }, - clear: function() { - store = {}; - }, - }; -})(); + getItem: function (key) { + return store[key] || null + }, + setItem: function (key, value) { + store[key] = value.toString() + }, + removeItem: function (key) { + delete store[key] + }, + clear: function () { + store = {} + }, + } + })() -Object.defineProperty(window, 'localStorage', { + Object.defineProperty(window, 'localStorage', { value: localStorageMock, -}); - -export function mockPreferredColorScheme(theme) { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: theme === 'dark', - media: query, - onchange: null, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })) - }) + }) } diff --git a/__tests__/mocks/localStorage.mock.js b/__tests__/mocks/localStorage.mock.js new file mode 100644 index 0000000..ce8f2bf --- /dev/null +++ b/__tests__/mocks/localStorage.mock.js @@ -0,0 +1,3 @@ +import { mockLocalStorage } from './device.mock' + +mockLocalStorage() diff --git a/__tests__/mocks/matchMedia.mock.js b/__tests__/mocks/matchMedia.mock.js new file mode 100644 index 0000000..051c6b9 --- /dev/null +++ b/__tests__/mocks/matchMedia.mock.js @@ -0,0 +1,3 @@ +import { mockMatchMedia } from './device.mock' + +mockMatchMedia() From c4dc43b1446e5cb22086f12eb5f94fae46ecbecc Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:58:55 +0100 Subject: [PATCH 05/22] test: tests for theme helper --- __tests__/theme.helper.test.js | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 __tests__/theme.helper.test.js diff --git a/__tests__/theme.helper.test.js b/__tests__/theme.helper.test.js new file mode 100644 index 0000000..1713274 --- /dev/null +++ b/__tests__/theme.helper.test.js @@ -0,0 +1,68 @@ +import './mocks/matchMedia.mock' +import './mocks/localStorage.mock' +import { mockPreferredColorScheme } from './mocks/device.mock' +import { write, clear } from '../src/adapter/storage.adapter' +import { colors, flipThemeByColor, getTheme, getColorByTheme } from '../src/helper/theme.helper' + +const storageKey = 'theme-pref' + +beforeEach(() => { + clear() +}) + +describe('getTheme()', () => { + test.each([ + [undefined, undefined, 'auto'], + [undefined, 'dark', 'dark'], + [undefined, 'light', 'light'], + [undefined, 'auto', 'auto'], + ['dark', undefined, 'dark'], + ['light', undefined, 'light'], + ['auto', undefined, 'auto'], + ['dark', 'light', 'dark'], + ['light', 'dark', 'light'], + ['auto', 'light', 'auto'], + ])( + 'should get the theme from storage or the fallback', + (storedTheme, defaultTheme, expectedTheme) => { + if (storedTheme) { + write(storageKey, storedTheme) + } + + expect(getTheme(storageKey, defaultTheme)).toEqual(expectedTheme) + }, + ) +}) + +describe('getColorByTheme()', () => { + test.each([ + ['dark', colors.dark], + ['light', colors.light], + ['auto', colors.dark], + ])('should get the color based on the theme', (theme, expectedColor) => { + if (theme === 'auto') { + mockPreferredColorScheme(expectedColor) + } + + expect(getColorByTheme(theme)).toEqual(expectedColor) + }) +}) + +describe('flipThemeByColor()', () => { + test.each([ + [colors.dark, 'light'], + [colors.light, 'dark'], + ])('should get the opposite theme based on the color', (color, expectedTheme) => { + expect(flipThemeByColor(color)).toEqual(expectedTheme) + }) +}) + +describe('colors', () => { + test.each(['dark', 'light'])( + 'should automatically determine the color based on the system preferred color', + (prefColor) => { + mockPreferredColorScheme(prefColor) + expect(colors.auto).toEqual(prefColor) + }, + ) +}) From b54ca4572a3895d3a2a6d361ae175192d825c455 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:59:17 +0100 Subject: [PATCH 06/22] refactor: moved isServer function --- src/helper/env.helper.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/helper/env.helper.js diff --git a/src/helper/env.helper.js b/src/helper/env.helper.js new file mode 100644 index 0000000..61b864e --- /dev/null +++ b/src/helper/env.helper.js @@ -0,0 +1 @@ +export const isServer = () => typeof window === 'undefined' From 9e38d7cf62012a0c4d9e63127829517fd78eefe8 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 03:59:34 +0100 Subject: [PATCH 07/22] test: created a new test component --- __tests__/assets/ThemeAutoColor.jsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 __tests__/assets/ThemeAutoColor.jsx diff --git a/__tests__/assets/ThemeAutoColor.jsx b/__tests__/assets/ThemeAutoColor.jsx new file mode 100644 index 0000000..c8cb693 --- /dev/null +++ b/__tests__/assets/ThemeAutoColor.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import { useTheme } from '../../src/client' + +export default function ThemeAutoColor() { + const { theme, colors } = useTheme() + + return ( + <> +
Active Theme: {theme}
+
Auto-determined Color: {colors.auto}
+ + ) +} From dedc70a9b376c7c8fd80ab064260ebec5e22957c Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 04:00:12 +0100 Subject: [PATCH 08/22] test: added new tests + updated old ones --- __tests__/ThemeProvider.test.jsx | 138 +++++-------- __tests__/useTheme.test.jsx | 332 +++++++++++++++---------------- 2 files changed, 219 insertions(+), 251 deletions(-) diff --git a/__tests__/ThemeProvider.test.jsx b/__tests__/ThemeProvider.test.jsx index 8bd96ef..275bf24 100644 --- a/__tests__/ThemeProvider.test.jsx +++ b/__tests__/ThemeProvider.test.jsx @@ -1,6 +1,6 @@ import React from 'react' import { render, screen, fireEvent } from '@testing-library/react' -import { themes, ThemeProvider } from '../src/client' +import { ThemeProvider } from '../src/client' import { mockLocalStorage, mockMatchMedia, mockPreferredColorScheme } from './mocks/device.mock' import { read, write, clear } from '../src/adapter/storage.adapter' import ThemeAutoToggle from './assets/ThemeAutoToggle' @@ -19,6 +19,19 @@ beforeEach(() => { }) describe('provider', () => { + test('should set storage key according to the specified value', () => { + const storageKey = 'theme-test' + const expectedTheme = 'light' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(expectedTheme) + }) + test.each(['light', 'dark'])( 'should use the `defaultTheme` when nothing is stored in `localStorage`', (theme) => { @@ -36,52 +49,48 @@ describe('provider', () => { }, ) - test.skip.each(['light', 'dark'])( - 'should use the `defaultTheme` when nothing is stored in `localStorage`', - (theme) => { + test.each(['light', 'dark'])( + 'should auto-determine theme color when nothing is stored in `localStorage` and `defaultTheme` is set to "auto"', + (color) => { const storageKey = 'test' + mockPreferredColorScheme(color) render( - + , ) - expect(read(storageKey)).toEqual(theme) - expect(document.documentElement.classList[0]).toBe(theme) - expect(document.documentElement.style.colorScheme).toBe(theme) + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(color) + expect(document.documentElement.style.colorScheme).toBe(color) }, ) - test.skip.each(['light', 'dark', 'auto'])( - 'should use the `defaultTheme` when nothing is stored in `localStorage`', - (theme) => { + test.each(['light', 'dark'])( + 'should set `color-scheme` and `class` to "%s" theme color according to saved theme preference', + (color) => { const storageKey = 'test' - - let userTheme = theme - - if (userTheme === 'auto') { - userTheme = 'dark' - mockPreferredColorScheme(userTheme) - } + write(storageKey, color) render( - + , ) - expect(read(storageKey)).toEqual(userTheme) - expect(document.documentElement.classList[0]).toBe(userTheme) - expect(document.documentElement.style.colorScheme).toBe(userTheme) + expect(document.documentElement.classList[0]).toBe(color) + expect(document.documentElement.style.colorScheme).toBe(color) }, ) - test.skip.each(['light', 'dark'])( - 'should set `color-scheme` and `class` to "%s" theme according to saved preference', - (theme) => { - const storageKey = 'test' - write(storageKey, theme) + test.each(['light', 'dark', 'auto'])( + 'should use system resolved "%s" color and "auto" theme when no `defaultTheme` is provided and nothing is stored in `localStorage`', + (color) => { + const storageKey = 'sys-resolved-theme' + const prefColor = color === 'auto' ? 'dark' : color + + mockPreferredColorScheme(prefColor) render( @@ -89,37 +98,31 @@ describe('provider', () => { , ) - expect(document.documentElement.classList[0]).toBe(theme) - expect(document.documentElement.style.colorScheme).toBe(theme) + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(prefColor) + expect(document.documentElement.style.colorScheme).toBe(prefColor) }, ) - test.skip.each(['light', 'dark', 'auto'])( - 'should set resolve to system resolved theme "%s"', - (theme) => { + test.each(['light', 'dark'])( + 'should set theme color automatically based on user system preference', + (sysPrefColor) => { const storageKey = 'sys-resolved-theme' - mockPreferredColorScheme(theme) - - let userTheme = theme - - if (userTheme === 'auto') { - userTheme = 'dark' - mockPreferredColorScheme(userTheme) - } + mockPreferredColorScheme(sysPrefColor) render( - + , ) - expect(read(storageKey)).toEqual(userTheme) - expect(document.documentElement.classList[0]).toBe(userTheme) - expect(document.documentElement.style.colorScheme).toBe(userTheme) + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(sysPrefColor) + expect(document.documentElement.style.colorScheme).toBe(sysPrefColor) }, ) - test.skip.each([ + test.each([ ['light', 'dark'], ['dark', 'light'], ])('should ignore nested `ThemeProvider`', (expectedTheme, nestedTheme) => { @@ -136,7 +139,7 @@ describe('provider', () => { expect(document.documentElement.classList[0]).toBe(expectedTheme) }) - test.skip.each([ + test.each([ ['light', 'dark'], ['dark', 'light'], ])( @@ -158,7 +161,7 @@ describe('provider', () => { }, ) - test.skip.each([ + test.each([ ['light', 'dark'], ['dark', 'light'], ])( @@ -180,38 +183,7 @@ describe('provider', () => { }, ) - test.skip('should set storage key according to the specified value', () => { - const storageKey = 'theme-test' - const expectedTheme = 'light' - - render( - - - , - ) - - expect(read(storageKey)).toEqual(expectedTheme) - }) - - test.skip.each(['light', 'dark'])( - 'should set theme automatically based on user system preference', - (sysTheme) => { - const storageKey = 'sys-resolved-theme' - mockPreferredColorScheme(sysTheme) - - render( - - - , - ) - - expect(read(storageKey)).toEqual(sysTheme) - expect(document.documentElement.classList[0]).toBe(sysTheme) - expect(document.documentElement.style.colorScheme).toBe(sysTheme) - }, - ) - - test.skip.each(['light', 'dark'])('should switch from "auto" to "%s"', (theme) => { + test.each(['light', 'dark'])('should switch from "auto" to "%s"', (theme) => { const storageKey = 'sys-resolved-theme' const oppositeTheme = theme === 'dark' ? 'light' : 'dark' mockPreferredColorScheme(oppositeTheme) @@ -222,7 +194,7 @@ describe('provider', () => { , ) - expect(read(storageKey)).toEqual(oppositeTheme) + expect(read(storageKey)).toEqual('auto') expect(document.documentElement.classList[0]).toBe(oppositeTheme) expect(document.documentElement.style.colorScheme).toBe(oppositeTheme) @@ -233,7 +205,7 @@ describe('provider', () => { expect(document.documentElement.style.colorScheme).toBe(theme) }) - test.skip.each(['light', 'dark'])('should switch from "%s" to "auto"', (theme) => { + test.each(['light', 'dark'])('should switch from "%s" to "auto"', (theme) => { const storageKey = 'sys-resolved-theme' const oppositeTheme = theme === 'dark' ? 'light' : 'dark' mockPreferredColorScheme(oppositeTheme) @@ -248,14 +220,14 @@ describe('provider', () => { expect(document.documentElement.classList[0]).toBe(theme) expect(document.documentElement.style.colorScheme).toBe(theme) - fireEvent.click(screen.getByText(new RegExp(`auto theme`, 'i'))) + fireEvent.click(screen.getByText('Auto Theme')) expect(read(storageKey)).toEqual('auto') expect(document.documentElement.classList[0]).toBe(oppositeTheme) expect(document.documentElement.style.colorScheme).toBe(oppositeTheme) }) - test.skip('should not set `colorScheme` and class name to "auto"', () => { + test('should not set `colorScheme` and class name to "auto"', () => { const storageKey = 'sys-resolved-theme' render( diff --git a/__tests__/useTheme.test.jsx b/__tests__/useTheme.test.jsx index 5329a59..a1afdb1 100644 --- a/__tests__/useTheme.test.jsx +++ b/__tests__/useTheme.test.jsx @@ -1,174 +1,170 @@ -/* -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { colors, ThemeProvider } from '../src/client'; -import { mockDeviceStorage, mockPreferredColorScheme } from './assets/device.mock'; -import { clear, read } from '../src/adapter/storage.adapter'; -import ThemeAutoToggle from './assets/ThemeAutoToggle'; -import ThemeManualToggle from './assets/ThemeManualToggle'; -import ThemeSwitcher from './assets/ThemeSwitcher'; -import '@testing-library/jest-dom'; +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import { ThemeProvider } from '../src/client' +import { mockLocalStorage, mockMatchMedia, mockPreferredColorScheme } from './mocks/device.mock' +import { clear, read } from '../src/adapter/storage.adapter' +import ThemeAutoToggle from './assets/ThemeAutoToggle' +import ThemeManualToggle from './assets/ThemeManualToggle' +import ThemeSwitcher from './assets/ThemeSwitcher' +import '@testing-library/jest-dom' beforeAll(() => { - mockDeviceStorage(); -}); + mockLocalStorage() + mockMatchMedia() +}) beforeEach(() => { - clear(); - document.documentElement.style.colorScheme = '' - document.documentElement.removeAttribute('class'); -}); + clear() + document.documentElement.style.colorScheme = '' + document.documentElement.removeAttribute('class') +}) describe('useTheme', () => { - test.skip.each([ - [colors.light, colors.dark], - [colors.dark, colors.light], - ])('should toggle "%s" theme to "%s"', (themeFrom, themeTo) => { - const storageKey = 'test'; - - render( - - - - ); - - expect(document.documentElement.classList[0]).toBe(themeFrom); - expect(document.documentElement.style.colorScheme).toBe(themeFrom); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(document.documentElement.classList[0]).toBe(themeTo); - expect(document.documentElement.style.colorScheme).toBe(themeTo); - }); - - test.skip.each([ - [colors.light, colors.dark], - [colors.dark, colors.light], - ])('should toggle from system resolved "%s" theme to opposite theme "%s"', (themeFrom, themeTo) => { - const storageKey = 'sys-resolved-theme'; - mockPreferredColorScheme(themeFrom); - - render( - - - - ); - - expect(document.documentElement.classList[0]).toBe(themeFrom); - expect(document.documentElement.style.colorScheme).toBe(themeFrom); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(document.documentElement.classList[0]).toBe(themeTo); - expect(document.documentElement.style.colorScheme).toBe(themeTo); - }); - - test.skip.each([ - [colors.light, colors.dark], - [colors.dark, colors.light], - ])('should get right values to manually set theme from "%s" to "%s"', (themeFrom, themeTo) => { - const storageKey = 'test'; - - render( - - - - ); - - expect(document.documentElement.classList[0]).toBe(themeFrom); - expect(document.documentElement.style.colorScheme).toBe(themeFrom); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(document.documentElement.classList[0]).toBe(themeTo); - expect(document.documentElement.style.colorScheme).toBe(themeTo); - }); - - test.skip.each([ - colors.light, - colors.dark, - ])('should get "%s" as the active theme and color', (theme) => { - const storageKey = 'user-theme'; - const oppositeTheme = (theme === colors.light) ? colors.dark : colors.light; - - render( - - - - ); - - fireEvent.click(screen.getByText(new RegExp(`${theme} theme`, 'i'))); - - expect(screen.getByText(`Active Theme: ${theme}`)).toBeInTheDocument(); - expect(screen.getByText(`Active Color: ${theme}`)).toBeInTheDocument(); - expect(read(storageKey)).toEqual(theme); - }); - - test.skip.each([ - colors.light, - colors.dark, - ])('should get "%s" as the active color when theme is set to "auto"', (colorScheme) => { - const storageKey = 'user-theme'; - mockPreferredColorScheme(colorScheme); - - render( - - - - ); - - expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); - expect(screen.getByText(`Active Color: ${colorScheme}`)).toBeInTheDocument(); - }); - - /!*test.skip.each([ - colors.light, - colors.dark, - ])('should get "%s" as the active color when theme is set to "auto"', (colorScheme) => { - const storageKey = 'user-theme'; - const oppositeColor = (colorScheme === colors.light) ? colors.dark : colors.light; - - mockPreferredColorScheme(oppositeColor); - - render( - - - - ); - - expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); - expect(screen.getByText(`Active Color: ${oppositeColor}`)).toBeInTheDocument(); - - mockPreferredColorScheme(colorScheme); - - fireEvent.click(screen.getByText(new RegExp(`${oppositeTheme} theme`, 'i'))); - - expect(screen.getByText(`Active Theme: ${colors.auto}`)).toBeInTheDocument(); - expect(screen.getByText(`Active Color: ${color}`)).toBeInTheDocument(); - });*!/ - - test.skip.each([ - [colors.light, colors.dark], - [colors.dark, colors.light], - ])('should switch to opposite color of "%s" when toggling from "auto"', (sysTheme, switchToTheme) => { - const storageKey = 'sys-resolved-theme'; - mockPreferredColorScheme(sysTheme); - - render( - - - - ); - - expect(read(storageKey)).toEqual(colors.auto); - expect(document.documentElement.classList[0]).toBe(sysTheme); - expect(document.documentElement.style.colorScheme).toBe(sysTheme); - - fireEvent.click(screen.getByText(/toggle theme/i)); - - expect(read(storageKey)).toEqual(colors.auto); - expect(document.documentElement.classList[0]).toBe(switchToTheme); - expect(document.documentElement.style.colorScheme).toBe(switchToTheme); - }); -}); -*/ + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])('should toggle "%s" theme to "%s"', (themeFrom, themeTo) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(themeFrom) + expect(document.documentElement.classList[0]).toBe(themeFrom) + expect(document.documentElement.style.colorScheme).toBe(themeFrom) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(themeTo) + expect(document.documentElement.classList[0]).toBe(themeTo) + expect(document.documentElement.style.colorScheme).toBe(themeTo) + }) + + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])( + 'should toggle from system resolved "%s" theme to opposite theme "%s" when using `toggle` function', + (themeFrom, themeTo) => { + const storageKey = 'sys-resolved-theme' + mockPreferredColorScheme(themeFrom) + + render( + + + , + ) + + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(themeFrom) + expect(document.documentElement.style.colorScheme).toBe(themeFrom) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(themeTo) + expect(document.documentElement.classList[0]).toBe(themeTo) + expect(document.documentElement.style.colorScheme).toBe(themeTo) + }, + ) + + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])('should get right values to manually set theme from "%s" to "%s"', (themeFrom, themeTo) => { + const storageKey = 'test' + + render( + + + , + ) + + expect(read(storageKey)).toEqual(themeFrom) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(themeTo) + }) + + test.each(['light', 'dark'])('should get "%s" as the active `theme` and `color`', (theme) => { + const storageKey = 'user-theme' + const oppositeTheme = theme === 'light' ? 'dark' : 'light' + + render( + + + , + ) + + fireEvent.click(screen.getByText(new RegExp(`${theme} theme`, 'i'))) + + expect(screen.getByText(`Active Theme: ${theme}`)).toBeInTheDocument() + expect(screen.getByText(`Active Color: ${theme}`)).toBeInTheDocument() + expect(read(storageKey)).toEqual(theme) + }) + + test.each(['light', 'dark'])( + 'should get "%s" as the active `color` when theme is set to "auto"', + (colorScheme) => { + const storageKey = 'user-theme' + mockPreferredColorScheme(colorScheme) + + render( + + + , + ) + + expect(screen.getByText('Active Theme: auto')).toBeInTheDocument() + expect(screen.getByText(`Active Color: ${colorScheme}`)).toBeInTheDocument() + }, + ) + + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])( + 'should switch to opposite color of "%s" when toggling from "auto"', + (sysPrefColor, switchToTheme) => { + const storageKey = 'sys-resolved-theme' + mockPreferredColorScheme(sysPrefColor) + + render( + + + , + ) + + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(sysPrefColor) + expect(document.documentElement.style.colorScheme).toBe(sysPrefColor) + + fireEvent.click(screen.getByText(/toggle theme/i)) + + expect(read(storageKey)).toEqual(switchToTheme) + expect(document.documentElement.classList[0]).toBe(switchToTheme) + expect(document.documentElement.style.colorScheme).toBe(switchToTheme) + }, + ) + + test.each(['light', 'dark'])( + 'should auto-determine color to be "%s" via `colors.auto`', + (prefColor) => { + const storageKey = 'sys-resolved-theme' + mockPreferredColorScheme(prefColor) + + render( + + + , + ) + + expect(read(storageKey)).toEqual('auto') + expect(document.documentElement.classList[0]).toBe(prefColor) + expect(document.documentElement.style.colorScheme).toBe(prefColor) + }, + ) +}) From 3c70f3eda200cdfada0013ba01df816fb41f5a48 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 04:01:12 +0100 Subject: [PATCH 09/22] test: updated to use themes instead of colors object --- __tests__/assets/ThemeManualToggle.jsx | 18 ++++++++---------- __tests__/assets/ThemeSwitcher.jsx | 22 ++++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/__tests__/assets/ThemeManualToggle.jsx b/__tests__/assets/ThemeManualToggle.jsx index 1e66e7d..126cf4c 100644 --- a/__tests__/assets/ThemeManualToggle.jsx +++ b/__tests__/assets/ThemeManualToggle.jsx @@ -1,16 +1,14 @@ 'use client' -import React from 'react'; -import { useTheme } from '../../src/client'; +import React from 'react' +import { useTheme } from '../../src/client' export default function ToggleThemeButton() { - const { theme, color, setTheme } = useTheme(); + const { theme, themes, colors, setTheme } = useTheme() - return ( - - ) + return ( + + ) } diff --git a/__tests__/assets/ThemeSwitcher.jsx b/__tests__/assets/ThemeSwitcher.jsx index 255abcc..38d52dd 100644 --- a/__tests__/assets/ThemeSwitcher.jsx +++ b/__tests__/assets/ThemeSwitcher.jsx @@ -1,14 +1,16 @@ -import React from 'react'; -import { useTheme } from '../../src/client'; +import React from 'react' +import { useTheme } from '../../src/client' export default function ThemeSwitcher() { - const { theme, color, setTheme } = useTheme(); + const { theme, themes, color, setTheme } = useTheme() - return ( - <> -
Active Theme: {theme}
- - - - ) + return ( + <> +
Active Theme: {theme}
+
Active Color: {color}
+ + + + + ) } From a17d423b7a9765e97c288d066615ca6695282f99 Mon Sep 17 00:00:00 2001 From: designcise Date: Mon, 4 Dec 2023 04:02:06 +0100 Subject: [PATCH 10/22] feat: updated script to correctly apply theme color on page load --- src/component/AntiFlickerScript.jsx | 30 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/component/AntiFlickerScript.jsx b/src/component/AntiFlickerScript.jsx index 679a9ca..0e70605 100644 --- a/src/component/AntiFlickerScript.jsx +++ b/src/component/AntiFlickerScript.jsx @@ -1,12 +1,22 @@ -import React, { memo } from 'react'; +import React, { memo } from 'react' +import { themes, colors as palette, getColorByTheme } from '../helper/theme.helper' -export default memo(function AntiFlickerScript({ storageKey, defaultTheme, color }) { - const classList = Object.values(color).join("','"); - const preferredTheme = `localStorage.getItem('${storageKey}')`; - const fallbackTheme = defaultTheme ? `'${defaultTheme}'` : `(window.matchMedia('(prefers-color-scheme: ${color.dark})').matches ? '${color.dark}' : '${color.light}')`; - const script = '(function(root){' - + `const theme=${preferredTheme}??${fallbackTheme};` - + `root.classList.remove('${classList}');root.classList.add(theme);root.style.colorScheme=theme;` - + `})(document.documentElement)`; +export default memo( + function AntiFlickerScript({ storageKey, defaultTheme }) { + const { [themes.auto]: _, ...colors } = palette + const classList = Object.values(colors).join("','") + const preferredTheme = `localStorage.getItem('${storageKey}')` + const fallbackTheme = + defaultTheme && defaultTheme !== themes.auto + ? `'${getColorByTheme(defaultTheme)}'` + : `(window.matchMedia('(prefers-color-scheme: ${colors.dark})').matches ? '${colors.dark}' : '${colors.light}')` + const script = + '(function(root){' + + `const pref=${preferredTheme};` + + `const theme=(pref&&pref!=='${themes.auto}')?pref:${fallbackTheme};` + + `root.classList.remove('${classList}');root.classList.add(theme);root.style.colorScheme=theme;` + + `})(document.documentElement)` return