Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!

like image 792
DavidWorldpeace Avatar asked Jan 29 '18 14:01

DavidWorldpeace


People also ask

Why is styled-components not working?

Possible reasons. There are several common reasons for this to happen: You have several applications that are using styled-components running on the same page (e.g., several entry points in webpack are loaded on the same page) You have another styled-components library somewhere in your dependencies.

Can't resolve React is in styled-components?

To solve the error "Module not found: Error: Can't resolve 'styled-components'", make sure to install the styled-components package by opening your terminal in your project's root directory and running the command npm install styled-components and restart your development server.

Can you use Classnames with styled-components?

The styled method works perfectly on all of your own or any third-party component, as long as they attach the passed className prop to a DOM element.


2 Answers

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');
})
like image 57
Anurag Hazra Avatar answered Oct 09 '22 01:10

Anurag Hazra


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!

like image 35
DavidWorldpeace Avatar answered Oct 09 '22 01:10

DavidWorldpeace