Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: setTheme now works correctly when a function is passed. #286

Merged
merged 6 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next-themes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ All your theme configuration is passed to ThemeProvider.
useTheme takes no parameters, but returns:

- `theme`: Active theme name
- `setTheme(name)`: Function to update the theme
- `setTheme(name)`: Function to update the theme. The API is identical to the [set function](https://react.dev/reference/react/useState#setstate) returned by `useState`-hook. Pass the new theme value or use a callback to set the new theme based on the current theme.
- `forcedTheme`: Forced page theme or falsy. If `forcedTheme` is set, you should disable any theme switching UI
- `resolvedTheme`: If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme`
- `systemTheme`: If `enableSystem` is true, represents the System theme preference ("dark" or "light"), regardless what the active theme is
Expand Down
185 changes: 104 additions & 81 deletions next-themes/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// @vitest-environment jsdom

import * as React from 'react'
import { act, render, screen } from '@testing-library/react'
import { act, render, renderHook, screen } from '@testing-library/react'
import { vi, beforeAll, beforeEach, afterEach, afterAll, describe, test, it, expect } from 'vitest'
import { cleanup } from '@testing-library/react'

import { ThemeProvider, useTheme } from '../src/index'
import { ThemeProviderProps } from '../src/types'

let originalLocalStorage: Storage
const localStorageMock: Storage = (() => {
Expand Down Expand Up @@ -90,86 +91,81 @@ afterAll(() => {
window.localStorage = originalLocalStorage
})

function makeWrapper(props: ThemeProviderProps) {
return ({ children }: { children: React.ReactNode }) => (
<ThemeProvider {...props}>{children}</ThemeProvider>
)
}

describe('defaultTheme', () => {
test('should return system when no default-theme is set', () => {
render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)

expect(screen.getByTestId('theme').textContent).toBe('system')
test('should return system-theme when no default-theme is set', () => {
setDeviceTheme('light')

const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})
expect(result.current.theme).toBe('system')
expect(result.current.systemTheme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return light when no default-theme is set and enableSystem=false', () => {
render(
<ThemeProvider enableSystem={false}>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ enableSystem: false })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return light when light is set as default-theme', () => {
render(
<ThemeProvider defaultTheme="light">
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'light' })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return dark when dark is set as default-theme', () => {
render(
<ThemeProvider defaultTheme="dark">
<HelperComponent />
</ThemeProvider>
)

expect(screen.getByTestId('theme').textContent).toBe('dark')
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'dark' })
})
expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
})
})

describe('provider', () => {
it('ignores nested ThemeProviders', () => {
act(() => {
render(
const { result } = renderHook(() => useTheme(), {
wrapper: ({ children }) => (
<ThemeProvider defaultTheme="dark">
<ThemeProvider defaultTheme="light">
<HelperComponent />
</ThemeProvider>
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
</ThemeProvider>
)
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
})
})

describe('storage', () => {
test('should not set localStorage with default value', () => {
act(() => {
render(
<ThemeProvider defaultTheme="dark">
<HelperComponent />
</ThemeProvider>
)
renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'dark' })
})

expect(window.localStorage.setItem).toBeCalledTimes(0)
expect(window.localStorage.getItem('theme')).toBeNull()
})

test('should set localStorage when switching themes', () => {
act(() => {
render(
<ThemeProvider>
<HelperComponent forceSetTheme="dark" />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})
result.current.setTheme('dark')

expect(window.localStorage.setItem).toBeCalledTimes(1)
expect(window.localStorage.getItem('theme')).toBe('dark')
Expand Down Expand Up @@ -290,47 +286,40 @@ describe('forcedTheme', () => {
test('should render saved theme when no forcedTheme is set', () => {
localStorageMock.setItem('theme', 'dark')

render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(result.current.theme).toBe('dark')
expect(result.current.forcedTheme).toBeUndefined()
})

test('should render light theme when forcedTheme is set to light', () => {
localStorageMock.setItem('theme', 'dark')

act(() => {
render(
<ThemeProvider forcedTheme="light">
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({
forcedTheme: 'light'
})
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(screen.getByTestId('forcedTheme').textContent).toBe('light')
expect(result.current.theme).toBe('dark')
expect(result.current.forcedTheme).toBe('light')
})
})

describe('system', () => {
describe('system theme', () => {
test('resolved theme should be set', () => {
setDeviceTheme('dark')

act(() => {
render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})

expect(screen.getByTestId('theme').textContent).toBe('system')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(screen.getByTestId('resolvedTheme').textContent).toBe('dark')
expect(result.current.theme).toBe('system')
expect(result.current.systemTheme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
expect(result.current.forcedTheme).toBeUndefined()
})

test('system theme should be set, even if theme is not system', () => {
Expand All @@ -353,18 +342,14 @@ describe('system', () => {
test('system theme should not be set if enableSystem is false', () => {
setDeviceTheme('dark')

act(() => {
render(
<ThemeProvider defaultTheme="light" enableSystem={false}>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ enableSystem: false, defaultTheme: 'light' })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(screen.getByTestId('resolvedTheme').textContent).toBe('light')
expect(screen.getByTestId('systemTheme').textContent).toBe('')
expect(result.current.theme).toBe('light')
expect(result.current.systemTheme).toBeUndefined()
expect(result.current.resolvedTheme).toBe('light')
expect(result.current.forcedTheme).toBeUndefined()
})
})

Expand Down Expand Up @@ -407,3 +392,41 @@ describe('color-scheme', () => {
expect(document.documentElement.style.colorScheme).toBe('dark')
})
})

describe('setTheme', () => {
test('setTheme(<literal>)', () => {
const { result, rerender } = renderHook(() => useTheme(), {
wrapper: ({ children }) => <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
})
expect(result.current?.setTheme).toBeDefined()
expect(result.current.resolvedTheme).toBe('light')
result.current.setTheme('dark')
rerender()
expect(result.current.resolvedTheme).toBe('dark')
})

test('setTheme(<function>)', () => {
const { result, rerender } = renderHook(() => useTheme(), {
wrapper: ({ children }) => <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
})
expect(result.current?.setTheme).toBeDefined()
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')

const toggleTheme = vi.fn((theme: string) => (theme === 'light' ? 'dark' : 'light'))

result.current.setTheme(toggleTheme)
expect(toggleTheme).toBeCalledTimes(1)
rerender()

expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')

result.current.setTheme(toggleTheme)
expect(toggleTheme).toBeCalledTimes(2)
rerender()

expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})
})
6 changes: 3 additions & 3 deletions next-themes/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const Theme = ({
}, [])

const setTheme = React.useCallback(
theme => {
const newTheme = typeof theme === 'function' ? theme(theme) : theme
value => {
const newTheme = typeof value === 'function' ? value(theme) : value
setThemeState(newTheme)

// Save to storage
Expand All @@ -90,7 +90,7 @@ const Theme = ({
// Unsupported
}
},
[forcedTheme]
[theme]
)

const handleMediaQuery = React.useCallback(
Expand Down
Loading