Skip to content
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

Merged
53 changes: 46 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ This allows you to use all the useful
- [Installation](#installation)
- [With TypeScript](#with-typescript)
- [Usage](#usage)
- [Differences from DOM Testing Library](#differences-from-dom-testing-library)
- [Other Solutions](#other-solutions)
- [Contributors](#contributors)
- [LICENSE](#license)
Expand Down Expand Up @@ -95,7 +96,7 @@ and should be added as follows in `tsconfig.json`:

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

```
```javascript
import '@testing-library/cypress/add-commands'
```

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

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

To configure DOM Testing Library, use the following custom command:

```javascript
cy.configureCypressTestingLibrary(config)
```

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.findAllByText('Jackie Chan').eq(0).click();
cy.queryAllByText('Button Text').should('exist')
Copy link
Contributor Author

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

cy.queryAllByText('Non-existing Button Text').should('not.exist')
cy.queryAllByLabelText('Label text', {timeout: 7000}).should('exist')
cy.findAllByText('Jackie Chan').click();

// 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')
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 from 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.

`findAll*` can select more than one element and is closer in functionality to how
Cypress built-in commands work. `findAll*` is preferred to `find*` queries.
`find*` commands will fail if more than one element is found that matches the criteria
which is not how built-in Cypress commands work, but is provided for closer compatibility
to other Testing Libraries.

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()
```

If you intend to enforce only 1 element is returned by a selector, the following
examples will both fail if more than one element is found.

```javascript
cy.findAllByText('Some Text').should('have.length', 1)
cy.findByText('Some Text').should('exist')
Comment on lines +142 to +166
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the updated documentation about find* queries vs findAll* queries.

```

## Other Solutions

I'm not aware of any, if you are please [make a pull request][prs] and add it
Expand Down
6 changes: 6 additions & 0 deletions cypress/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"max-lines-per-function": "off",
"jest/valid-expect-in-promise": "off"
}
}
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>
NicholasBoll marked this conversation as resolved.
Show resolved Hide resolved
</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
18 changes: 18 additions & 0 deletions cypress/integration/configure.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types="cypress" />
describe('configuring fallback globally', () => {
beforeEach(() => {
cy.visit('cypress/fixtures/test-app/')
cy.configureCypressTestingLibrary({ fallbackRetryWithoutPreviousSubject: false })
})

it('findByText with a previous subject', () => {
cy.get('#nested')
.findByText('Button Text 1')
.should('not.exist')
cy.get('#nested')
.findByText('Button Text 2')
.should('exist')
})
})

/* global cy */
107 changes: 99 additions & 8 deletions cypress/integration/find.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="cypress" />
describe('find* dom-testing-library commands', () => {
beforeEach(() => {
cy.visit('cypress/fixtures/test-app/')
Expand Down Expand Up @@ -86,22 +87,48 @@ 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', { fallbackRetryWithoutPreviousSubject: 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()
cy.get('#nested').then(subject => {
cy.findByText('Button Text 1', {container: subject}).should('not.exist')
cy.findByText('Button Text 2', {container: subject}).should('exist')
})
})

Expand All @@ -110,23 +137,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
Copy link
Member

Choose a reason for hiding this comment

The 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.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.findByText(regex, {timeout: 100})
})

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.contain(errorMessage)
NicholasBoll marked this conversation as resolved.
Show resolved Hide resolved
})

cy.findByLabelText('Label 3', {timeout: 100})
})

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests for
Screen Shot 2020-01-30 at 10 33 01 PM

})
})

Expand Down
56 changes: 47 additions & 9 deletions cypress/integration/query.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="cypress" />
describe('query* dom-testing-library commands', () => {
beforeEach(() => {
cy.visit('cypress/fixtures/test-app/')
Expand Down Expand Up @@ -95,12 +96,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 +148,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",
NicholasBoll marked this conversation as resolved.
Show resolved Hide resolved
"validate": "kcd-scripts validate build,lint,test",
"setup": "npm install && npm run validate -s"
},
Expand Down
Loading