Skip to content

Commit

Permalink
refactor: tweaks and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjastrzebski committed Sep 1, 2023
1 parent 515b447 commit ce969d4
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 103 deletions.
67 changes: 25 additions & 42 deletions src/helpers/__tests__/format-default.test.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -32,89 +25,79 @@ 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" and "opacity" styles but no other style', () => {
const result = defaultMapProps({
style: [{ flex: 1 }, { display: 'none', flex: 2 }, { opacity: 0.5 }],
});

expect(result).toStrictEqual({
style: { display: 'none' },
style: { display: 'none', opacity: 0.5 },
});
});

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 },
});
});

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({});
});
Expand Down
35 changes: 27 additions & 8 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { StyleSheet, ViewStyle } from 'react-native';
import { MapPropsFunction } from './format';

const propsToDisplay = [
'accessibilityElementsHidden',
Expand All @@ -26,15 +25,20 @@ const propsToDisplay = [
'value',
];

const stylePropsToDisplay = ['display', 'opacity'] as const;

/**
* Preserve props that are helpful in diagnosing test failures, while stripping rest
*/
export const defaultMapProps: MapPropsFunction = (props) => {
export function defaultMapProps(
props: Record<string, unknown>
): Record<string, unknown> {
const result: Record<string, unknown> = {};

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);
Expand All @@ -54,7 +58,7 @@ export const defaultMapProps: MapPropsFunction = (props) => {
});

return result;
};
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
Expand All @@ -65,17 +69,32 @@ function removeUndefinedKeys(prop: unknown) {
return prop;
}

let hasKeys = false;
const result: Record<string, unknown> = {};
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<string, unknown> = {};
let hasAnyStyle = false;

stylePropsToDisplay.forEach((styleProp) => {
if (styleProp in style) {
result[styleProp] = style[styleProp];
hasAnyStyle = true;
}
});

return hasAnyStyle ? result : undefined;
}
68 changes: 67 additions & 1 deletion src/matchers/__tests__/to-be-disabled.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TouchableNativeFeedback,
TouchableOpacity,
TouchableWithoutFeedback,
Text,
View,
} from 'react-native';
import { render } from '../..';
Expand All @@ -30,7 +31,72 @@ const ALL_COMPONENTS = {
...ARIA_DISABLED_PROP_COMPONENTS,
};

describe('.toBeDisabled', () => {
test('toBeDisabled/toBeEnabled works with disabled Pressable', () => {
const screen = render(
<Pressable disabled testID="subject">
<Text>Button</Text>
</Pressable>
);

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:
<View
accessibilityState={
{
"disabled": true,
}
}
testID="subject"
/>"
`);

expect(() => expect(pressable).not.toBeDisabled())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeDisabled()
Received element is disabled:
<View
accessibilityState={
{
"disabled": true,
}
}
testID="subject"
/>"
`);

expect(() => expect(title).toBeEnabled()).toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeEnabled()
Received element is not enabled:
<Text>
Button
</Text>"
`);

expect(() => expect(title).not.toBeDisabled())
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeDisabled()
Received element is disabled:
<Text>
Button
</Text>"
`);
});

describe('toBeDisabled()', () => {
Object.entries(DISABLED_PROP_COMPONENTS).forEach(([name, Component]) => {
test(`handle disabled prop for element ${name}`, () => {
const { queryByTestId } = render(
Expand Down
2 changes: 0 additions & 2 deletions src/matchers/__tests__/to-be-visible.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ test('toBeVisible() on empty Modal', () => {
Received element is visible:
<Modal
hardwareAccelerated={false}
testID="modal"
visible={true}
/>"
`);
});
Expand Down
66 changes: 29 additions & 37 deletions src/matchers/to-be-disabled.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint } from 'jest-matcher-utils';
import { isHostTextInput } from '../helpers/host-component-names';
import { checkHostElement, formatMessage } from './utils';

function isElementDisabled(element: ReactTestInstance) {
if (isHostTextInput(element) && element?.props?.editable === false) {
return true;
}

return (
!!element?.props?.['aria-disabled'] ||
!!element?.props?.accessibilityState?.disabled
);
}

function isAncestorDisabled(element: ReactTestInstance): boolean {
const parent = element.parent;
return (
parent != null && (isElementDisabled(element) || isAncestorDisabled(parent))
);
}
import { isTextInputEditable } from '../helpers/text-input';
import { getHostParent } from '../helpers/component-tree';
import { checkHostElement, formatElement } from './utils';

export function toBeDisabled(
this: jest.MatcherContext,
Expand All @@ -34,17 +18,10 @@ export function toBeDisabled(
message: () => {
const is = isDisabled ? 'is' : 'is not';
return [
formatMessage(
matcherHint(
`${this.isNot ? '.not' : ''}.toBeDisabled`,
'element',
''
),
'',
'',
`Received element ${is} disabled:`,
null
),
matcherHint(`${this.isNot ? '.not' : ''}.toBeDisabled`, 'element', ''),
'',
`Received element ${is} disabled:`,
formatElement(element),
].join('\n');
},
};
Expand All @@ -63,14 +40,29 @@ export function toBeEnabled(
message: () => {
const is = isEnabled ? 'is' : 'is not';
return [
formatMessage(
matcherHint(`${this.isNot ? '.not' : ''}.toBeEnabled`, 'element', ''),
'',
'',
`Received element ${is} enabled:`,
null
),
matcherHint(`${this.isNot ? '.not' : ''}.toBeEnabled`, 'element', ''),
'',
`Received element ${is} enabled:`,
formatElement(element),
].join('\n');
},
};
}

function isElementDisabled(element: ReactTestInstance) {
if (isHostTextInput(element) && !isTextInputEditable(element)) {
return true;
}

const { accessibilityState, 'aria-disabled': ariaDisabled } = element.props;
return ariaDisabled ?? accessibilityState?.disabled ?? false;
}

function isAncestorDisabled(element: ReactTestInstance): boolean {
const parent = getHostParent(element);
if (parent == null) {
return false;
}

return isElementDisabled(parent) || isAncestorDisabled(parent);
}

0 comments on commit ce969d4

Please sign in to comment.