如何使用redux状态更改而不是组件中的setState来触发Jest中的重新渲染?

时间:2016-12-29 00:03:58

标签: reactjs redux react-redux jestjs

所以Jest有一个非常棒的功能,允许在状态变化的结果中对组件进行动态重新渲染。它在网站上的React教程中进行了说明:

https://facebook.github.io/jest/docs/tutorial-react.html

以下是相关代码的简化版本:

test('Link changes the class when hovered', () => {
  const component = renderer.create(
    <Link page="http://www.facebook.com">Facebook</Link>
  );

  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually trigger the callback
  tree.props.onMouseEnter();

  // AUTOMATIC RE-RENDERING MID-TEST!!!!
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();

});

这真棒,除了2017年编写React的惯用方法是尽可能少的有状态组件,这意味着使用Redux。但是,我似乎无法将上述内容与Redux配合使用。这就是我所拥有的:

export function renderWithStore(store, Component, mapStateToProps) {
  return renderer.create(
    <Provider store={store}>
      <Component {...mapStateToProps(store.getState())} />
    </Provider>
  )
}

如果您想知道mapStateToProps是什么,它是为测试组件生成道具而提供的功能(就像您使用connect一样)。我有意将其调用移动到<Provider>内嵌套,因为很明显,如果你只是通过一个带有它的道具的组件实例(通常会renderer.create),它就不会# 39;能够响应商店中的状态变化。这就是组件工厂与其道具单独一起传递的原因。

为清楚起见,这里有mapStateToProps的样子:

({total}) => {
    return {total}
}

那么我们怎样才能实现&#34;活着&#34;我们可以在使用setState时使用哪些组件?我们怎样才能使renderer.create的回报继续响应还原状态变化?

也许,以某种方式调用forceUpdate来回应store.subscribe是解决方案?我现在要尝试一下。任何想法都会非常感激。

更新:

我将renderWithStore函数更改为订阅并强制执行更新,如下所示:

export function renderWithStore(store, Component, mapStateToProps) {
  let comp;

  store.subscribe(() => {
    comp.forceUpdate();
  });

  return renderer.create(
    <Provider ref={c => comp = c} store={store}>
      <Component {...mapStateToProps(store.getState())} />
    </Provider>
  )
}

它没有用。但它正在取得进展。正确调用强制更新,我在console.log()内调用Component来检查重新呈现是否正在发生。它实际上正在发生。但是,对component.toJSON的调用似乎重新渲染了初始组件,而不是更新的组件,这仍然与它对setState的响应方式不同。不知何故,似乎Jest在内部对setState做了一个特殊的例外 - 尝试而不是forceUpdate ......

1 个答案:

答案 0 :(得分:0)

好吧,我想通了,我想我想出了一些对使用redux和jest的反应开发者非常有用的东西。这是:

import React from 'react'
import renderer from 'react-test-renderer'

import {Provider} from 'react-redux'
import createHistory from 'history/createBrowserHistory'
import configureStore from '../src/configureStore'

import {loadSomethingAsync} from '../src/actions/async';


export async function createStore({shouldLoad=true}) {
  const history = createHistory()
  const store = configureStore(history)

  if(shouldLoad) {
    await store.dispatch(loadSomethingAsync())
  }

  return store
}

export function connect(store, Component) {
  return (mapStateToProps) => {
    let wrapper;

    store.subscribe(() => {
      wrapper.forceUpdate();
    });

    return renderer.create(
      <Provider store={store}>
        <Wrapper
          ref={wr => wrapper = wr}
          store={store}
          Component={Component}
          mapStateToProps={mapStateToProps}
        />
      </Provider>
    )
  }
}


class Wrapper extends React.Component {
  render() {
    let {store, Component, mapStateToProps} = this.props

    let props = typeof mapStateToProps === 'function'
      ? mapStateToProps(store.getState(), store.dispatch.bind(store))
      : mapStateToProps //object or undefined

    return <Component {...props} />
  }
}

用法:

test('do something', async () => {
  const store = await createStore()
  const mapStateToProps = (state) => {
    let {foo} = state
    return {foo};
  }

  const render = connect(store, MyComponent) // reusable with
  const component = render(mapStateToProps)  // different props


  let tree = component.toJSON()
  expect(tree).toMatchSnapshot()

  store.dispatch({type: 'SOMETHING_HAPPENED'}}) 
  // or through a handler that does the same, eg: tree.props.onClick()

  tree = component.toJSON()
  expect(tree).toMatchSnapshot() // snapshot reflects state change :)


  // now let's try re-using the enclosed component within
  // our render function but with different mapStateToProps:

  let tree2 = render({foo: 'bar'}); // you can also pass a plain object
  expect(tree2).toMatchSnapshot()
});

就是这样。您的组件现在处于“活动状态”,即与redux同步,您不必连续呼叫renderer.create()connect基本上类似于您习惯使用的connect()函数,但是组件和状态映射器的角色相反,允许您在不同的测试中重用相同的组件和不同的道具。