qunit-dom
qunit-dom copied to clipboard
External query
Extend qunit.dom() to accept an object with an elements property so external objects that have their own DOM querying implementation like page objects can be plugged into qunit-dom's assertion API.
This is based on a Discord discussion with @Turbo87, and is aimed at supporting my new and still very much WIP page object library (see the tests for a rough idea of the usage patterns), and also at supporting any number of extensions to qunit-dom's querying functionality like #864.
Some notes:
Why elementsDescription?
I chose the name elementsDescription rather than the more terse description to support page object implementations where the page object nodes are passed directly into qunit-dom. In this case, any properties read by qunit-dom need to be reserved for "internal use" by the page object implementation, disallowing users from using those names for child nodes. This seems reasonable for elements and element, but much less so for description. For example, using ember-cli-page-object's API (imagining that it were extended to plug into qunit-dom):
import PageObject from 'ember-cli-page-object';
const page = PageObject.create({
imageTiles: {
scope: '[data-test-image-tile]',
image: { scope: 'img' },
description: { scope: '[data-test-description]' }
}
});
// fine
assert.dom(page.imageTiles.objectAt(0).image).hasAttribute('src', 'funnyCatPic.jpg');
// whoops, description is a reserved property name that `qunit-dom` expects to contain a string!
// So either `assert.dom(page.imageTiles.objectAt(0))` would get a description of `[object Object]`
// or `assert.dom(page.imageTiles.objectAt(0).description)` would get a description string passed
// to it instead of a page object
assert.dom(page.imageTiles.objectAt(0)).exists();
assert.dom(page.imageTiles.objectAt(0).description).hasText('haha, a funny cat picture, how original');
I'm definitely open to other ideas for how to expose this information to qunit-dom in a way that isn't overly likely to cause page object footguns.
<unknown> vs. <not found>
<unknown> was used for the assertion message when null was passed in as the target, but if a selector was passed in that didn't match anything, the selector string would be used. In the other assertion messages where the element was not found, <not found> was used. I couldn't figure out what value this distinction was providing (if any, maybe just an accident of how the code evolved?), so I couldn't figure out how to translate it to the DOMQuery abstraction, so I just eliminated it, meaning some of the assertion strings have changed (as you can see from the tests). If there's a reason to keep <unknown> I'm happy to discuss how to revive it.
documentation
As mentioned in the second commit, updating the documentation was a bit of a PITA mainly, I think, because of some limitations of documentation. I did try switching to generating the documentation off of the typescript so we don't have to hard-code the ExternalQuery documentation in documentation.yml, but then it generated a separate entry for each typescript function overload signature, which seemed less than helpful. Anyway, this is the best I could come up with but am happy to entertain other ideas.
thanks for working on this first draft @bendemboski. we will need a bit of time to think about this though, so don't expect this to land in the very near future.
you made a good point about description potentially causing conflicts. maybe it would indeed be better to use properties scoped to qunit-dom instead, since these objects are never supposed to be manually created anyway.
the <unknown> was definitely intentional, not sure I fully understand why that had to be changed 🤔
the
was definitely intentional, not sure I fully understand why that had to be changed 🤔
I'm just not sure how to adapt that Element ${this.target || '<unknown>'} should exist logic. Because this.target could be a selector or an element (and we know we didn't match an element at this point), that logic actually comes down to:
if (this.target === null) {
return '<unknown>'; // !== this.targetDescription
} else {
return this.target; // === this.targetDescription
}
To re-implement it using the query interface, we'd have to know the type of the target constructor argument (which as I have it set up is no longer this.target, but is behind an interface). So we'd either need to do something like
if (this.query instanceof ElementQuery) {
return '<unknown>'; // !== this.targetDescription
} else {
return this.query.description; // === this.targetDescription
}
or we'd have to add another property to the DOMQuery interface.
I suppose the way I'd probably do it is the latter -- add a notFoundDescription to the DOMQuery interface, and implement it as <unknown> in ElementQuery, and this.description in SelectorQuery and WrappedQuery. Just, as I said, since I couldn't figure out the user value that the <unknown> vs. <not found> distinction was providing, I wasn't sure if maintaining it was worth the extra complexity.
I've opened an RFC to discuss this as a more general pattern that could be used in other testing libraries as well.