Home > Software engineering >  How to mock a window variable in a single story
How to mock a window variable in a single story

Time:11-26

I am using Navigator online in my React Application to determine if the client is online. I am showing an offline Fallback Component when the client is offline. For now I have made the Component Pure - so I can display it in Storybook by passing the online status as property. But this is not always a suitable solution.

So I wonder how can you mock global (window) variables for a single story in Storybook? The only - very dirty solution - I found looks as following:

ClientOffline.decorators = [
  (Story) => {
    const navigatiorInitally = global.navigator

    Object.defineProperty(global, 'navigator', {
      value: { onLine: false },
      writable: false,
    })

    useEffect(() => {
      return () => {
        Object.defineProperty(global, 'navigator', {
          value: navigatiorInitally,
          writable: false,
        })
        // eslint-disable-next-line
       location.reload() //needed because otherwilse other stories throw an error
      }
    }, [])

    return <Story />
  },
]

Does anybody have a more straightforward solution?

CodePudding user response:

In this answer I assume that all side effects (and communicating with Navigator is a side effect) are separated from component body into hooks.

Let's assume that you have component that looks like this:

function AwesomeComponent () {
  let [connectionStatus, setConnectionStatus] = useState('unknown');
  useEffect(function checkConnection() {
    if (typeof window === 'undefined' || window.navigator.onLine) setConnectionStatus('online'); 
    else setConnectionStatus('offline');
  }, []);
  
  if (connectionStatus === 'online') return <OnlineComponent/>
  if (connectionStatus === 'offline') return <FallbackComponent/>
  return null;
}

Your can extract your hook into separate module. In my example it would be both - state and effect.

function useConnectionStatus() {
  let [connectionStatus, setConnectionStatus] = useState("unknown");
  useEffect(function checkConnection() {
    if (typeof window === "undefined" || window.navigator.onLine)
      setConnectionStatus("online");
    else setConnectionStatus("offline");
  }, []);
  return connectionStatus;
}

This way you separate logic from presentation and can mock individual modules. Storybook have guide to mock individual modules and configure them per story. Better consult the docs, since software is changing and in time something may be done in some other way.

Let's say, you named your file useConnectionStatus.js. To mock it, you will have to create __mock__ folder, create your mocked module there. For example it would be something like this:

// __mock__/useConnectionStatus.js
let connectionStatus = 'online';
export default function useConnectionStatus(){
  return connectionStatus;
}

export function decorator(story, { parameters }) {
  if (parameters && parameters.offline) {
    connectionStatus = 'offline';
  }
  return story();  
}

Next step is to modify webpack config to use your mocked hook instead of actual hook. Documentation provide a way to do this from your .storybook/main.js. At the time of writing it is done like this:

// .storybook/main.js

module.exports = {
  // Your Storybook configuration

  webpackFinal: (config) => {
    config.resolve.alias['path/to/original/useConnectionStatus'] = require.resolve('../__mocks__/useConnectionStatus.js');
    return config;
  },
};

Now, decorate your previews with our new decorator, and you will be able to set configuration for every specific story separately.

// inside your story
Story.parameters = {
  offline: true
}
  • Related