ReactTraining / react-media

CSS media queries for React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to set window size in jest test?

grantspilsbury opened this issue · comments

How should I setup a test where a different component is rendered depending on screen size?

This is the component:

<Media query={`(max-width: ${theme.screenSizes.sm.max}px)`}>
  {matches => (matches ? this.renderMobileView() : this.renderDesktopView())}
</Media>

It only executes renderDesktopView but I'd like to also test this.renderMobileView().
I've added this to my test global.window.innerWidth = 300; with no success

adding
global.window.dispatchEvent(new Event('resize'));
after the innerWidth set also doesn't work

to note, I'm using enzyme's shallow()

also, maybe a separate issue, I'm not sure, but whenever I try to render a component with in it, I see a console error:
React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

The tests here manually mock the window.matchMedia API. Would something like that work for you?

window.matchMedia = {
  matches: true, // <-- Set according to what you want to test
  addListener: () => {},
  removeListener: () => {}
}

...run your test

I wasn't able to test if that works or not. The Media element just won't render and it gives the error I posted above. Below is the code I tried:

import * as React from 'react';
import * as Enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import Media from 'react-media';

Enzyme.configure({ adapter: new Adapter() });
const { mount } = Enzyme;

describe('Component', () => {
    it('should render the dang thing', () => {
        const component = mount(
            <div>
                <Media query="(max-width: 721px)">
                    { matches => matches ? (
                        <div>This matches</div>
                    ) : <div>This doesn't match</div> }
                </Media>
            </div>
        );
        expect(component).toMatchSnapshot();
    });
});

And the error I receive when trying to run the test:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports

@ethanova Your snipped actually looks good to me, so I'm guessing it's a setup issue.

Did you try import * as Media from 'react-media'?

@edorivai That helped me get in the right direction. Should it always be used as * as Media or is it just for testing that its needed? It works fine as just import Media for actual usage. Should the readme be updated to reflect all this somehow?

Working test:

import * as Media from 'react-media';
describe('Component monster test rally', () => {
    it('should render the dang thing', () => {
        window.matchMedia = (matches: string) => ({
            matches: true, // <-- Set according to what you want to test
            addListener: () => {},
            removeListener: () => {},
            media: ''
        });
        const component = mount(
            <div>
                <Media targetWindow={window} query="(max-width: 721px)">
                    { matches => matches ? (
                        <div>This matches</div>
                    ) : <div>This doesn't match</div> }
                </Media>
            </div>
        );
        expect(component).toMatchSnapshot();
    });
    it('should render the dang thing with the non-match', () => {
        window.matchMedia = (matches: string) => ({
            matches: false, // <-- Set according to what you want to test
            addListener: () => {},
            removeListener: () => {},
            media: ''
        });
        const component = mount(
            <div>
                <Media targetWindow={window} query="(max-width: 721px)">
                    { matches => matches ? (
                        <div>This matches</div>
                    ) : <div>This doesn't match</div> }
                </Media>
            </div>
        );
        expect(component).toMatchSnapshot();
    });
});``` 

I guess it depends on your setup. If you're using "plain es6" you shouldn't need the * as. Maybe if you share a minimal reproduction repo I can help you out further, but just from your test file I cannot see why you'd need the * as.

Ah dang, whatever the setup is for this project makes it so that the test requires * as Media but when changed in actual components it breaks the React App, so I can't keep the import at * as Media. Which means that I can't keep the test 😢

Is it a typescript project?

In that case take a look at the allowSyntheticDefaultImports flag which you should be able to set in your tsconfig.json

Looks like that might have broken some other things TypeError: Adapter is not a constructor

3 | import * as Adapter from 'enzyme-adapter-react-16';
4 | 
5 | Enzyme.configure({ adapter: new Adapter() });```

Yeah, if you set that flag, you won't need any * as anymore 😄 :

import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

@ethanova It seems to me that your issues are not react-media specific anymore, so I'm going to close this issue.

@grantspilsbury Please let me know if the proposed solution doesn't work for you!

@edorivai I am experiencing another issue, close to this one, maybe you can help me out.

I have component which has two <Media> components, one for mobile media query and another for desktop.

When I test it with jest, using your snippet above (matches: true,) it renders both components, obviously. How can I test, for example, mobile view, so only one component, the one for mobile query, is rendered? I tried solution with setting window.innerWidth and then triggering resize event, but it doesn't help.

I found a solution, maybe not the prettiest one, but it works. Method matchMedia takes media-query string as a parameter, so I am just matching it to the one I want to test:

global.matchMedia = media => ({
      addListener: () => {},
      removeListener: () => {},
      matches: media === '(min-width: 545px)',
});

Hi Khrystyna,
WHat is media in matches: media === '(min-width: 545px)',.
Thank you :)

@Neelimaordre It's on the first line of the snippet; the argument passed into the matchMedia function.

@Khrystyna basically overwrites the original window.matchMedia with a hardcoded one, to define that the media query only matches when the query is equal to 'min-width: 545px'.

Either this or this could give you some inspiration

@Khrystyna can you send a screenshot of how you solved it using the matchMedia function?

I found a solution, maybe not the prettiest one, but it works. Method matchMedia takes media-query string as a parameter, so I am just matching it to the one I want to test:

global.matchMedia = media => ({
      addListener: () => {},
      removeListener: () => {},
      matches: media === '(min-width: 545px)',
});

thanks! worked perfectly! new stuff learned ;)