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);
},
);
});
});