web-dev-qa-db-ja.com

テーマを含むスタイル付きコンポーネントをJestで動作させることができません

問題

私は JestEnzyme を使用して、私のテストを記述していますReactコンポーネントは素晴らしい Styled Components =ライブラリ。

ただし、テーマを実装したため、すべてのテストが失敗しています。例を挙げましょう。

これは私のLooksBrowserコンポーネントのコードです(少し読みやすくするために、すべてのインポートとprop-typeを削除しました)。

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

次に、私のLooksBrowserコンポーネントのテスト(以下は完全なコードです):

_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();
});
_

テーマを実装する前は、これらのテストはすべて成功していました。テーマを実装した後、すべてのテストで次のエラーが発生します。

_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 |
_

わかりました。テーマは定義されていません。

ソリューションワン

だからいくつかググリングした後、私は次の「解決策」を見つけました:

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

推奨される解決策は、テーマを小道具として渡すことです:const wrapper = shallow(<Button theme={theme} />)

そこで、次のコードをLooksBrowserテストファイルに追加します。

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

そして、すべてのテストを編集して手動でテーマを渡します。例えば:

_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();
});
_

これを実行した後、テストを再度実行します。それでも同じエラーが発生します。

ソリューション2

コンポーネントをスタイル付きコンポーネント ThemeProvider でラップすることにしました。これにより、_Compare snapshots_および_Renders without crashing_テストのエラーが修正されます。

ただし、LooksBrowserコンポーネントのprops/stateも変更して結果をテストしているため、これは機能しなくなりました。これは、setProps関数とsetState関数はルート/ラッパーコンポーネントでのみ使用できるためです。

したがって、コンポーネントをThemeProviderコンポーネントでラップすることも有効な解決策ではありません。

ソリューション3

私は自分のスタイル付きコンポーネントの1つの小道具のログを試すことにしました。だから私はSlideTitleサブコンポーネントをこれに変更しました:

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

次のエラーが発生します。

_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、テーマの小道具全体が空になっているようです。手動でテーマをSlideTitleに渡してみましょう(これは恐ろしいソリューションですが、プロジェクト全体のすべてのスタイルコンポーネントにテーマを手動で渡す必要があることを意味します)。

だから私は次のコードを追加しました:

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

そして、もう一度テストを実行します。端末に次の行が表示されます。

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

ええ、これは私が探しているものです!下にスクロールすると、同じエラーが再び表示されます...yikes

ソリューション4

Jest Styled Componentsのドキュメント では、次の解決策も見ました。

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

const wrapper = shallowWithTheme(<Button />, theme)
_

OK、有望に見えます。したがって、この関数をテストファイルに追加し、_Check if componentDidUpdate gets called_テストを次のように更新しました。

_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();
});
_

テストを実行すると、次のエラーが発生します。

_Error
Cannot tween a null target. thrown
_

shallow を使用しているので意味があります。したがって、浅い代わりに mount を使用するように関数を変更します。

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

私はもう一度テストを実行して出来上がり:

_TypeError: Cannot read property 'SlideTitle' of undefined_

私は公式にはアイデアがありません。

誰かがこれについて何か考えを持っているなら、それは大いに高く評価されるでしょう!よろしくお願いします。

編集

また、Githubで2つの問題を公開しました。1つは Styled Components repo にあり、もう1つは Jest Styled Components repo にあります。

私はこれまでに提供されたすべての解決策を何の助けもなく試みました。したがって、ここに誰かがこの問題を解決する方法についてアイデアを持っている場合、それらを共有してください!

10
DavidWorldpeace

私は同僚の助けを借りてこの問題をなんとか修正しました。テストを壊したSlideTitleコンポーネントを拡張する別のコンポーネントを用意しました:

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

私はコードをこれにリファクタリングしました:

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

そして私のテストは再び合格し始めました!

3
DavidWorldpeace

コンポーネントをThemeProviderで囲み、themeオブジェクトをコンポーネントに渡すと、うまく動作します。

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