Can't get Jest to work with Styled Components which contain theming

13,655

Solution 1

I've managed to fix this problem with help of a colleague. I had another component extend the SlideTitle component which broke the test:

const SlideSubtitle = SlideTitle.extend`
  font-family: ${props => 
  props.theme.LooksBrowser.SlideSubtitle.FontFamily};
`;

I refactored my code to this:

const SlideTitlesSharedStyling = styled.p`
  flex: 0 0 auto;
  text-transform: uppercase;
  line-height: 1;
  color: ${props => props.color};
  font-size: ${PXToVW(52)};
`;

const SlideTitle = SlideTitlesSharedStyling.extend`
  font-family: ${props => props.theme.LooksBrowser.SlideTitle.FontFamily};
`;

const SlideSubtitle = SlideTitlesSharedStyling.extend`
  font-family: ${props => props.theme.LooksBrowser.SlideSubtitle.FontFamily};
`;

And my tests starting passing again!

Solution 2

Wrapping the ThemeProvider around the component and passing the theme object to it, works fine for me.

import React from 'react';
import { ThemeProvider } from 'styled-components';
import { render, cleanup } from '@testing-library/react';

import Home from '../Home';
import { themelight } from '../../Layout/theme';

afterEach(cleanup);

test('home renders correctly', () => {
  let { getByText } = render(
    <ThemeProvider theme={themelight}>
      <Home name={name} />
    </ThemeProvider>
  );

  getByText('ANURAG HAZRA');
})
Share:
13,655

Related videos on Youtube

DavidWorldpeace
Author by

DavidWorldpeace

Updated on June 04, 2022

Comments

  • DavidWorldpeace
    DavidWorldpeace almost 2 years

    The Problem

    I've been using Jest and Enzyme to write tests for my React components build with the awesome Styled Components library.

    However, since I've implemented theming all my tests are breaking. Let me give you an example.

    This is the code of my LooksBrowser component (I've removed all of my imports and prop-types to make it a little more readable):

    const LooksBrowserWrapper = styled.div`
      position: relative;
      padding: 0 0 56.25%;
    `;
    
    const CurrentSlideWrapper = styled.div`
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
    `;
    
    const NextSlideWrapper = CurrentSlideWrapper.extend`
      z-index: 1;
    `;
    
    const SlideImage = styled.img`
      display: block;
      width: 100%;
    `;
    
    const SlideText = styled.div`
      display: flex;
      position: absolute;
      top: 25%;
      left: ${PXToVW(72)};
      height: 25%;
      flex-direction: column;
      justify-content: center;
    `;
    
    const SlideTitle = styled.p`
      flex: 0 0 auto;
      text-transform: uppercase;
      line-height: 1;
      color: ${props => props.color};
      font-family: ${props => props.theme.LooksBrowser.SlideTitle.FontFamily};
      font-size: ${PXToVW(52)};
    `;
    
    const SlideSubtitle = SlideTitle.extend`
      font-family: ${props => props.theme.LooksBrowser.SlideSubtitle.FontFamily};
    `;
    
    export default class LooksBrowser extends React.Component {
      state = {
        currentSlide: {
          imageURL: this.props.currentSlide.imageURL,
          index: this.props.currentSlide.index,
          subtitle: this.props.currentSlide.subtitle,
          textColor: this.props.currentSlide.textColor,
          title: this.props.currentSlide.title
        },
        nextSlide: {
          imageURL: this.props.nextSlide.imageURL,
          index: this.props.nextSlide.index,
          subtitle: this.props.nextSlide.subtitle,
          textColor: this.props.nextSlide.textColor,
          title: this.props.nextSlide.title
        },
        nextSlideIsLoaded: false
      };
    
      componentDidMount() {
        this.setVariables();
      }
    
      componentWillReceiveProps(nextProps) {
        // Only update the state when the nextSlide data is different than the current nextSlide data
        // and when the LooksBrowser component isn't animating
        if (this.props.nextSlide.imageURL !== nextProps.nextSlide.imageURL && !this.isAnimating) {
          this.setState(prevState => update(prevState, {
            nextSlide: {
              imageURL: { $set: nextProps.nextSlide.imageURL },
              index: { $set: nextProps.nextSlide.index },
              subtitle: { $set: nextProps.nextSlide.subtitle },
              textColor: { $set: nextProps.nextSlide.textColor },
              title: { $set: nextProps.nextSlide.title }
            }
          }));
        }
      }
    
      componentDidUpdate() {
        if (!this.isAnimating) {
          if (this.state.nextSlide.imageURL !== '' && this.state.nextSlideIsLoaded) {
            // Only do the animation when the nextSlide is done loading and it defined inside of the state
            this.animateToNextSlide();
          } else if (this.state.currentSlide.imageURL !== this.props.nextSlide.imageURL && this.state.nextSlide.imageURL !== this.props.nextSlide.imageURL) {
            // This usecase is for when the LooksBrowser already received another look while still being in an animation
            // After the animation is done it checks if the new nextSlide data is different than the current currentSlide data
            // And also checks if the current nextSlide state data is different than the new nextSlide data
            // If so, it updates the nextSlide part of the state so that in the next render animateToNextSlide will be called
            this.setState(prevState => update(prevState, {
              nextSlide: {
                imageURL: { $set: this.props.nextSlide.imageURL },
                index: { $set: this.props.nextSlide.index },
                subtitle: { $set: this.props.nextSlide.subtitle },
                textColor: { $set: this.props.nextSlide.textColor },
                title: { $set: this.props.nextSlide.title }
              }
            }));
          } else if (!this.state.nextSlideIsLoaded) {
            // Reset currentSlide position to prevent 'flash'
            TweenMax.set(this.currentSlide, {
              x: '0%'
            });
          }
        }
      }
    
      setVariables() {
        this.TL = new TimelineMax();
        this.isAnimating = false;
      }
    
      nextSlideIsLoaded = () => {
        this.setState(prevState => update(prevState, {
          nextSlideIsLoaded: { $set: true }
        }));
      };
    
      animateToNextSlide() {
        const AnimateForward = this.state.currentSlide.index < this.state.nextSlide.index;
        this.isAnimating = true;
    
        this.TL.clear();
        this.TL
          .set(this.currentSlide, {
            x: '0%'
          })
          .set(this.nextSlide, {
            x: AnimateForward ? '100%' : '-100%'
          })
          .to(this.currentSlide, 0.7, {
            x: AnimateForward ? '-100%' : '100%',
            ease: Quad.easeInOut
          })
          .to(this.nextSlide, 0.7, {
            x: '0%',
            ease: Quad.easeInOut,
            onComplete: () => {
              this.isAnimating = false;
              this.setState(prevState => update(prevState, {
                currentSlide: {
                  imageURL: { $set: prevState.nextSlide.imageURL },
                  index: { $set: prevState.nextSlide.index },
                  subtitle: { $set: prevState.nextSlide.subtitle },
                  textColor: { $set: prevState.nextSlide.textColor },
                  title: { $set: prevState.nextSlide.title }
                },
                nextSlide: {
                  imageURL: { $set: '' },
                  index: { $set: 0 },
                  subtitle: { $set: '' },
                  textColor: { $set: '' },
                  title: { $set: '' }
                },
                nextSlideIsLoaded: { $set: false }
              }));
            }
          }, '-=0.7');
      }
    
      render() {
        return(
          <LooksBrowserWrapper>
            <CurrentSlideWrapper innerRef={div => this.currentSlide = div} >
              <SlideImage src={this.state.currentSlide.imageURL} alt={this.state.currentSlide.title} />
              <SlideText>
                <SlideTitle color={this.state.currentSlide.textColor}>{this.state.currentSlide.title}</SlideTitle>
                <SlideSubtitle color={this.state.currentSlide.textColor}>{this.state.currentSlide.subtitle}</SlideSubtitle>
              </SlideText>
            </CurrentSlideWrapper>
            {this.state.nextSlide.imageURL &&
              <NextSlideWrapper innerRef={div => this.nextSlide = div}>
                <SlideImage src={this.state.nextSlide.imageURL} alt={this.state.nextSlide.title} onLoad={this.nextSlideIsLoaded} />
                <SlideText>
                  <SlideTitle color={this.state.nextSlide.textColor}>{this.state.nextSlide.title}</SlideTitle>
                  <SlideSubtitle color={this.state.nextSlide.textColor}>{this.state.nextSlide.subtitle}</SlideSubtitle>
                </SlideText>
              </NextSlideWrapper>
            }
          </LooksBrowserWrapper>
        );
      }
    }
    

    Then now my tests for my LooksBrowser component (following is the full code):

    import React from 'react';
    import Enzyme, { mount } from 'enzyme';
    import renderer from 'react-test-renderer';
    import Adapter from 'enzyme-adapter-react-16';
    import 'jest-styled-components';
    import LooksBrowser from './../src/app/components/LooksBrowser/LooksBrowser';
    
    Enzyme.configure({ adapter: new Adapter() });
    
    test('Compare snapshots', () => {
      const Component = renderer.create(<LooksBrowser currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />);
    
      const Tree = Component.toJSON();
      expect(Tree).toMatchSnapshot();
    });
    
    test('Renders without crashing', () => {
      mount(<LooksBrowser currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />);
    });
    
    test('Check if componentDidUpdate gets called', () => {
      const spy = jest.spyOn(LooksBrowser.prototype, 'componentDidUpdate');
      const Component = mount(<LooksBrowser currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />);
    
      Component.setProps({ nextSlide: { imageURL: 'http://localhost:3001/img/D2_VW_SPW.jpg', index: 2, subtitle: 'Don\'t walk here at night', title: 'What A View', textColor: '#fff' } });
      expect(spy).toBeCalled();
    });
    
    test('Check if animateToNextSlide gets called', () => {
      const spy = jest.spyOn(LooksBrowser.prototype, 'animateToNextSlide');
      const Component = mount(<LooksBrowser currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />);
    
      Component.setProps({ nextSlide: { imageURL: 'http://localhost:3001/img/D2_VW_SPW.jpg', index: 2, subtitle: 'Don\'t walk here at night', title: 'What A View', textColor: '#fff' } });
      Component.setState({ nextSlideIsLoaded: true });
      expect(spy).toBeCalled();
    });
    

    Before I implemented theming all of these tests were passing. After I implemented theming I get the following error from every test:

    TypeError: Cannot read property 'SlideTitle' of undefined
    
      44 |   line-height: 1;
      45 |   color: ${props => props.color};
    > 46 |   font-family: ${props => props.theme.LooksBrowser.SlideTitle.FontFamily};
      47 |   font-size: ${PXToVW(52)};
      48 | `;
      49 |
    

    Ok, makes sense. The theme is not defined.

    Solution One

    So after some googling I found the following 'solution':

    https://github.com/styled-components/jest-styled-components#theming

    The recommended solution is to pass the theme as a prop: const wrapper = shallow(<Button theme={theme} />)

    So I add the following code to my LooksBrowser test file:

    const theme = {
      LooksBrowser: {
        SlideTitle: {
          FontFamily: 'Futura-Light, sans-serif'
        },
        SlideSubtitle: {
          FontFamily: 'Futura-Demi, sans-serif'
        }
      }
    };
    

    And edit all my tests to pass the theme manually. For example:

    test('Compare snapshots', () => {
      const Component = renderer.create(<LooksBrowser theme={theme} currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />);
    
      const Tree = Component.toJSON();
      expect(Tree).toMatchSnapshot();
    });
    

    After I've done this I run my tests again. Still the same error occurs.

    Solution Two

    I decided to wrap my components inside of the Styled Components ThemeProvider. This fixes the errors from my Compare snapshots and Renders without crashing tests.

    However, since I'm also changing the props/state of my LooksBrowser component and testing the results, this doesn't work anymore. This is because the setProps and setState functions only can be used on the root/wrapper component.

    So wrapping my components in the ThemeProvider component isn't a valid solution either.

    Solution Three

    I decided to try the log the props of one of my Styled Components. So I changed my SlideTitle subcomponent to this:

    const SlideTitle = styled.p`
      flex: 0 0 auto;
      text-transform: uppercase;
      line-height: 1;
      color: ${props => {
        console.log(props.theme.LooksBrowser.SlideTitle.FontFamily);
        return props.color;
      }};
      font-family: ${props => props.theme.LooksBrowser.SlideTitle.FontFamily};
      font-size: ${PXToVW(52)};
    `;
    

    I get the following error:

    TypeError: Cannot read property 'SlideTitle' of undefined
    
      44 |   line-height: 1;
      45 |   color: ${props => {
    > 46 |     console.log(props.theme.LooksBrowser.SlideTitle.FontFamily);
      47 |     return props.color;
      48 |   }};
      49 |   font-family: ${props => props.theme.LooksBrowser.SlideTitle.FontFamily};
    

    Ok, seems like the entire theme prop is just empty. Let's try manually passing the theme to the SlideTitle (which is a horrendous solution btw, it would mean I need to pass my theme manually to every Styled Component in my entire project).

    So I added the following code:

    <SlideTitle theme={this.props.theme} color{this.state.currentSlide.textColor}>{this.state.currentSlide.title}</SlideTitle>
    

    And I run my tests again. I see the following line in my terminal:

    console.log src/app/components/LooksBrowser/LooksBrowser.js:46
    Futura-Light, sans-serif
    

    Yeah, this is what I'm looking for! I scroll down and see the same error again... yikes.

    Solution Four

    In the Jest Styled Components documentation I also saw the following solution:

    const shallowWithTheme = (tree, theme) => {
      const context = shallow(<ThemeProvider theme={theme} />)
        .instance()
        .getChildContext()
      return shallow(tree, { context })
    }
    
    const wrapper = shallowWithTheme(<Button />, theme)
    

    Ok, looks promising. So I added this function to my test file and updated my Check if componentDidUpdate gets called test to this:

    test('Check if componentDidUpdate gets called', () => {
      const spy = jest.spyOn(LooksBrowser.prototype, 'componentDidUpdate');
      const Component = shallowWithTheme(<LooksBrowser currentSlide={{ imageURL: 'http://localhost:3001/img/D1_VW_SPW.jpg', index: 1, subtitle: 'Where amazing happens', title: 'The United States of America', textColor: '#fff' }} nextSlide={{ imageURL: '', index: 0, subtitle: '', title: '', textColor: '' }} />, Theme);
    
      Component.setProps({ nextSlide: { imageURL: 'http://localhost:3001/img/D2_VW_SPW.jpg', index: 2, subtitle: 'Don\'t walk here at night', title: 'What A View', textColor: '#fff' } });
      expect(spy).toBeCalled();
    });
    

    I run the test and get the following error:

    Error
    Cannot tween a null target. thrown
    

    Makes sense since I'm using shallow. So I change the function to use mount instead of shallow:

    const shallowWithTheme = (tree, theme) => {
      const context = mount(<ThemeProvider theme={theme} />)
        .instance()
        .getChildContext()
      return mount(tree, { context })
    }
    

    I run my test again and voila:

    TypeError: Cannot read property 'SlideTitle' of undefined

    I'm officially out of ideas.

    If anyone has any thoughts on this it would be greatly appreciated! Thanks in advance everyone.

    EDIT

    I've now also opened two issues on Github, one in the Styled Components repo and one in the Jest Styled Components repo.

    I've tried all of the solutions provided there so far without any avail. So if anyone here has any ideas on how to fix this problem please share them!

  • gkri
    gkri over 4 years
    This worked from me! Only difference is that I have a more complex ThemeProvider setup (gist.github.com/eyerean/84210966cc8822d88260c3c08c891981), but I can still wrap the components in my tests with it and the tests pass.