Jest on Reactjs для имитации щелчка с помощью справочной функции - PullRequest
0 голосов
/ 13 июля 2020

Я пытаюсь протестировать кнопку из компонента. У меня есть ссылка на событие щелчка. Ссылка является дочерним компонентом.

Компонент основного класса Speaker. js:

import React, {Component} from 'react';
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux';
import ActionCreators from '../state/actions';
import {Input, Col, Button, Icon} from 'antd';
import List from '../components/List';
import Partner from '../constants/Partner';
import Selector from '../components/Selector';
import {sortData} from '../utils/utils.js'

class SpeakersComponent extends Component {

  state = {
    speakers: this.props.event && this.props.event.speakers ? this.props.event.speakers : [],
  };

  constructor(props) {
    super(props);
    // if (props.event)
    //   console.log(props.event.speakers)
    this.state = {
      ...this.state,
      props : props,
      speakers: props.event && props.event.speakers ? props.event.speakers : [],
    };
    this.show = React.createRef();
  }

  componentDidMount() {
    this.props.actions.getAllSpeakers();
    if (!this.props.companies) {
      this.props.actions.getAllPartners();
    }
  }

  UNSAFE_componentWillReceiveProps(props) {
    if (props.event) {
      this.setState({speakers: props.event.speakers || []});
    }
  }

  sendToApi = (payload, callback) => {
    this.props.actions.ajaxStart({
      method: 'post',
      endpoint: '/api/events',
      body: {
        ...payload,
        city: this.props.event.city.city,
        year: this.props.event.year,
      },
      loadingMessage: true,
      successMessage: true,
      callback,
    });
  }

  deleteSpeaker = (index) => {
    this.state.speakers.splice(index, 1);
    this.sendToApi({
      speakers: this.state.speakers.map(s => s.id),
    });
  };

  addSpeaker = async (_, speaker, __, update) => {
    if (!speaker) {
      return;
    }
    if (!update || update === undefined) {
      await this.setState({speakers: [...this.state.speakers, speaker]});
    }
    this.sendToApi({
      speakers: this.state.speakers.map(s => s.id),
    }, () => {
      this.props.actions.getAllSpeakers();
      this.props.actions.getEvent(this.props.event.city.city, this.props.event.year);
    });
  };

  render() {
    // if (this.state.speakers)
    //   console.log(this.state.speakers)
    const speaker = {
      id: {
        label: 'id',
        disabled: true,
        valuesForId: ["name"],
        optional: true,
      },
      name: {},
      title: {},
      description: {
        render: <Input.TextArea />,
      },
      company: {
        render: 'selector',
        props: {
          data: this.props.companies,
          config: Partner,
          valueField: 'id',
          displayField: 'name',
          endpoint: '/api/partners',
          default: item => item.company
        },
      },
      picture: {
        render: 'imagePicker',
        optional: true,
        props: {
          renameFileTo: (_, formData) => formData.id,
          mode: 'sizes',
          sizes: [100, 190, 500],
          prefix: 'event/speakers',
          authorizedExts: ['jpg'],
        },
      },
    };
    return (
      <Col offset={6} span={12} data-test="speakersComponent">
        <List
          dataSource={sortData(this.state.speakers)}
          displayField='name'
          onItemClick={console.info}
          name='speaker'
          onDelete={this.deleteSpeaker}
          mainPage={true}
        />
        <Button onClick={() => this.selector.show()} style={{marginTop: 10}}>
          <Icon type='plus' /> Add or edit speaker
        </Button>
        <Selector
          endpoint='/api/speakers'
          _ref={ref => this.selector = ref}
          value={this.addSpeaker}
          data={sortData(this.props.speakers)}
          config={speaker}
          valueField='id'
          displayField='name'
          id='speaker'
        />
      </Col>
    );
  }
}

