如何将模拟事件传递给 React.cloneElement() 方法返回的反应元素

How to pass mock event to the react element that React.cloneElement() method returned

index.ts:

import React, { ChangeEvent, cloneElement, Component, isValidElement } from 'react';

interface MyComponentProps {
  children?: React.ReactNode;
}

export default class MyComponent extends Component<MyComponentProps> {
  render() {
    const { children } = this.props;

    const items = React.Children.map(children, (element, index) => {
      if (!isValidElement(element)) {
        return element;
      }
      return cloneElement(element, {
        key: index,
        onChange: (e: ChangeEvent<HTMLInputElement>) => {
          element.props.onChange && element.props.onChange(e);
          // do other things
          console.log('handle change event');
        },
      });
    });
    return <div>{items}</div>;
  }
}

index.test.ts:

import { mount } from 'enzyme';
import React, { ChangeEvent } from 'react';
import MyComponent from '.';

class Test extends React.Component<{ onChange?: (e: ChangeEvent<HTMLInputElement>) => void }> {
  render() {
    return <input {...this.props} />;
  }
}

describe('react-cloneElement-enzyme-change-event', () => {
  it('should handle change event', () => {
    const onChange = jest.fn();
    const wrapper = mount(
      <MyComponent>
        <Test onChange={onChange} />
        <Test />
      </MyComponent>
    );
    const input = wrapper.find('div').children().at(0).find('input');
    // expect(jest.isMockFunction(input.prop('onChange'))).toBeTruthy(); // failed
    const event = {} as ChangeEvent<HTMLInputElement>;
    input.simulate('change', event);
    expect(onChange).toBeCalled();
    expect(onChange).toBeCalledWith({}); // failed. The actual value is React synthetic event object
  });
});

此断言 expect(onChange).toBeCalledWith({}); 失败。我发现在执行 cloneElement 后,input 上的 onChange 事件处理程序已被替换为非模拟 onChange: () => {}

因此,即使在执行 input.simulate('change', event) 时传递了模拟事件对象,onChange 事件处理程序也会传递 React 合成事件对象,而不是模拟事件。

如何将模拟事件对象传递给克隆元素的 onChange

包版本:

"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"jest": "^26.6.3",
"react": "^16.14.0",

最小的、可重现的示例存储库:https://github.com/mrdulin/jest-v26-codelab/tree/main/issues/react-cloneElement-enzyme-change-event

显然 enzyme-adapter-react-16 有两种不同的实现方式 simulateEvent 用于浅层和挂载,而对于挂载,它正在创建一个模拟事件,该事件与给定数据合并。

如果你通过了:

    const event = {
      target: {
        value: 'z',
      },
    } as ChangeEvent<HTMLInputElement>;
    input.simulate('change', event);

你可以期待:

expect(onChange).toHaveBeenCalledWith(expect.objectContaining(event));

因为 onChange 属性被匿名箭头函数覆盖。

我们可以使用 .invoke(invokePropName)(...args) => Any 调用一个函数 prop 并将我们模拟的事件对象传递给它。

例如

index.test.tsx:

import { mount } from 'enzyme';
import React, { ChangeEvent } from 'react';
import MyComponent from '.';

class Test extends React.Component<{ onChange?: (e: ChangeEvent<HTMLInputElement>) => void }> {
  render() {
    return <input {...this.props} />;
  }
}

describe('react-cloneElement-enzyme-change-event', () => {
  it('should handle change event', () => {
    const logSpy = jest.spyOn(console, 'log');
    const onChange = jest.fn();
    const wrapper = mount(
      <MyComponent>
        <Test onChange={onChange} />
        <Test />
      </MyComponent>
    );
    const input = wrapper.find('div').children().at(0).find('input');
    expect(jest.isMockFunction(input.prop('onChange'))).toBeFalsy();
    const event = {} as ChangeEvent<HTMLInputElement>;
    input.invoke('onChange')!(event);
    expect(onChange).toBeCalledWith({});
    expect(logSpy).toBeCalledWith('handle change event');
  });
});

单元测试结果:

 PASS  issues/react-cloneElement-enzyme-change-event/index.test.tsx
  react-cloneElement-enzyme-change-event
    ✓ should handle change event (84 ms)

  console.log
    handle change event

      at CustomConsole.<anonymous> (node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866:25)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |      90 |       75 |     100 |      90 |                   
 index.tsx |      90 |       75 |     100 |      90 | 13                
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.727 s