There have been a lot of workaround and a lot of confused users around how to test theming.
https://github.com/styled-components/styled-components/issues/624
The general problem is that wrapping a StyledComponent element in the ThemeProvider is not acceptable as it won't allow access to all enzyme methods for example.
Thus it's desirable to write a quick helper that creates a theme context or sth similar. Maybe it should also just be solved using some documentation.
For better or for worse, I'm going to lock the issue on the SC repo and continue a discussion for a solution here, since this can be solved using a simple helper and some text, instead of a huge, elongated issue on the styled-components repo, where people don't immediately spot the right workaround.
cc @MicheleBertoli (sorry 馃槅 let's see if this goes well)
Hi @philpl , what do you mean add theme prop in enzyme in https://github.com/styled-components/styled-components/issues/624#issuecomment-320680806? Could you please give me a example?
I tried to add theme as prop in the withTheme(ListItem) HOC component but still not working:
# ListItem.js
export default withTheme(ListItem)
# ListItem.test.js
import ListItem from './ListItem'
const wrapper = mount(
<ListItem
id={summary.number}
theme={injectTheme}
/>);
@JimmyLv This approach indeed doesn't work for withTheme since it requires a theme context, as otherwise it's not serving its purpose. I suggest you use the unwrapped version of your component in your test to pass the theme prop manually for now.
But this is a nice example of why a helper for adding the theme context would be nice and can't always be replaced by just passing the theme prop manually.
Thanks for opening the issue @philpl, I agree this repo is the right place to discuss testing and I'm more than happy to think about a solution.
How do you pass the theme prop?
Hello @chhuang,
Suppose you are testing the following component (which expects a theme):
const Button = styled.button`
color: ${props => props.theme.main};
`
const theme = {
main: 'mediumseagreen',
}
If you pass a theme prop to it:
test('theming', () => {
const tree = renderer.create(
<Button theme={theme} />
).toJSON()
expect(tree).toMatchSnapshot()
})
it works as expected.
Thanks @MicheleBertoli!
Is it possible to pass the theme to the nested components? e.g. the test will break if I have another component using theme inside Button.
In that case, you can use the ThemeProvider.
For example, suppose you have a Wrapper component:
const Wrapper = ({ children }) => <div>{children}</div>
The following test works as expected:
test('theming', () => {
const tree = renderer.create(
<ThemeProvider theme={theme}>
<Wrapper>
<Wrapper>
<Wrapper>
<Button />
</Wrapper>
</Wrapper>
</Wrapper>
</ThemeProvider>
).toJSON()
expect(tree).toMatchSnapshot()
})
I hope this helps, @chhuang.
@MicheleBertoli one problem with passing the theme as a prop + taking a snapshot is that the whole theme gets caught in the snapshot. If the theme changes (a new color is added) the test will fail, which makes the test very fragile.
@carlkenne that's true only if you are rendering the tree using Enzyme, but it doesn't apply to the react-test-renderer - which is the recommended way for generating snapshots.
@carlkenne if you are using Enzyme, you can try enzyme-to-json. react-test-renderer is fine.
@philpl new to open source, so forgive me for butting in, but is it unreasonable or undesirable to use the same workaround described in the previous thread as a 'blessed' method as part of this project? I've used that same workaround for testing some of my work with success, but it is fragile to upstream changes, as noted here.
Sorry again for butting in!
Thank you very much @dangoslen.
I'm still not sure whether it makes more sense to add a util to this package or just provide a couple of examples (passing the theme as a prop, mocking the context with Enzyme) to the README.
Is there a way to test the text content of a styled component using enzymes shallow method and the styled-components' ThemeProvider wrapper?
class MyComponent extends React.component {
...
render(){
return (
<StyledComponent_A>
<StyledComponent_B>
some text
</StyledComponent_B>
</StyledComponent_A>
);
}
}
test('text is \'some text\'', () => {
const wrapper = shallow(
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider
);
const text = wrapper.dive().find(StyledComponent_B).text();
expect(text).toBe('some text');
});
Hello @joetidee, your test would fail even without the ThemeProvider.
To access the text of a styled component rendered with Enzyme's shallow, you should use children:
const text = wrapper.dive().find(StyledComponent_B).children().text();
I hope this helps.
To access the text of a styled component rendered with Enzyme's shallow, you should use children
Thanks a lot, I've been tearing my hair out trying to find out why text() returned only <styled.td /> :slightly_smiling_face: Is this documented anywhere?
Unfortunately, I don't think this specific behaviour is documented somewhere @kjarnet.
I believe is due to how style components are generated and the fact that text() goes only one level deep with shallow.
It's sometimes very inconvenient to have <ThemeProvider> as a wrapper around your components when using mount()...as has been pointed out previously, you first have to drill down to the actual component you're testing and you can no longer use methods like props() and state() on the Enzyme wrapper.
I came up with a solution (inspired by @chuanxie's workaround) that works on the current version of styled-components, but it doesn't work with styled-components 4. I'm still looking for a solution for that, which might be trickier since styled-components 4 uses the new React context API.
Anyway, in case it's helpful for others, here's the solution that works with styled-components 3.3.3:
import React from 'react'
import enzyme from 'enzyme'
import { ThemeProvider } from 'styled-components'
import * as theme from './theme'
/**
* Wrapper for enzyme's mount() that includes the theme for styled-components
*/
export function mountWithTheme(children, options = {}) {
return enzyme.mount(children, buildRenderOptions(options))
}
/**
* Wrapper for enzyme's shallow() that includes the theme for styled-components
*/
export function shallowWithTheme(children, options = {}) {
return enzyme.shallow(children, buildRenderOptions(options))
}
/**
* Wrapper for enzyme's render() that includes the theme for styled-components
*/
export function renderWithTheme(children, options = {}) {
return enzyme.render(children, buildRenderOptions(options))
}
let themeProvider
function buildRenderOptions(options = {}) {
const { context, childContextTypes } = options
if (!themeProvider) {
themeProvider = enzyme.mount(<ThemeProvider theme={theme} />, options)
.instance()
}
return {
...options,
context: {
...context,
...themeProvider.getChildContext(),
},
childContextTypes: {
...childContextTypes,
...themeProvider.constructor.childContextTypes,
},
}
}
@mbrowne any thoughts? I just upgraded to styled components version 4 and from your comments it appears this isn't a feasible solution and I'm doing some progressive research now to see if there's another way around this. Just poking around the fire and researching a bunch of different articles online to try to fully understand what we are up against.
@mbrowne ok so ultimately what I was trying to do was mount a component and test a click simulation and every attempt made would result in a prop from the theme being undefined. I reorganized some things around and it just worked. I ended up getting around the theme provider with a simple mount then find on the first nested element (Accordion). Here is the test:
it("test styled props on Accordion", () => {
const AccordionComponent = ThemedAccordionComponent.find('Accordion');
expect(AccordionComponent.state().closed).toEqual(true);
AccordionComponent.simulate('click');
expect(AccordionComponent.state().closed).toEqual(false);
});
Here is the Accordion component:
```import React from "react";
import PropTypes from "prop-types";
import { Icon } from "../Icon";
import styled from "styled-components";
const AccordionContainer = styled.div
display: flex;
flex-direction: column;
flex: 1;
justify-content: ${props => props.justifyContent};
background-color: ${props => props.theme.color[props.color]};
${props => props.theme.fontSize(14)};
;
const ChildrenContainer = styled.div
display: flex;
flex-direction: column;
;
const LabelWrapper = styled.div
padding: 10px;
;
/**
render() {
let {
props: {
children,
hasIcon,
iconColor,
iconFlexDirection,
iconExpand,
iconName,
iconSize,
label,
color,
justifyContent
},
state: { closed }
} = this;
return (
<AccordionContainer color={color} justifyContent={justifyContent}
onClick={() => this.setState({ closed: !closed })}
>
<div>
{hasIcon ? (
<>
<LabelWrapper>
<Icon
fontSize={iconSize}
name={iconName}
color={iconColor}
flexDirection={iconFlexDirection}
expand={iconExpand}
/>
</LabelWrapper>
{!closed && <ChildrenContainer>{children}</ChildrenContainer>}
</>
) : (
<>
<LabelWrapper>
<div>{label}</div>
</LabelWrapper>
{!closed && <ChildrenContainer>{children}</ChildrenContainer>}
</>
)}
</div>
</AccordionContainer>
);
}
}
Accordion.propTypes = {
color: PropTypes.string,
closed: PropTypes.bool,
justifyContent: PropTypes.string,
hasIcon: PropTypes.bool,
iconName: PropTypes.string,
iconColor: PropTypes.string,
iconExpand: PropTypes.bool,
iconSize: PropTypes.number,
label: PropTypes.string
};
Accordion.defaultProps = {
closed: true,
hasIcon: false,
iconExpand: false,
justifyContent: "flex-start"
};
```
Prior to writing this post my onClick was bound to the div within the Accordion component and I moved it to the AccordionContainer. That actually made a pretty big difference and the theme is getting pulled through (at least I think it is I'll develop a mock test to verify that soon):
By the way here is the full Accordion.spec.js (hopefully this helps someone else):
```import React from "react";
import { shallow, mount, render } from "enzyme";
import styled, { ThemeProvider } from "styled-components";
import theme from "../../styles/theme";
import { Accordion } from "./Accordion";
import sinon from "sinon";
import { renderWithTheme, mountWithTheme, shallowWithTheme } from "../../utils/test-utils";
import renderer from 'react-test-renderer';
import 'jest-styled-components';
describe("Accordion", () => {
let AccordionJSX = (
iconColor="#777"
iconSize={14}
hasIcon={true}
>
HELLO ACCORDION
);
it("Should render without throwing an error", () => {
expect(shallow(AccordionJSX)).not.toBeNull();
});
let ThemedAccordionComponent = mount(AccordionJSX);
it("Should have a styled-components theme", () => {
expect(ThemedAccordionComponent.props().theme).not.toBeNull();
});
it('check props passed in', () => {
expect(ThemedAccordionComponent.props().children.props).toEqual({
iconName: 'home',
iconColor: '#777',
iconSize: 14,
hasIcon: true,
children: 'HELLO ACCORDION',
closed: true,
iconExpand: false,
justifyContent: 'flex-start'
});
});
it("test styled props on Accordion", () => {
const AccordionComponent = ThemedAccordionComponent.find('Accordion');
expect(AccordionComponent.state().closed).toEqual(true);
// AccordionComponent.simulate('click');
expect(AccordionComponent.state().closed).toEqual(false);
});
});```
Most helpful comment
In that case, you can use the
ThemeProvider.For example, suppose you have a
Wrappercomponent:The following test works as expected:
I hope this helps, @chhuang.