Skip to content

Commit c67414d

Browse files
committed
feat: add more helpful debugging information to queries
* Add element selector information for debugging (outlines element when you click on command) (fixes testing-library#103) * Add @testing-library/dom errors (from `get*` queries) to failure messages - these are more helpful than the generic `find*('input') does not exist` messages (fixes testing-library#103) * Add retryability to `findBy*` when multiple elements are found (fixes testing-library#83) * Add option to disable logging of all commands * `query*` and `find*` have a consistent code path and error messaging (fixes testing-library#103) * Remove usage of Cypress commands in queries (fixes testing-library#103)
1 parent 2f62901 commit c67414d

File tree

6 files changed

+250
-67
lines changed

6 files changed

+250
-67
lines changed

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,49 @@ To show some simple examples (from
109109
[cypress/integration/query.spec.js](cypress/integration/query.spec.js) or [cypress/integration/find.spec.js](cypress/integration/find.spec.js)):
110110

111111
```javascript
112-
cy.queryByText('Button Text').should('exist')
113-
cy.queryByText('Non-existing Button Text').should('not.exist')
114-
cy.queryByLabelText('Label text', {timeout: 7000}).should('exist')
112+
cy.queryAllByText('Button Text').should('exist')
113+
cy.queryAllByText('Non-existing Button Text').should('not.exist')
114+
cy.queryAllByLabelText('Label text', {timeout: 7000}).should('exist')
115115
cy.findAllByText('Jackie Chan').click({multiple: true})
116+
117+
// findAllByText _inside_ a form element
116118
cy.get('form').within(() => {
117-
cy.findByText('Button Text').should('exist')
119+
cy.findAllByText('Button Text').should('exist')
118120
})
119121
cy.get('form').then(subject => {
120-
cy.findByText('Button Text', {container: subject}).should('exist')
122+
cy.findAllByText('Button Text', {container: subject}).should('exist')
121123
})
124+
cy.get('form').findAllByText('Button Text').should('exist')
122125
```
123126

127+
### Differences DOM Testing Library
128+
124129
`Cypress Testing Library` supports both jQuery elements and DOM nodes. This is
125130
necessary because Cypress uses jQuery elements, while `DOM Testing Library`
126131
expects DOM nodes. When you pass a jQuery element as `container`, it will get
127132
the first DOM node from the collection and use that as the `container` parameter
128133
for the `DOM Testing Library` functions.
129134

135+
`get*` queries are disabled. `find*` queries do not use the Promise API of
136+
`DOM Testing Library`, but instead forward to the `get*` queries and use Cypress'
137+
built-in retryability using error messages from `get*` APIs to forward as error
138+
messages if a query fails. `query*` also uses `get*` APIs, but disables retryability.
139+
140+
`findBy*` is less useful in Cypress compared to `findAllBy*`. If you intend to limit
141+
to only 1 element, the following will work:
142+
143+
```javascript
144+
cy.findAllByText('Some Text').should('have.length', 1)
145+
```
146+
147+
Cypress handles actions when there is only one element found. For example, the following
148+
will work without having to limit to only 1 returned element. The `cy.click` will
149+
automatically fail if more than 1 element is returned by the `findAllByText`:
150+
151+
```javascript
152+
cy.findAllByText('Some Text').click()
153+
```
154+
130155
## Other Solutions
131156

132157
I'm not aware of any, if you are please [make a pull request][prs] and add it

cypress/fixtures/test-app/index.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ <h2>*ByLabel and *ByPlaceholder</h2>
2929

3030
<label for="by-text-input-2">Label 2</label>
3131
<input type="text" placeholder="Input 2" id="by-text-input-2" />
32+
33+
<p>Intentionally inaccessible label for error checking</p>
34+
<label>Label 3</label>
3235
</section>
3336
<section>
3437
<h2>*ByText</h2>
@@ -89,6 +92,24 @@ <h2>*AllByText</h2>
8992
<h2>*ByText on another page</h2>
9093
<a onclick='setTimeout(function() { window.location = "/cypress/fixtures/test-app/next-page.html"; }, 100);'>Next Page</a>
9194
</section>
95+
<section>
96+
<h2>Eventual existence</h2>
97+
<button id="eventually-will-exist"></button>
98+
<script>
99+
setTimeout(() => {
100+
document.querySelector('#eventually-will-exist').innerHTML = 'Eventually Exists'
101+
}, 500)
102+
</script>
103+
</section>
104+
<section>
105+
<h2>Eventual non-existence</h2>
106+
<button id="eventually-will-not-exist">Eventually not exists</button>
107+
<script>
108+
setTimeout(() => {
109+
document.querySelector('#eventually-will-not-exist').remove()
110+
}, 500)
111+
</script>
112+
</section>
92113
<!-- Prettier unindents the script tag below -->
93114
<script>
94115
document

cypress/integration/find.spec.js

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable max-lines-per-function */
2+
13
describe('find* dom-testing-library commands', () => {
24
beforeEach(() => {
35
cy.visit('cypress/fixtures/test-app/')
@@ -86,6 +88,21 @@ describe('find* dom-testing-library commands', () => {
8688

8789
/* Test the behaviour around these queries */
8890

91+
it('findByText should handle non-existence', () => {
92+
cy.findByText('Does Not Exist')
93+
.should('not.exist')
94+
})
95+
96+
it('findByText should handle eventual existence', () => {
97+
cy.findByText('Eventually Exists')
98+
.should('exist')
99+
})
100+
101+
it('findByText should handle eventual non-existence', () => {
102+
cy.findByText('Eventually Not exists')
103+
.should('not.exist')
104+
})
105+
89106
it("findByText with should('not.exist')", () => {
90107
cy.findAllByText(/^Button Text \d$/).should('exist')
91108
cy.findByText('Non-existing Button Text', {timeout: 100}).should(
@@ -123,23 +140,59 @@ describe('find* dom-testing-library commands', () => {
123140
cy.findByText('New Page Loaded').should('exist')
124141
})
125142

143+
it('findByText should set the Cypress element to the found element', () => {
144+
// This test is a little strange since snapshots show what element
145+
// is selected, but snapshots themselves don't give access to those
146+
// elements. I had to make the implementation specific so that the `$el`
147+
// is the `subject` when the log is added and the `$el` is the `value`
148+
// when the log is changed. It would be better to extract the `$el` from
149+
// each snapshot
150+
151+
cy.on('log:changed', (attrs, log) => {
152+
if (log.get('name') === 'findByText') {
153+
expect(log.get('$el')).to.have.text('Button Text 1')
154+
}
155+
})
156+
157+
cy.findByText('Button Text 1')
158+
})
159+
126160
it('findByText should error if no elements are found', () => {
127161
const regex = /Supercalifragilistic/
128-
const errorMessage = `Timed out retrying: Expected to find element: 'findByText(${regex})', but never found it.`
162+
const errorMessage = `Unable to find an element with the text: /Supercalifragilistic/`
163+
cy.on('fail', err => {
164+
expect(err.message).to.contain(errorMessage)
165+
})
166+
167+
cy.findByText(regex, {timeout: 100}) // Every find query is implicitly a `.should('exist')
168+
})
169+
170+
it('findByText should default to Cypress non-existence error message', () => {
171+
const errorMessage = `Expected <button> not to exist in the DOM, but it was continuously found.`
172+
cy.on('fail', err => {
173+
expect(err.message).to.contain(errorMessage)
174+
})
175+
176+
cy.findByText('Button Text 1', {timeout: 100})
177+
.should('not.exist')
178+
})
179+
180+
it('findByLabelText should forward useful error messages from @testing-library/dom', () => {
181+
const errorMessage = `Found a label with the text of: Label 3, however no form control was found associated to that label.`
129182
cy.on('fail', err => {
130-
expect(err.message).to.eq(errorMessage)
183+
expect(err.message).to.contain(errorMessage)
131184
})
132185

133-
cy.findByText(regex, {timeout: 100}) // Doesn't explicitly need .should('exist') if it's the last element?
186+
cy.findByLabelText('Label 3', {timeout: 100}).should('exist')
134187
})
135188

136189
it('findByText finding multiple items should error', () => {
137190
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\`)).`
138191
cy.on('fail', err => {
139-
expect(err.message).to.eq(errorMessage)
192+
expect(err.message).to.contain(errorMessage)
140193
})
141194

142-
cy.findByText(/^Button Text/i)
195+
cy.findByText(/^Button Text/i, {timeout: 100})
143196
})
144197
})
145198

cypress/integration/query.spec.js

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable max-lines-per-function */
2+
13
describe('query* dom-testing-library commands', () => {
24
beforeEach(() => {
35
cy.visit('cypress/fixtures/test-app/')
@@ -95,12 +97,30 @@ describe('query* dom-testing-library commands', () => {
9597
})
9698
})
9799

100+
it('queryByText should set the Cypress element to the found element', (done) => {
101+
// This test is a little strange since snapshots show what element
102+
// is selected, but snapshots themselves don't give access to those
103+
// elements. I had to make the implementation specific so that the `$el`
104+
// is the `subject` when the log is added and the `$el` is the `value`
105+
// when the log is changed. It would be better to extract the `$el` from
106+
// each snapshot
107+
108+
cy.on('log:changed', (attrs, log) => {
109+
if (log.get('name') === 'queryByText') {
110+
expect(log.get('$el')).to.have.text('Button Text 1')
111+
done()
112+
}
113+
})
114+
115+
cy.queryByText('Button Text 1')
116+
})
117+
98118
it('query* will return immediately, and never retry', () => {
99119
cy.queryByText('Next Page').click()
100120

101-
const errorMessage = `expected 'queryByText(\`New Page Loaded\`)' to exist in the DOM`
121+
const errorMessage = `Unable to find an element with the text: New Page Loaded.`
102122
cy.on('fail', err => {
103-
expect(err.message).to.eq(errorMessage)
123+
expect(err.message).to.contain(errorMessage)
104124
})
105125

106126
cy.queryByText('New Page Loaded', { timeout: 300 }).should('exist')
@@ -129,23 +149,42 @@ describe('query* dom-testing-library commands', () => {
129149
.and('not.exist')
130150
})
131151

132-
it('queryAllByText with a should(\'exist\') must provide selector error message', () => {
152+
it('queryAllByText should forward existence error message from @testing-library/dom', () => {
133153
const text = 'Supercalifragilistic'
134-
const errorMessage = `expected 'queryAllByText(\`${text}\`)' to exist in the DOM`
154+
const errorMessage = `Unable to find an element with the text: Supercalifragilistic.`
155+
cy.on('fail', err => {
156+
expect(err.message).to.contain(errorMessage)
157+
})
158+
159+
cy.queryAllByText(text, {timeout: 100}).should('exist')
160+
})
161+
162+
it('queryByLabelText should forward useful error messages from @testing-library/dom', () => {
163+
const errorMessage = `Found a label with the text of: Label 3, however no form control was found associated to that label.`
164+
cy.on('fail', err => {
165+
expect(err.message).to.contain(errorMessage)
166+
})
167+
168+
cy.queryByLabelText('Label 3', {timeout: 100}).should('exist')
169+
})
170+
171+
it('queryAllByText should default to Cypress non-existence error message', () => {
172+
const errorMessage = `Expected <button> not to exist in the DOM, but it was continuously found.`
135173
cy.on('fail', err => {
136-
expect(err.message).to.eq(errorMessage)
174+
expect(err.message).to.contain(errorMessage)
137175
})
138176

139-
cy.queryAllByText(text, {timeout: 100}).should('exist') // NOT POSSIBLE WITH QUERYALL?
177+
cy.queryAllByText('Button Text 1', {timeout: 100})
178+
.should('not.exist')
140179
})
141180

142181
it('queryByText finding multiple items should error', () => {
143-
const errorMessage = `Found multiple elements with the text: /^queryByText/i\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`
182+
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\`)).`
144183
cy.on('fail', err => {
145-
expect(err.message).to.eq(errorMessage)
184+
expect(err.message).to.contain(errorMessage)
146185
})
147186

148-
cy.queryByText(/^queryByText/i)
187+
cy.queryByText(/^Button Text/i)
149188
})
150189
})
151190

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"test:cypress:run": "cypress run",
1616
"test:cypress:open": "cypress open",
1717
"test:cypress": "npm run test:cypress:run",
18-
"test:cypress:dev": "test:cypress:open",
18+
"test:cypress:dev": "npm run test:cypress:open",
1919
"validate": "kcd-scripts validate build,lint,test",
2020
"setup": "npm install && npm run validate -s"
2121
},

0 commit comments

Comments
 (0)