I got a great question via e-mail about how to test your app when you’re using a third-party form component library. In my tutorials I recommend putting a data-test attribute on your <input> elements to select them by:

<input data-test="email" />

In Cypress, for example, you can select this element like so:

cy.get("[data-test='email']").type("example@example.com");

But what do you do when you don’t create the <input> elements yourself–a third party library does? Depending on the library you’re using, you can get an error like the following:

CypressError: cy.type() failed because it requires a valid typeable element

Cypress found the element that was being selected, but apparently it’s not an element that can be typed into. What’s going on here?

The key to testing these form controls is to dive into the details of the HTML elements the third-party form component is creating. Cypress doesn’t know about your components; it only knows about the DOM elements that are created.

Let’s look at how a few different Material Design libraries render and how we can test each.

Vuetify

In Vuetify, for Vue.js, a text field component is written like this:

<v-text-field
  id="myfield"
  class="myfield"
  data-test="myfield"
/>

Note that I’ve added an ID, a class, and a data-test attribute. Although I recommend selecting elements via data-test attributes, I want to demo IDs and classes as well because your existing tests may already be using IDs or classes, and component libraries can handle them differently.

Here are the DOM elements created by this component:

<div class="v-input myfield v-text-field v-text-field--placeholder theme--light">
  <div class="v-input__control">
    <div class="v-input__slot">
      <div class="v-text-field__slot">
        <input id="myfield" data-test="myfield" type="text">
      </div>
    </div>
  </div>
</div>

Notice that the id and data-test attribute are placed on the <input> element itself, so they can be used to get the <input> directly:

cy.get("#myfield").type("By ID");
cy.get("[data-test='myfield']").type("By data attribute");

By contrast, the class is applied to the outermost <div>. This makes sense from a styling standpoint, but if we try to select the element with that class:

cy.get(".myfield").type("By class");

Cypress gives us the error I mentioned earlier:

CypressError: cy.type() failed because it requires a valid typeable element

This is because .myfield is the div, not the input, and you can’t type into a div. Instead, we need to ask for the input element inside the element with class .myfield:

cy.get(".myfield input").type("By class and input element");

This works. Unfortunately, this means your test is coupled to implementation details of a third-party component library. If Vuetify is updated to apply the class to the input instead of the containing div, your tests will break. Luckily, if you use the data-test attribute I recommend it’s applied to the input directly, so your tests avoid coupling to the third-party library.

Material-UI

Let’s compare this to one of the popular Material Design libraries for React: Material-UI. Here’s how you create a text field in Material-UI:

<TextField
  id="myfield"
  class="myfield"
  data-test="myfield"
/>

And here’s the rendered DOM:

<div class="myfield" data-test="myfield">
  <div class="MuiInputBase-root-18 MuiInput-root-5 MuiInput-underline-9 MuiInputBase-formControl-19 MuiInput-formControl-6">
    <input id="myfield" type="text" value="" aria-invalid="false" class="MuiInputBase-input-28 MuiInput-input-13">
  </div>
</div>

As before, the id is applied to the input, and the class is applied to the containing div. The data-test attribute is different, though: whereas with Vuetify it was applied to the input, in Material-UI the data-test attribute is applied to the containing div.

This means that for Material-UI it’s only the ID field that you can select directly:

cy.get("#myfield").type("By ID");

For either data-test attributes or classes, you need to explicitly select the containing input:

cy.get("[data-test='myfield'] input").type("By data attribute");
cy.get(".myfield input").type("By class and input element");

Ember-Paper

To round out our comparison, let’s look at Ember-Paper, a Material Design library for Ember.js:

<Form.input
  data-test-myfield
  id="myfield"
  class="myfield"
  @type="text"
  @value={{myfield}}
  @onChange={{action (mut myField)}}
/>

(Note that the form of the data attribute is slightly different, because of how the ember-test-selectors library sets up data attributes.)

Here’s the DOM it renders:

<md-input-container id="myfield" data-test-myfield="" class="md-default-theme ember-view myfield">
  <input class="md-input   ng-dirty" id="input-ember206" aria-describedby="ember206-char-count ember206-error-messages" type="text">
</md-input-container>

(Yes, that’s literally a DOM element called md-input-container. Weird, huh? Your guess is as good as mine!)

Anyways, the way attributes are handled is consistent, but not in the direction we’d prefer: the id, class, and data-test attribute are all applied to the containing element, not to the input. So no matter which we use, we need to select the input explicitly:

await fillIn('#myfield input', 'By ID');
await fillIn('.myfield input', 'By Class');
await fillIn('[data-test-myfield] input', 'By Data Attribute')

The General Case

There are dozens and dozens of different UI component libraries, so you may need to do your own research to get the tests working in your app. The key is to look into how the third-party components render to the DOM. There’s nothing standardized about how attributes are applied: the components have full control over what they render. So you’ll need to see where they put data-test attributes, or whatever alternate attribute you use for selecting elements.

I think it’s ideal for data-test attributes to be applied to the input themselves, so if your UI library doesn’t do it that way, it could be worth you opening an issue requesting a change–or, even better, a pull request to add that functionality!