Skip to content

Commit efba541

Browse files
committed
feat: Add .toAppearBefore/.toAppearAfter matcher
1 parent 779b712 commit efba541

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ clear to read and to maintain.
8383
- [`toHaveErrorMessage`](#tohaveerrormessage)
8484
- [`toBePressed`](#tobepressed)
8585
- [`toBePartiallyPressed`](#tobepartiallypressed)
86+
- [`toAppearBefore`](#toappearbefore)
87+
- [`toAppearAfter`](#toappearafter)
8688
- [Deprecated matchers](#deprecated-matchers)
8789
- [`toBeEmpty`](#tobeempty)
8890
- [`toBeInTheDOM`](#tobeinthedom)
@@ -1386,6 +1388,72 @@ screen
13861388
.toBePartiallyPressed()
13871389
```
13881390

1391+
<hr />
1392+
1393+
### `toAppearBefore`
1394+
1395+
This checks if a given element appears before another element in the DOM tree,
1396+
as per
1397+
[`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition).
1398+
1399+
```typescript
1400+
toAppearBefore()
1401+
```
1402+
1403+
#### Examples
1404+
1405+
```html
1406+
<div>
1407+
<span data-testid="text-a">Text A</span>
1408+
<span data-testid="text-b">Text B</span>
1409+
</div>
1410+
```
1411+
1412+
```javascript
1413+
const textA = queryByTestId('text-a')
1414+
const textB = queryByTestId('text-b')
1415+
1416+
expect(textA).toAppearBefore(textB)
1417+
expect(textB).not.toAppearBefore(textA)
1418+
```
1419+
1420+
> Note: This matcher does not take into account CSS styles that may modify the
1421+
> display order of elements, eg:
1422+
>
1423+
> - `flex-direction: row-reverse`,
1424+
> - `flex-direction: column-reverse`,
1425+
> - `display: grid`
1426+
1427+
### `toAppearAfter`
1428+
1429+
This checks if a given element appears after another element in the DOM tree, as
1430+
per
1431+
[`compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition).
1432+
1433+
```typescript
1434+
toAppearAfter()
1435+
```
1436+
1437+
#### Examples
1438+
1439+
```html
1440+
<div>
1441+
<span data-testid="text-a">Text A</span>
1442+
<span data-testid="text-b">Text B</span>
1443+
</div>
1444+
```
1445+
1446+
```javascript
1447+
const textA = queryByTestId('text-a')
1448+
const textB = queryByTestId('text-b')
1449+
1450+
expect(textB).toAppearAfter(textA)
1451+
expect(textA).not.toAppearAfter(textB)
1452+
```
1453+
1454+
> Note: This matcher does not take into account CSS styles that may modify the
1455+
> display order of elements, see [`toAppearBefore()`](#toappearbefore)
1456+
13891457
## Deprecated matchers
13901458

13911459
### `toBeEmpty`

src/__tests__/to-appear-before.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {render} from './helpers/test-utils'
2+
3+
describe('.toAppearBefore', () => {
4+
const {queryByTestId} = render(`
5+
<div>
6+
<div data-testid='div-a'>
7+
<span data-testid='text-a'>Text A</span>
8+
<span data-testid='text-b'>Text B</span>
9+
</div>
10+
</div>
11+
`)
12+
13+
const textA = queryByTestId('text-a')
14+
const textB = queryByTestId('text-b')
15+
const divA = queryByTestId('div-a')
16+
17+
it('returns correct result when first element is before second element', () => {
18+
expect(textA).toAppearBefore(textB)
19+
})
20+
21+
it('returns correct for .not() invocation', () => {
22+
expect(textB).not.toAppearBefore(textA)
23+
})
24+
25+
it('errors out when first element is not before second element', () => {
26+
expect(() => expect(textB).toAppearBefore(textA)).toThrowError(
27+
/Received: Node.DOCUMENT_POSITION_PRECEDING \(2\)/i,
28+
)
29+
})
30+
31+
it('errors out when first element is parent of second element', () => {
32+
expect(() => expect(divA).toAppearBefore(textA)).toThrowError(
33+
/Received: Unknown document position \(20\)/i,
34+
)
35+
})
36+
37+
it('errors out when first element is child of second element', () => {
38+
expect(() => expect(textA).toAppearBefore(divA)).toThrowError(
39+
/Received: Unknown document position \(10\)/i,
40+
)
41+
})
42+
43+
it('errors out when either first or second element is not HTMLElement', () => {
44+
expect(() => expect(1).toAppearBefore(textB)).toThrowError()
45+
expect(() => expect(textA).toAppearBefore(1)).toThrowError()
46+
})
47+
})
48+
49+
describe('.toAppearAfter', () => {
50+
const {queryByTestId} = render(`
51+
<div>
52+
<div data-testid='div-a'>
53+
<span data-testid='text-a'>Text A</span>
54+
<span data-testid='text-b'>Text B</span>
55+
</div>
56+
</div>
57+
`)
58+
59+
const textA = queryByTestId('text-a')
60+
const textB = queryByTestId('text-b')
61+
const divA = queryByTestId('div-a')
62+
63+
it('returns correct result when first element is after second element', () => {
64+
expect(textB).toAppearAfter(textA)
65+
})
66+
67+
it('returns correct for .not() invocation', () => {
68+
expect(textA).not.toAppearAfter(textB)
69+
})
70+
71+
it('errors out when first element is not after second element', () => {
72+
expect(() => expect(textA).toAppearAfter(textB)).toThrowError(
73+
/Received: Node.DOCUMENT_POSITION_FOLLOWING \(4\)/i,
74+
)
75+
})
76+
77+
it('errors out when first element is parent of second element', () => {
78+
expect(() => expect(divA).toAppearAfter(textA)).toThrowError(
79+
/Received: Unknown document position \(20\)/i,
80+
)
81+
})
82+
83+
it('errors out when first element is child of second element', () => {
84+
expect(() => expect(textA).toAppearAfter(divA)).toThrowError(
85+
/Received: Unknown document position \(10\)/i,
86+
)
87+
})
88+
89+
it('errors out when either first or second element is not HTMLElement', () => {
90+
expect(() => expect(1).toAppearAfter(textB)).toThrowError()
91+
expect(() => expect(textA).toAppearAfter(1)).toThrowError()
92+
})
93+
})

src/matchers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export {toHaveErrorMessage} from './to-have-errormessage'
2727
export {toHaveSelection} from './to-have-selection'
2828
export {toBePressed} from './to-be-pressed'
2929
export {toBePartiallyPressed} from './to-be-partially-pressed'
30+
export {toAppearBefore, toAppearAfter} from './to-appear-before'

src/to-appear-before.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {checkHtmlElement} from './utils'
2+
3+
// ref: https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
4+
const DOCUMENT_POSITIONS_STRINGS = {
5+
[Node.DOCUMENT_POSITION_DISCONNECTED]: 'Node.DOCUMENT_POSITION_DISCONNECTED',
6+
[Node.DOCUMENT_POSITION_PRECEDING]: 'Node.DOCUMENT_POSITION_PRECEDING',
7+
[Node.DOCUMENT_POSITION_FOLLOWING]: 'Node.DOCUMENT_POSITION_FOLLOWING',
8+
[Node.DOCUMENT_POSITION_CONTAINS]: 'Node.DOCUMENT_POSITION_CONTAINS',
9+
[Node.DOCUMENT_POSITION_CONTAINED_BY]: 'Node.DOCUMENT_POSITION_CONTAINED_BY',
10+
[Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC]:
11+
'Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC',
12+
}
13+
14+
const makeDocumentPositionErrorString = documentPosition => {
15+
if (documentPosition in DOCUMENT_POSITIONS_STRINGS) {
16+
return `${DOCUMENT_POSITIONS_STRINGS[documentPosition]} (${documentPosition})`
17+
}
18+
19+
return `Unknown document position (${documentPosition})`
20+
}
21+
22+
const checkToAppear = (methodName, targetDocumentPosition) => {
23+
// eslint-disable-next-line func-names
24+
return function (element, secondElement) {
25+
checkHtmlElement(element, toAppearBefore, this)
26+
checkHtmlElement(secondElement, toAppearBefore, this)
27+
28+
const documentPosition = element.compareDocumentPosition(secondElement)
29+
const pass = documentPosition === targetDocumentPosition
30+
31+
return {
32+
pass,
33+
message: () => {
34+
return [
35+
this.utils.matcherHint(
36+
`${this.isNot ? '.not' : ''}.${methodName}`,
37+
'element',
38+
'secondElement',
39+
),
40+
'',
41+
`Received: ${makeDocumentPositionErrorString(documentPosition)}`,
42+
].join('\n')
43+
},
44+
}
45+
}
46+
}
47+
48+
export function toAppearBefore(element, secondElement) {
49+
return checkToAppear(
50+
'toAppearBefore',
51+
Node.DOCUMENT_POSITION_FOLLOWING,
52+
).apply(this, [element, secondElement])
53+
}
54+
55+
export function toAppearAfter(element, secondElement) {
56+
return checkToAppear('toAppearAfter', Node.DOCUMENT_POSITION_PRECEDING).apply(
57+
this,
58+
[element, secondElement],
59+
)
60+
}

types/matchers.d.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,46 @@ declare namespace matchers {
810810
* [testing-library/jest-dom#tobepartiallypressed](https://github.com/testing-library/jest-dom#tobepartiallypressed)
811811
*/
812812
toBePartiallyPressed(): R
813+
/*
814+
* @description
815+
* 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).
816+
*
817+
* @example
818+
* <div>
819+
* <span data-testid="text-a">Text A</span>
820+
* <span data-testid="text-b">Text B</span>
821+
* </div>
822+
*
823+
* const textA = queryByTestId('text-a')
824+
* const textB = queryByTestId('text-b')
825+
*
826+
* expect(textA).toAppearBefore(textB)
827+
* expect(textB).not.toAppearBefore(textA)
828+
*
829+
* @See
830+
* [testing-library/jest-dom#toappearbefore](https://github.com/testing-library/jest-dom#toappearbefore)
831+
*/
832+
toAppearBefore(element: HTMLElement | SVGElement): R
833+
/*
834+
* @description
835+
* 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).
836+
*
837+
* @example
838+
* <div>
839+
* <span data-testid="text-a">Text A</span>
840+
* <span data-testid="text-b">Text B</span>
841+
* </div>
842+
*
843+
* const textA = queryByTestId('text-a')
844+
* const textB = queryByTestId('text-b')
845+
*
846+
* expect(textB).toAppearAfter(textA)
847+
* expect(textA).not.toAppearAfter(textB)
848+
*
849+
* @See
850+
* [testing-library/jest-dom#toappearafter](https://github.com/testing-library/jest-dom#toappearafter)
851+
*/
852+
toAppearAfter(element: HTMLElement | SVGElement): R
813853
}
814854
}
815855

0 commit comments

Comments
 (0)