Skip to content

[v12] refactor: exclude hidden elements by defaults #1317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions examples/basic/jest-setup.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
/* eslint-disable no-undef, import/no-extraneous-dependencies */
import { configure } from '@testing-library/react-native';

// Import Jest Native matchers
import '@testing-library/jest-native/extend-expect';

// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

// Enable excluding hidden elements from the queries by default
configure({
defaultIncludeHiddenElements: false,
});
2 changes: 0 additions & 2 deletions examples/react-navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ There are two types of recommeded tests:
1. Tests operating on navigator level - these use `renderNavigator` helper to render a navigator component used in the app. It is useful when you want to test a scenario that includes multiple screens.
2. Tests operating on single screen level - these use regular `render` helper but require refactoring screen components into `Screen` and `ScreenContent` components. Where `Screen` receives React Navigation props and/or uses hooks like `useNavigation` while `ScreenContent` does not have a direct relation to React Navigation API but gets props from `Screen` and calls relevant callbacks to trigger navigation.

> Note that this example applies `includeHiddenElements: false` by default, so all queries will ignore elements on the hidden screens, e.g. inactive tabs or screens present in stack navigators. This option is enabled in `jest-setup.js` file, using `defaultIncludeHiddenElements: false` option to `configure` function.

## Non-recommended tests

