Skip to content

Commit 7d580aa

Browse files
committed
test: color-mode.test.tsx
1 parent 728dd73 commit 7d580aa

File tree

2 files changed

+230
-23
lines changed

2 files changed

+230
-23
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// noinspection JSUnusedGlobalSymbols
2+
3+
/**
4+
* @file Tests for the ColorMode module.
5+
* @description These are unit tests for the ColorMode components and hooks. They verify
6+
* the rendering of ColorModeProvider, functionality of useColorMode hook, ColorModeButton,
7+
* and ColorModeWrapper in a mocked environment.
8+
* @module ColorModeTests
9+
*/
10+
11+
// region Imports
12+
import {
13+
ColorModeButton,
14+
ColorModeProvider,
15+
ColorModeWrapper,
16+
type UseColorModeReturn,
17+
useColorMode,
18+
} from "@/components/ui/color-mode"
19+
import { act, render, renderHook, screen } from "@testing-library/react"
20+
import userEvent from "@testing-library/user-event"
21+
import { useTheme } from "next-themes"
22+
import type React from "react"
23+
import { type RefObject, createRef } from "react"
24+
import type { Dispatch, SetStateAction } from "react"
25+
import { describe, expect, it, vi } from "vitest"
26+
27+
// endregion
28+
29+
// region Type Aliases
30+
interface UseThemeReturn {
31+
theme: string
32+
setTheme: Dispatch<SetStateAction<string>>
33+
themes: string[]
34+
}
35+
// endregion
36+
37+
// region Mocks
38+
vi.mock("next-themes", () => ({
39+
ThemeProvider: ({ children }: { children: React.ReactNode }): React.ReactElement => <div>{children}</div>,
40+
useTheme: vi.fn(),
41+
}))
42+
43+
// endregion
44+
45+
// region Tests
46+
describe("ColorMode", (): void => {
47+
/**
48+
* Test case: Renders ColorModeProvider with children.
49+
* It verifies that the provider renders its children correctly.
50+
*/
51+
it("renders ColorModeProvider with children", (): void => {
52+
render(
53+
<ColorModeProvider>
54+
<span data-testid="child">Test Child</span>
55+
</ColorModeProvider>,
56+
)
57+
const child: HTMLElement = screen.getByTestId("child")
58+
expect(child).toBeInTheDocument()
59+
expect(child).toHaveTextContent("Test Child")
60+
})
61+
62+
/**
63+
* Test case: useColorMode hook returns correct values and toggles theme.
64+
* It verifies that the hook returns the current theme and toggles between light and dark.
65+
*/
66+
it("useColorMode returns correct values and toggles theme", (): void => {
67+
let currentTheme = "light"
68+
const setTheme = vi.fn((newTheme: string | ((prev: string) => string)): void => {
69+
currentTheme = typeof newTheme === "string" ? newTheme : newTheme(currentTheme)
70+
})
71+
vi.mocked(useTheme).mockImplementation(
72+
(): UseThemeReturn => ({ theme: currentTheme, setTheme, themes: ["light", "dark", "system"] }),
73+
)
74+
75+
const { result, rerender } = renderHook((): UseColorModeReturn => useColorMode(), {
76+
wrapper: ({ children }: { children: React.ReactNode }): React.ReactElement => (
77+
<ColorModeProvider>{children}</ColorModeProvider>
78+
),
79+
})
80+
expect(result.current.colorMode).toBe("light")
81+
expect(typeof result.current.setColorMode).toBe("function")
82+
expect(typeof result.current.toggleColorMode).toBe("function")
83+
84+
act((): void => {
85+
result.current.toggleColorMode()
86+
})
87+
expect(setTheme).toHaveBeenCalledWith("dark")
88+
rerender()
89+
expect(result.current.colorMode).toBe("dark")
90+
91+
act((): void => {
92+
result.current.toggleColorMode()
93+
})
94+
expect(setTheme).toHaveBeenCalledWith("light")
95+
rerender()
96+
expect(result.current.colorMode).toBe("light")
97+
})
98+
99+
/**
100+
* Test case: Renders ColorModeButton with default aria-label and icon.
101+
* It verifies that the button renders with the correct aria-label and icon based on the current theme.
102+
*/
103+
it("renders ColorModeButton with default aria-label and icon", (): void => {
104+
vi.mocked(useTheme).mockReturnValue({ theme: "light", setTheme: vi.fn(), themes: ["light", "dark", "system"] })
105+
106+
render(
107+
<ColorModeProvider>
108+
<ColorModeButton />
109+
</ColorModeProvider>,
110+
)
111+
const button: HTMLElement = screen.getByRole("button", { name: /switch to dark mode/i })
112+
expect(button).toBeInTheDocument()
113+
expect(button).toHaveAttribute("aria-label", "Switch to dark mode")
114+
expect(button.querySelector("svg")).toBeInTheDocument()
115+
})
116+
117+
/**
118+
* Test case: Renders ColorModeButton with custom aria-label.
119+
* It verifies that the button uses a custom aria-label when provided.
120+
*/
121+
it("renders ColorModeButton with custom aria-label", (): void => {
122+
vi.mocked(useTheme).mockReturnValue({ theme: "light", setTheme: vi.fn(), themes: ["light", "dark", "system"] })
123+
124+
render(
125+
<ColorModeProvider>
126+
<ColorModeButton aria-label="Toggle Theme" />
127+
</ColorModeProvider>,
128+
)
129+
const button: HTMLElement = screen.getByRole("button", { name: /toggle theme/i })
130+
expect(button).toBeInTheDocument()
131+
expect(button).toHaveAttribute("aria-label", "Toggle Theme")
132+
})
133+
134+
/**
135+
* Test case: ColorModeButton toggles theme on click.
136+
* It verifies that clicking the button toggles the theme using useColorMode.
137+
*/
138+
it("ColorModeButton toggles theme on click", async (): Promise<void> => {
139+
let currentTheme = "light"
140+
const setTheme = vi.fn((newTheme: string | ((prev: string) => string)) => {
141+
currentTheme = typeof newTheme === "string" ? newTheme : newTheme(currentTheme)
142+
})
143+
vi.mocked(useTheme).mockImplementation(
144+
(): UseThemeReturn => ({ theme: currentTheme, setTheme, themes: ["light", "dark", "system"] }),
145+
)
146+
147+
render(
148+
<ColorModeProvider>
149+
<ColorModeButton />
150+
</ColorModeProvider>,
151+
)
152+
const button: HTMLElement = screen.getByRole("button", { name: /switch to dark mode/i })
153+
await act(async (): Promise<void> => {
154+
await userEvent.click(button)
155+
})
156+
expect(setTheme).toHaveBeenCalledWith("dark")
157+
158+
render(
159+
<ColorModeProvider>
160+
<ColorModeButton />
161+
</ColorModeProvider>,
162+
)
163+
const updatedButton: HTMLElement = screen.getByRole("button", { name: /switch to light mode/i })
164+
await act(async (): Promise<void> => {
165+
await userEvent.click(updatedButton)
166+
})
167+
expect(setTheme).toHaveBeenCalledWith("light")
168+
})
169+
170+
/**
171+
* Test case: Renders ColorModeWrapper with correct mode and props.
172+
* It verifies that the wrapper renders with the specified mode and additional props.
173+
*/
174+
it("renders ColorModeWrapper with correct mode and props", (): void => {
175+
render(<ColorModeWrapper mode="dark" data-test="wrapper-prop" />)
176+
const wrapper: HTMLElement = screen.getByTestId("span")
177+
expect(wrapper).toBeInTheDocument()
178+
expect(wrapper).toHaveClass("chakra-theme", "dark")
179+
expect(wrapper).toHaveAttribute("data-test", "wrapper-prop")
180+
})
181+
182+
/**
183+
* Test case: Forwards ref to ColorModeButton.
184+
* It verifies that the ref is forwarded to the underlying button element.
185+
*/
186+
it("forwards ref to ColorModeButton", (): void => {
187+
vi.mocked(useTheme).mockReturnValue({ theme: "light", setTheme: vi.fn(), themes: ["light", "dark", "system"] })
188+
189+
const ref: RefObject<HTMLButtonElement> = createRef<HTMLButtonElement>()
190+
render(
191+
<ColorModeProvider>
192+
<ColorModeButton ref={ref} />
193+
</ColorModeProvider>,
194+
)
195+
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
196+
})
197+
198+
/**
199+
* Test case: Forwards ref to ColorModeWrapper.
200+
* It verifies that the ref is forwarded to the underlying span element.
201+
*/
202+
it("forwards ref to ColorModeWrapper", (): void => {
203+
const ref: RefObject<HTMLSpanElement> = createRef<HTMLSpanElement>()
204+
render(<ColorModeWrapper mode="light" ref={ref} />)
205+
expect(ref.current).toBeInstanceOf(HTMLSpanElement)
206+
})
207+
})
208+
// endregion

