Search code examples
reactjstypescriptunit-testinghtmlelementsreact-testing-library

React Testing Library with TypeScript: Set an input's value


I'm building a React application using TypeScript. I do my component tests using React Testing Library.

Let's say you have simple form like this:

import React from 'react'

function Login({onSubmit}) {
  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault()
          const {username, password} = e.target.elements
          onSubmit({
            username: username.value,
            password: password.value,
          })
        }}
      >
        <label htmlFor="username">Username</label>
        <input id="username" />
        <label htmlFor="password">Password</label>
        <input id="password" type="password" />
        <br />
        <button type="submit">Submit</button>
      </form>
    </div>
  )
}

export {Login}

In this video Kent (the creator of the library) shows how you would test the forms input inputs. The tests would look like this:

import React from 'react'
import {renderIntoDocument, cleanup} from 'react-testing-library'
import {Login} from '../login'

afterEach(cleanup)

test('calls onSubmit with username and password', () => {
  const handleSubmit = jest.fn()
  const {getByLabelText, getByText} = renderIntoDocument(
    <Login onSubmit={handleSubmit} />,
  )
  getByLabelText(/username/i).value = 'chuck'
  getByLabelText(/password/i).value = 'norris'
  getByText(/submit/i).click()
  expect(handleSubmit).toHaveBeenCalledTimes(1)
  expect(handleSubmit).toHaveBeenCalledWith({
    username: 'chuck',
    password: 'norris',
  })
})

The problem is that he did it with plain JavaScript. When doing this with TypeScript the lines where he sets .value throw the following errors

[ts] Property 'value' does not exist on type 'HTMLElement'.

How would one test this functionality with TypeScript using React Testing Library? How would you set the input's values?


Solution

  • The typings provided by that library type the return value of getByLabelText as type: HTMLElement. Not all HTML elements have value properties, only things like HTMLInputElement do.

    The getByLabelText also has no sort of generic type through which you might be able to affect the output type, so essentially you will need to either unsafely cast the result to type HTMLInputElement or you will need to build a helper function that tells TypeScript whether or not the object is the right type:

    1. Unsafe cast. All you really need to do is update any calls to getByLabelText where you expect it to be a type with a value property to:

      (getByLabelText(/username/i) as HTMLInputElement).value = 'chuck';
      
    2. Type validation. This method is a bit safer as you can provide a type validation function that will cause TypeScript to update the type:

      function isElementInput<T extends HTMLElement>(element: T): T is HTMLInputElement {
          // Validate that element is actually an input
          return element instanceof HTMLInputElement;
      }
      
      // Update your attempted value sets:
      const elem = getByLabelText(/username/i);
      if (isElementInput(elem)) {
          elem.value = 'chuck';
      } else {
          // Handle failure here...
      }