There also exists another popular type of screen level tests, where users mock React Navigation objects like `navigation`, `route` and/or hooks like `useNavigation`, etc. We don't recommend this way of testing. **Mocking internal parts of the libraries is effectively testing implementation details, which goes against the Testing Library's [Guiding Principles](https://testing-library.com/docs/guiding-principles/)**.
Expand Down
6 changes: 0 additions & 6 deletions examples/react-navigation/jest-setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-undef, import/no-extraneous-dependencies */
import { configure } from '@testing-library/react-native';

// Import Jest Native matchers
import '@testing-library/jest-native/extend-expect';
Expand All @@ -10,8 +9,3 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
// Setup Reanimated mocking for Drawer navigation
global.ReanimatedDataMock = { now: () => Date.now() };
require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests();

// Enable excluding hidden elements from the queries by default
configure({
defaultIncludeHiddenElements: false,
});
16 changes: 8 additions & 8 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {

test('getConfig() returns existing configuration', () => {
expect(getConfig().asyncUtilTimeout).toEqual(1000);
expect(getConfig().defaultIncludeHiddenElements).toEqual(true);
expect(getConfig().defaultIncludeHiddenElements).toEqual(false);
});

test('configure() overrides existing config values', () => {
Expand All @@ -16,21 +16,21 @@ test('configure() overrides existing config values', () => {
expect(getConfig()).toEqual({
asyncUtilTimeout: 5000,
defaultDebugOptions: { message: 'debug message' },
defaultIncludeHiddenElements: true,
defaultIncludeHiddenElements: false,
});
});

test('resetToDefaults() resets config to defaults', () => {
configure({
asyncUtilTimeout: 5000,
defaultIncludeHiddenElements: false,
defaultIncludeHiddenElements: true,
});
expect(getConfig().asyncUtilTimeout).toEqual(5000);
expect(getConfig().defaultIncludeHiddenElements).toEqual(false);
expect(getConfig().defaultIncludeHiddenElements).toEqual(true);

resetToDefaults();
expect(getConfig().asyncUtilTimeout).toEqual(1000);
expect(getConfig().defaultIncludeHiddenElements).toEqual(true);
expect(getConfig().defaultIncludeHiddenElements).toEqual(false);
});

test('resetToDefaults() resets internal config to defaults', () => {
Expand All @@ -44,8 +44,8 @@ test('resetToDefaults() resets internal config to defaults', () => {
});

test('configure handles alias option defaultHidden', () => {
expect(getConfig().defaultIncludeHiddenElements).toEqual(true);

configure({ defaultHidden: false });
expect(getConfig().defaultIncludeHiddenElements).toEqual(false);

configure({ defaultHidden: true });
expect(getConfig().defaultIncludeHiddenElements).toEqual(true);
});
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type InternalConfig = Config & {

const defaultConfig: InternalConfig = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: true,
defaultIncludeHiddenElements: false,
};

let config = { ...defaultConfig };
Expand Down
132 changes: 114 additions & 18 deletions src/helpers/__tests__/accessiblity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ describe('isHiddenFromAccessibility', () => {
test('returns false for accessible elements', () => {
expect(
isHiddenFromAccessibility(
render(<View testID="subject" />).getByTestId('subject')
render(<View testID="subject" />).getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);

expect(
isHiddenFromAccessibility(
render(<Text testID="subject">Hello</Text>).getByTestId('subject')
render(<Text testID="subject">Hello</Text>).getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);

expect(
isHiddenFromAccessibility(
render(<TextInput testID="subject" />).getByTestId('subject')
render(<TextInput testID="subject" />).getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
});
Expand All @@ -37,7 +43,13 @@ describe('isHiddenFromAccessibility', () => {

test('detects elements with accessibilityElementsHidden prop', () => {
const view = render(<View testID="subject" accessibilityElementsHidden />);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects nested elements with accessibilityElementsHidden prop', () => {
Expand All @@ -46,7 +58,13 @@ describe('isHiddenFromAccessibility', () => {
<View testID="subject" />
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects deeply nested elements with accessibilityElementsHidden prop', () => {
Expand All @@ -59,14 +77,26 @@ describe('isHiddenFromAccessibility', () => {
</View>
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects elements with importantForAccessibility="no-hide-descendants" prop', () => {
const view = render(
<View testID="subject" importantForAccessibility="no-hide-descendants" />
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', () => {
Expand All @@ -75,12 +105,24 @@ describe('isHiddenFromAccessibility', () => {
<View testID="subject" />
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects elements with display=none', () => {
const view = render(<View testID="subject" style={{ display: 'none' }} />);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects nested elements with display=none', () => {
Expand All @@ -89,7 +131,13 @@ describe('isHiddenFromAccessibility', () => {
<View testID="subject" />
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects deeply nested elements with display=none', () => {
Expand All @@ -102,7 +150,13 @@ describe('isHiddenFromAccessibility', () => {
</View>
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects elements with display=none with complex style', () => {
Expand All @@ -116,12 +170,24 @@ describe('isHiddenFromAccessibility', () => {
]}
/>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('is not trigged by opacity = 0', () => {
const view = render(<View testID="subject" style={{ opacity: 0 }} />);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
});

test('detects siblings of element with accessibilityViewIsModal prop', () => {
Expand All @@ -131,7 +197,13 @@ describe('isHiddenFromAccessibility', () => {
<View testID="subject" />
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('detects deeply nested siblings of element with accessibilityViewIsModal prop', () => {
Expand All @@ -145,7 +217,13 @@ describe('isHiddenFromAccessibility', () => {
</View>
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(true);
});

test('is not triggered for element with accessibilityViewIsModal prop', () => {
Expand All @@ -154,7 +232,13 @@ describe('isHiddenFromAccessibility', () => {
<View accessibilityViewIsModal testID="subject" />
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
});

test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
Expand All @@ -165,7 +249,13 @@ describe('isHiddenFromAccessibility', () => {
</View>
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
});

test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
Expand All @@ -180,7 +270,13 @@ describe('isHiddenFromAccessibility', () => {
</View>
</View>
);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
});

test('has isInaccessible alias', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export function isHiddenFromAccessibility(
export const isInaccessible = isHiddenFromAccessibility;

function isSubtreeInaccessible(element: ReactTestInstance): boolean {
// Null props can happen for React.Fragments
if (element.props == null) {
return false;
}

// iOS: accessibilityElementsHidden
// See: https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios
if (element.props.accessibilityElementsHidden) {
Expand Down
2 changes: 1 addition & 1 deletion src/queries/__tests__/a11yState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ test('byA11yState queries support hidden option', () => {
</Pressable>
);

expect(getByA11yState({ expanded: false })).toBeTruthy();
expect(
getByA11yState({ expanded: false }, { includeHiddenElements: true })
).toBeTruthy();

expect(queryByA11yState({ expanded: false })).toBeFalsy();
expect(
queryByA11yState({ expanded: false }, { includeHiddenElements: false })
).toBeFalsy();
Expand Down
2 changes: 1 addition & 1 deletion src/queries/__tests__/a11yValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ test('byA11yValue queries support hidden option', () => {
</Text>
);

expect(getByA11yValue({ max: 10 })).toBeTruthy();
expect(
getByA11yValue({ max: 10 }, { includeHiddenElements: true })
).toBeTruthy();

expect(queryByA11yValue({ max: 10 })).toBeFalsy();
expect(
queryByA11yValue({ max: 10 }, { includeHiddenElements: false })
).toBeFalsy();
Expand Down
2 changes: 1 addition & 1 deletion src/queries/__tests__/displayValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ test('byDisplayValue queries support hidden option', () => {
<TextInput value="hidden" style={{ display: 'none' }} />
);

expect(getByDisplayValue('hidden')).toBeTruthy();
expect(
getByDisplayValue('hidden', { includeHiddenElements: true })
).toBeTruthy();

expect(queryByDisplayValue('hidden')).toBeFalsy();
expect(
queryByDisplayValue('hidden', { includeHiddenElements: false })
).toBeFalsy();
Expand Down
Loading