Enable theme context mocking in tests (#20519)

* Enable theme context mocking in tests

* Expose mockThemeContext from grafana/ui

* Add docs

* Update contribute/style-guides/themes.md

Co-Authored-By: Marcus Olsson <olsson.e.marcus@gmail.com>

* Update packages/grafana-ui/src/themes/ThemeContext.tsx

Co-Authored-By: Marcus Olsson <olsson.e.marcus@gmail.com>

* Update contribute/style-guides/themes.md

Co-Authored-By: Marcus Olsson <olsson.e.marcus@gmail.com>

* Docs update

* Update contribute/style-guides/themes.md

Co-Authored-By: Marcus Olsson <olsson.e.marcus@gmail.com>
This commit is contained in:
Dominik Prokop 2019-11-21 16:52:57 +01:00 committed by GitHub
parent fcad439c29
commit bff08ab99f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 404 deletions

View File

@ -29,7 +29,7 @@ const Foo: React.FunctionComponent<FooProps> = () => {
```
#### Using `withTheme` HOC
#### Using `withTheme` higher-order component (HOC)
With this method your component will be automatically wrapped in `ThemeContext.Consumer` and provided with current theme via `theme` prop. Component used with `withTheme` must implement `Themeable` interface.
@ -43,6 +43,36 @@ const Foo: React.FunctionComponent<FooProps> = () => ...
export default withTheme(Foo);
```
### Test components that use ThemeContext
When implementing snapshot tests for components that use the `withTheme` HOC, the snapshot will contain the entire theme object. Any change to the theme renders the snapshot outdated.
To make your snapshot theme independent, use the `mockThemeContext` helper function:
```tsx
import { mockThemeContext } from '@grafana/ui';
import { MyComponent } from './MyComponent';
describe('MyComponent', () => {
let restoreThemeContext;
beforeAll(() => {
// Create ThemeContext mock before any snapshot test is executed
restoreThemeContext = mockThemeContext({ type: GrafanaThemeType.Dark });
});
afterAll(() => {
// Make sure the theme is restored after snapshot tests are performed
restoreThemeContext();
});
it('renders correctyl', () => {
const wrapper = mount(<MyComponent />)
expect(wrapper).toMatchSnapshot();
});
});
```
### Using themes in Storybook
All stories are wrapped with `ThemeContext.Provider` using global decorator. To render `Themeable` component that's not wrapped by `withTheme` HOC you either create a new component in your story:

View File

@ -1,7 +1,9 @@
import React, { ChangeEvent } from 'react';
import { mount } from 'enzyme';
import { GrafanaThemeType } from '@grafana/data';
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
import { colors } from '../../utils';
import { mockThemeContext } from '../../themes/ThemeContext';
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
@ -25,6 +27,15 @@ function getCurrentThresholds(editor: ThresholdsEditor) {
}
describe('Render', () => {
let restoreThemeContext: any;
beforeAll(() => {
restoreThemeContext = mockThemeContext({ type: GrafanaThemeType.Dark });
});
afterAll(() => {
restoreThemeContext();
});
it('should render with base threshold', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();

View File

@ -72,206 +72,7 @@ exports[`Render should render with base threshold 1`] = `
onChange={[Function]}
theme={
Object {
"background": Object {
"dropdown": "#1f1f20",
"pageHeader": "linear-gradient(90deg, #292a2d, #000000)",
"scrollbar": "#343436",
"scrollbar2": "#343436",
},
"border": Object {
"radius": Object {
"lg": "5px",
"md": "3px",
"sm": "2px",
},
"width": Object {
"sm": "1px",
},
},
"breakpoints": Object {
"lg": "992px",
"md": "769px",
"sm": "544px",
"xl": "1200px",
"xs": "0",
},
"colors": Object {
"black": "#000000",
"blue": "#33b5e5",
"blue77": "#1f60c4",
"blue85": "#3274d9",
"blue95": "#5794f2",
"blueBase": "#3274d9",
"blueFaint": "#041126",
"blueLight": "#5794f2",
"blueShade": "#1f60c4",
"body": "#d8d9da",
"bodyBg": "#161719",
"brandDanger": "#e02f44",
"brandPrimary": "#eb7b18",
"brandSuccess": "#299c46",
"brandWarning": "#eb7b18",
"critical": "#e02f44",
"dark1": "#141414",
"dark10": "#424345",
"dark2": "#161719",
"dark3": "#1f1f20",
"dark4": "#212124",
"dark5": "#222426",
"dark6": "#262628",
"dark7": "#292a2d",
"dark8": "#2f2f32",
"dark9": "#343436",
"formDescription": "#9fa7b3",
"formInputBg": "#202226",
"formInputBgDisabled": "#141619",
"formInputBorder": "#343b40",
"formInputBorderActive": "#5794f2",
"formInputBorderHover": "#464c54",
"formInputBorderInvalid": "#e02f44",
"formInputDisabledText": "#9fa7b3",
"formInputFocusOutline": "#1f60c4",
"formInputText": "#c7d0d9",
"formInputTextStrong": "#c7d0d9",
"formInputTextWhite": "#ffffff",
"formLabel": "#9fa7b3",
"formLegend": "#c7d0d9",
"formValidationMessageBg": "#e02f44",
"formValidationMessageText": "#ffffff",
"gray05": "#0b0c0e",
"gray1": "#555555",
"gray10": "#141619",
"gray15": "#202226",
"gray2": "#8e8e8e",
"gray25": "#343b40",
"gray3": "#b3b3b3",
"gray33": "#464c54",
"gray4": "#d8d9da",
"gray5": "#ececec",
"gray6": "#f4f5f8",
"gray7": "#fbfbfb",
"gray70": "#9fa7b3",
"gray85": "#c7d0d9",
"gray95": "#e9edf2",
"gray98": "#f7f8fa",
"grayBlue": "#212327",
"greenBase": "#299c46",
"greenShade": "#23843b",
"headingColor": "#d8d9da",
"inputBlack": "#09090b",
"link": "#d8d9da",
"linkDisabled": "#8e8e8e",
"linkExternal": "#33b5e5",
"linkHover": "#ffffff",
"online": "#299c46",
"orange": "#eb7b18",
"orangeDark": "#ff780a",
"pageBg": "#161719",
"pageHeaderBorder": "#343436",
"purple": "#9933cc",
"queryGreen": "#74e680",
"queryKeyword": "#66d9ef",
"queryOrange": "#eb7b18",
"queryPurple": "#fe85fc",
"queryRed": "#e02f44",
"red": "#d44a3a",
"red88": "#e02f44",
"redBase": "#e02f44",
"redShade": "#c4162a",
"text": "#d8d9da",
"textEmphasis": "#ececec",
"textFaint": "#222426",
"textStrong": "#ffffff",
"textWeak": "#8e8e8e",
"variable": "#32d1df",
"warn": "#f79520",
"white": "#ffffff",
"yellow": "#ecbb13",
},
"height": Object {
"lg": "48px",
"md": "32px",
"sm": "24px",
},
"isDark": true,
"isLight": false,
"name": "Grafana Dark",
"panelHeaderHeight": 28,
"panelPadding": 8,
"shadow": Object {
"pageHeader": "inset 0px -4px 14px #1f1f20",
},
"spacing": Object {
"d": "14px",
"formButtonHeight": 32,
"formFieldsetMargin": "16px",
"formInputAffixPaddingHorizontal": "4px",
"formInputHeight": "32px",
"formInputMargin": "16px",
"formInputPaddingHorizontal": "8px",
"formLabelMargin": "0 0 4px 0",
"formLabelPadding": "0 0 0 2px",
"formLegendMargin": "0 0 16px 0",
"formMargin": "32px",
"formSpacingBase": 8,
"formValidationMessagePadding": "4px 8px",
"gutter": "30px",
"insetSquishMd": "4px 8px",
"lg": "24px",
"md": "16px",
"sm": "8px",
"xl": "32px",
"xs": "4px",
"xxs": "2px",
},
"type": "dark",
"typography": Object {
"fontFamily": Object {
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
},
"heading": Object {
"h1": "28px",
"h2": "24px",
"h3": "21px",
"h4": "18px",
"h5": "16px",
"h6": "14px",
},
"lineHeight": Object {
"lg": 1.5,
"md": 1.3333333333333333,
"sm": 1.1,
"xs": 1,
},
"link": Object {
"decoration": "none",
"hoverDecoration": "none",
},
"size": Object {
"base": "13px",
"lg": "18px",
"md": "14px",
"root": "14px",
"sm": "12px",
"xs": "10px",
},
"weight": Object {
"bold": 600,
"light": 300,
"regular": 400,
"semibold": 500,
},
},
"zIndex": Object {
"dropdown": "1000",
"modal": "1050",
"modalBackdrop": "1040",
"navbarFixed": "1020",
"sidemenu": "1025",
"tooltip": "1030",
"typeahead": "1060",
},
}
}
>
@ -283,206 +84,7 @@ exports[`Render should render with base threshold 1`] = `
onChange={[Function]}
theme={
Object {
"background": Object {
"dropdown": "#1f1f20",
"pageHeader": "linear-gradient(90deg, #292a2d, #000000)",
"scrollbar": "#343436",
"scrollbar2": "#343436",
},
"border": Object {
"radius": Object {
"lg": "5px",
"md": "3px",
"sm": "2px",
},
"width": Object {
"sm": "1px",
},
},
"breakpoints": Object {
"lg": "992px",
"md": "769px",
"sm": "544px",
"xl": "1200px",
"xs": "0",
},
"colors": Object {
"black": "#000000",
"blue": "#33b5e5",
"blue77": "#1f60c4",
"blue85": "#3274d9",
"blue95": "#5794f2",
"blueBase": "#3274d9",
"blueFaint": "#041126",
"blueLight": "#5794f2",
"blueShade": "#1f60c4",
"body": "#d8d9da",
"bodyBg": "#161719",
"brandDanger": "#e02f44",
"brandPrimary": "#eb7b18",
"brandSuccess": "#299c46",
"brandWarning": "#eb7b18",
"critical": "#e02f44",
"dark1": "#141414",
"dark10": "#424345",
"dark2": "#161719",
"dark3": "#1f1f20",
"dark4": "#212124",
"dark5": "#222426",
"dark6": "#262628",
"dark7": "#292a2d",
"dark8": "#2f2f32",
"dark9": "#343436",
"formDescription": "#9fa7b3",
"formInputBg": "#202226",
"formInputBgDisabled": "#141619",
"formInputBorder": "#343b40",
"formInputBorderActive": "#5794f2",
"formInputBorderHover": "#464c54",
"formInputBorderInvalid": "#e02f44",
"formInputDisabledText": "#9fa7b3",
"formInputFocusOutline": "#1f60c4",
"formInputText": "#c7d0d9",
"formInputTextStrong": "#c7d0d9",
"formInputTextWhite": "#ffffff",
"formLabel": "#9fa7b3",
"formLegend": "#c7d0d9",
"formValidationMessageBg": "#e02f44",
"formValidationMessageText": "#ffffff",
"gray05": "#0b0c0e",
"gray1": "#555555",
"gray10": "#141619",
"gray15": "#202226",
"gray2": "#8e8e8e",
"gray25": "#343b40",
"gray3": "#b3b3b3",
"gray33": "#464c54",
"gray4": "#d8d9da",
"gray5": "#ececec",
"gray6": "#f4f5f8",
"gray7": "#fbfbfb",
"gray70": "#9fa7b3",
"gray85": "#c7d0d9",
"gray95": "#e9edf2",
"gray98": "#f7f8fa",
"grayBlue": "#212327",
"greenBase": "#299c46",
"greenShade": "#23843b",
"headingColor": "#d8d9da",
"inputBlack": "#09090b",
"link": "#d8d9da",
"linkDisabled": "#8e8e8e",
"linkExternal": "#33b5e5",
"linkHover": "#ffffff",
"online": "#299c46",
"orange": "#eb7b18",
"orangeDark": "#ff780a",
"pageBg": "#161719",
"pageHeaderBorder": "#343436",
"purple": "#9933cc",
"queryGreen": "#74e680",
"queryKeyword": "#66d9ef",
"queryOrange": "#eb7b18",
"queryPurple": "#fe85fc",
"queryRed": "#e02f44",
"red": "#d44a3a",
"red88": "#e02f44",
"redBase": "#e02f44",
"redShade": "#c4162a",
"text": "#d8d9da",
"textEmphasis": "#ececec",
"textFaint": "#222426",
"textStrong": "#ffffff",
"textWeak": "#8e8e8e",
"variable": "#32d1df",
"warn": "#f79520",
"white": "#ffffff",
"yellow": "#ecbb13",
},
"height": Object {
"lg": "48px",
"md": "32px",
"sm": "24px",
},
"isDark": true,
"isLight": false,
"name": "Grafana Dark",
"panelHeaderHeight": 28,
"panelPadding": 8,
"shadow": Object {
"pageHeader": "inset 0px -4px 14px #1f1f20",
},
"spacing": Object {
"d": "14px",
"formButtonHeight": 32,
"formFieldsetMargin": "16px",
"formInputAffixPaddingHorizontal": "4px",
"formInputHeight": "32px",
"formInputMargin": "16px",
"formInputPaddingHorizontal": "8px",
"formLabelMargin": "0 0 4px 0",
"formLabelPadding": "0 0 0 2px",
"formLegendMargin": "0 0 16px 0",
"formMargin": "32px",
"formSpacingBase": 8,
"formValidationMessagePadding": "4px 8px",
"gutter": "30px",
"insetSquishMd": "4px 8px",
"lg": "24px",
"md": "16px",
"sm": "8px",
"xl": "32px",
"xs": "4px",
"xxs": "2px",
},
"type": "dark",
"typography": Object {
"fontFamily": Object {
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
},
"heading": Object {
"h1": "28px",
"h2": "24px",
"h3": "21px",
"h4": "18px",
"h5": "16px",
"h6": "14px",
},
"lineHeight": Object {
"lg": 1.5,
"md": 1.3333333333333333,
"sm": 1.1,
"xs": 1,
},
"link": Object {
"decoration": "none",
"hoverDecoration": "none",
},
"size": Object {
"base": "13px",
"lg": "18px",
"md": "14px",
"root": "14px",
"sm": "12px",
"xs": "10px",
},
"weight": Object {
"bold": 600,
"light": 300,
"regular": 400,
"semibold": 500,
},
},
"zIndex": Object {
"dropdown": "1000",
"modal": "1050",
"modalBackdrop": "1040",
"navbarFixed": "1020",
"sidemenu": "1025",
"tooltip": "1030",
"typeahead": "1060",
},
}
}
/>

View File

@ -3,19 +3,29 @@ import hoistNonReactStatics from 'hoist-non-react-statics';
import { getTheme } from './getTheme';
import { Themeable } from '../types/theme';
import { GrafanaThemeType } from '@grafana/data';
import { GrafanaTheme, GrafanaThemeType } from '@grafana/data';
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T, K> = Omit<T, keyof K>;
/**
* Mock used in tests
*/
let ThemeContextMock: React.Context<GrafanaTheme> | null = null;
// Use Grafana Dark theme by default
export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
ThemeContext.displayName = 'ThemeContext';
export const withTheme = <P extends Themeable, S extends {} = {}>(Component: React.ComponentType<P>) => {
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
/**
* If theme context is mocked, let's use it instead of the original context
* This is used in tests when mocking theme using mockThemeContext function defined below
*/
const ContextComponent = ThemeContextMock || ThemeContext;
// @ts-ignore
return <ThemeContext.Consumer>{theme => <Component {...props} theme={theme} />}</ThemeContext.Consumer>;
return <ContextComponent.Consumer>{theme => <Component {...props} theme={theme} />}</ContextComponent.Consumer>;
};
WithTheme.displayName = `WithTheme(${Component.displayName})`;
@ -25,5 +35,15 @@ export const withTheme = <P extends Themeable, S extends {} = {}>(Component: Rea
};
export function useTheme() {
return useContext(ThemeContext);
return useContext(ThemeContextMock || ThemeContext);
}
/**
* Enables theme context mocking
*/
export const mockThemeContext = (theme: Partial<GrafanaTheme>) => {
ThemeContextMock = React.createContext(theme as GrafanaTheme);
return () => {
ThemeContextMock = null;
};
};

View File

@ -1,5 +1,5 @@
import { ThemeContext, withTheme, useTheme } from './ThemeContext';
import { ThemeContext, withTheme, useTheme, mockThemeContext } from './ThemeContext';
import { getTheme, mockTheme } from './getTheme';
import { selectThemeVariant } from './selectThemeVariant';
export { stylesFactory } from './stylesFactory';
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme };
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext };