怎么用Jest测试img.onerror

IT技术 unit-testing reactjs jestjs enzyme
2021-03-23 14:17:14

我创建了一个 React 组件来加载图像并确定图像是否成功加载。

import React from 'react';
import PropTypes from 'prop-types';
import { LOADING, SUCCESS, ERROR } from '../helpers';

class Image extends React.Component {
  static propTypes = {
    onError: PropTypes.func,
    onLoad: PropTypes.func,
    src: PropTypes.string.isRequired,
  }

  static defaultProps = {
    onError: null,
    onLoad: null,
  }

  constructor(props) {
    super(props);
    this.state = { imageStatus: LOADING };
    this.initImage();
  }

  componentDidMount() {
    this.image.onload = this.handleImageLoad;
    this.image.onerror = this.handleImageError;
    this.image.src = this.props.src;
  }

  initImage() {
    this.image = document.createElement('img');
    this.handleImageLoad = this.handleImageLoad.bind(this);
    this.handleImageError = this.handleImageError.bind(this);
  }

  handleImageLoad(ev) {
    this.setState({ imageStatus: SUCCESS });
    if (this.props.onLoad) this.props.onLoad(ev);
  }

  handleImageError(ev) {
    this.setState({ imageStatus: ERROR });
    if (this.props.onError) this.props.onError(ev);
  }

  render() {
    switch (this.state.imageStatus) {
      case LOADING:
        return this.renderLoading();
      case SUCCESS:
        return this.renderSuccess();
      case ERROR:
        return this.renderError();
      default:
        throw new Error('unknown value for this.state.imageStatus');
    }
  }
}

export default Image;

我正在尝试使用 Jest + Enzyme 创建一个测试来测试图像何时无法加载。

  it('should call any passed in onError after an image load error', () => {
    const onError = jest.fn();
    mount(<Image {...props} src="crap.junk"} onError={onError} />);
    expect(onError).toHaveBeenCalled();
  });

无论我做什么,Jest 总能找到一种方法来成功渲染图像。即使将 src 设置为 false 仍然以某种方式呈现图像。有谁知道你到底是如何强迫 jest 使图像加载失败的?

4个回答

由于在幕后,document.createElement('img')相当于new Image(),我们可以模拟 jsdom(由 Jest 使用)实现的一部分Image来实现您的目标。

(为简单起见,我已将您的组件重命名为ImageComponent

describe('Callbacks', () => {
  const LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC';
  const LOAD_SUCCESS_SRC = 'LOAD_SUCCESS_SRC';

  beforeAll(() => {
    // Mocking Image.prototype.src to call the onload or onerror
    // callbacks depending on the src passed to it
    Object.defineProperty(global.Image.prototype, 'src', {
      // Define the property setter
      set(src) {
        if (src === LOAD_FAILURE_SRC) {
          // Call with setTimeout to simulate async loading
          setTimeout(() => this.onerror(new Error('mocked error')));
        } else if (src === LOAD_SUCCESS_SRC) {
          setTimeout(() => this.onload());
        }
      },
    });
  });

  it('Calls onError when passed bad src', done => {
    const onError = ev => {
      expect(ev).toBeInstanceOf(Error);

      // Indicate to Jest that the test has finished
      done();
    };

    mount(<ImageComponent onError={onError} src={LOAD_FAILURE_SRC} />);
  });
});

在此jsdom 问题中解释了从未像在浏览器中那样调用onerror/的原因在同一问题线程中提供了一个会影响所有测试的替代修复程序onload

@Chris 它保留在同一个测试套件(即文件)中。如果您在同一个文件中有多个依赖 Image.src 来按预期工作的测试,就会出现问题。恕我直言,每个测试都应自行清理,以免影响其他测试。
2021-05-29 14:17:14
根据您要测试的内容,也可以返回给定的内容srcObject.defineProperty(global.Image.prototype, 'src', { set(src) { this._src = src; setTimeout(() => this.onload && this.onload()) }, get() { return this._src; }
2021-06-19 14:17:14
这对我有用。但是,似乎没有办法Object.defineProperty在测试套件结束时取消设置,以恢复Image.prototype.src到原来的状态。
2021-06-20 14:17:14
如果您查看此答案的编辑历史记录,您会发现我曾经在那里有这样的清理逻辑,但我删除了它,因为我确认修改后的原型仅存在于某个范围内(describe块或文件范围,我不记得了),然后被恢复。如果您想要旧的包含恢复的代码,请从答案历史记录中获取。
2021-06-20 14:17:14

如果您正在使用@testing-library/react,则可以在imgwith上触发错误fireEvent.error

const onError = jest.fn();
render(<Image alt="test" src="crap.junk"} onError={onError} />);
fireEvent.error(screen.getByAltText("test"));
expect(onError).toHaveBeenCalled();

你可以嘲笑document.createElement

 it('should call any passed in onError after an image load error', () => {
    const img = {}
    global.document.createElement = (type) => type === 'img' ? img : null
    const onError = jest.fn();
    mount(<Image {...props} src="crap.junk"} onError={onError} />);
    img.onload()
    expect(onError).toHaveBeenCalled();
  });
我放弃。我只是决定使用 wrapper.instance() 然后直接调用 instance.image.onerror() 。我突然想到我测试过度了。我必须相信 HTMLImageElement 会在出现图像错误时触发 onerror。如果不是,则 Web 浏览器的源代码存在重大错误。几率:零。所以我只是手动触发错误来模拟错误。案件结案。谢谢,不过。
2021-05-22 14:17:14
我认为您正在做一些事情,但是在模拟 document.createElement 时存在问题。我的 Image 函数还根据参数创建跨度和 div,因此返回 null 会导致测试崩溃。现在我正在测试是否可以添加任何属性。它不工作。这是据我所知gist.github.com/mrbinky3000/567b532928f456ee74e55ebe959c66ec
2021-06-13 14:17:14

我用笑话+酶。我只是手动调用 onError 方法,如下所示:

function Avatar (props) {
    const handleError = (e) => {
        e.target.src = getDefaultSrc()
    }

    const getDefaultSrc = () => {
        return `/default.png`
    }

    const { user: { avatar } } = props
    return <img className='avatar' src={avatar} onError={handleError} />
}

// avatar.test.js
it('should show default image', () => {
    const wrapper = mount(<Avatar {...props} />)
    const handleError = wrapper.find('.avatar').prop('onError')
    expect(typeof handleError).toBe('function')
    const fakeE = {
        target: {
            src: ''
        }
    }
    
    handleError(fakeE)
    expect(fakeE.target.src).toBe('/default.png')
})