如何测试包裹在 antd Form 中的组件?

How to test components wrapped in an antd Form?

我已经为此苦苦挣扎了几个月,现在。虽然有很多关于测试 antd 包装组件的正确方法的猜测,none 的建议适用于这个特定组件。

所以,我有一个组件,它是一个带有 antd 形式的模式。在这个表单中,我有几个字段:一个输入,一个 select 和一个树 select,没什么特别的。

基本上是这样的:

class FormModal extends React.Component {
  static propTypes = {
    data: propTypes.object,
    form: propTypes.object,
    scopes: propTypes.array.isRequired,
    clients: propTypes.array.isRequired,
    treeData: propTypes.array.isRequired,
    isEditing: propTypes.bool.isRequired,
    isSaving: propTypes.bool.isRequired,
    onCancel: propTypes.func.isRequired,
    onSave: propTypes.func.isRequired,
    onFilterTreeData: propTypes.func.isRequired,
    visible: propTypes.bool.isRequired
  }

  static defaultProps = {
    data: null,
    form: {}
  }

  state = {
    selectedScopes: [],
    newScopes: [],
    inputVisible: false,
    inputValue: ''
  };

  componentDidMount() {
    // do stuff
  }

  handleSave = () => {
    // do stuff
  }

  handleSelectedScopesChange = (event) => {
    // do stuff
  }

  updateTreeSelect = () => {
    const { form } = this.props;
    const { selectedScopes } = this.state;

    form.setFieldsValue({
      allowedScopes: selectedScopes
    });
  }

  handleRemoveTag = (removedTag) => {
    const selectedScopes = this.state.selectedScopes.filter(scope => scope !== removedTag);
    const newScopes = this.state.newScopes.filter(scope => scope !== removedTag);
    this.setState({ selectedScopes, newScopes }, this.updateTreeSelect);
  }

  showInput = () => {
    this.setState({ inputVisible: true }, () => this.input.focus());
  }

  handleInputChange = (e) => {
    const inputValue = e.target.value;
    this.setState({ inputValue });
  }

  handleInputConfirm = () => {
    const { newScopes, inputValue } = this.state;
    let tags = newScopes;
    if (inputValue && tags.indexOf(inputValue) === -1) {
      tags = [inputValue, ...tags];
    }

    this.setState({
      newScopes: tags,
      inputVisible: false,
      inputValue: '',
    });
  }

  saveInputRef = input => this.input = input

  renderTags = (scopeArrays) => {
    const tags = scopeArrays.map(scopeArray =>
      scopeArray.map((permition) => {
        let scopeType = null;
        if (permition.includes('read') || permition.includes('get')) scopeType = 'blue';
        if (permition.includes('create') || permition.includes('write') || permition.includes('send')) scopeType = 'green';
        if (permition.includes('update')) scopeType = 'gold';
        if (permition.includes('delete')) scopeType = 'red';
        return (
          <Tag
            key={permition}
            color={scopeType || 'purple'}
            style={{ margin: '2px 4px 2px 0' }}
            closable
            afterClose={() => this.handleRemoveTag(permition)}
          >
            {permition}
          </Tag>
        );
      })
    );

    return [].concat(...tags);
  }

