Open doits opened 6 years ago
@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
@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.
This example is totally common. This feature is essential, especially since elements are often styled at the component level and not given ids (by many, many developers) and classes are not unique enough.
Just to add to the precedent for adding this feature, it already exists in vue-testing-library, see the second code block here.
Hey Folks,
Sorry in advance if I'm in the wrong place to ask this.
I got here because I was hoping to do something along the lines of what @doits alluded to: wrapper.find("button:contains(Logout)")
from https://github.com/vuejs/vue-test-utils/issues/960#issuecomment-421082617
But I guess contains
is not a valid CSS selector to use because I got this error:
SyntaxError: unknown pseudo-class selector ':contains(upload)'
However, this page in the documentation suggested that pseudo selectors are supported: https://vue-test-utils.vuejs.org/api/selectors.html
Is there a list of supported pseudo selectors? Or did I miss something?
Thank you for any guidance you can provide π
Pretty sure you can do that with Vue testing library - https://github.com/testing-library/vue-testing-library
Pretty sure you can do that with Vue testing library - https://github.com/testing-library/vue-testing-library
Thank you @dobromir-hristov for the recommendation. I was hoping to not need another library, but that is a very tempting library to include. Thanks again for your help π
Some simplified helpers. These a) don't require specifying a selector and b) include a friendly error handler for your tests:
getAllByText = (wrapper, text) => {
/*
Get all elements with the given text.
*/
return wrapper.findAll("*").filter(node => node.text() === text)
}
getByText = (wrapper, text) => {
/*
Get the first element that has the given text.
*/
const results = getAllByText(wrapper, text)
if (results.length === 0) {
throw new Error(`getByText() found no element with the text: "${text}".`)
}
return results.at(0)
}
As others have mentioned, this is similar in functionality to getByText()
from https://testing-library.com/docs/bs-react-testing-library/examples/#getbytext.
I do not think I agree with that. Selector is important.
For example with something like this - what are you hoping to do?
<button>
<div class="d-flex justify-space-between">
<svg>
<use href="#mdiContentSave">
</svg>
Save
</div>
</button>
I would also like to review the implementation proposed in the issue.
wrapper.find("a", { text: "Log" })
// sugar for
wrapper.findAll("a").filter(node => node.text().match(/Log/)).at(0)
wrapper.find("a", { exactText: "Log" })
// sugar for
wrapper.findAll("a").filter(node => node.text() == "Log").at(0)
First of all, we need to ditch the Array.at
.
For wrapper.findAll
(not shown), we should return an array, so don't index it - just return the whole thing. Array.filter
would be correct here.
wrapper.findAll("a").filter(node => node.text().match(/Log/))
For wrapper.find
, we have Array.find
. So it should just be,
wrapper.findAll("a").find(node => node.text().match(/Log/))
find
and findAll
are sufficient since users can index findAll
however they want if they need more flexibility.
Second of all, this isn't type-safe. Just do it up in TypeScript. We need some optional chaining.
wrapper.findAll("a")?.filter(node => node.text().match(/Log/))
wrapper.find("a")?.find(node => node.text().match(/Log/))
Last, and most importantly, we need to decide on the kind of matching - equals, contains, or regular expression. And whether multiple of these options will be provided. I think I'd suggest contains. I can't see the use case for a regex.
wrapper.find("a")?.find(node => node.text().includes("Log"))
On the proposed solution... chained calls will probably fit with the rest of the library better...
wrapper.findAll("a").withText("Log");
The problem with this isn't findAll
, it's find
...
Could be solved by adding methods instead of options or chaining...
wrapper.findAllWithText("a", "Log");
wrapper.findWithText("a", "Log");
Open to feedback on that...
If in the end the options idea is chosen and multiple matching methodologies are allowed, I highly recommend against using "text" for anything other than equals / exact match.
I know this is closed, but I left this comment for future reference. If you want to use, find by text follow these steps:
vitest.config.js
file.defineConfig
method define test
property.setupFiles
option with the path of setup file in your test
property.Examples:
vitest.config.js
export default defineConfig({
...,
test: {
...,
setupFiles: ["tests/Vue/setup.js"],
},
});
setup.js
const customBehaviors = () => {
return {
findComponentByText(searchedComponent, text) {
return this.findAllComponents(searchedComponent)
.filter((c) => {
return c.text() === text;
})
.at(0);
},
findElementByText(searchedElement, text) {
return this.findAll(searchedElement)
.filter((c) => {
return c.text() === text;
})
.at(0);
},
};
};
config.plugins.VueWrapper.install(customBehaviors);
example.test.js
describe("Example", () => {
it("disables the confirm button when disabled is true", async () => {
const wrapper = mount(Component);
const searchedComponent = wrapper.findComponentByText({ name: "SearchedComponent" }, "CONFIRM");
});
});
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:
Currently, I can do this:
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 afilter
: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
orfindAll
call:There could be a second option
exactText
which only finds element where the text is exactly the one provided.A possible way of implementing this with the current provided methods would be:
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.