diff --git a/src/helpers/__tests__/format-default.test.tsx b/src/helpers/__tests__/format-default.test.tsx index 8f2e784b2..d7334caf1 100644 --- a/src/helpers/__tests__/format-default.test.tsx +++ b/src/helpers/__tests__/format-default.test.tsx @@ -1,12 +1,5 @@ -import { ReactTestRendererJSON } from 'react-test-renderer'; import { defaultMapProps } from '../format-default'; -const node: ReactTestRendererJSON = { - type: 'View', - props: {}, - children: null, -}; - describe('mapPropsForQueryError', () => { test('preserves props that are helpful for debugging', () => { const props = { @@ -32,39 +25,34 @@ describe('mapPropsForQueryError', () => { defaultValue: 'DEFAULT_VALUE', }; - const result = defaultMapProps(props, node); + const result = defaultMapProps(props); expect(result).toStrictEqual(props); }); test('does not preserve less helpful props', () => { - const result = defaultMapProps( - { - style: [{ flex: 1 }, { display: 'flex' }], - onPress: () => null, - key: 'foo', - }, - node - ); + const result = defaultMapProps({ + style: [{ flex: 1 }, { flexDirection: 'row' }], + onPress: () => null, + key: 'foo', + }); expect(result).toStrictEqual({}); }); - test('preserves "display: none" style but no other style', () => { - const result = defaultMapProps( - { style: [{ flex: 1 }, { display: 'none', flex: 2 }] }, - node - ); + test('preserves "display: none" and "opacity: 0" styles but no other style', () => { + const result = defaultMapProps({ + style: [{ flex: 1 }, { display: 'none', flex: 2 }, { opacity: 0 }], + }); expect(result).toStrictEqual({ - style: { display: 'none' }, + style: { display: 'none', opacity: 0 }, }); }); test('removes undefined keys from accessibilityState', () => { - const result = defaultMapProps( - { accessibilityState: { checked: undefined, selected: false } }, - node - ); + const result = defaultMapProps({ + accessibilityState: { checked: undefined, selected: false }, + }); expect(result).toStrictEqual({ accessibilityState: { selected: false }, @@ -72,49 +60,44 @@ describe('mapPropsForQueryError', () => { }); test('removes accessibilityState if all keys are undefined', () => { - const result = defaultMapProps( - { accessibilityState: { checked: undefined, selected: undefined } }, - node - ); + const result = defaultMapProps({ + accessibilityState: { checked: undefined, selected: undefined }, + }); expect(result).toStrictEqual({}); }); test('does not fail if accessibilityState is a string, passes through', () => { - const result = defaultMapProps({ accessibilityState: 'foo' }, node); + const result = defaultMapProps({ accessibilityState: 'foo' }); expect(result).toStrictEqual({ accessibilityState: 'foo' }); }); test('does not fail if accessibilityState is an array, passes through', () => { - const result = defaultMapProps({ accessibilityState: [1] }, node); + const result = defaultMapProps({ accessibilityState: [1] }); expect(result).toStrictEqual({ accessibilityState: [1] }); }); test('does not fail if accessibilityState is null, passes through', () => { - const result = defaultMapProps({ accessibilityState: null }, node); + const result = defaultMapProps({ accessibilityState: null }); expect(result).toStrictEqual({ accessibilityState: null }); }); test('does not fail if accessibilityState is nested object, passes through', () => { const accessibilityState = { 1: { 2: 3 }, 2: undefined }; - const result = defaultMapProps({ accessibilityState }, node); + const result = defaultMapProps({ accessibilityState }); expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } }); }); test('removes undefined keys from accessibilityValue', () => { - const result = defaultMapProps( - { accessibilityValue: { min: 1, max: undefined } }, - node - ); + const result = defaultMapProps({ + accessibilityValue: { min: 1, max: undefined }, + }); expect(result).toStrictEqual({ accessibilityValue: { min: 1 } }); }); test('removes accessibilityValue if all keys are undefined', () => { - const result = defaultMapProps( - { accessibilityValue: { min: undefined } }, - node - ); + const result = defaultMapProps({ accessibilityValue: { min: undefined } }); expect(result).toStrictEqual({}); }); diff --git a/src/helpers/format-default.ts b/src/helpers/format-default.ts index 1f2faab51..133c8717e 100644 --- a/src/helpers/format-default.ts +++ b/src/helpers/format-default.ts @@ -1,7 +1,7 @@ import { StyleSheet, ViewStyle } from 'react-native'; -import { MapPropsFunction } from './format'; const propsToDisplay = [ + 'accessible', 'accessibilityElementsHidden', 'accessibilityHint', 'accessibilityLabel', @@ -24,17 +24,20 @@ const propsToDisplay = [ 'testID', 'title', 'value', -]; +] as const; /** * Preserve props that are helpful in diagnosing test failures, while stripping rest */ -export const defaultMapProps: MapPropsFunction = (props) => { +export function defaultMapProps( + props: Record +): Record { const result: Record = {}; const styles = StyleSheet.flatten(props.style as ViewStyle); - if (styles?.display === 'none') { - result.style = { display: 'none' }; + const styleToDisplay = extractStyle(styles); + if (styleToDisplay !== undefined) { + result.style = styleToDisplay; } const accessibilityState = removeUndefinedKeys(props.accessibilityState); @@ -54,7 +57,7 @@ export const defaultMapProps: MapPropsFunction = (props) => { }); return result; -}; +} function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -65,17 +68,32 @@ function removeUndefinedKeys(prop: unknown) { return prop; } + let hasKeys = false; const result: Record = {}; Object.entries(prop).forEach(([key, value]) => { if (value !== undefined) { result[key] = value; + hasKeys = true; } }); - // If object does not have any props we will ignore it. - if (Object.keys(result).length === 0) { + return hasKeys ? result : undefined; +} + +function extractStyle(style: ViewStyle | undefined) { + if (style == null) { return undefined; } - return result; + const result: Record = {}; + if (style.display === 'none') { + result.display = 'none'; + } + + if (style.opacity === 0) { + result.opacity = 0; + } + + const hasAnyKeys = Object.keys(result).length > 0; + return hasAnyKeys ? result : undefined; } 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..854a970c5 --- /dev/null +++ b/src/matchers/__tests__/to-be-disabled.test.tsx @@ -0,0 +1,272 @@ +import React from 'react'; +import { + Button, + Pressable, + TextInput, + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + Text, + View, +} from 'react-native'; +import { render } from '../..'; +import '../extend-expect'; + +test('toBeDisabled/toBeEnabled supports basic case', () => { + const screen = render( + + + + + + + + + Text + + + + + ); + + expect(screen.getByTestId('disabled-parent')).toBeDisabled(); + expect(screen.getByTestId('disabled-child')).toBeDisabled(); + expect(screen.getByTestId('enabled-view')).not.toBeDisabled(); + expect(screen.getByTestId('enabled-text')).not.toBeDisabled(); + expect(screen.getByTestId('enabled-text-input')).not.toBeDisabled(); + expect(screen.getByTestId('enabled-pressable')).not.toBeDisabled(); + + expect(() => expect(screen.getByTestId('disabled-parent')).not.toBeDisabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeDisabled() + + Received element is disabled: + " + `); + + expect(() => expect(screen.getByTestId('enabled-view')).toBeDisabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeDisabled() + + Received element is not disabled: + " + `); + + expect(screen.getByTestId('disabled-parent')).not.toBeEnabled(); + expect(screen.getByTestId('disabled-child')).not.toBeEnabled(); + expect(screen.getByTestId('enabled-view')).toBeEnabled(); + expect(screen.getByTestId('enabled-text')).toBeEnabled(); + expect(screen.getByTestId('enabled-text-input')).toBeEnabled(); + expect(screen.getByTestId('enabled-pressable')).toBeEnabled(); + + expect(() => expect(screen.getByTestId('disabled-parent')).toBeEnabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeEnabled() + + Received element is not enabled: + " + `); + + expect(() => expect(screen.getByTestId('enabled-view')).not.toBeEnabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeEnabled() + + Received element is enabled: + " + `); +}); + +test('toBeDisabled/toBeEnabled supports Pressable with "disabled" prop', () => { + const screen = render( + + Button + + ); + + const pressable = screen.getByTestId('subject'); + expect(pressable).toBeDisabled(); + expect(pressable).not.toBeEnabled(); + + const title = screen.getByText('Button'); + expect(title).toBeDisabled(); + expect(title).not.toBeEnabled(); + + expect(() => expect(pressable).toBeEnabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeEnabled() + + Received element is not enabled: + " + `); + + expect(() => expect(pressable).not.toBeDisabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeDisabled() + + Received element is disabled: + " + `); + + expect(() => expect(title).toBeEnabled()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeEnabled() + + Received element is not enabled: + + Button + " + `); + + expect(() => expect(title).not.toBeDisabled()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeDisabled() + + Received element is disabled: + + Button + " + `); +}); + +test.each([ + ['Pressable', Pressable], + ['TouchableOpacity', TouchableOpacity], + ['TouchableHighlight', TouchableHighlight], + ['TouchableWithoutFeedback', TouchableWithoutFeedback], + ['TouchableNativeFeedback', TouchableNativeFeedback], +] as const)( + 'toBeDisabled/toBeEnabled supports %s with "disabled" prop', + (_, Component) => { + const screen = render( + // @ts-expect-error disabled prop is not available on all Touchables + + Button + + ); + + const touchable = screen.getByTestId('subject'); + expect(touchable).toBeDisabled(); + expect(touchable).not.toBeEnabled(); + + const title = screen.getByText('Button'); + expect(title).toBeDisabled(); + expect(title).not.toBeEnabled(); + + expect(() => expect(touchable).toBeEnabled()).toThrow(); + expect(() => expect(touchable).not.toBeDisabled()).toThrow(); + expect(() => expect(title).toBeEnabled()).toThrow(); + expect(() => expect(title).not.toBeDisabled()).toThrow(); + } +); + +test.each([ + ['View', View], + ['Text', Text], + ['TextInput', TextInput], + ['Pressable', Pressable], + ['TouchableOpacity', TouchableOpacity], + ['TouchableWithoutFeedback', TouchableWithoutFeedback], + ['TouchableNativeFeedback', TouchableNativeFeedback], +] as const)( + 'toBeDisabled/toBeEnabled supports %s with "aria-disabled" prop', + (_, Component) => { + const screen = render( + // @ts-expect-error too generic for typescript + + Hello + + ); + + const view = screen.getByTestId('subject'); + expect(view).toBeDisabled(); + expect(view).not.toBeEnabled(); + expect(() => expect(view).toBeEnabled()).toThrow(); + expect(() => expect(view).not.toBeDisabled()).toThrow(); + } +); + +test.each([ + ['View', View], + ['Text', Text], + ['TextInput', TextInput], + ['Pressable', Pressable], + ['TouchableOpacity', TouchableOpacity], + ['TouchableHighlight', TouchableHighlight], + ['TouchableWithoutFeedback', TouchableWithoutFeedback], + ['TouchableNativeFeedback', TouchableNativeFeedback], +] as const)( + 'toBeDisabled/toBeEnabled supports %s with "accessibilityState.disabled" prop', + (_, Component) => { + const screen = render( + // @ts-expect-error disabled prop is not available on all Touchables + + Hello + + ); + + const view = screen.getByTestId('subject'); + expect(view).toBeDisabled(); + expect(view).not.toBeEnabled(); + expect(() => expect(view).toBeEnabled()).toThrow(); + expect(() => expect(view).not.toBeDisabled()).toThrow(); + } +); + +test('toBeDisabled/toBeEnabled supports "editable" prop on TextInput', () => { + const screen = render( + + + + + + ); + + expect(screen.getByTestId('enabled-by-default')).not.toBeDisabled(); + expect(screen.getByTestId('enabled')).not.toBeDisabled(); + expect(screen.getByTestId('disabled')).toBeDisabled(); + + expect(screen.getByTestId('enabled-by-default')).toBeEnabled(); + expect(screen.getByTestId('enabled')).toBeEnabled(); + expect(screen.getByTestId('disabled')).not.toBeEnabled(); +}); + +test('toBeDisabled/toBeEnabled supports "disabled" prop on Button', () => { + const screen = render( + +