/** * @jest-environment jsdom */ import renderer from 'react-test-renderer' import { render, fireEvent, cleanup, act } from '@testing-library/react' import { useTheme } from '@emotion/react' import { matchers } from '@emotion/jest' import mockConsole from 'jest-mock-console' import { ThemeProvider, ThemeUIContextValue, useThemeUI } from '@theme-ui/core' import { ColorModeProvider, useColorMode, InitializeColorMode } from '../src' import { Theme } from '@theme-ui/css' import { renderJSON } from '@theme-ui/test-utils' const STORAGE_KEY = 'theme-ui-color-mode' const defaultColorModeName = undefined afterEach(() => { cleanup() window.matchMedia = undefined as any }) beforeEach(() => { localStorage.removeItem(STORAGE_KEY) }) expect.extend(matchers) test('renders with color modes', () => { let mode const Mode = () => { const [colorMode] = useColorMode() mode = colorMode return
Mode
} renderer.act(() => { renderer.create( ) }) expect(mode).toBe(defaultColorModeName) }) test('renders with initial color mode name', () => { let mode const Mode = () => { const [colorMode] = useColorMode() mode = colorMode return
Mode
} renderer.act(() => { renderer.create( ) }) expect(mode).toBe('light') }) test('useColorMode updates color mode state', () => { let mode const Button = () => { const [colorMode, setMode] = useColorMode() mode = colorMode return ( } const { findByRole } = render( ) expect(rawColors).toStrictEqual({ primary: 'tomato', modes: { light: { primary: 'tomato' }, dark: { primary: 'black' }, }, }) const button = await findByRole('button') fireEvent.click(button) expect(rawColors).toStrictEqual({ primary: 'black', modes: { light: { primary: 'tomato' }, dark: { primary: 'black' }, }, }) }) test('raw color modes are are not passed to theme-ui context if modes are not defined', () => { let colors const Grabber = () => { const context = useThemeUI() colors = context.theme?.rawColors return null } render( ) expect(colors).toStrictEqual({ primary: 'tomato', }) }) test('InitializeColorMode renders', () => { const json = renderJSON() expect(json).toMatchSnapshot() }) test('colorMode accepts function from previous state to new one', () => { type MyColorMode = 'serious' | 'cute' | 'hackerman' const theme: Theme = { config: { initialColorModeName: 'serious', }, colors: { primary: 'black', modes: { cute: { primary: 'pink', }, hackerman: { primary: 'chartreuse', }, }, }, } let primaryColor const Grabber = () => { const context = useThemeUI() primaryColor = context.theme?.rawColors?.primary return null } const colorModes: MyColorMode[] = ['serious', 'cute', 'hackerman'] const NextColorModeButton = () => { // user can specify their color mode name type const [, setColorMode] = useColorMode() return ( ) } const root = render( ) expect(primaryColor).toBe('black') act(() => { root.getByText('next color mode').click() }) expect(primaryColor).toBe('pink') act(() => { root.getByText('next color mode').click() }) expect(primaryColor).toBe('chartreuse') }) test('warns when localStorage is disabled', () => { const restoreConsole = mockConsole() const spy = jest .spyOn(Storage.prototype, 'getItem') .mockImplementation(() => { throw new Error('SecurityError: The operation is insecure.') }) let mode = '' const Consumer = () => { const [colorMode] = useColorMode() mode = colorMode return null } render( ) expect(mode).toBe(undefined) expect((console.warn as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` [ "localStorage is disabled and color mode might not work as expected.", "Please check your Site Settings.", [Error: SecurityError: The operation is insecure.], ] `) spy.mockClear() restoreConsole() }) test('rawColors are properly inherited in nested providers #1', () => { let finalTheme: Theme = {} const Grabber = () => { const context = useThemeUI() finalTheme = context.theme return null } const outerTheme: Theme = { colors: { text: 'black', modes: { dark: { text: 'white' } }, }, } const nestedTheme: Theme = { colors: { background: 'white', modes: { dark: { background: 'black' } }, }, } render( ) expect(finalTheme.rawColors).toStrictEqual({ text: 'black', background: 'white', modes: { __default: { text: 'black', background: 'white', }, dark: { text: 'white', background: 'black', }, }, }) }) test('rawColors are properly inherited in nested providers #2', () => { let finalTheme: Theme = {} const Grabber = () => { const context = useThemeUI() finalTheme = context.theme return null } const outerTheme: Theme = { colors: { text: 'black', modes: { dark: { text: 'white' } }, }, } const nestedTheme: Theme = { colors: { background: 'white', modes: { dark: { background: 'black' } }, }, } const nestedTheme2: Theme = { rawColors: { primary: 'blue', modes: { dark: { primary: 'red' } }, }, } render( ) expect(finalTheme.rawColors).toStrictEqual({ text: 'black', primary: 'blue', background: 'white', modes: { __default: { text: 'black', primary: 'blue', background: 'white', }, dark: { text: 'white', primary: 'red', background: 'black', }, }, }) expect(finalTheme.colors).toStrictEqual({ text: 'var(--theme-ui-colors-text)', background: 'var(--theme-ui-colors-background)', primary: 'var(--theme-ui-colors-primary)', }) }) test('rawColors with no color modes are merged in nested providers', () => { let finalTheme: Theme = {} const Grabber = () => { const context = useThemeUI() finalTheme = context.theme return null } const outerTheme: Theme = { colors: { text: 'black', }, } const nestedTheme: Theme = { rawColors: { background: 'white', }, } const nestedTheme2: Theme = { rawColors: { primary: 'blue', }, } render( ) expect(finalTheme.rawColors).toStrictEqual({ text: 'black', primary: 'blue', background: 'white', }) expect(finalTheme.colors).toStrictEqual({ text: 'var(--theme-ui-colors-text)', background: 'var(--theme-ui-colors-background)', primary: 'var(--theme-ui-colors-primary)', }) }) function mockMatchMedia(pattern: string) { const listeners: Function[] = [] const callListeners = (init: MediaQueryListEventInit) => { for (const listener of listeners) { listener(new Event('change', init)) } } const setPattern = (media: string) => { const matches = pattern === media pattern = media callListeners({ media, matches }) } const impl = (query: string) => { const matches = query.includes(pattern) return { matches, media: query, addEventListener( _type: 'change', listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void, _options?: boolean | AddEventListenerOptions ) { listeners.push(listener) }, removeEventListener( _type: 'change', listener: (ev: MediaQueryListEvent) => void ) { listeners.splice(listeners.indexOf(listener), 1) }, } as MediaQueryList } return { callListeners, setPattern, impl } } test('when useColorSchemeMediaQuery is set to "system", color mode aligns to user preference', () => { const matchMedia = mockMatchMedia('(prefers-color-scheme: dark)') window.matchMedia = jest.fn().mockImplementation(matchMedia.impl) const theme: Theme = { config: { useColorSchemeMediaQuery: 'system', }, colors: { background: 'white', modes: { dark: { background: 'black' }, }, }, } let colorMode: string | undefined const GetColorMode = () => { const context = useThemeUI() colorMode = context.colorMode return null } render( ) expect(colorMode).toBe('dark') act(() => { matchMedia.setPattern('(prefers-color-scheme: light)') }) expect(colorMode).toBe('light') })