Skip to content

Commit 547389d

Browse files
authored
feat: add more helpful debugging information to queries (#108)
* feat: add more helpful debugging information to queries * Add element selector information for debugging (outlines element when you click on command) (fixes #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 #103) * Add retryability to `findBy*` when multiple elements are found (fixes #83) * Add option to disable logging of all commands * `query*` and `find*` have a consistent code path and error messaging (fixes #103) * Remove usage of Cypress commands in queries (fixes #103) * feat: add ability for queries to inherit previous subject * Fixes #109 without breaking change caused by #100 * feat: add parent/child log type detection * chore: implement feedback * docs: update readme to complete my thought process * slightly simplify config fn * Update README.md Co-authored-by: Kent C. Dodds <[email protected]> Closes #103, Closes #109, Closes #110
1 parent e5cb2a5 commit 547389d

File tree

10 files changed

+370
-84
lines changed

10 files changed

+370
-84
lines changed

README.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ This allows you to use all the useful
6161
- [Installation](#installation)
6262
- [With TypeScript](#with-typescript)
6363
- [Usage](#usage)
64+
- [Differences from DOM Testing Library](#differences-from-dom-testing-library)
6465
- [Other Solutions](#other-solutions)
6566
- [Contributors](#contributors)
6667
- [LICENSE](#license)
@@ -95,7 +96,7 @@ and should be added as follows in `tsconfig.json`:
9596

9697
Add this line to your project's `cypress/support/commands.js`:
9798

98-
```
99+
```javascript
99100
import '@testing-library/cypress/add-commands'
100101
```
101102

@@ -105,28 +106,66 @@ and `queryAllBy` commands.
105106

106107
You can find [all Library definitions here](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/testing-library__cypress/index.d.ts).
107108

109+
To configure DOM Testing Library, use the following custom command:
110+
111+
```javascript
112+
cy.configureCypressTestingLibrary(config)
113+
```
114+
108115
To show some simple examples (from
109116
[cypress/integration/query.spec.js](cypress/integration/query.spec.js) or [cypress/integration/find.spec.js](cypress/integration/find.spec.js)):
110117

111118
```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')
115-
cy.findAllByText('Jackie Chan').eq(0).click();
119+
cy.queryAllByText('Button Text').should('exist')
120+
cy.queryAllByText('Non-existing Button Text').should('not.exist')
121+
cy.queryAllByLabelText('Label text', {timeout: 7000}).should('exist')
122+
cy.findAllByText('Jackie Chan').click();
123+
124+
// findAllByText _inside_ a form element
116125
cy.get('form').within(() => {
117-
cy.findByText('Button Text').should('exist')
126+
cy.findAllByText('Button Text').should('exist')
118127
})
119128
cy.get('form').then(subject => {
120-
cy.findByText('Button Text', {container: subject}).should('exist')
129+
cy.findAllByText('Button Text', {container: subject}).should('exist')
121130
})
131+
cy.get('form').findAllByText('Button Text').should('exist')
122132
```
123133

134+
### Differences from DOM Testing Library
135+
124136
`Cypress Testing Library` supports both jQuery elements and DOM nodes. This is
125137
necessary because Cypress uses jQuery elements, while `DOM Testing Library`
126138
expects DOM nodes. When you pass a jQuery element as `container`, it will get
127139
the first DOM node from the collection and use that as the `container` parameter
128140
for the `DOM Testing Library` functions.
129141

142+
`get*` queries are disabled. `find*` queries do not use the Promise API of
143+
`DOM Testing Library`, but instead forward to the `get*` queries and use Cypress'
144+
built-in retryability using error messages from `get*` APIs to forward as error
145+
messages if a query fails. `query*` also uses `get*` APIs, but disables retryability.
146+
147+
`findAll*` can select more than one element and is closer in functionality to how
148+
Cypress built-in commands work. `findAll*` is preferred to `find*` queries.
149+
`find*` commands will fail if more than one element is found that matches the criteria
150+
which is not how built-in Cypress commands work, but is provided for closer compatibility
151+
to other Testing Libraries.
152+
153+
Cypress handles actions when there is only one element found. For example, the following
154+
will work without having to limit to only 1 returned element. The `cy.click` will
155+
automatically fail if more than 1 element is returned by the `findAllByText`:
156+
157+
```javascript
158+
cy.findAllByText('Some Text').click()
159+
```
160+
161+
If you intend to enforce only 1 element is returned by a selector, the following
162+
examples will both fail if more than one element is found.
163+
164+
```javascript
165+
cy.findAllByText('Some Text').should('have.length', 1)
166+
cy.findByText('Some Text').should('exist')
167+
```
168+
130169
## Other Solutions
131170

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

cypress/.eslintrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rules": {
3+
"max-lines-per-function": "off",
4+
"jest/valid-expect-in-promise": "off"
5+
}
6+
}

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/configure.spec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference types="cypress" />
2+
describe('configuring fallback globally', () => {
3+
beforeEach(() => {
4+
cy.visit('cypress/fixtures/test-app/')
5+
cy.configureCypressTestingLibrary({ fallbackRetryWithoutPreviousSubject: false })
6+
})
7+
8+
it('findByText with a previous subject', () => {
9+
cy.get('#nested')
10+
.findByText('Button Text 1')
11+
.should('not.exist')
12+
cy.get('#nested')
13+
.findByText('Button Text 2')
14+
.should('exist')
15+
})
16+
})
17+
18+
/* global cy */

cypress/integration/find.spec.js

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="cypress" />
12
describe('find* dom-testing-library commands', () => {
23
beforeEach(() => {
34
cy.visit('cypress/fixtures/test-app/')
@@ -86,22 +87,48 @@ describe('find* dom-testing-library commands', () => {
8687

8788
/* Test the behaviour around these queries */
8889

90+
it('findByText should handle non-existence', () => {
91+
cy.findByText('Does Not Exist')
92+
.should('not.exist')
93+
})
94+
95+
it('findByText should handle eventual existence', () => {
96+
cy.findByText('Eventually Exists')
97+
.should('exist')
98+
})
99+
100+
it('findByText should handle eventual non-existence', () => {
101+
cy.findByText('Eventually Not exists')
102+
.should('not.exist')
103+
})
104+
89105
it("findByText with should('not.exist')", () => {
90106
cy.findAllByText(/^Button Text \d$/).should('exist')
91107
cy.findByText('Non-existing Button Text', {timeout: 100}).should(
92108
'not.exist',
93109
)
94110
})
95111

112+
it('findByText with a previous subject', () => {
113+
cy.get('#nested')
114+
.findByText('Button Text 1', { fallbackRetryWithoutPreviousSubject: false })
115+
.should('not.exist')
116+
cy.get('#nested')
117+
.findByText('Button Text 2')
118+
.should('exist')
119+
})
120+
96121
it('findByText within', () => {
97122
cy.get('#nested').within(() => {
98-
cy.findByText('Button Text 2').click()
123+
cy.findByText('Button Text 1').should('not.exist')
124+
cy.findByText('Button Text 2').should('exist')
99125
})
100126
})
101127

102128
it('findByText in container', () => {
103-
return cy.get('#nested').then(subject => {
104-
cy.findByText(/^Button Text/, {container: subject}).click()
129+
cy.get('#nested').then(subject => {
130+
cy.findByText('Button Text 1', {container: subject}).should('not.exist')
131+
cy.findByText('Button Text 2', {container: subject}).should('exist')
105132
})
106133
})
107134

@@ -110,23 +137,87 @@ describe('find* dom-testing-library commands', () => {
110137
cy.findByText('New Page Loaded').should('exist')
111138
})
112139

140+
it('findByText should set the Cypress element to the found element', () => {
141+
// This test is a little strange since snapshots show what element
142+
// is selected, but snapshots themselves don't give access to those
143+
// elements. I had to make the implementation specific so that the `$el`
144+
// is the `subject` when the log is added and the `$el` is the `value`
145+
// when the log is changed. It would be better to extract the `$el` from
146+
// each snapshot
147+
148+
cy.on('log:changed', (attrs, log) => {
149+
if (log.get('name') === 'findByText') {
150+
expect(log.get('$el')).to.have.text('Button Text 1')
151+
}
152+
})
153+
154+
cy.findByText('Button Text 1')
155+
})
156+
113157
it('findByText should error if no elements are found', () => {
114158
const regex = /Supercalifragilistic/
115-
const errorMessage = `Timed out retrying: Expected to find element: 'findByText(${regex})', but never found it.`
159+
const errorMessage = `Unable to find an element with the text: /Supercalifragilistic/`
116160
cy.on('fail', err => {
117-
expect(err.message).to.eq(errorMessage)
161+
expect(err.message).to.contain(errorMessage)
118162
})
119163

120-
cy.findByText(regex, {timeout: 100}) // Doesn't explicitly need .should('exist') if it's the last element?
164+
cy.findByText(regex, {timeout: 100})
165+
})
166+
167+
it('findByText should default to Cypress non-existence error message', () => {
168+
const errorMessage = `Expected <button> not to exist in the DOM, but it was continuously found.`
169+
cy.on('fail', err => {
170+
expect(err.message).to.contain(errorMessage)
171+
})
172+
173+
cy.findByText('Button Text 1', {timeout: 100})
174+
.should('not.exist')
175+
})
176+
177+
it('findByLabelText should forward useful error messages from @testing-library/dom', () => {
178+
const errorMessage = `Found a label with the text of: Label 3, however no form control was found associated to that label.`
179+
cy.on('fail', err => {
180+
expect(err.message).to.contain(errorMessage)
181+
})
182+
183+
cy.findByLabelText('Label 3', {timeout: 100})
121184
})
122185

123186
it('findByText finding multiple items should error', () => {
124187
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\`)).`
125188
cy.on('fail', err => {
126-
expect(err.message).to.eq(errorMessage)
189+
expect(err.message).to.contain(errorMessage)
127190
})
128191

129-
cy.findByText(/^Button Text/i)
192+
cy.findByText(/^Button Text/i, {timeout: 100})
193+
})
194+
195+
it('findByText should not break existing code', () => {
196+
cy.window()
197+
.findByText('Button Text 1')
198+
.should('exist')
199+
})
200+
201+
it('findByText should show as a parent command if it starts a chain', () => {
202+
const assertLog = (attrs, log) => {
203+
if(log.get('name') === 'findByText') {
204+
expect(log.get('type')).to.equal('parent')
205+
cy.off('log:added', assertLog)
206+
}
207+
}
208+
cy.on('log:added', assertLog)
209+
cy.findByText('Button Text 1')
210+
})
211+
212+
it('findByText should show as a child command if it continues a chain', () => {
213+
const assertLog = (attrs, log) => {
214+
if(log.get('name') === 'findByText') {
215+
expect(log.get('type')).to.equal('child')
216+
cy.off('log:added', assertLog)
217+
}
218+
}
219+
cy.on('log:added', assertLog)
220+
cy.get('body').findByText('Button Text 1')
130221
})
131222
})
132223

cypress/integration/query.spec.js

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="cypress" />
12
describe('query* dom-testing-library commands', () => {
23
beforeEach(() => {
34
cy.visit('cypress/fixtures/test-app/')
@@ -95,12 +96,30 @@ describe('query* dom-testing-library commands', () => {
9596
})
9697
})
9798

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

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

106125
cy.queryByText('New Page Loaded', { timeout: 300 }).should('exist')
@@ -129,23 +148,42 @@ describe('query* dom-testing-library commands', () => {
129148
.and('not.exist')
130149
})
131150

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

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

142180
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\`)).`
181+
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\`)).`
144182
cy.on('fail', err => {
145-
expect(err.message).to.eq(errorMessage)
183+
expect(err.message).to.contain(errorMessage)
146184
})
147185

148-
cy.queryByText(/^queryByText/i)
186+
cy.queryByText(/^Button Text/i)
149187
})
150188
})
151189

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)