Skip to content

[FEATURE] Updating props, simulating events, and method chaining on the same instance #672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mattcarlotta opened this issue May 20, 2020 · 2 comments

Comments

@mattcarlotta
Copy link

mattcarlotta commented May 20, 2020

Describe the feature you'd like:

This feature is very similar to the requested feature in #65; however, I'd like to take it a bit further by introducing/implementing a reusable factory function to manipulate an instance with some optional method chaining.

The current recommendation was/is to create an updateProps function within each test. However, if we provide a factory mount (or append this to the render ) function, we can eliminate some of the unnecessary boilerplate and just manipulate the instance:

const onClick = jest.fn();

// initializes Button with `onClick` and returns all methods provided by RTL 
// plus some additional methods: "find", "setProps" and "simulate"
const wrapper = mount(<Input dataTestId="input" onClick={onClick} /> 

// updating props

// updates the Input instance with props: { dataTestId, onClick, foo: false }
wrapper.setProps({ foo: false }); 
// updates the Input instance with props: { dataTestId, onClick, foo: false, bar: true })
wrapper.setProps({ bar: true });
...etc

// simulating events

const event = { target: { value: "hi" } };
// finds an element within the wrapper, at the first position, and simulates an event with some method chaining
wrapper.find("[data-testid=input]").at(0).simulate("change", event);

// misc

// provides the length of the selected element(s)
wrapper.find("[data-testid=input]").length
// provides the existence of the selected element(s)
wrapper.find("[data-testid=input]").exists 

Suggested implementation:

Handling instance manipulation internally while providing public methods to either find or setProps or simulate from the same instance. A working prototype with side-by-side tests can be found here:
Edit React-Testing-Library w/ Enzyme-like methods

Factory mount function:

import isEmpty from 'lodash.isempty'
import {cloneElement} from 'react'
import {render, fireEvent} from '@testing-library/react'

/**
 * A class that wraps RTL's render and supplies it with Enzyme-like methods
 * @class EnzymeWrapper
 * @param {node} Component - Component to be mounted
 * @method find - finds an element by string using querySelector
 * @method setProps - merges old props with new props and rerenders the Component
 * @method setSelection - sets additional properties and methods on a selection
 * @method simulate - simulates a fireEvent by type with passed in options
 * @returns {object} - an Enzyme-like wrapper
 */
class EnzymeWrapper {
  constructor(Component) {
    this.Component = Component
    this.props = Component.props
    this.wrapper = render(Component)
    this.find = this.find.bind(this)
    this.setProps = this.setProps.bind(this)

    return {
      ...this.wrapper,
      find: this.find,
      setProps: this.setProps,
    }
  }

  find(byString) {
    const elements = this.wrapper.container.querySelectorAll(byString)
    let selection = this.wrapper.container.querySelector(byString)

    selection = this.setSelection(elements, selection)

    return selection
  }

  setProps(props) {
    this.props = {...this.props, ...props}

    this.wrapper.rerender(cloneElement(this.Component, this.props))
  }

  setSelection(elements, selection) {
    selection.simulate = (eventType, opts) =>
      this.simulate(selection, eventType, opts)
    selection.length = !isEmpty(elements) ? elements.length : 0
    selection.exists = selection.length >= 1
    selection.at = pos => {
      if (!elements || elements[pos] === undefined)
        throw Error(
          'wrapper::at(): Unable to locate an element at that position.',
        )

      let nextSelection = elements[pos]
      nextSelection = this.setSelection(nextSelection, nextSelection)

      return nextSelection
    }

    return selection
  }

  simulate(element, eventType, opts) {
    if (isEmpty(element)) {
      throw Error(
        `wrapper::simulate(): unable to locate any elements to simulate an event.`,
      )
    }
    if (!eventType) {
      throw Error(
        `wrapper::simulate(): unable to call simulate without an event type.`,
      )
    }

    fireEvent[eventType](element, opts)

    return element
  }
}

/**
 * Factory function to initialize a RTL rendered React component with an Enzyme-like class
 * @function mount
 * @param {node} Component - Component to be mounted
 * @returns {object} - an Enzyme-like wrapper
 */
export const mount = Component => new EnzymeWrapper(Component);

Teachability, Documentation, Adoption, Migration Strategy:

The above is just a prototype/P.o.C. and will most likely need to be refactored for asynchronous actions and optimized for performance. However, for those familiar with enzyme, the methods provided should be pretty self-explanatory and straight forward. The idea would be to reduce the need of having to pass in the same component to update it with props (like renderer(<Input {...nextProps} />); ) or having to import fireEvent to execute an event change on an element that may already exist within the container instance (like fireEvent.change(queryByTestId("input"), event).

@marcosvega91
Copy link
Member

Hey,

setProps could be a good idea.

material-ui has implemented it. I don't know why it is not implemented here

https://www.github.com/mui-org/material-ui/tree/master/test%2Futils%2FcreateClientRender.js

@eps1lon
Copy link
Member

eps1lon commented May 24, 2020

material-ui has implemented it. I don't know why it is not implemented here

Because it's generally not something you should use. It's only ever useful if you're writing a component library. At that point you should know what you're doing and can write that helper yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants