Vue-test-utils: Provide simple way to find/filter nodes based on text content

Created on 7 Sep 2018  Â·  15Comments  Â·  Source: vuejs/vue-test-utils

What problem does this feature solve?

It should be possible to find nodes by the text they contain. For example when I want to click the logout link in my component:

<template>
  <a href="/my_account">My Account</a>
  <a @click="logout">Logout</a>
</template>

Currently, I can do this:

wrapper.findAll("a").at(1).trigger("click")

I dislike using findAll.at because requires the test to know the order of the links. For my test I don't care about the order, but I just want to click on the link with the text "Logout".

If later the component changes and the order is rearranged or a new a-tag is inserted, I have to change my test, even though it has nothing to do with the order.

Second option is to use findAll with a filter:

wrapper.findAll("a").filter(node => node.text().match(/Logout/)).at(0).trigger("click")

This works, but is really cumbersome to write for such a simple thing. I would expect an easier method to be provided.

What does the proposed API look like?

I come from ruby where it is simply an option to the find or findAll call:

wrapper.findAll("a", { text: "Logout" }) // find all `a` which contain the text
wrapper.find("a", { text: "Logout" }) // find the first `a` which contains the text

There could be a second option exactText which only finds element where the text is exactly the one provided.

wrapper.find("a", { exactText: "Log" }) // will not find `<a>Logout</a>`

wrapper.find("a", { text: "Log" }) // will find `<a>Logout</a>`

A possible way of implementing this with the current provided methods would be:

wrapper.find("a", { text: "Log" })
==>
wrapper.findAll("a").filter(node => node.text().match(/Log/)).at(0)

wrapper.find("a", { exactText: "Log" })
==>
wrapper.findAll("a").filter(node => node.text() == "Log").at(0)

Not sure if there is a more efficient method though.

I think it would be handy to have this filtering by text provided by vue-test-utils directly, since it makes writing tests much easier and error prone.

feature request intend to implement

Most helpful comment

Yes, I think we should add support to find components based on text. Tests that use text as selectors can be more durable to refactoring, because text is (sometimes) less likely to change than class names/ DOM attributes.

All 15 comments

@eddyerburgh
I think this example is not common.
I think that the element usually has attributes.
I think CSS Selector is enough.
This feature is not essential.
If it is necessary to change many same CSS Selector,
it will resolve by abstracting CSS Selector.
I think this feature is unnecessary.

@38elements Thanks for your feedback. I think it comes down to the way how components are tested if such extenstions to the finders are common/meaningful or not.

I like to stay as close to what the normal user sees. This means when I want to test a click on the logout button I don't want to write in my test:

Click on the button with the class logout

or

Click on the button with the data-logout attribute

Because a normal user does not decide on the class or attribute which button to click.

I'd rather write in my test:

Click on the button with the text 'Logout'

Because this is what a user should be able to do. He does not see any class or attribute.

For me this makes more sense because it stays closer to what the user actually sees and does and not on some hidden attributes or classes.

Actually this is my real example why I proposed this syntax:

<template>
  <div class="buttonlogout">
    <a
      href="#"
      @click.prevent="toggleOpen"
    >Logout
      <i class="fas fa-caret-down" />
    </a>
    <div
      v-if="open"
      class="modal-container"
    >
      <p>Do you really want to log out?</p>
      <button
        class="button primary"
        @click.prevent="logoutClose"
      >
        Logout
      </button>
    </div>
  </div>
</template>

Of course I could test here (by using the primary class)

Click on the primary button

... but for me this is a different test than ...

Click on the logout button

... which I actually want to write! I want to test the logout button (which is defined by the text "Logout" for a normal user).

Besides it might happen that I change the primary button later to be a different one than the logout button, so I would have to refactor my "click on the logout button" spec just because I changed some classes – which feels wrong to me. The logout button would be the same then.

Maybe I'm a nitpicker on that difference, but in ruby I have come a good way along with such tests which even survived refactoring whole components by testing what the user sees and not hidden attributes or classes.


