import React from "react"; import { act, renderHook } from "@testing-library/react-native"; import { darkTokens, iosTokens } from "@jobber/design"; import merge from "lodash/merge"; import { type ColorSchemeName, useColorScheme } from "react-native"; import { AtlantisThemeContextProvider, useAtlantisTheme, } from "./AtlantisThemeContext"; import type { AtlantisThemeContextProviderProps, EffectiveTheme, } from "./types"; jest.mock("react-native", () => { const actual = jest.requireActual("react-native"); Object.defineProperty(actual, "useColorScheme", { configurable: true, value: jest.fn(), }); return actual; }); const expectedDarkTokens = { ...merge({}, iosTokens, darkTokens), "shadow-low": iosTokens["shadow-low"], "shadow-base": iosTokens["shadow-base"], "shadow-high": iosTokens["shadow-high"], }; const expectedLightTokens = iosTokens; const mockUseColorScheme = useColorScheme as jest.MockedFunction< typeof useColorScheme >; function Wrapper({ children, theme, onThemeChange, dangerouslyOverrideTheme, }: AtlantisThemeContextProviderProps) { return ( {children} ); } function WrapperWithOverride({ children, dangerouslyOverrideTheme, }: AtlantisThemeContextProviderProps) { return ( {children} ); } describe("ThemeContext", () => { beforeEach(() => { mockUseColorScheme.mockReturnValue("light"); }); it("defaults to the light theme", () => { const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); expect(result.current.theme).toBe("light"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it("updates the theme and tokens", async () => { const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("dark")); expect(result.current.theme).toBe("dark"); expect(result.current.effectiveTheme).toBe("dark"); expect(result.current.tokens).toEqual(expectedDarkTokens); await act(async () => result.current.setTheme("light")); expect(result.current.theme).toBe("light"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it.each(["shadow-low", "shadow-base", "shadow-high"] as const)( "keeps the platform-native %s token in dark mode", async shadowToken => { const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("dark")); expect(result.current.tokens[shadowToken]).toEqual( iosTokens[shadowToken], ); expect(typeof result.current.tokens[shadowToken]).toBe("object"); }, ); it("resolves the system theme from the OS color scheme", async () => { mockUseColorScheme.mockReturnValue("dark"); const { result, rerender } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("system")); expect(result.current.theme).toBe("system"); expect(result.current.effectiveTheme).toBe("dark"); expect(result.current.tokens).toEqual(expectedDarkTokens); mockUseColorScheme.mockReturnValue("light"); rerender({}); expect(result.current.theme).toBe("system"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it("uses a controlled selected theme", () => { mockUseColorScheme.mockReturnValue("dark"); const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); expect(result.current.theme).toBe("system"); expect(result.current.effectiveTheme).toBe("dark"); expect(result.current.tokens).toEqual(expectedDarkTokens); }); it("reports selected theme changes from a controlled provider", async () => { const onThemeChange = jest.fn(); const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("system")); expect(onThemeChange).toHaveBeenCalledWith("system"); expect(result.current.theme).toBe("light"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it("falls back to light when system color scheme is unavailable", async () => { mockUseColorScheme.mockReturnValue( null as unknown as ReturnType, ); const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("system")); expect(result.current.theme).toBe("system"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); describe("when theme is forced for provider", () => { it("preserves the forced effective theme when the selected theme changes", async () => { const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); // Update the selected theme through the forced subtree. await act(async () => result.current.setTheme("dark")); expect(result.current.theme).toBe("dark"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it("preserves the parent selected theme while forcing the effective theme", () => { mockUseColorScheme.mockReturnValue("dark"); const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); expect(result.current.theme).toBe("system"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it("forwards selected theme changes through a forced subtree", async () => { const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); await act(async () => result.current.setTheme("dark")); expect(result.current.theme).toBe("dark"); expect(result.current.effectiveTheme).toBe("light"); expect(result.current.tokens).toEqual(expectedLightTokens); }); it.each([ { defaultTheme: "light", colorScheme: "dark", expectedEffectiveTheme: "light", expectedTokens: expectedLightTokens, }, { defaultTheme: "dark", colorScheme: "light", expectedEffectiveTheme: "dark", expectedTokens: expectedDarkTokens, }, ] as { defaultTheme: EffectiveTheme; colorScheme: ColorSchemeName; expectedEffectiveTheme: EffectiveTheme; expectedTokens: typeof iosTokens; }[])( "provides the dangerouslyOverrideTheme $defaultTheme tokens", ({ defaultTheme, colorScheme, expectedEffectiveTheme, expectedTokens, }) => { mockUseColorScheme.mockReturnValue(colorScheme); const { result } = renderHook(useAtlantisTheme, { wrapper: (props: AtlantisThemeContextProviderProps) => ( ), }); expect(result.current.theme).toBe("light"); expect(result.current.effectiveTheme).toBe(expectedEffectiveTheme); expect(result.current.tokens).toEqual(expectedTokens); }, ); }); });