Skip to content

Commit e2c3404

Browse files
committed
Unit test ThemeProvider
1 parent 427ad62 commit e2c3404

6 files changed

Lines changed: 166 additions & 6 deletions

File tree

packages/ra-ui-materialui/src/button/Button.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ const theme = createTheme({
8585

8686
declare module '@mui/material/styles' {
8787
interface Palette {
88-
userDefined: PaletteColor;
88+
userDefined?: PaletteColor;
8989
}
9090
interface PaletteOptions {
91-
userDefined: PaletteColor;
91+
userDefined?: PaletteColor;
9292
}
9393
}
9494

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import expect from 'expect';
4+
import { StoreContextProvider, memoryStore } from 'ra-core';
5+
import { Button, ThemeOptions } from '@mui/material';
6+
7+
import { ThemeProvider } from './ThemeProvider';
8+
import { ThemesContext } from './ThemesContext';
9+
import { ThemeTestWrapper } from '../ThemeTestWrapper';
10+
11+
const lightTheme: ThemeOptions = {};
12+
const darkTheme: ThemeOptions = { palette: { mode: 'dark' } };
13+
14+
const LIGHT_MODE_TEXT_COLOR = 'rgb(25, 118, 210)'; // text is dark blue in light mode
15+
const DARK_MODE_TEXT_COLOR = 'rgb(144, 202, 249)'; // text is light blue in dark mode
16+
17+
describe('ThemeProvider', () => {
18+
it('should create a material-ui theme context based on the ThemesContext and theme preference light', () => {
19+
render(
20+
<StoreContextProvider value={memoryStore({ theme: 'light' })}>
21+
<ThemesContext.Provider value={{ lightTheme, darkTheme }}>
22+
<ThemeProvider>
23+
<Button>Test</Button>
24+
</ThemeProvider>
25+
</ThemesContext.Provider>
26+
</StoreContextProvider>
27+
);
28+
const button = screen.getByText('Test');
29+
expect(getComputedStyle(button).color).toBe(LIGHT_MODE_TEXT_COLOR);
30+
});
31+
32+
it('should create a material-ui theme context based on the ThemesContext and theme preference dark', () => {
33+
render(
34+
<StoreContextProvider value={memoryStore({ theme: 'dark' })}>
35+
<ThemesContext.Provider value={{ lightTheme, darkTheme }}>
36+
<ThemeProvider>
37+
<Button>Test</Button>
38+
</ThemeProvider>
39+
</ThemesContext.Provider>
40+
</StoreContextProvider>
41+
);
42+
const button = screen.getByText('Test');
43+
expect(getComputedStyle(button).color).toBe(DARK_MODE_TEXT_COLOR);
44+
});
45+
46+
it('should default to a light theme when no theme preference is set', () => {
47+
render(
48+
<ThemesContext.Provider value={{ lightTheme, darkTheme }}>
49+
<ThemeProvider>
50+
<Button>Test</Button>
51+
</ThemeProvider>
52+
</ThemesContext.Provider>
53+
);
54+
const button = screen.getByText('Test');
55+
expect(getComputedStyle(button).color).toBe(LIGHT_MODE_TEXT_COLOR);
56+
});
57+
58+
it('should default to light theme when the browser detects a light mode preference', () => {
59+
render(
60+
<ThemeTestWrapper mode="light">
61+
<ThemesContext.Provider value={{ lightTheme, darkTheme }}>
62+
<ThemeProvider>
63+
<Button>Test</Button>
64+
</ThemeProvider>
65+
</ThemesContext.Provider>
66+
</ThemeTestWrapper>
67+
);
68+
const button = screen.getByText('Test');
69+
expect(getComputedStyle(button).color).toBe(LIGHT_MODE_TEXT_COLOR);
70+
});
71+
72+
it('should default to dark theme when the browser detects a dark mode preference', () => {
73+
render(
74+
<ThemeTestWrapper mode="dark">
75+
<ThemesContext.Provider value={{ lightTheme, darkTheme }}>
76+
<ThemeProvider>
77+
<Button>Test</Button>
78+
</ThemeProvider>
79+
</ThemesContext.Provider>
80+
</ThemeTestWrapper>
81+
);
82+
const button = screen.getByText('Test');
83+
expect(getComputedStyle(button).color).toBe(DARK_MODE_TEXT_COLOR);
84+
});
85+
86+
it('should fallback to using theme prop when used outside of a ThemesContext (for backwards compatibility)', () => {
87+
render(
88+
<ThemeProvider theme={darkTheme}>
89+
<Button>Test</Button>
90+
</ThemeProvider>
91+
);
92+
const button = screen.getByText('Test');
93+
expect(getComputedStyle(button).color).toBe(DARK_MODE_TEXT_COLOR);
94+
});
95+
});

packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@ import { useTheme } from './useTheme';
1111
import { useThemesContext } from './useThemesContext';
1212

1313
/**
14-
* This sets the Material UI theme based on the store.
14+
* This sets the Material UI theme based on the preferred theme type.
1515
*
1616
* @param props
1717
* @param props.children The children of the component.
18-
* @param props.theme The initial theme.
18+
* @param {ThemeOptions} props.theme The initial theme. Optional, use the one from the context if not provided.
19+
*
20+
* @example
21+
*
22+
* import { ThemesContext, ThemeProvider } from 'react-admin';
23+
*
24+
* const App = () => (
25+
* <ThemesContext.Provider value={{ lightTheme, darkTheme }}>
26+
* <ThemeProvider>
27+
* <Button>Test</Button>
28+
* </ThemeProvider>
29+
* </ThemesContext.Provider>
30+
* );
1931
*/
2032
export const ThemeProvider = ({
2133
children,
@@ -50,5 +62,8 @@ export const ThemeProvider = ({
5062

5163
export interface ThemeProviderProps {
5264
children: ReactNode;
65+
/**
66+
* @deprecated Use the `ThemesProvider` component instead.
67+
*/
5368
theme?: RaThemeOptions;
5469
}

packages/ra-ui-materialui/src/layout/Theme/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export interface RaThemeOptions extends MuiThemeOptions {
1111
};
1212
components?: ComponentsTheme;
1313
}
14+
15+
export type ThemeType = 'light' | 'dark';

packages/ra-ui-materialui/src/layout/Theme/useTheme.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useStore } from 'ra-core';
2-
import { RaThemeOptions } from './types';
2+
import { RaThemeOptions, ThemeType } from './types';
33

4-
export type ThemeType = 'light' | 'dark';
54
export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void;
65

76
/**
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import { createTheme, ThemeProvider } from '@mui/material/styles';
3+
4+
/**
5+
* Test utility to simulate a preferred theme mode (light or dark)
6+
*
7+
* Do not use inside a browser.
8+
*
9+
* @example
10+
*
11+
* <ThemeTestWrapper mode="dark">
12+
* <MyComponent />
13+
* <ThemeTestWrapper>
14+
*/
15+
export const ThemeTestWrapper = ({
16+
mode = 'light',
17+
children,
18+
}: ThemeTestWrapperProps): JSX.Element => {
19+
const theme = createTheme();
20+
const ssrMatchMedia = query => ({
21+
matches:
22+
mode === 'dark' && query === '(prefers-color-scheme: dark)'
23+
? true
24+
: false,
25+
});
26+
27+
return (
28+
<ThemeProvider
29+
theme={{
30+
...theme,
31+
components: {
32+
MuiUseMediaQuery: {
33+
defaultProps: {
34+
ssrMatchMedia,
35+
matchMedia: ssrMatchMedia,
36+
},
37+
},
38+
},
39+
}}
40+
>
41+
{children}
42+
</ThemeProvider>
43+
);
44+
};
45+
46+
export interface ThemeTestWrapperProps {
47+
mode: 'light' | 'dark';
48+
children: JSX.Element;
49+
}

0 commit comments

Comments
 (0)