From 5a8f357cc8e969115e6c515e1a775b918e13208b Mon Sep 17 00:00:00 2001 From: suany0805 Date: Wed, 6 Nov 2024 15:56:46 -0500 Subject: [PATCH 01/18] WIP toBeDisabled --- packages/native/src/index.ts | 0 packages/native/src/toBeDisabled.ts | 59 +++++++++++++++++++ packages/native/src/utils.ts | 19 ++++++ .../native/test/unit/toBeDisabled.test.ts | 0 4 files changed, 78 insertions(+) create mode 100644 packages/native/src/index.ts create mode 100644 packages/native/src/toBeDisabled.ts create mode 100644 packages/native/src/utils.ts create mode 100644 packages/native/test/unit/toBeDisabled.test.ts diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/native/src/toBeDisabled.ts b/packages/native/src/toBeDisabled.ts new file mode 100644 index 0000000..374038b --- /dev/null +++ b/packages/native/src/toBeDisabled.ts @@ -0,0 +1,59 @@ +import type { ReactTestInstance } from 'react-test-renderer'; +import { matcherHint } from 'jest-matcher-utils'; +import { checkReactElement, 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) { + checkReactElement(element); + + const isDisabled = isElementDisabled(element) || isAncestorDisabled(element); + + return { + pass: isDisabled, + message: () => { + const is = isDisabled ? 'is' : 'is not'; + return [ + matcherHint(`${this.isNot ? '.not' : ''}.toBeDisabled`, 'element', ''), + '', + `Received element ${is} disabled:`, + element, + ].join('\n'); + }, + }; +} + diff --git a/packages/native/src/utils.ts b/packages/native/src/utils.ts new file mode 100644 index 0000000..3eaecab --- /dev/null +++ b/packages/native/src/utils.ts @@ -0,0 +1,19 @@ +import { ReactTestInstance } from "react-test-renderer"; + +export function checkReactElement( + element: ReactTestInstance | null | undefined + ): asserts element is ReactTestInstance { + if (!element) { + throw new Error('Value must be a React element'); + } + + // @ts-expect-error internal _fiber property of ReactTestInstance + if (!element._fiber && !VALID_ELEMENTS.includes(element.type.toString())) { + throw new Error('Value must be a React element'); + } + } + +export function getType({ type }: ReactTestInstance) { + // @ts-expect-error: ReactTestInstance contains too loose typing + return type.displayName || type.name || type; + } diff --git a/packages/native/test/unit/toBeDisabled.test.ts b/packages/native/test/unit/toBeDisabled.test.ts new file mode 100644 index 0000000..e69de29 From 3dbcce9e2a4a563f1afb289cb7a1924e3bfdc43e Mon Sep 17 00:00:00 2001 From: suany0805 Date: Mon, 11 Nov 2024 15:06:43 -0500 Subject: [PATCH 02/18] WIP: add tests --- package.json | 3 + .../native/test/unit/toBeDisabled.test.ts | 160 ++++++++++++++++++ yarn.lock | 3 +- 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 28f391c..c9c9141 100644 --- a/package.json +++ b/package.json @@ -35,5 +35,8 @@ "eslint-plugin-sonarjs": "^0.24.0", "turbo": "^1.12.4", "typescript": "^5.4.2" + }, + "dependencies": { + "@testing-library/react-native": "^12.8.1" } } diff --git a/packages/native/test/unit/toBeDisabled.test.ts b/packages/native/test/unit/toBeDisabled.test.ts index e69de29..6c9af15 100644 --- a/packages/native/test/unit/toBeDisabled.test.ts +++ b/packages/native/test/unit/toBeDisabled.test.ts @@ -0,0 +1,160 @@ +import React from 'react'; +import { + Button, + TouchableHighlight, + TouchableOpacity, + TouchableWithoutFeedback, + TouchableNativeFeedback, + View, + TextInput, + Pressable, +} from 'react-native'; +import { render } from '@testing-library/react-native'; + +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( + +