diff --git a/README.md b/README.md index e93abdb2..98643c2b 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ clear to read and to maintain. - [`toHaveErrorMessage`](#tohaveerrormessage) - [`toBePressed`](#tobepressed) - [`toBePartiallyPressed`](#tobepartiallypressed) + - [`toAppearBefore`](#toappearbefore) + - [`toAppearAfter`](#toappearafter) - [Deprecated matchers](#deprecated-matchers) - [`toBeEmpty`](#tobeempty) - [`toBeInTheDOM`](#tobeinthedom) @@ -1386,6 +1388,72 @@ screen .toBePartiallyPressed() ``` +
+ +### `toAppearBefore` + +This checks if a given element appears before another element in the DOM tree, +as per +[`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition). + +```typescript +toAppearBefore() +``` + +#### Examples + +```html +
+ Text A + Text B +
+``` + +```javascript +const textA = queryByTestId('text-a') +const textB = queryByTestId('text-b') + +expect(textA).toAppearBefore(textB) +expect(textB).not.toAppearBefore(textA) +``` + +> Note: This matcher does not take into account CSS styles that may modify the +> display order of elements, eg: +> +> - `flex-direction: row-reverse`, +> - `flex-direction: column-reverse`, +> - `display: grid` + +### `toAppearAfter` + +This checks if a given element appears after another element in the DOM tree, as +per +[`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition). + +```typescript +toAppearAfter() +``` + +#### Examples + +```html +
+ Text A + Text B +
+``` + +```javascript +const textA = queryByTestId('text-a') +const textB = queryByTestId('text-b') + +expect(textB).toAppearAfter(textA) +expect(textA).not.toAppearAfter(textB) +``` + +> Note: This matcher does not take into account CSS styles that may modify the +> display order of elements, see [`toAppearBefore()`](#toappearbefore) + ## Deprecated matchers ### `toBeEmpty` diff --git a/src/__tests__/to-appear-before.js b/src/__tests__/to-appear-before.js new file mode 100644 index 00000000..c6af15e0 --- /dev/null +++ b/src/__tests__/to-appear-before.js @@ -0,0 +1,105 @@ +import {render} from './helpers/test-utils' + +describe('.toAppearBefore', () => { + const {queryByTestId} = render(` +
+
+ Text A + Text B +
+
+ `) + + const textA = queryByTestId('text-a') + const textB = queryByTestId('text-b') + const divA = queryByTestId('div-a') + + it('returns correct result when first element is before second element', () => { + expect(textA).toAppearBefore(textB) + }) + + it('returns correct for .not() invocation', () => { + expect(textB).not.toAppearBefore(textA) + }) + + it('errors out when first element is not before second element', () => { + expect(() => expect(textB).toAppearBefore(textA)).toThrowError( + /Received: Node.DOCUMENT_POSITION_PRECEDING \(2\)/i, + ) + }) + + it('errors out when .not is used but first element is actually before second element', () => { + expect(() => expect(textA).not.toAppearBefore(textB)).toThrowError( + /\.not\.toAppearBefore/i, + ) + }) + + it('errors out when first element is parent of second element', () => { + expect(() => expect(divA).toAppearBefore(textA)).toThrowError( + /Received: Unknown document position \(20\)/i, + ) + }) + + it('errors out when first element is child of second element', () => { + expect(() => expect(textA).toAppearBefore(divA)).toThrowError( + /Received: Unknown document position \(10\)/i, + ) + }) + + it('errors out when either first or second element is not HTMLElement', () => { + expect(() => expect(1).toAppearBefore(textB)).toThrowError() + expect(() => expect(textA).toAppearBefore(1)).toThrowError() + }) +}) + +describe('.toAppearAfter', () => { + const {queryByTestId} = render(` +
+
+ Text A + Text B +
+
+ `) + + const textA = queryByTestId('text-a') + const textB = queryByTestId('text-b') + const divA = queryByTestId('div-a') + + it('returns correct result when first element is after second element', () => { + expect(textB).toAppearAfter(textA) + }) + + it('returns correct for .not() invocation', () => { + expect(textA).not.toAppearAfter(textB) + }) + + it('errors out when first element is not after second element', () => { + expect(() => expect(textA).toAppearAfter(textB)).toThrowError( + /Received: Node.DOCUMENT_POSITION_FOLLOWING \(4\)/i, + ) + }) + + it('errors out when .not is used but first element is actually after second element', () => { + expect(() => expect(textB).not.toAppearAfter(textA)).toThrowError( + /\.not\.toAppearAfter/i, + ) + }) + + it('errors out when first element is parent of second element', () => { + expect(() => expect(divA).toAppearAfter(textA)).toThrowError( + /Received: Unknown document position \(20\)/i, + ) + }) + + it('errors out when first element is child of second element', () => { + expect(() => expect(textA).toAppearAfter(divA)).toThrowError( + /Received: Unknown document position \(10\)/i, + ) + }) + + it('errors out when either first or second element is not HTMLElement', () => { + expect(() => expect(1).toAppearAfter(textB)).toThrowError() + expect(() => expect(textA).toAppearAfter(1)).toThrowError() + }) +}) diff --git a/src/matchers.js b/src/matchers.js index e121edb8..f67054f4 100644 --- a/src/matchers.js +++ b/src/matchers.js @@ -27,3 +27,4 @@ export {toHaveErrorMessage} from './to-have-errormessage' export {toHaveSelection} from './to-have-selection' export {toBePressed} from './to-be-pressed' export {toBePartiallyPressed} from './to-be-partially-pressed' +export {toAppearBefore, toAppearAfter} from './to-appear-before' diff --git a/src/to-appear-before.js b/src/to-appear-before.js new file mode 100644 index 00000000..66ccd212 --- /dev/null +++ b/src/to-appear-before.js @@ -0,0 +1,60 @@ +import {checkHtmlElement} from './utils' + +// ref: https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition +const DOCUMENT_POSITIONS_STRINGS = { + [Node.DOCUMENT_POSITION_DISCONNECTED]: 'Node.DOCUMENT_POSITION_DISCONNECTED', + [Node.DOCUMENT_POSITION_PRECEDING]: 'Node.DOCUMENT_POSITION_PRECEDING', + [Node.DOCUMENT_POSITION_FOLLOWING]: 'Node.DOCUMENT_POSITION_FOLLOWING', + [Node.DOCUMENT_POSITION_CONTAINS]: 'Node.DOCUMENT_POSITION_CONTAINS', + [Node.DOCUMENT_POSITION_CONTAINED_BY]: 'Node.DOCUMENT_POSITION_CONTAINED_BY', + [Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC]: + 'Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC', +} + +function makeDocumentPositionErrorString(documentPosition) { + if (documentPosition in DOCUMENT_POSITIONS_STRINGS) { + return `${DOCUMENT_POSITIONS_STRINGS[documentPosition]} (${documentPosition})` + } + + return `Unknown document position (${documentPosition})` +} + +function checkToAppear(methodName, targetDocumentPosition) { + // eslint-disable-next-line func-names + return function (element, secondElement) { + checkHtmlElement(element, toAppearBefore, this) + checkHtmlElement(secondElement, toAppearBefore, this) + + const documentPosition = element.compareDocumentPosition(secondElement) + const pass = documentPosition === targetDocumentPosition + + return { + pass, + message: () => { + return [ + this.utils.matcherHint( + `${this.isNot ? '.not' : ''}.${methodName}`, + 'element', + 'secondElement', + ), + '', + `Received: ${makeDocumentPositionErrorString(documentPosition)}`, + ].join('\n') + }, + } + } +} + +export function toAppearBefore(element, secondElement) { + return checkToAppear( + 'toAppearBefore', + Node.DOCUMENT_POSITION_FOLLOWING, + ).apply(this, [element, secondElement]) +} + +export function toAppearAfter(element, secondElement) { + return checkToAppear('toAppearAfter', Node.DOCUMENT_POSITION_PRECEDING).apply( + this, + [element, secondElement], + ) +} diff --git a/types/matchers.d.ts b/types/matchers.d.ts index 34fdaff8..82de01dc 100755 --- a/types/matchers.d.ts +++ b/types/matchers.d.ts @@ -810,6 +810,46 @@ declare namespace matchers { * [testing-library/jest-dom#tobepartiallypressed](https://github.com/testing-library/jest-dom#tobepartiallypressed) */ toBePartiallyPressed(): R + /* + * @description + * This checks if a given element appears before another element in the DOM tree, as per [`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition). + * + * @example + *
+ * Text A + * Text B + *
+ * + * const textA = queryByTestId('text-a') + * const textB = queryByTestId('text-b') + * + * expect(textA).toAppearBefore(textB) + * expect(textB).not.toAppearBefore(textA) + * + * @See + * [testing-library/jest-dom#toappearbefore](https://github.com/testing-library/jest-dom#toappearbefore) + */ + toAppearBefore(element: HTMLElement | SVGElement): R + /* + * @description + * This checks if a given element appears after another element in the DOM tree, as per [`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition). + * + * @example + *
+ * Text A + * Text B + *
+ * + * const textA = queryByTestId('text-a') + * const textB = queryByTestId('text-b') + * + * expect(textB).toAppearAfter(textA) + * expect(textA).not.toAppearAfter(textB) + * + * @See + * [testing-library/jest-dom#toappearafter](https://github.com/testing-library/jest-dom#toappearafter) + */ + toAppearAfter(element: HTMLElement | SVGElement): R } }