frontend/tests/setupTests.tsx

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,16 @@ import { vi } from "vitest"
1515

1616
/**
1717
* Mocks `window.matchMedia` for the JSDOM environment.
18-
* This is necessary because JSDOM, the environment Vitest uses for tests, does not
19-
* implement this browser API. Many UI libraries, including Chakra UI, rely on it
20-
* to detect user preferences like OS color scheme (dark/light mode). Without this
21-
* mock, tests for components that use such features would fail.
18+
* @description Necessary because JSDOM does not implement this browser API.
2219
*/
2320
Object.defineProperty(window, "matchMedia", {
2421
writable: true,
2522
value: vi.fn().mockImplementation((query: string) => ({
2623
matches: false,
2724
media: query,
2825
onchange: null,
29-
addListener: vi.fn(), // Deprecated, but added for legacy compatibility
30-
removeListener: vi.fn(), // Deprecated, but added for legacy compatibility
26+
addListener: vi.fn(),
27+
removeListener: vi.fn(),
3128
addEventListener: vi.fn(),
3229
removeEventListener: vi.fn(),
3330
dispatchEvent: vi.fn(),
@@ -36,24 +33,23 @@ Object.defineProperty(window, "matchMedia", {
3633

3734
/**
3835
* Globally mocks the `@chakra-ui/react` library for all tests.
39-
*
40-
* This mock is crucial and solves three fundamental problems when testing
41-
* Chakra UI components in a JSDOM environment:
42-
* 1. Hoisting Issues: `vi.mock` is called at the top level, allowing Vitest to
43-
* correctly hoist it before any imports occur.
44-
* 2. CSS Parsing Errors: The mock provides a simple `ChakraProvider` that does
45-
* not inject any CSS, bypassing JSDOM's inability to parse modern CSS syntax
46-
* like `@layer`.
47-
* 3. Context Errors: Components that rely on Chakra's context (like `useTheme`)
48-
* are replaced with simple, "dumb" versions that do not use `useContext`,
49-
* preventing "context is undefined" errors.
36+
* @description Provides mocks to avoid CSS parsing and context errors in JSDOM.
5037
*/
5138
vi.mock("@chakra-ui/react", () => ({
5239
ChakraProvider: ({ children }: { children: ReactNode }): React.ReactElement => <>{children}</>,
5340
AbsoluteCenter: (props: ComponentPropsWithoutRef<"div">): React.ReactElement => (
5441
<div data-testid="absolute-center" {...props} />
5542
),
56-
Span: (props: ComponentPropsWithoutRef<"span">): React.ReactElement => <span {...props} />,
43+
Span: React.forwardRef<HTMLSpanElement, ComponentPropsWithoutRef<"span">>(
44+
(
45+
{
46+
colorPalette,
47+
colorScheme,
48+
...props
49+
}: ComponentPropsWithoutRef<"span"> & { colorPalette?: string; colorScheme?: string },
50+
ref: ForwardedRef<HTMLSpanElement>,
51+
): React.ReactElement => <span data-testid="span" ref={ref} {...props} />,
52+
),
5753
Spinner: (props: ComponentPropsWithoutRef<"div">): React.ReactElement => (
5854
<div role="status" data-testid="spinner" {...props} />
5955
),
@@ -63,9 +59,10 @@ vi.mock("@chakra-ui/react", () => ({
6359
),
6460
),
6561
IconButton: React.forwardRef<HTMLButtonElement, ComponentPropsWithoutRef<"button">>(
66-
(props: ComponentPropsWithoutRef<"button">, ref: ForwardedRef<HTMLButtonElement>): React.ReactElement => (
67-
<button data-testid="icon-button" ref={ref} {...props} />
68-
),
62+
(
63+
{ boxSize, ...props }: ComponentPropsWithoutRef<"button"> & { boxSize?: string },
64+
ref: ForwardedRef<HTMLButtonElement>,
65+
): React.ReactElement => <button data-testid="icon-button" ref={ref} data-box-size={boxSize} {...props} />,
6966
),
7067
Checkbox: {
7168
Root: (props: ComponentPropsWithoutRef<"div">): React.ReactElement => (
@@ -87,7 +84,9 @@ vi.mock("@chakra-ui/react", () => ({
8784
<span data-testid="checkbox-label" {...props} />
8885
),
8986
},
90-
// Provides a dummy function for `defineRecipe` so that imports in files
91-
// like `theme.tsx` do not fail, even if they are not directly used in a test.
87+
ClientOnly: ({ children }: { children: ReactNode; fallback?: ReactNode }): React.ReactElement => <>{children}</>,
88+
Skeleton: ({ boxSize, ...props }: ComponentPropsWithoutRef<"div"> & { boxSize?: string }): React.ReactElement => (
89+
<div data-testid="skeleton" data-box-size={boxSize} {...props} />
90+
),
9291
defineRecipe: vi.fn(() => ({})),
9392
}))

0 commit comments

Comments
 (0)