diff --git a/contribute/style-guides/themes.md b/contribute/style-guides/themes.md index 4097ef3915b..a9699e3c8a3 100644 --- a/contribute/style-guides/themes.md +++ b/contribute/style-guides/themes.md @@ -29,7 +29,7 @@ const Foo: React.FunctionComponent = () => { ``` -#### 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 = () => ... 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() + 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: diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 3e269830562..a930c0fe072 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -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) => { 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(); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap index 6b45eb7bf33..3256f88b997 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -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", - }, } } /> diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx index d557139d078..ab89fb3b285 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -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 = Pick>; type Subtract = Omit; +/** + * Mock used in tests + */ +let ThemeContextMock: React.Context | null = null; + // Use Grafana Dark theme by default export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark)); ThemeContext.displayName = 'ThemeContext'; export const withTheme =

(Component: React.ComponentType

) => { const WithTheme: React.FunctionComponent> = 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 {theme => }; + return {theme => }; }; WithTheme.displayName = `WithTheme(${Component.displayName})`; @@ -25,5 +35,15 @@ export const withTheme =

(Component: Rea }; export function useTheme() { - return useContext(ThemeContext); + return useContext(ThemeContextMock || ThemeContext); } + +/** + * Enables theme context mocking + */ +export const mockThemeContext = (theme: Partial) => { + ThemeContextMock = React.createContext(theme as GrafanaTheme); + return () => { + ThemeContextMock = null; + }; +}; diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index dedbd347bca..d9eaced9a73 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -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 };