Skip to content

Commit

Permalink
feat: add more helpful debugging information to queries
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
NicholasBoll committed Jan 30, 2020
1 parent 2f62901 commit c67414d
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 67 deletions.
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
```

### 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
`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
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
Expand Down
21 changes: 21 additions & 0 deletions cypress/fixtures/test-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ <h2>*ByLabel and *ByPlaceholder</h2>

<label for="by-text-input-2">Label 2</label>
<input type="text" placeholder="Input 2" id="by-text-input-2" />

<p>Intentionally inaccessible label for error checking</p>
<label>Label 3</label>
</section>
<section>
<h2>*ByText</h2>
Expand Down Expand Up @@ -89,6 +92,24 @@ <h2>*AllByText</h2>
<h2>*ByText on another page</h2>
<a onclick='setTimeout(function() { window.location = "/cypress/fixtures/test-app/next-page.html"; }, 100);'>Next Page</a>
</section>
<section>
<h2>Eventual existence</h2>
<button id="eventually-will-exist"></button>
<script>
setTimeout(() => {
document.querySelector('#eventually-will-exist').innerHTML = 'Eventually Exists'
}, 500)
</script>
</section>
<section>
<h2>Eventual non-existence</h2>
<button id="eventually-will-not-exist">Eventually not exists</button>
<script>
setTimeout(() => {
document.querySelector('#eventually-will-not-exist').remove()
}, 500)
</script>
</section>
<!-- Prettier unindents the script tag below -->
<script>
document
Expand Down
63 changes: 58 additions & 5 deletions cypress/integration/find.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

describe('find* dom-testing-library commands', () => {
beforeEach(() => {
cy.visit('cypress/fixtures/test-app/')
Expand Down Expand Up @@ -86,6 +88,21 @@ 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(
Expand Down Expand Up @@ -123,23 +140,59 @@ 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

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')
})

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)
})

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})
})
})

Expand Down
57 changes: 48 additions & 9 deletions cypress/integration/query.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */

describe('query* dom-testing-library commands', () => {
beforeEach(() => {
cy.visit('cypress/fixtures/test-app/')
Expand Down Expand Up @@ -95,12 +97,30 @@ describe('query* dom-testing-library commands', () => {
})
})

it('queryByText should set the Cypress element to the found element', (done) => {
// 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

cy.on('log:changed', (attrs, log) => {
if (log.get('name') === 'queryByText') {
expect(log.get('$el')).to.have.text('Button Text 1')
done()
}
})

cy.queryByText('Button Text 1')
})

it('query* will return immediately, and never retry', () => {
cy.queryByText('Next Page').click()

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

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

it('queryAllByText with a should(\'exist\') must provide selector error message', () => {
it('queryAllByText should forward existence error message from @testing-library/dom', () => {
const text = 'Supercalifragilistic'
const errorMessage = `expected 'queryAllByText(\`${text}\`)' to exist in the DOM`
const errorMessage = `Unable to find an element with the text: Supercalifragilistic.`
cy.on('fail', err => {
expect(err.message).to.contain(errorMessage)
})

cy.queryAllByText(text, {timeout: 100}).should('exist')
})

it('queryByLabelText 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.contain(errorMessage)
})

cy.queryByLabelText('Label 3', {timeout: 100}).should('exist')
})

it('queryAllByText 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.eq(errorMessage)
expect(err.message).to.contain(errorMessage)
})

cy.queryAllByText(text, {timeout: 100}).should('exist') // NOT POSSIBLE WITH QUERYALL?
cy.queryAllByText('Button Text 1', {timeout: 100})
.should('not.exist')
})

it('queryByText finding multiple items should error', () => {
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\`)).`
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.queryByText(/^queryByText/i)
cy.queryByText(/^Button Text/i)
})
})

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"test:cypress:run": "cypress run",
"test:cypress:open": "cypress open",
"test:cypress": "npm run test:cypress:run",
"test:cypress:dev": "test:cypress:open",
"test:cypress:dev": "npm run test:cypress:open",
"validate": "kcd-scripts validate build,lint,test",
"setup": "npm install && npm run validate -s"
},
Expand Down
Loading

0 comments on commit c67414d

Please sign in to comment.