From faebd54595edcf43e3d45e47e7e5698b206c0760 Mon Sep 17 00:00:00 2001 From: Jan Jaworski Date: Wed, 23 Aug 2023 09:22:36 +0200 Subject: [PATCH 1/9] feat: add toBeDisabled matcher --- .../__tests__/to-be-disabled.test.tsx | 0 src/matchers/extend-expect.d.ts | 1 + src/matchers/extend-expect.ts | 2 + src/matchers/to-be-disabled.tsx | 71 +++++++++++++++++++ src/matchers/utils.tsx | 5 ++ 5 files changed, 79 insertions(+) create mode 100644 src/matchers/__tests__/to-be-disabled.test.tsx create mode 100644 src/matchers/to-be-disabled.tsx diff --git a/src/matchers/__tests__/to-be-disabled.test.tsx b/src/matchers/__tests__/to-be-disabled.test.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts index 7072b020e..8dfffac9d 100644 --- a/src/matchers/extend-expect.d.ts +++ b/src/matchers/extend-expect.d.ts @@ -6,6 +6,7 @@ export interface JestNativeMatchers { toBeVisible(): R; toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R; toHaveTextContent(expectedText: TextMatch, options?: TextMatchOptions): R; + toBeDisabled(): R; } // Implicit Jest global `expect`. diff --git a/src/matchers/extend-expect.ts b/src/matchers/extend-expect.ts index 327302c57..cc1b06113 100644 --- a/src/matchers/extend-expect.ts +++ b/src/matchers/extend-expect.ts @@ -5,6 +5,7 @@ import { toBeEmptyElement } from './to-be-empty-element'; import { toBeVisible } from './to-be-visible'; import { toHaveDisplayValue } from './to-have-display-value'; import { toHaveTextContent } from './to-have-text-content'; +import { toBeDisabled } from './to-be-disabled'; expect.extend({ toBeOnTheScreen, @@ -12,4 +13,5 @@ expect.extend({ toBeVisible, toHaveDisplayValue, toHaveTextContent, + toBeDisabled, }); diff --git a/src/matchers/to-be-disabled.tsx b/src/matchers/to-be-disabled.tsx new file mode 100644 index 000000000..1edbf1899 --- /dev/null +++ b/src/matchers/to-be-disabled.tsx @@ -0,0 +1,71 @@ +import type { ReactTestInstance } from 'react-test-renderer'; +import { matcherHint } from 'jest-matcher-utils'; +import { checkHostElement, formatMessage, getType } from './utils'; + +// Elements that support 'disabled' +const DISABLE_TYPES = [ + 'Button', + 'Slider', + 'Switch', + 'Text', + 'TouchableHighlight', + 'TouchableOpacity', + 'TouchableWithoutFeedback', + 'TouchableNativeFeedback', + 'View', + 'TextInput', + 'Pressable', +]; +function isElementDisabled(element: ReactTestInstance) { + if (getType(element) === 'TextInput' && element?.props?.editable === false) { + return true; + } + + if (!DISABLE_TYPES.includes(getType(element))) { + return false; + } + + return ( + !!element?.props?.disabled || + !!element?.props?.accessibilityState?.disabled || + !!element?.props?.accessibilityStates?.includes('disabled') + ); +} + +function isAncestorDisabled(element: ReactTestInstance): boolean { + const parent = element.parent; + return ( + parent != null && (isElementDisabled(element) || isAncestorDisabled(parent)) + ); +} + +export function toBeDisabled( + this: jest.MatcherContext, + element: ReactTestInstance +) { + if (element !== null || !this.isNot) { + checkHostElement(element, toBeDisabled, this); + } + + const isDisabled = isElementDisabled(element) || isAncestorDisabled(element); + + return { + pass: isDisabled, + message: () => { + const is = isDisabled ? 'is' : 'is not'; + return [ + formatMessage( + matcherHint( + `${this.isNot ? '.not' : ''}.toBeDisabled`, + 'element', + '' + ), + '', + `Received element ${is} disabled:`, + printElement(element), + null + ), + ].join('\n'); + }, + }; +} diff --git a/src/matchers/utils.tsx b/src/matchers/utils.tsx index f6808b09c..d5b53fbdc 100644 --- a/src/matchers/utils.tsx +++ b/src/matchers/utils.tsx @@ -121,3 +121,8 @@ export function formatMessage( function formatValue(value: unknown) { return typeof value === 'string' ? value : stringify(value); } + +export function getType({ type }: ReactTestInstance) { + // @ts-expect-error: ReactTestInstance contains too loose typing + return type.displayName || type.name || type; +} From 9d25396e9be3bec02756c1bfa7d086f8b579fbe2 Mon Sep 17 00:00:00 2001 From: Jan Jaworski Date: Thu, 24 Aug 2023 11:23:32 +0200 Subject: [PATCH 2/9] add tests and toBeEnabled matcher --- .../__tests__/to-be-disabled.test.tsx | 175 ++++++++++++++++++ src/matchers/extend-expect.d.ts | 1 + src/matchers/extend-expect.ts | 3 +- src/matchers/to-be-disabled.tsx | 27 ++- 4 files changed, 202 insertions(+), 4 deletions(-) diff --git a/src/matchers/__tests__/to-be-disabled.test.tsx b/src/matchers/__tests__/to-be-disabled.test.tsx index e69de29bb..bd7002a2c 100644 --- a/src/matchers/__tests__/to-be-disabled.test.tsx +++ b/src/matchers/__tests__/to-be-disabled.test.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { + Button, + Pressable, + TextInput, + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native'; +import { render } from '../..'; +import '../extend-expect'; + +const ALLOWED_COMPONENTS = { + View, + TextInput, + TouchableHighlight, + TouchableOpacity, + TouchableWithoutFeedback, + TouchableNativeFeedback, + Pressable, +}; + +describe('.toBeDisabled', () => { + Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => { + test(`handle disabled prop for element ${name}`, () => { + const { queryByTestId } = render( + //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604) + + + + ); + + expect(queryByTestId(name)).toBeDisabled(); + expect(() => expect(queryByTestId(name)).not.toBeDisabled()).toThrow(); + }); + }); + + Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => { + test(`handle disabled in accessibilityState for element ${name}`, () => { + const { queryByTestId } = render( + //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604) + + + + ); + + expect(queryByTestId(name)).toBeDisabled(); + expect(() => expect(queryByTestId(name)).not.toBeDisabled()).toThrow(); + }); + }); + + test('handle editable prop for TextInput', () => { + const { getByTestId, getByPlaceholderText } = render( + + + + + + ); + + // Check host TextInput + expect(getByTestId('disabled')).toBeDisabled(); + expect(getByTestId('enabled-by-default')).not.toBeDisabled(); + expect(getByTestId('enabled')).not.toBeDisabled(); + + // Check composite TextInput + expect(getByPlaceholderText('disabled')).toBeDisabled(); + expect(getByPlaceholderText('enabled-by-default')).not.toBeDisabled(); + expect(getByPlaceholderText('enabled')).not.toBeDisabled(); + }); +}); + +describe('.toBeEnabled', () => { + Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => { + test(`handle disabled prop for element ${name} when undefined`, () => { + const { queryByTestId } = render( + //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604) + + + + ); + + expect(queryByTestId(name)).toBeEnabled(); + expect(() => expect(queryByTestId(name)).not.toBeEnabled()).toThrow(); + }); + }); + + Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => { + test(`handle disabled in accessibilityState for element ${name} when false`, () => { + const { queryByTestId } = render( + //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604) + + + + ); + + expect(queryByTestId(name)).toBeEnabled(); + expect(() => expect(queryByTestId(name)).not.toBeEnabled()).toThrow(); + }); + }); + + test('handle editable prop for TextInput', () => { + const { getByTestId, getByPlaceholderText } = render( + + + + + + ); + + // Check host TextInput + expect(getByTestId('enabled-by-default')).toBeEnabled(); + expect(getByTestId('enabled')).toBeEnabled(); + expect(getByTestId('disabled')).not.toBeEnabled(); + + // Check composite TextInput + expect(getByPlaceholderText('enabled-by-default')).toBeEnabled(); + expect(getByPlaceholderText('enabled')).toBeEnabled(); + expect(getByPlaceholderText('disabled')).not.toBeEnabled(); + }); +}); + +describe('for .toBeEnabled/Disabled Button', () => { + test('handles disabled prop for button', () => { + const { queryByTestId } = render( + +