diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
index 36f65be2..48600aa2 100644
--- a/.codesandbox/ci.json
+++ b/.codesandbox/ci.json
@@ -1,3 +1,4 @@
{
- "sandboxes": ["github/kentcdodds/react-testing-library-examples"]
+ "sandboxes": ["github/kentcdodds/react-testing-library-examples"],
+ "node": "12"
}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index a937c57d..8c9ec07e 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -16,7 +16,7 @@ jobs:
if: ${{ !contains(github.head_ref, 'all-contributors') }}
strategy:
matrix:
- node: [10.14.2, 12, 14, 15, 16]
+ node: [12, 14, 16]
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
diff --git a/package.json b/package.json
index a1df7345..78ddf6c9 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"author": "Kent C. Dodds (https://kentcdodds.com)",
"license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">=12"
},
"scripts": {
"build": "kcd-scripts build --no-ts-defs --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --no-ts-defs --bundle --no-clean",
@@ -46,7 +46,7 @@
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.6",
"lz-string": "^1.4.4",
- "pretty-format": "^26.6.2"
+ "pretty-format": "^27.0.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.6",
@@ -54,7 +54,7 @@
"jest-serializer-ansi": "^1.0.3",
"jest-watch-select-projects": "^2.0.0",
"jsdom": "^16.4.0",
- "kcd-scripts": "^7.5.3",
+ "kcd-scripts": "^11.0.0",
"typescript": "^4.1.2"
},
"eslintConfig": {
@@ -63,6 +63,7 @@
"plugin:import/typescript"
],
"rules": {
+ "@typescript-eslint/prefer-includes": "off",
"import/prefer-default-export": "off",
"import/no-unassigned-import": "off",
"import/no-useless-path-segments": "off",
diff --git a/src/DOMElementFilter.ts b/src/DOMElementFilter.ts
new file mode 100644
index 00000000..bf5ff686
--- /dev/null
+++ b/src/DOMElementFilter.ts
@@ -0,0 +1,261 @@
+/**
+ * Source: https://github.com/facebook/jest/blob/e7bb6a1e26ffab90611b2593912df15b69315611/packages/pretty-format/src/plugins/DOMElement.ts
+ */
+/* eslint-disable -- trying to stay as close to the original as possible */
+/* istanbul ignore file */
+import type {Config, NewPlugin, Printer, Refs} from 'pretty-format'
+
+function escapeHTML(str: string): string {
+ return str.replace(//g, '>')
+}
+// Return empty string if keys is empty.
+const printProps = (
+ keys: Array,
+ props: Record,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer,
+): string => {
+ const indentationNext = indentation + config.indent
+ const colors = config.colors
+ return keys
+ .map(key => {
+ const value = props[key]
+ let printed = printer(value, config, indentationNext, depth, refs)
+
+ if (typeof value !== 'string') {
+ if (printed.indexOf('\n') !== -1) {
+ printed =
+ config.spacingOuter +
+ indentationNext +
+ printed +
+ config.spacingOuter +
+ indentation
+ }
+ printed = '{' + printed + '}'
+ }
+
+ return (
+ config.spacingInner +
+ indentation +
+ colors.prop.open +
+ key +
+ colors.prop.close +
+ '=' +
+ colors.value.open +
+ printed +
+ colors.value.close
+ )
+ })
+ .join('')
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node_type_constants
+const NodeTypeTextNode = 3
+
+// Return empty string if children is empty.
+const printChildren = (
+ children: Array,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer,
+): string =>
+ children
+ .map(child => {
+ const printedChild =
+ typeof child === 'string'
+ ? printText(child, config)
+ : printer(child, config, indentation, depth, refs)
+
+ if (
+ printedChild === '' &&
+ typeof child === 'object' &&
+ child !== null &&
+ (child as Node).nodeType !== NodeTypeTextNode
+ ) {
+ // A plugin serialized this Node to '' meaning we should ignore it.
+ return ''
+ }
+ return config.spacingOuter + indentation + printedChild
+ })
+ .join('')
+
+const printText = (text: string, config: Config): string => {
+ const contentColor = config.colors.content
+ return contentColor.open + escapeHTML(text) + contentColor.close
+}
+
+const printComment = (comment: string, config: Config): string => {
+ const commentColor = config.colors.comment
+ return (
+ commentColor.open +
+ '' +
+ commentColor.close
+ )
+}
+
+// Separate the functions to format props, children, and element,
+// so a plugin could override a particular function, if needed.
+// Too bad, so sad: the traditional (but unnecessary) space
+// in a self-closing tagColor requires a second test of printedProps.
+const printElement = (
+ type: string,
+ printedProps: string,
+ printedChildren: string,
+ config: Config,
+ indentation: string,
+): string => {
+ const tagColor = config.colors.tag
+ return (
+ tagColor.open +
+ '<' +
+ type +
+ (printedProps &&
+ tagColor.close +
+ printedProps +
+ config.spacingOuter +
+ indentation +
+ tagColor.open) +
+ (printedChildren
+ ? '>' +
+ tagColor.close +
+ printedChildren +
+ config.spacingOuter +
+ indentation +
+ tagColor.open +
+ '' +
+ type
+ : (printedProps && !config.min ? '' : ' ') + '/') +
+ '>' +
+ tagColor.close
+ )
+}
+
+const printElementAsLeaf = (type: string, config: Config): string => {
+ const tagColor = config.colors.tag
+ return (
+ tagColor.open +
+ '<' +
+ type +
+ tagColor.close +
+ ' …' +
+ tagColor.open +
+ ' />' +
+ tagColor.close
+ )
+}
+
+const ELEMENT_NODE = 1
+const TEXT_NODE = 3
+const COMMENT_NODE = 8
+const FRAGMENT_NODE = 11
+
+const ELEMENT_REGEXP = /^((HTML|SVG)\w*)?Element$/
+
+const testNode = (val: any) => {
+ const constructorName = val.constructor.name
+ const {nodeType, tagName} = val
+ const isCustomElement =
+ (typeof tagName === 'string' && tagName.includes('-')) ||
+ (typeof val.hasAttribute === 'function' && val.hasAttribute('is'))
+
+ return (
+ (nodeType === ELEMENT_NODE &&
+ (ELEMENT_REGEXP.test(constructorName) || isCustomElement)) ||
+ (nodeType === TEXT_NODE && constructorName === 'Text') ||
+ (nodeType === COMMENT_NODE && constructorName === 'Comment') ||
+ (nodeType === FRAGMENT_NODE && constructorName === 'DocumentFragment')
+ )
+}
+
+export const test: NewPlugin['test'] = (val: any) =>
+ val?.constructor?.name && testNode(val)
+
+type HandledType = Element | Text | Comment | DocumentFragment
+
+function nodeIsText(node: HandledType): node is Text {
+ return node.nodeType === TEXT_NODE
+}
+
+function nodeIsComment(node: HandledType): node is Comment {
+ return node.nodeType === COMMENT_NODE
+}
+
+function nodeIsFragment(node: HandledType): node is DocumentFragment {
+ return node.nodeType === FRAGMENT_NODE
+}
+
+export default function createDOMElementFilter(
+ filterNode: (node: Node) => boolean,
+): NewPlugin {
+ return {
+ test: (val: any) => val?.constructor?.name && testNode(val),
+ serialize: (
+ node: HandledType,
+ config: Config,
+ indentation: string,
+ depth: number,
+ refs: Refs,
+ printer: Printer,
+ ) => {
+ if (nodeIsText(node)) {
+ return printText(node.data, config)
+ }
+
+ if (nodeIsComment(node)) {
+ return printComment(node.data, config)
+ }
+
+ const type = nodeIsFragment(node)
+ ? `DocumentFragment`
+ : node.tagName.toLowerCase()
+
+ if (++depth > config.maxDepth) {
+ return printElementAsLeaf(type, config)
+ }
+
+ return printElement(
+ type,
+ printProps(
+ nodeIsFragment(node)
+ ? []
+ : Array.from(node.attributes)
+ .map(attr => attr.name)
+ .sort(),
+ nodeIsFragment(node)
+ ? {}
+ : Array.from(node.attributes).reduce>(
+ (props, attribute) => {
+ props[attribute.name] = attribute.value
+ return props
+ },
+ {},
+ ),
+ config,
+ indentation + config.indent,
+ depth,
+ refs,
+ printer,
+ ),
+ printChildren(
+ Array.prototype.slice
+ .call(node.childNodes || node.children)
+ .filter(filterNode),
+ config,
+ indentation + config.indent,
+ depth,
+ refs,
+ printer,
+ ),
+ config,
+ indentation,
+ )
+ },
+ }
+}
diff --git a/src/__node_tests__/index.js b/src/__node_tests__/index.js
index 0f88c323..6c663360 100644
--- a/src/__node_tests__/index.js
+++ b/src/__node_tests__/index.js
@@ -66,13 +66,13 @@ test('works without a browser context on a dom node (JSDOM Fragment)', () => {
expect(dtl.getByLabelText(container, /username/i)).toMatchInlineSnapshot(`
`)
expect(dtl.getByLabelText(container, /password/i)).toMatchInlineSnapshot(`
`)
})
diff --git a/src/__node_tests__/screen.js b/src/__node_tests__/screen.js
index 896393d0..6dba2399 100644
--- a/src/__node_tests__/screen.js
+++ b/src/__node_tests__/screen.js
@@ -4,6 +4,6 @@ test('the screen export throws a helpful error message when no global document i
expect(() =>
screen.getByText(/hello world/i),
).toThrowErrorMatchingInlineSnapshot(
- `"For queries bound to document.body a global document has to be available... Learn more: https://testing-library.com/s/screen-global-error"`,
+ `For queries bound to document.body a global document has to be available... Learn more: https://testing-library.com/s/screen-global-error`,
)
})
diff --git a/src/__tests__/__snapshots__/get-by-errors.js.snap b/src/__tests__/__snapshots__/get-by-errors.js.snap
index 45f4cde1..42bc84ae 100644
--- a/src/__tests__/__snapshots__/get-by-errors.js.snap
+++ b/src/__tests__/__snapshots__/get-by-errors.js.snap
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`getByLabelText query will throw the custom error returned by config.getElementError 1`] = `"My custom error: Unable to find a label with the text of: TEST QUERY"`;
+exports[`getByLabelText query will throw the custom error returned by config.getElementError 1`] = `My custom error: Unable to find a label with the text of: TEST QUERY`;
-exports[`getByText query will throw the custom error returned by config.getElementError 1`] = `"My custom error: Unable to find an element with the text: TEST QUERY. 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[`getByText query will throw the custom error returned by config.getElementError 1`] = `My custom error: Unable to find an element with the text: TEST QUERY. 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.`;
diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js
index 84334230..2f0b76de 100644
--- a/src/__tests__/ariaAttributes.js
+++ b/src/__tests__/ariaAttributes.js
@@ -5,7 +5,7 @@ test('`selected` throws on unsupported roles', () => {
expect(() =>
getByRole('textbox', {selected: true}),
).toThrowErrorMatchingInlineSnapshot(
- `"\\"aria-selected\\" is not supported on role \\"textbox\\"."`,
+ `"aria-selected" is not supported on role "textbox".`,
)
})
@@ -14,7 +14,7 @@ test('`pressed` throws on unsupported roles', () => {
expect(() =>
getByRole('textbox', {pressed: true}),
).toThrowErrorMatchingInlineSnapshot(
- `"\\"aria-pressed\\" is not supported on role \\"textbox\\"."`,
+ `"aria-pressed" is not supported on role "textbox".`,
)
})
@@ -23,7 +23,7 @@ test('`checked` throws on unsupported roles', () => {
expect(() =>
getByRole('textbox', {checked: true}),
).toThrowErrorMatchingInlineSnapshot(
- `"\\"aria-checked\\" is not supported on role \\"textbox\\"."`,
+ `"aria-checked" is not supported on role "textbox".`,
)
})
@@ -32,7 +32,7 @@ test('`expanded` throws on unsupported roles', () => {
expect(() =>
getByRole('heading', {expanded: true}),
).toThrowErrorMatchingInlineSnapshot(
- `"\\"aria-expanded\\" is not supported on role \\"heading\\"."`,
+ `"aria-expanded" is not supported on role "heading".`,
)
})
@@ -142,9 +142,9 @@ test('`selected: true` matches `aria-selected="true"` on supported roles', () =>
'selected-listbox-option',
])
- expect(
- getAllByRole('rowheader', {selected: true}).map(({id}) => id),
- ).toEqual(['selected-rowheader', 'selected-native-rowheader'])
+ expect(getAllByRole('rowheader', {selected: true}).map(({id}) => id)).toEqual(
+ ['selected-rowheader', 'selected-native-rowheader'],
+ )
expect(getAllByRole('treeitem', {selected: true}).map(({id}) => id)).toEqual([
'selected-treeitem',
@@ -208,7 +208,7 @@ test('`level` throws on unsupported roles', () => {
expect(() =>
getByRole('button', {level: 3}),
).toThrowErrorMatchingInlineSnapshot(
- `"Role \\"button\\" cannot have \\"level\\" property."`,
+ `Role "button" cannot have "level" property.`,
)
})
diff --git a/src/__tests__/base-queries-warn-on-invalid-container.js b/src/__tests__/base-queries-warn-on-invalid-container.js
index c4684005..6796a874 100644
--- a/src/__tests__/base-queries-warn-on-invalid-container.js
+++ b/src/__tests__/base-queries-warn-on-invalid-container.js
@@ -86,8 +86,8 @@ describe('synchronous queries throw on invalid container type', () => {
])('%s', (_queryName, query) => {
expect(() =>
query('invalid type for container', 'irrelevant text'),
- ).toThrowErrorMatchingInlineSnapshot(
- `"Expected container to be an Element, a Document or a DocumentFragment but got string."`,
+ ).toThrowError(
+ `Expected container to be an Element, a Document or a DocumentFragment but got string.`,
)
})
})
@@ -120,8 +120,8 @@ describe('asynchronous queries throw on invalid container type', () => {
queryOptions,
waitOptions,
),
- ).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Expected container to be an Element, a Document or a DocumentFragment but got string."`,
+ ).rejects.toThrowError(
+ `Expected container to be an Element, a Document or a DocumentFragment but got string.`,
)
})
})
diff --git a/src/__tests__/deprecation-warnings.js b/src/__tests__/deprecation-warnings.js
deleted file mode 100644
index 14744159..00000000
--- a/src/__tests__/deprecation-warnings.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import {waitForElement, waitForDomChange, wait} from '..'
-
-afterEach(() => {
- console.warn.mockClear()
-})
-
-test('deprecation warnings only warn once', async () => {
- await wait(() => {}, {timeout: 1})
- await waitForElement(() => {}, {timeout: 1}).catch(e => e)
- await waitForDomChange({timeout: 1}).catch(e => e)
- expect(console.warn.mock.calls).toMatchInlineSnapshot(`
- Array [
- Array [
- "\`wait\` has been deprecated and replaced by \`waitFor\` instead. In most cases you should be able to find/replace \`wait\` with \`waitFor\`. Learn more: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.",
- ],
- Array [
- "\`waitForElement\` has been deprecated. Use a \`find*\` query (preferred: https://testing-library.com/docs/dom-testing-library/api-queries#findby) or use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor",
- ],
- Array [
- "\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.",
- ],
- ]
- `)
-
- console.warn.mockClear()
- await wait(() => {}, {timeout: 1})
- await waitForElement(() => {}, {timeout: 1}).catch(e => e)
- await waitForDomChange({timeout: 1}).catch(e => e)
- expect(console.warn).not.toHaveBeenCalled()
-})
diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js
index 3e3ee6de..8564ddff 100644
--- a/src/__tests__/element-queries.js
+++ b/src/__tests__/element-queries.js
@@ -32,68 +32,78 @@ test('get throws a useful error message', () => {
getByAltText,
getByTitle,
getByRole,
- } = render('')
+ } = render(
+ `Hello, Dave
+
+ Hello, Dave
+
+ Hello, Dave
+
+
+
+ Hello, Dave
+
+