hipstersmoothie / storybook-dark-mode

A storybook addon that lets your users toggle between dark and light mode.
MIT License
435 stars 56 forks source link

Storyshots: TypeError: window.matchMedia is not a function #145

Closed soullivaneuh closed 3 years ago

soullivaneuh commented 3 years ago

Storyshots testing are not working anymore since I installed your addons.

Here is the storyshot test configuration:

// @see https://github.com/storybookjs/storybook/tree/next/addons/storyshots/storyshots-core#using-a-custom-renderer
import initStoryshots from '@storybook/addon-storyshots';
import { render } from '@testing-library/react';

const reactTestingLibrarySerializer: jest.SnapshotSerializerPlugin = {
  // @ts-expect-error unknown var val
  print: (val, serialize, indent) => serialize(val.container.firstChild),
  // eslint-disable-next-line no-prototype-builtins
  test: (val) => val && val.hasOwnProperty('container'),
};

initStoryshots({
  renderer: render,
  snapshotSerializers: [reactTestingLibrarySerializer],
});

Failing with the following stacktrace:

 FAIL  src/storybook.test.ts
  ● Test suite failed to run

    TypeError: window.matchMedia is not a function

      4 |   DocsContainerProps,
      5 | } from '@storybook/addon-docs/blocks'
    > 6 | import { useDarkMode } from 'storybook-dark-mode'
        | ^
      7 | import { themes } from '@storybook/theming';
      8 |
      9 | // @see https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-802018811

      at Object.<anonymous> (node_modules/storybook-dark-mode/src/Tool.tsx:37:35)
      at Object.<anonymous> (node_modules/storybook-dark-mode/src/index.tsx:4:1)
      at Object.<anonymous> (.storybook/components/DocContainer.tsx:6:1)
      at Object.<anonymous> (.storybook/components/index.ts:1:1)
      at Object.<anonymous> (.storybook/preview.ts:4:1)
      at Object.configure [as default] (node_modules/@storybook/addon-storyshots/dist/frameworks/configure.js:75:23)
      at Object.load (node_modules/@storybook/addon-storyshots/dist/frameworks/react/loader.js:24:24)
      at Object.loadFramework [as default] (node_modules/@storybook/addon-storyshots/dist/frameworks/frameworkLoader.js:26:19)
      at Object.testStorySnapshots [as default] (node_modules/@storybook/addon-storyshots/dist/api/index.js:60:39)
      at Object.<anonymous> (src/storybook.test.ts:12:15)

Related file:

import React, { FC } from 'react'
import {
  DocsContainer as BaseContainer,
  DocsContainerProps,
} from '@storybook/addon-docs/blocks'
import { useDarkMode } from 'storybook-dark-mode'
import { themes } from '@storybook/theming';

// @see https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-802018811
export const DocsContainer: FC<DocsContainerProps> = ({ children, context }) => {
  const dark = useDarkMode()

  return (
    <BaseContainer
      context={{
        ...context,
        parameters: {
          ...context.parameters,
          docs: {
            theme: dark ? themes.dark : themes.light
          },
        },
      }}
    >
      {children}
    </BaseContainer>
  );
}

export default null;
soullivaneuh commented 3 years ago

Looks to be related to: https://github.com/testing-library/user-event/issues/426#issuecomment-671832307

Hacking your code with this:

exports.prefersDark = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : false;

"Solves" the issue.

Is that acceptable for a PR?

soullivaneuh commented 3 years ago

After some research, it's look like the error comes from jsdom that does not support all the window functionalities.

I first try to use beforeParse JSDom function to mock the function using testEnvironmentOptions jest option like described here but without any success.

Finally, I followed the Jest recommendation about mocking methods which are not implemented in JSDOM with a setupFilesAfterEnv configuration:

// jest.config.ts
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
  setupFilesAfterEnv: [
    '@testing-library/jest-dom/extend-expect',
    '<rootDir>/jest.mocks.ts',
  ],
  // Rest of the config
};

export default config;
// jest.mocks.ts
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

export default null;

I'm quite surprised there is no other way like a mock library for that case, but it works and I don't think there is anything else to do on your project.

Except maybe, adding a hint to your documentation.

hipstersmoothie commented 3 years ago

If you want to make a or adding a note I'll merge it!