Anyway, I can do this already with the helper functions I wrote in the OP (and I do), but I'd feel having such functionality implement in vue-test-utils directly would also benefit other users.

The implementation could also be different, eg by extending the css selectors like wrapper.find("button:contains(Logout)"), if this feels better.

I think that this is necessary.

Yes, I think we should add support to find components based on text. Tests that use text as selectors can be more durable to refactoring, because text is (sometimes) less likely to change than class names/ DOM attributes.

I think a common use for this may be (as it is for me) testing pagination. I don't think dom attributes or styles or ids should be necessary to select a div with text 3. For my e2e, I use testcafe, and they have a selector like this
Selector('.page__btn').withText('3')

I had several buttons on my modal and also had to do:

wrapper.findAll("button").filter(node => node.text().match(/Annuler/)).at(0).trigger("click")

But that seems too cumbersome for just testing that one button.
Please, has support been added for this? I mean if there's a shorter way to do it now as suggested by @doits?

Is anybody working on this? I could implement it

@denisinvader no one is working on it, it would be great if you could :)

For anyone looking to use the feature before it gets implemented, you can also implement a helper function like this

function withWrapperArray(wrapperArray) {
  return {
    childSelectorHasText: (selector, str) => wrapperArray.filter(i => i.find(selector).text().match(str)),
    hasText: (str) => wrapperArray.filter(i => i.text().match(str)),
  }
}

usage:

// find first element that that has class name `.filter__name` with a textvalue of `Vehicles`
withWrapperArray(wrapper.findAll('.filter__name')).hasText('Vehicles').at(0);

// find an element that has a child with matching  text content 
withWrapperArray(wrapper.findAll('.filter')).childSelectorHasText('label', 'Saab').at(0)
// get the input that's a sibling of the matched label
withWrapperArray(wrapper.findAll('.filter')).childSelectorHasText('label', 'Saab').at(0).find('input')

this and more in gist
https://gist.github.com/dasDaniel/d4fb8f0eb6f36acdbe7b1ce66c034949

@dasDaniel solution, but typescript variation (for ts-jest tests):

import { WrapperArray } from '@vue/test-utils/types';
import Vue from 'vue';

function withWrapperArray(wrapperArray: WrapperArray<Vue>): Record<string, Function> {
    return {
        childSelectorHasText: (
            selector: string,
            str: string,
        ): WrapperArray<Vue> => wrapperArray
            .filter(i => i.find(selector).text().match(str)),

        hasText: (str: string): WrapperArray<Vue> => wrapperArray
            .filter(i => i.text().match(str)),
    };
}

@viT-1 Thanks for the snippet. It might be even better to drop the generic return type (i.e. drop Record<string, Function>).

That way Typescript will infer the return type from the returned object. Then the code completion (e.g. in Webstorm) will offer the methods hasText and childSelectorHasText correctly. And will also know that they return WrapperArray.

Modified code:

import Vue from 'vue'
import { WrapperArray } from '@vue/test-utils'

function withNameFilter(wrapperArray: WrapperArray<Vue>) {
  return {
    childSelectorHasText: (selector: string, str: string): WrapperArray<Vue> =>
      wrapperArray.filter((i) => i.find(selector).text().match(str)),

    hasText: (str: string): WrapperArray<Vue> =>
      wrapperArray.filter((i) => i.text().match(str)),
  }
}

+1 for adding this

In VTU 2, you would be able to add these as plugins, or use https://github.com/testing-library/vue-testing-library

+1 for adding this please!

+1 for adding this. One more feature that would clear out some of the "data-test" tags around my project

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fungus1487 picture fungus1487  Â·  3Comments

matt-sanders picture matt-sanders  Â·  3Comments

AustinGil picture AustinGil  Â·  3Comments

eddyerburgh picture eddyerburgh  Â·  4Comments

kjugi picture kjugi  Â·  3Comments