Skip to content

Commit

Permalink
feat(debug): Adding debug log when a get call fails (#3)
Browse files Browse the repository at this point in the history
* feat(debug_helper): Adding debug helper when a get call fails

* fix(review): Few minor changes

* moving pretty-format to the dep

* fix(review_comments): Making the fixes mentioned in the review comments

* Update README.md

* Update element-queries.js
  • Loading branch information
antsmartian authored and Kent C. Dodds committed Apr 10, 2018
1 parent d45b449 commit c8069e3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 34 deletions.
37 changes: 37 additions & 0 deletions README.md
Expand Up @@ -87,6 +87,7 @@ when a real user uses it.
* [Custom Jest Matchers - Typescript](#custom-jest-matchers---typescript)
* [`TextMatch`](#textmatch)
* [`query` APIs](#query-apis)
* [Debugging](#debugging)
* [Implementations](#implementations)
* [FAQ](#faq)
* [Other Solutions](#other-solutions)
Expand Down Expand Up @@ -540,6 +541,42 @@ expect(submitButton).toBeNull() // it doesn't exist
expect(submitButton).not.toBeInTheDOM()
```

## Debugging

When you use any `get` calls in your test cases, the current state of the `container`
(DOM) gets printed on the console. For example:

```javascript
// <div>Hello world</div>
getByText(container, 'Goodbye world') // will fail by throwing error
```

The above test case will fail, however it prints the state of your DOM under test,
so you will get to see:

```
Unable to find an element with the text: Goodbye world. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Here is the state of your container:
<div>
<div>
Hello World!
</div>
</div>
```

Note: Since the DOM size can get really large, you can set the limit of DOM content
to be printed via environment variable `DEBUG_PRINT_LIMIT`. The default value is
`7000`. You will see `...` in the console, when the DOM content is stripped off,
because of the length you have set or due to default size limit. Here's how you
might increase this limit when running tests:

```
DEBUG_PRINT_LIMIT=10000 npm test
```

This works on macOS/linux, you'll need to do something else for windows. If you'd
like a solution that works for both, see [`cross-env`](https://www.npmjs.com/package/cross-env)

## Implementations

This library was not built to be used on its own. The original implementation
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -35,6 +35,7 @@
],
"dependencies": {
"jest-matcher-utils": "^22.4.3",
"pretty-format": "^22.4.3",
"mutationobserver-shim": "^0.3.2",
"wait-for-expect": "^0.4.0"
},
Expand Down
58 changes: 51 additions & 7 deletions src/__tests__/__snapshots__/element-queries.js.snap
@@ -1,15 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`get throws a useful error message 1`] = `"Unable to find a label with the text of: LucyRicardo"`;
exports[`get throws a useful error message 1`] = `
"Unable to find a label with the text of: LucyRicardo
exports[`get throws a useful error message 2`] = `"Unable to find an element with the placeholder text of: LucyRicardo"`;
<div>
<div />
</div>"
`;

exports[`get throws a useful error message 3`] = `"Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible."`;
exports[`get throws a useful error message 2`] = `
"Unable to find an element with the placeholder text of: LucyRicardo
exports[`get throws a useful error message 4`] = `"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]"`;
<div>
<div />
</div>"
`;

exports[`get throws a useful error message 5`] = `"Unable to find an element with the alt text: LucyRicardo"`;
exports[`get throws a useful error message 3`] = `
"Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
exports[`label with no form control 1`] = `"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`;
<div>
<div />
</div>"
`;

exports[`totally empty label 1`] = `"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`;
exports[`get throws a useful error message 4`] = `
"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]
<div>
<div />
</div>"
`;

exports[`get throws a useful error message 5`] = `
"Unable to find an element with the alt text: LucyRicardo
<div>
<div />
</div>"
`;

exports[`label with no form control 1`] = `
"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly.
<div>
<label>
All alone
</label>
</div>"
`;

exports[`totally empty label 1`] = `
"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly.
<div>
<label />
</div>"
`;
38 changes: 17 additions & 21 deletions src/__tests__/__snapshots__/wait-for-element.js.snap
@@ -1,48 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[
`it returns immediately if the callback returns the value before any mutations 1`
] = `
exports[`it returns immediately if the callback returns the value before any mutations 1`] = `
<div
data-test-attribute="something changed once"
>
<div
data-testid="initial-element"
/>
</div>
`
`;

exports[`it throws if timeout is exceeded 1`] = `
Array [
[Error: Timed out in waitForElement.],
]
`
`;

exports[`it throws if timeout is exceeded 2`] = `
<div
data-test-attribute="something changed twice"
/>
`
`;

exports[
`it throws the same error that the callback has thrown if timeout is exceeded 1`
] = `
exports[`it throws the same error that the callback has thrown if timeout is exceeded 1`] = `
Array [
[Error: Unable to find an element by: [data-testid="test"]],
[Error: Unable to find an element by: [data-testid="test"]
<div
data-test-attribute="something changed twice"
/>],
]
`
`;

exports[
`it throws the same error that the callback has thrown if timeout is exceeded 2`
] = `
exports[`it throws the same error that the callback has thrown if timeout is exceeded 2`] = `
<div
data-test-attribute="something changed twice"
/>
`
`;

exports[
`it waits for the callback to return a value and only reacts to DOM mutations 1`
] = `
exports[`it waits for the callback to return a value and only reacts to DOM mutations 1`] = `
<div>
<div
data-testid="initial-element"
Expand All @@ -66,16 +62,16 @@ exports[
data-testid="the-element-we-are-looking-for"
/>
</div>
`
`;

exports[`it waits for the next DOM mutation with default callback 1`] = `
<body>
<div />
</body>
`
`;

exports[`it waits for the next DOM mutation with default callback 2`] = `
<body>
<div />
</body>
`
`;
32 changes: 32 additions & 0 deletions src/__tests__/element-queries.js
Expand Up @@ -193,4 +193,36 @@ test('using jest helpers to check element class names', () => {
).toThrowError()
})

test('test the debug helper prints the dom state here', () => {
const originalDebugPrintLimit = process.env.DEBUG_PRINT_LIMIT
const Large = `<div>
${Array.from({length: 7000}, (v, key) => key).map(() => {
return `<div data-testid="debugging" data-otherid="debugging">
Hello World!
</div>`
})}
</div>`

const {getByText} = render(Large) // render large DOM which exceeds 7000 limit
expect(() => expect(getByText('not present')).toBeInTheDOM()).toThrowError()

const Hello = `<div data-testid="debugging" data-otherid="debugging">
Hello World!
</div>`
const {getByTestId} = render(Hello)
process.env.DEBUG_PRINT_LIMIT = 5 // user should see `...`
expect(() => expect(getByTestId('not present')).toBeInTheDOM()).toThrowError(
/\.\.\./,
)

const {getByLabelText} = render(Hello)
process.env.DEBUG_PRINT_LIMIT = 10000 // user shouldn't see `...`
expect(() =>
expect(getByLabelText('not present')).toBeInTheDOM(/^((?!\.\.\.).)*$/),
).toThrowError()

//all good replacing it with old value
process.env.DEBUG_PRINT_LIMIT = originalDebugPrintLimit
})

/* eslint jsx-a11y/label-has-for:0 */
45 changes: 39 additions & 6 deletions src/queries.js
@@ -1,5 +1,8 @@
import prettyFormat from 'pretty-format'
import {matches} from './matches'

const {DOMElement, DOMCollection} = prettyFormat.plugins

// Here are the queries for the library.
// The queries here should only be things that are accessible to both users who are using a screen reader
// and those who are not using a screen reader (with the exception of the data-testid attribute query).
Expand Down Expand Up @@ -78,7 +81,11 @@ function getText(node) {
function getByTestId(container, id, ...rest) {
const el = queryByTestId(container, id, ...rest)
if (!el) {
throw new Error(`Unable to find an element by: [data-testid="${id}"]`)
throw new Error(
`Unable to find an element by: [data-testid="${id}"] \n\n${htmlElementToDisplay(
container,
)}`,
)
}
return el
}
Expand All @@ -87,7 +94,9 @@ function getByPlaceholderText(container, text, ...rest) {
const el = queryByPlaceholderText(container, text, ...rest)
if (!el) {
throw new Error(
`Unable to find an element with the placeholder text of: ${text}`,
`Unable to find an element with the placeholder text of: ${text} \n\n${htmlElementToDisplay(
container,
)}`,
)
}
return el
Expand All @@ -99,10 +108,16 @@ function getByLabelText(container, text, ...rest) {
const label = queryLabelByText(container, text)
if (label) {
throw new Error(
`Found a label with the text of: ${text}, however no form control was found associated to that label. Make sure you're using the "for" attribute or "aria-labelledby" attribute correctly.`,
`Found a label with the text of: ${text}, however no form control was found associated to that label. Make sure you're using the "for" attribute or "aria-labelledby" attribute correctly. \n\n${htmlElementToDisplay(
container,
)}`,
)
} else {
throw new Error(`Unable to find a label with the text of: ${text}`)
throw new Error(
`Unable to find a label with the text of: ${text} \n\n${htmlElementToDisplay(
container,
)}`,
)
}
}
return el
Expand All @@ -112,7 +127,9 @@ function getByText(container, text, ...rest) {
const el = queryByText(container, text, ...rest)
if (!el) {
throw new Error(
`Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`,
`Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. \n\n${htmlElementToDisplay(
container,
)}`,
)
}
return el
Expand All @@ -129,11 +146,27 @@ function queryByAltText(container, alt) {
function getByAltText(container, alt) {
const el = queryByAltText(container, alt)
if (!el) {
throw new Error(`Unable to find an element with the alt text: ${alt}`)
throw new Error(
`Unable to find an element with the alt text: ${alt} \n\n${htmlElementToDisplay(
container,
)}`,
)
}
return el
}

function htmlElementToDisplay(htmlElement) {
const debugContent = prettyFormat(htmlElement, {
plugins: [DOMElement, DOMCollection],
printFunctionName: false,
highlight: true,
})
const maxLength = process.env.DEBUG_PRINT_LIMIT || 7000
return htmlElement.outerHTML.length > maxLength
? `${debugContent.slice(0, maxLength)}...`
: debugContent
}

export {
queryByPlaceholderText,
getByPlaceholderText,
Expand Down

0 comments on commit c8069e3

Please sign in to comment.