const mapStateToProps = state => {
  return {
    speakers: state.data.speakers,
    companies: state.data.partners,
  };
};

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators(ActionCreators, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(SpeakersComponent);

здесь у вас есть дочерний компонент Selector. js

import React, {Component} from 'react';
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux';
import ActionCreators from '../state/actions';
import {Modal} from 'antd';
import List from './List';
import Form from './Form';

class SelectorComponent extends Component {
  state = {
    visible: false,
    value: null,
    isEditing: false,
  };

  componentDidMount() {
    this.update(this.props);
    this.props._ref(this);
  }

  UNSAFE_componentWillReceiveProps(props) {
    if (props.data && !this.props.data) {
      this.update(props);
    }
  }

  update = (props) => {
    let value;
    let defaultValue = this.props.default;

    if (typeof defaultValue === 'function') {
      defaultValue = defaultValue(this.props.item);
    }
    if (defaultValue) {
      value = (props.data || []).find(e => e[this.props.valueField] === defaultValue[this.props.valueField]);
      this.setState({value});
      this.props.value(this.props.id, value);
    }
  }

  clear = () => {
    this.setState({value: null});
    this.props.value(this.props.id, null);
  }

  show = () => this.setState({visible: true});

  postSubmit = (values, apiValidated) => {
    this.props.value(this.props.id, {
      ...this.state.value,
      ...values,
    }, apiValidated);
  }

  onCustomizationOk = async () => {
    this.setState({
      customizationVisible: false,
    });

    if (typeof this.props.value === 'function') {
      this.validate();
    }
  }

  onOk = async (value, _, apiValidated, update) => {
    await this.setState({
      visible: false,
      value,
      isEditing : update,
    });

    if (this.props.customizationConfig) {
      this.setState({customizationVisible: true});
    } else if (typeof this.props.value === 'function') {
      this.props.value(this.props.id, value, apiValidated, update);
    }
  };

  onCancel = () => {
    this.setState({
      visible: false,
      customizationVisible: false,
    });
  };

  render() {
    return (
      <div>
        <Modal
          visible={this.state.visible}
          closable={false}
          title={<div>Select a {this.props.id}</div>}
          footer={null}
          onCancel={this.onCancel}
        >
          <List
            endpoint={this.props.endpoint}
            dataSource={(this.props.data || [])}
            displayField={this.props.displayField}
            valueField={this.props.valueField}
            onItemClick={item => this.onOk(item)}
            config={this.props.config}
            name={this.props.id}
            style={{maxHeight: 500, overflowY: 'scroll'}}
            onOk={this.onOk}
          />
        </Modal>
        {
          !this.props.customizationConfig || this.state.isEditing ? null : (
            <Modal
              visible={this.state.customizationVisible}
              closable={false}
              title={<div>Customize {this.props.id}</div>}
              onOk={this.onCustomizationOk}
              onCancel={this.onCancel}
            >
              <Form
                loadingMessage={null}
                successMessage={null}
                payload={{[Object.keys(this.props.customizationConfig).find(k => this.props.customizationConfig[k] === null)]: this.state.value}}
                config={this.props.customizationConfig}
                substituteValidation={ref => this.validate = ref}
                disableApiValidation={true}
                postSubmit={this.postSubmit}
                isEditing={this.state.customizationVisible}
              />
            </Modal>
          )
        }
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
  };
};

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators(ActionCreators, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(SelectorComponent);

мой тестовый файл speaker.test. js

//https://www.youtube.com/watch?v=92F8_9UG04g
import Speakers from '../src/containers/Speakers';
import Adapter from 'enzyme-adapter-react-16';
import Selector from "../src/components/Selector";
import List from '../src/components/List';
import React from 'react';
import {eventData} from './event.data';
import { Provider } from 'react-redux';
import { shallow, configure, mount } from "enzyme";
import { findByTestAtrr, testStore } from '../src/utils/utilsTestJest';
import renderer from 'react-test-renderer';
import thunk from "redux-thunk";
import configureMockStore from 'redux-mock-store'

jest.mock('../src/components/Selector');

configure({ adapter: new Adapter() });

let middlewares = [ thunk ],
    mockStore = configureMockStore(middlewares);

const setUpWrapper = (initialState={}) => {
  const store = testStore(initialState);
  const wrapper = shallow(
                <Speakers store={store}/>
  );
  // console.log(wrapper.debug());
  return wrapper;
};

const setUpSnapshot = (initialState={}) => {
  const store = testStore(initialState);
  const speakerComponent = renderer.create(
      <Provider store={store}>
          <Speakers event={eventData}/>
      </Provider>
  ).toJSON();
  // console.log(speakerComponent);
  return speakerComponent;
};

const setUpMount = (initialState={}) => {
  const store = testStore(initialState);
  let speakerComponent = mount(<Provider store={store}><Speakers event={eventData.speakers}/></Provider>)
  return speakerComponent;
};

describe('Speakers Container', () => {

  let wrapperSpeakers;
  let snapshotSpeakers;
  let mountSpeakers;
  let childContainerSelector;
  beforeEach(() => {
    const initialState = {
      data: eventData.speakers
    };
    wrapperSpeakers = setUpWrapper(initialState);
    snapshotSpeakers = setUpSnapshot(initialState);
    mountSpeakers = setUpMount(initialState);
    childContainerSelector = wrapperSpeakers.dive().dive().find('SelectorComponent');
  })

  it('Should render without errors', () => {
    const component = findByTestAtrr(wrapperSpeakers.childAt(0).dive(), 'speakersComponent');
    expect(component.length).toBe(1);
  })

  it('render correctly speaker component', () => {
    expect(snapshotSpeakers).toMatchSnapshot();
  });

  it('check state', () => {
    const wrapperSpeakersInstance = wrapperSpeakers.dive().dive().instance();
    wrapperSpeakersInstance.componentDidMount();
    wrapperSpeakersInstance.setState({speakers: eventData.speakers});
    expect(wrapperSpeakersInstance.state.speakers).not.toBeNull();
    expect(wrapperSpeakersInstance.state.speakers).toEqual(eventData.speakers);
  })

  it('button click', () => {
    const instance = wrapperSpeakers.dive().dive();
    let mockFn = jest.fn();
    instance.show = mockFn
    instance.update();
    console.log(instance.debug())
    instance.find('Button').simulate('click');
    // let button = wrapperSpeakers.dive().dive().find('Button');
    // console.log(wrapperSpeakers.dive().dive().find('Button').simulate('click'));
  })
});

Когда я тестирую свой компонент, я получаю следующую ошибку:

    ✓ Should render without errors (61 ms)
    ✓ render correctly speaker component (17 ms)
    ✓ check state (17 ms)
    ✕ button click (58 ms)

  ● Speakers Container › button click

    TypeError: Cannot read property 'show' of undefined

      125 |           mainPage={true}
      126 |         />
    > 127 |         <Button onClick={() => this.selector.show()} style={{marginTop: 10}}>
          |                                              ^
      128 |           <Icon type='plus' /> Add or edit speaker
      129 |         </Button>
      130 |         <Selector

      at handler (src/containers/Speakers.js:127:46)
      at fn (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:716:13)
      at withSetStateAllowed (node_modules/enzyme-adapter-utils/src/Utils.js:99:18)
      at Object.simulateEvent (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:712:11)
      at ShallowWrapper.call (node_modules/enzyme/src/ShallowWrapper.js:1134:7)
      at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1654:21)
      at ShallowWrapper.simulate (node_modules/enzyme/src/ShallowWrapper.js:1133:17)
      at Object.<anonymous> (__test__/speakers2.test.js:87:29)

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 7 passed, 8 total
Snapshots:   1 passed, 1 total
Time:        2.578 s, estimated 3 s
Ran all test suites related to changed files.

Есть ли у вас идея решения моей проблемы? Я не видел топи c с этой ошибкой.

1 Ответ

0 голосов
/ 13 июля 2020

Похоже, проблема в том, что ссылка не определена при первом рендеринге. Попробуйте использовать новый API ссылок.

Создание ссылок

  1. Создайте this.selector ref
  2. Присоедините к Selector компоненту
  3. Используйте this.selector.current для доступа к ссылке

Обновленный компонент

import React, { createRef, Component} from 'react';
...

class SpeakersComponent extends Component {

  ...

  constructor(props) {
    super(props);
    this.state = {
      ...this.state,
      props : props,
      speakers: props.event && props.event.speakers ? props.event.speakers : [],
    };

    this.selector = createRef(); // <-- (1) create the ref here

    // IDK, maybe you meant to use this one for something? It isn't used anywhere.
    this.show = React.createRef();
  }

  ...

  render() {
    ...
    return (
      <Col offset={6} span={12} data-test="speakersComponent">
        <List
          dataSource={sortData(this.state.speakers)}
          displayField='name'
          onItemClick={console.info}
          name='speaker'
          onDelete={this.deleteSpeaker}
          mainPage={true}
        />
        <Button
          onClick={() => this.selector.current.show()} // <-- (3) use this.selector.current to access ref
          style={{marginTop: 10}}
        >
          <Icon type='plus' /> Add or edit speaker
        </Button>
        <Selector
          endpoint='/api/speakers'
          _ref={this.selector} // <-- (2) attach this.selector ref
          value={this.addSpeaker}
          data={sortData(this.props.speakers)}
          config={speaker}
          valueField='id'
          displayField='name'
          id='speaker'
        />
      </Col>
    );
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...