jeudi 13 octobre 2016

How can I test a potential aynchronous React render?

I've recently decided to start writing tests for a project I'm a part of, and we have a React component that I'm having some real trouble figuring out how to test properly.

The component is called FallbackedImage and it takes either a string or array of string and renders the first valid image, which, as you probably understand, is an asynchronous action.

I want to test if it does, in fact, render an image if supplied a valid string, but I can't just run

it("renders img if src is valid string", () => {
  const wrapper = mount(<FallbackedImage src={validLink} />)
  expect(wrapper.find("img")).to.have.length(1)
})

because the image will not have rendered yet, and wrapper.find("img") will therefore have a length of 0.

I have come up with two "solutions", but they're hacky and I don't want to use them:

setTimeout

This assumes that the image will have loaded within 300ms. If the image takes longer to load, the test will fail; if it takes less time, then I've wasted precious milliseconds.

it("renders img if src is valid string", done => {
  const wrapper = mount(<FallbackedImage src={validLink} />)
  window.setTimeout(() => {
    expect(wrapper.find("img")).to.have.length(1)
    done()
  }, 300)
})

Using the component's onLoad prop

This assumes that the onLoad-functionality works as intended, which doesn't feel right since that is not in scope for this particular test.

it("renders img if src is valid string", done => {
  const onLoad = () => {
    expect(wrapper.find("img")).to.have.length(1)
    done()
  }

  const wrapper = mount(
    <FallbackedImage
      onLoad={onLoad}
      src={validLink} />
  )
})

How am I supposed to test this? I'm using chai, Enzyme, and Mocha, if that helps. Also, here's the FallbackedImage-component so you can see what I'm working with:

import React, { Component, PropTypes } from "react"
import { noop } from "../../utils"

export default class FallbackedImage extends Component {
  constructor(props) {
    super(props)
    this.componentDidMount = this.tryImage.bind(this)
    this.testImg = new window.Image()
    this.testImg.onerror = this.onError.bind(this)
    this.testImg.onload = this.onLoad.bind(this)
    this.photos = [].concat(props.src) // Make array from props.src if it's not
    this.state = { index: 0 }
  }

  static propTypes = {
    className: PropTypes.string,
    onLoad: PropTypes.func,
    src: PropTypes.oneOfType([
      PropTypes.array,
      PropTypes.string,
    ]),
  }

  static defaultProps = {
    onLoad: noop,
  }

  onError() {
    const nextIndex = this.state.index + 1
    if (nextIndex <= this.photos.length) {
      this.setState({ index: nextIndex }, this.tryImage)
    }
  }

  onLoad() {
    const { src } = this.testImg
    this.setState({ src })
    this.props.onLoad(src)
  }

  tryImage() {
    this.testImg.src = this.photos[this.state.index]
  }

  shouldComponentUpdate(_nextProps, nextState) {
    return !!nextState.src
  }

  render() {
    const { src } = this.state
    return src
      ? <img src={src} className={this.props.className} />
      : null
  }
}

Aucun commentaire:

Enregistrer un commentaire