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.
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.
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.
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.
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.
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!
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');
})