From a9f32a69e46bedc9413dd0bdf74637f7a3b2b579 Mon Sep 17 00:00:00 2001 From: Wiktor Sieprawski Date: Tue, 19 Sep 2023 12:38:49 +0200 Subject: [PATCH] feat: `toBeExpanded` matcher (#1497) * feat: toBeExpanded matcher * feat: toBeCollapsed matcher * chore: shorten syntax for isElementCollapsed check * refactor: clean up --------- Co-authored-by: Maciej Jastrzebski --- src/helpers/accessiblity.ts | 14 +++ .../__tests__/to-be-collapsed.test.tsx | 97 +++++++++++++++++++ .../__tests__/to-be-expanded.test.tsx | 96 ++++++++++++++++++ src/matchers/extend-expect.d.ts | 2 + src/matchers/extend-expect.ts | 4 + src/matchers/index.tsx | 2 + src/matchers/to-be-collapsed.tsx | 28 ++++++ src/matchers/to-be-expanded.tsx | 28 ++++++ 8 files changed, 271 insertions(+) create mode 100644 src/matchers/__tests__/to-be-collapsed.test.tsx create mode 100644 src/matchers/__tests__/to-be-expanded.test.tsx create mode 100644 src/matchers/to-be-collapsed.tsx create mode 100644 src/matchers/to-be-expanded.tsx diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index ee440bc37..7419d3356 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -213,6 +213,20 @@ export function isElementBusy( return ariaBusy ?? accessibilityState?.busy ?? false; } +export function isElementCollapsed( + element: ReactTestInstance +): NonNullable { + const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props; + return (ariaExpanded ?? accessibilityState?.expanded) === false; +} + +export function isElementExpanded( + element: ReactTestInstance +): NonNullable { + const { accessibilityState, 'aria-expanded': ariaExpanded } = element.props; + return ariaExpanded ?? accessibilityState?.expanded ?? false; +} + export function isElementSelected( element: ReactTestInstance ): NonNullable { diff --git a/src/matchers/__tests__/to-be-collapsed.test.tsx b/src/matchers/__tests__/to-be-collapsed.test.tsx new file mode 100644 index 000000000..c46100002 --- /dev/null +++ b/src/matchers/__tests__/to-be-collapsed.test.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { View } from 'react-native'; +import { render, screen } from '../..'; +import '../extend-expect'; + +test('toBeCollapsed() basic case', () => { + render( + <> + + + + + + + ); + + expect(screen.getByTestId('expanded')).not.toBeCollapsed(); + expect(screen.getByTestId('expanded-aria')).not.toBeCollapsed(); + expect(screen.getByTestId('not-expanded')).toBeCollapsed(); + expect(screen.getByTestId('not-expanded-aria')).toBeCollapsed(); + expect(screen.getByTestId('default')).not.toBeCollapsed(); +}); + +test('toBeCollapsed() error messages', () => { + render( + <> + + + + + + + ); + + expect(() => expect(screen.getByTestId('expanded')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); + + expect(() => expect(screen.getByTestId('expanded-aria')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); + + expect(() => expect(screen.getByTestId('not-expanded')).not.toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeCollapsed() + + Received element is collapsed: + " + `); + + expect(() => + expect(screen.getByTestId('not-expanded-aria')).not.toBeCollapsed() + ).toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeCollapsed() + + Received element is collapsed: + " + `); + + expect(() => expect(screen.getByTestId('default')).toBeCollapsed()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeCollapsed() + + Received element is not collapsed: + " + `); +}); diff --git a/src/matchers/__tests__/to-be-expanded.test.tsx b/src/matchers/__tests__/to-be-expanded.test.tsx new file mode 100644 index 000000000..07b891f75 --- /dev/null +++ b/src/matchers/__tests__/to-be-expanded.test.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { View } from 'react-native'; +import { render, screen } from '../..'; +import '../extend-expect'; + +test('toBeExpanded() basic case', () => { + render( + <> + + + + + + + ); + + expect(screen.getByTestId('expanded')).toBeExpanded(); + expect(screen.getByTestId('expanded-aria')).toBeExpanded(); + expect(screen.getByTestId('not-expanded')).not.toBeExpanded(); + expect(screen.getByTestId('not-expanded-aria')).not.toBeExpanded(); + expect(screen.getByTestId('default')).not.toBeExpanded(); +}); + +test('toBeExpanded() error messages', () => { + render( + <> + + + + + + + ); + + expect(() => expect(screen.getByTestId('expanded')).not.toBeExpanded()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeExpanded() + + Received element is expanded: + " + `); + + expect(() => expect(screen.getByTestId('expanded-aria')).not.toBeExpanded()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeExpanded() + + Received element is expanded: + " + `); + + expect(() => expect(screen.getByTestId('not-expanded')).toBeExpanded()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeExpanded() + + Received element is not expanded: + " + `); + + expect(() => expect(screen.getByTestId('not-expanded-aria')).toBeExpanded()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeExpanded() + + Received element is not expanded: + " + `); + + expect(() => expect(screen.getByTestId('default')).toBeExpanded()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeExpanded() + + Received element is not expanded: + " + `); +}); diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts index 2a9a595c0..dd8bfa173 100644 --- a/src/matchers/extend-expect.d.ts +++ b/src/matchers/extend-expect.d.ts @@ -6,10 +6,12 @@ import type { Style } from './to-have-style'; export interface JestNativeMatchers { toBeOnTheScreen(): R; toBeChecked(): R; + toBeCollapsed(): R; toBeDisabled(): R; toBeBusy(): R; toBeEmptyElement(): R; toBeEnabled(): R; + toBeExpanded(): R; toBePartiallyChecked(): R; toBeSelected(): R; toBeVisible(): R; diff --git a/src/matchers/extend-expect.ts b/src/matchers/extend-expect.ts index aaad5ba32..c262f3d1c 100644 --- a/src/matchers/extend-expect.ts +++ b/src/matchers/extend-expect.ts @@ -2,9 +2,11 @@ import { toBeOnTheScreen } from './to-be-on-the-screen'; import { toBeChecked } from './to-be-checked'; +import { toBeCollapsed } from './to-be-collapsed'; import { toBeDisabled, toBeEnabled } from './to-be-disabled'; import { toBeBusy } from './to-be-busy'; import { toBeEmptyElement } from './to-be-empty-element'; +import { toBeExpanded } from './to-be-expanded'; import { toBePartiallyChecked } from './to-be-partially-checked'; import { toBeSelected } from './to-be-selected'; import { toBeVisible } from './to-be-visible'; @@ -17,10 +19,12 @@ import { toHaveTextContent } from './to-have-text-content'; expect.extend({ toBeOnTheScreen, toBeChecked, + toBeCollapsed, toBeDisabled, toBeBusy, toBeEmptyElement, toBeEnabled, + toBeExpanded, toBePartiallyChecked, toBeSelected, toBeVisible, diff --git a/src/matchers/index.tsx b/src/matchers/index.tsx index 9b4db7f6c..66a4a0942 100644 --- a/src/matchers/index.tsx +++ b/src/matchers/index.tsx @@ -1,7 +1,9 @@ export { toBeBusy } from './to-be-busy'; export { toBeChecked } from './to-be-checked'; +export { toBeCollapsed } from './to-be-collapsed'; export { toBeDisabled, toBeEnabled } from './to-be-disabled'; export { toBeEmptyElement } from './to-be-empty-element'; +export { toBeExpanded } from './to-be-expanded'; export { toBeOnTheScreen } from './to-be-on-the-screen'; export { toBePartiallyChecked } from './to-be-partially-checked'; export { toBeSelected } from './to-be-selected'; diff --git a/src/matchers/to-be-collapsed.tsx b/src/matchers/to-be-collapsed.tsx new file mode 100644 index 000000000..857ab1b9d --- /dev/null +++ b/src/matchers/to-be-collapsed.tsx @@ -0,0 +1,28 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { matcherHint } from 'jest-matcher-utils'; +import { isElementCollapsed } from '../helpers/accessiblity'; +import { checkHostElement, formatElement } from './utils'; + +export function toBeCollapsed( + this: jest.MatcherContext, + element: ReactTestInstance +) { + checkHostElement(element, toBeCollapsed, this); + + return { + pass: isElementCollapsed(element), + message: () => { + const matcher = matcherHint( + `${this.isNot ? '.not' : ''}.toBeCollapsed`, + 'element', + '' + ); + return [ + matcher, + '', + `Received element is ${this.isNot ? '' : 'not '}collapsed:`, + formatElement(element), + ].join('\n'); + }, + }; +} diff --git a/src/matchers/to-be-expanded.tsx b/src/matchers/to-be-expanded.tsx new file mode 100644 index 000000000..1fbb8060d --- /dev/null +++ b/src/matchers/to-be-expanded.tsx @@ -0,0 +1,28 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { matcherHint } from 'jest-matcher-utils'; +import { isElementExpanded } from '../helpers/accessiblity'; +import { checkHostElement, formatElement } from './utils'; + +export function toBeExpanded( + this: jest.MatcherContext, + element: ReactTestInstance +) { + checkHostElement(element, toBeExpanded, this); + + return { + pass: isElementExpanded(element), + message: () => { + const matcher = matcherHint( + `${this.isNot ? '.not' : ''}.toBeExpanded`, + 'element', + '' + ); + return [ + matcher, + '', + `Received element is ${this.isNot ? '' : 'not '}expanded:`, + formatElement(element), + ].join('\n'); + }, + }; +}