  render() {
    const {
      selectedScopes,
      newScopes,
      inputValue,
      inputVisible
    } = this.state;

    const {
      form,
      treeData,
      clients,
      isEditing,
      isSaving,
      onCancel,
      onFilterTreeData,
      visible
    } = this.props;

    const {
      getFieldDecorator,
      getFieldsError,
    } = form;

    const selectedScopesTags = this.renderTags([newScopes, selectedScopes]);

    const clientOptions = clients.map(client => (<Option key={client._id}>{client.name}</Option>));

    return (
      <Modal
        className="user-modal"
        title={isEditing ? 'Editing Group' : 'Creating Group'}
        visible={visible}
        onCancel={onCancel}
        footer={[
          <Button key="cancel" onClick={onCancel}>Cancel</Button>,
          <Button
            key="save"
            type="primary"
            loading={isSaving}
            onClick={this.handleSave}
            disabled={formRules.hasErrors(getFieldsError())}
          >
            Save
          </Button>
        ]}
      >
        <Form layout="vertical" onSubmit={this.handleSave}>
          <Row gutter={24}>
            <Col span={12}>
              <FormItem label="Name">
                {getFieldDecorator(
                  'name',
                  { rules: [formRules.required, { max: 20, message: 'Group name can\'t excede 20 characters' }] }
                )(
                  <Input />
                )}
              </FormItem>
            </Col>
            <Col span={12}>
              <FormItem label="Client">
                {getFieldDecorator(
                  'client', { rules: [formRules.required] }
                )(
                  <Select placeholder="Please select client">
                    {clientOptions}
                  </Select>
                )}
              </FormItem>
            </Col>
            <Col span={24}>
              <FormItem label="Scopes">
                {getFieldDecorator(
                  'allowedScopes'
                )(
                  <TreeSelect
                    treeData={treeData}
                    filterTreeNode={onFilterTreeData}
                    onChange={this.handleSelectedScopesChange}
                    treeCheckable
                    dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
                    showCheckedStrategy="SHOW_PARENT"
                    searchPlaceholder="Filter by scopes"
                    className="groups__filter groups__filter--fill"
                  />
                )}
              </FormItem>
            </Col>
            <Col span={24}>
              <Card
                title="Selected Scopes"
                style={{ width: '100%' }}
              >
                <div>
                  {inputVisible && (
                    <Input
                      ref={this.saveInputRef}
                      type="text"
                      size="small"
                      style={{ width: 350 }}
                      value={inputValue}
                      onChange={this.handleInputChange}
                      onBlur={this.handleInputConfirm}
                      onPressEnter={this.handleInputConfirm}
                    />
                  )}
                  {!inputVisible && (
                    <Tag
                      onClick={this.showInput}
                      style={{ background: '#fff', borderStyle: 'dashed', margin: '5px 0' }}
                    >
                      <Icon type="plus" /> New Scope
                    </Tag>
                  )}
                </div>
                { selectedScopesTags.length > 0 ? (
                  selectedScopesTags
                ) : <p>No scopes selected yet.</p> }
              </Card>
            </Col>
          </Row>
        </Form>
      </Modal>
    );
  }
}

export default Form.create()(FormModal);

我知道这个组件正在敦促重构,但这不是我现在的工作。我需要 UI 测试它并验证是否一切正常。

我正在尝试测试表单域是否正确呈现。我正在使用 Jest 和 Enzyme,到目前为止我得到了这个:

describe('Groups modal', () => {
  let props;
  const groupsModal = () =>
    mount(
      <FormModal.WrappedComponent {...props} />
    )

  beforeEach(() => {
    props = {
      data: null,
      scopes: [],
      clients: [],
      treeData: [],
      isEditing: false,
      isSaving: false,
      onCancel: jest.fn(),
      onSave: jest.fn(),
      onFilterTreeData: jest.fn(),
      visible: true,
      form: {
        getFieldsError: jest.fn(() => { return {} }),
        getFieldDecorator: () => (component) => component
      }
    };
  });

  it('should render properly', () => {
    const wrapperDivChilds = groupsModal().find('.user-modal').children();

    expect(wrapperDivChilds.length).toBeGreaterThanOrEqual(1);
  });

  describe('form fields', () => {
    it('should render name input', () => {
      const nameInput = groupsModal().find(Input);

      expect(nameInput.length).toBe(1);
    });

    it('should render clients select', () => {
      const clientsSelect = groupsModal().find(Select);

      expect(clientsSelect.length).toBe(1);
    });

    it('should render scopes tree select', () => {
      const scopesTreeSelect = groupsModal().find(TreeSelect);

      expect(scopesTreeSelect.length).toBe(1);
    });
  });
});

我验证输入是否已呈现的所有测试都失败了。 如您所见,我尝试模拟表单装饰器函数,但仍然没有成功...

所以,我的问题是:我应该如何测试这个组件?

如果你只想断言最初呈现的信息,你可以直接导入你的包装组件并执行:

const component = mount(<WrappedComponent {...props} form={formMock} />);

另一方面,如果您不想使用模拟表单或不想断言任何与 form-observer-wrappedComponent 链相关的逻辑,您可以执行下一步操作:

const wrapper = mount(<FormWrapperComponent {...props} />);
// props include anything needed for initial mapPropsToFields
const component = wrapper.find(WrappedComponent);
// Do anything with form itself
const { form } = component.instance().props;

有点耗时,但对我来说效果很好,花了一些时间来了解如何避免使用表单模拟。在这种情况下,您不需要模拟与 Form 本身相关的任何内容,如果任何字段未按您的预期呈现,您可以确定问题出在另一段代码中。