-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add more helpful debugging information to queries #108
Changes from 4 commits
c67414d
c9a25b6
b58f747
1f14ce1
217a289
baf066d
aa76c50
a5bfc6f
d85d918
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,24 +109,49 @@ To show some simple examples (from | |
[cypress/integration/query.spec.js](cypress/integration/query.spec.js) or [cypress/integration/find.spec.js](cypress/integration/find.spec.js)): | ||
|
||
```javascript | ||
cy.queryByText('Button Text').should('exist') | ||
cy.queryByText('Non-existing Button Text').should('not.exist') | ||
cy.queryByLabelText('Label text', {timeout: 7000}).should('exist') | ||
cy.queryAllByText('Button Text').should('exist') | ||
cy.queryAllByText('Non-existing Button Text').should('not.exist') | ||
cy.queryAllByLabelText('Label text', {timeout: 7000}).should('exist') | ||
cy.findAllByText('Jackie Chan').click({multiple: true}) | ||
|
||
// findAllByText _inside_ a form element | ||
cy.get('form').within(() => { | ||
cy.findByText('Button Text').should('exist') | ||
cy.findAllByText('Button Text').should('exist') | ||
}) | ||
cy.get('form').then(subject => { | ||
cy.findByText('Button Text', {container: subject}).should('exist') | ||
cy.findAllByText('Button Text', {container: subject}).should('exist') | ||
}) | ||
cy.get('form').findAllByText('Button Text').should('exist') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I felt this example was missing. It was one of the reasons I overlooked this library and the I found out #100 was merged just recently. This is more idiomatic for Cypress. |
||
``` | ||
|
||
### Differences DOM Testing Library | ||
|
||
`Cypress Testing Library` supports both jQuery elements and DOM nodes. This is | ||
necessary because Cypress uses jQuery elements, while `DOM Testing Library` | ||
expects DOM nodes. When you pass a jQuery element as `container`, it will get | ||
the first DOM node from the collection and use that as the `container` parameter | ||
for the `DOM Testing Library` functions. | ||
|
||
`get*` queries are disabled. `find*` queries do not use the Promise API of | ||
NicholasBoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`DOM Testing Library`, but instead forward to the `get*` queries and use Cypress' | ||
built-in retryability using error messages from `get*` APIs to forward as error | ||
messages if a query fails. `query*` also uses `get*` APIs, but disables retryability. | ||
|
||
`findBy*` is less useful in Cypress compared to `findAllBy*`. If you intend to limit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Does the difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why I would use I like that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the question is around idiomatic Cypress code vs idiomatic @testing-library/* code. All Cypress commands that return elements (actually jQuery-wrapped NodeLists) return 1 or more elements or fail. The only exception to this is if the command is followed by as Most of the time this is exactly what you expect to happen. It avoids a larger API footprint of ensuring only 1 element. Most of the time there is no issue. For example: cy.contains('button', 'Submit').click()
I'm perfectly happy to remove any documentation suggesting single vs many queries. I just wanted to pose a question - should the API reflect something Cypress users would most likely expect or something non-Cypress users would most expect? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The thing I personally find most useful about this library is helpers for common things I need to do. Like selecting an input element by its label. Perhaps other people value the API to be more strictly adhering to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cypress does have a mechanism for asserting there should be only one element if necessary: cy.get('some-selector').should('have.length', 1) Sometimes the intent is to make sure there is only one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'll make the language more positive.
Do you mean the current functionality that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IMHO idiomatic @testing-library/* code not only provides useful common helpers (generally for selecting elements) but also encourages best practices. So, it may deviate from idiomatic code in the library / framework it augments because that library doesn't support/encourage best practice. Consider the example:
I would always prefer use the second version, because when my code changes so that selector matches a second element, I expect a test failure on selecting an element, not clicking the incorrect element. It's possible to avoid this case using: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the update to the readme okay? https://github.com/testing-library/cypress-testing-library/#differences-from-dom-testing-library There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The update to the readme definitely documents this change 👍 I'm not sure I agree it should be the recommendation of |
||
to only 1 element, the following will work: | ||
|
||
```javascript | ||
cy.findAllByText('Some Text').should('have.length', 1) | ||
``` | ||
|
||
Cypress handles actions when there is only one element found. For example, the following | ||
will work without having to limit to only 1 returned element. The `cy.click` will | ||
automatically fail if more than 1 element is returned by the `findAllByText`: | ||
|
||
```javascript | ||
cy.findAllByText('Some Text').click() | ||
``` | ||
|
||
## Other Solutions | ||
|
||
I'm not aware of any, if you are please [make a pull request][prs] and add it | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
/* eslint-disable max-lines-per-function */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this rule even makes sense in the context of a test file. Is the intent to limit how many tests there are in a file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't use {
"rules": {
"max-lines-per-function": "off",
"jest/valid-expect-in-promise": "off"
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That worked, thank you |
||
|
||
describe('find* dom-testing-library commands', () => { | ||
beforeEach(() => { | ||
cy.visit('cypress/fixtures/test-app/') | ||
|
@@ -86,22 +88,50 @@ describe('find* dom-testing-library commands', () => { | |
|
||
/* Test the behaviour around these queries */ | ||
|
||
it('findByText should handle non-existence', () => { | ||
cy.findByText('Does Not Exist') | ||
.should('not.exist') | ||
}) | ||
|
||
it('findByText should handle eventual existence', () => { | ||
cy.findByText('Eventually Exists') | ||
.should('exist') | ||
}) | ||
|
||
it('findByText should handle eventual non-existence', () => { | ||
cy.findByText('Eventually Not exists') | ||
.should('not.exist') | ||
}) | ||
|
||
it("findByText with should('not.exist')", () => { | ||
cy.findAllByText(/^Button Text \d$/).should('exist') | ||
cy.findByText('Non-existing Button Text', {timeout: 100}).should( | ||
'not.exist', | ||
) | ||
}) | ||
|
||
it('findByText with a previous subject', () => { | ||
cy.get('#nested') | ||
.findByText('Button Text 1', { fallbackToPreviousFunctionality: false }) | ||
.should('not.exist') | ||
cy.get('#nested') | ||
.findByText('Button Text 2') | ||
.should('exist') | ||
}) | ||
|
||
it('findByText within', () => { | ||
cy.get('#nested').within(() => { | ||
cy.findByText('Button Text 2').click() | ||
cy.findByText('Button Text 1').should('not.exist') | ||
cy.findByText('Button Text 2').should('exist') | ||
}) | ||
}) | ||
|
||
it('findByText in container', () => { | ||
return cy.get('#nested').then(subject => { | ||
cy.findByText(/^Button Text/, {container: subject}).click() | ||
// NOTE: Cypress' `then` doesn't actually return a promise | ||
// eslint-disable-next-line jest/valid-expect-in-promise | ||
cy.get('#nested').then(subject => { | ||
cy.findByText('Button Text 1', {container: subject}).should('not.exist') | ||
cy.findByText('Button Text 2', {container: subject}).should('exist') | ||
}) | ||
}) | ||
|
||
|
@@ -110,23 +140,87 @@ describe('find* dom-testing-library commands', () => { | |
cy.findByText('New Page Loaded').should('exist') | ||
}) | ||
|
||
it('findByText should set the Cypress element to the found element', () => { | ||
// This test is a little strange since snapshots show what element | ||
// is selected, but snapshots themselves don't give access to those | ||
// elements. I had to make the implementation specific so that the `$el` | ||
// is the `subject` when the log is added and the `$el` is the `value` | ||
// when the log is changed. It would be better to extract the `$el` from | ||
// each snapshot | ||
Comment on lines
+141
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
cy.on('log:changed', (attrs, log) => { | ||
if (log.get('name') === 'findByText') { | ||
expect(log.get('$el')).to.have.text('Button Text 1') | ||
} | ||
}) | ||
|
||
cy.findByText('Button Text 1') | ||
}) | ||
|
||
it('findByText should error if no elements are found', () => { | ||
const regex = /Supercalifragilistic/ | ||
const errorMessage = `Timed out retrying: Expected to find element: 'findByText(${regex})', but never found it.` | ||
const errorMessage = `Unable to find an element with the text: /Supercalifragilistic/` | ||
cy.on('fail', err => { | ||
expect(err.message).to.contain(errorMessage) | ||
}) | ||
|
||
cy.findByText(regex, {timeout: 100}) // Every find query is implicitly a `.should('exist') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should explicitly use
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with you. In the jest world, I recommend people wrap expect(getByText(/success/i)).toBeInTheDocument() |
||
}) | ||
|
||
it('findByText should default to Cypress non-existence error message', () => { | ||
const errorMessage = `Expected <button> not to exist in the DOM, but it was continuously found.` | ||
cy.on('fail', err => { | ||
expect(err.message).to.contain(errorMessage) | ||
}) | ||
|
||
cy.findByText('Button Text 1', {timeout: 100}) | ||
.should('not.exist') | ||
}) | ||
|
||
it('findByLabelText should forward useful error messages from @testing-library/dom', () => { | ||
const errorMessage = `Found a label with the text of: Label 3, however no form control was found associated to that label.` | ||
cy.on('fail', err => { | ||
expect(err.message).to.eq(errorMessage) | ||
expect(err.message).to.contain(errorMessage) | ||
NicholasBoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
|
||
cy.findByText(regex, {timeout: 100}) // Doesn't explicitly need .should('exist') if it's the last element? | ||
cy.findByLabelText('Label 3', {timeout: 100}).should('exist') | ||
}) | ||
|
||
it('findByText finding multiple items should error', () => { | ||
const errorMessage = `Found multiple elements with the text: /^Button Text/i\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).` | ||
cy.on('fail', err => { | ||
expect(err.message).to.eq(errorMessage) | ||
expect(err.message).to.contain(errorMessage) | ||
}) | ||
|
||
cy.findByText(/^Button Text/i) | ||
cy.findByText(/^Button Text/i, {timeout: 100}) | ||
}) | ||
|
||
it('findByText should not break existing code', () => { | ||
cy.window() | ||
.findByText('Button Text 1') | ||
.should('exist') | ||
}) | ||
Comment on lines
+195
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test verifies non-breaking changes for #110. |
||
|
||
it('findByText should show as a parent command if it starts a chain', () => { | ||
const assertLog = (attrs, log) => { | ||
if(log.get('name') === 'findByText') { | ||
expect(log.get('type')).to.equal('parent') | ||
cy.off('log:added', assertLog) | ||
} | ||
} | ||
cy.on('log:added', assertLog) | ||
cy.findByText('Button Text 1') | ||
}) | ||
|
||
it('findByText should show as a child command if it continues a chain', () => { | ||
const assertLog = (attrs, log) => { | ||
if(log.get('name') === 'findByText') { | ||
expect(log.get('type')).to.equal('child') | ||
cy.off('log:added', assertLog) | ||
} | ||
} | ||
cy.on('log:added', assertLog) | ||
cy.get('body').findByText('Button Text 1') | ||
Comment on lines
+201
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}) | ||
}) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import {commands} from './' | ||
|
||
commands.forEach(({name, command}) => { | ||
Cypress.Commands.add(name, command) | ||
commands.forEach(({name, command, options = {}}) => { | ||
Cypress.Commands.add(name, options, command) | ||
}) | ||
|
||
/* global Cypress */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I have a good reason for changing these. It is part of the questions of the pull request. I think
*By
and*AllBy
make sense for@test-library/dom
,@testing-library/react
, and company, but I'm not sure it does for Cypress.Personally, I recommend always using the
*ByAll
since that's what native Cypress commands do now, but I'm welcoming other opinions