Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a helper to quickly provide a theme context w/o the ThemeProvider HOC #61

Closed
kitten opened this issue Aug 7, 2017 · 19 comments
Closed

Comments

@kitten
Copy link
Member

kitten commented Aug 7, 2017

There have been a lot of workaround and a lot of confused users around how to test theming.

styled-components/styled-components#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)

@JimmyLv
Copy link

JimmyLv commented Aug 8, 2017

Hi @philpl , what do you mean add theme prop in enzyme in styled-components/styled-components#624 (comment)? 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}
        />);

@kitten
Copy link
Member Author

kitten commented Aug 8, 2017

@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.

@MicheleBertoli
Copy link
Member

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.

@chhuang
Copy link

chhuang commented Sep 25, 2017

How do you pass the theme prop?

@MicheleBertoli
Copy link
Member

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.

@chhuang
Copy link

chhuang commented Sep 26, 2017

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.

@MicheleBertoli
Copy link
Member

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.

@carlkenne
Copy link

@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.

@MicheleBertoli
Copy link
Member

@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.

@chhuang
Copy link

chhuang commented Sep 28, 2017

@carlkenne if you are using Enzyme, you can try enzyme-to-json. react-test-renderer is fine.

@dangoslen
Copy link

dangoslen commented Oct 2, 2017

@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!

@MicheleBertoli
Copy link
Member

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.

@joetidee
Copy link

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

@MicheleBertoli
Copy link
Member

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.

@kjarnet
Copy link

kjarnet commented Feb 12, 2018

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 /> 🙂 Is this documented anywhere?

@MicheleBertoli
Copy link
Member

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.

@mbrowne
Copy link

mbrowne commented Sep 13, 2018

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,
        },
    }
}

@JSONRice
Copy link

JSONRice commented Nov 4, 2019

@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.

@JSONRice
Copy link

JSONRice commented Nov 4, 2019

@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 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;
`;

/**
 * Accordion is nearly a Higher Order Component (HOC) in the fact that it encapsulates an Icon and when that
 * Icon is clicked an onClick callback provided should toggle the closed state.
 */
export class Accordion extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      closed: props.closed
    };
  }

  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 { 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 = (
    <ThemeProvider theme={theme}>
      <Accordion
        iconName="home"
        iconColor="#777"
        iconSize={14}
        hasIcon={true}
      >
        HELLO ACCORDION
      </Accordion>
    </ThemeProvider>
  );

  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);
  });
});```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants