Skip to content

fix: *ByRole queries to match their own text text content #1139

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
Show file tree
Hide file tree
Changes from 2 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
185 changes: 185 additions & 0 deletions src/__tests__/host-text-nesting.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import * as React from 'react';
import { Text, Pressable, View } from 'react-native';
import { render, within } from '../pure';

/**
* Our queries interact differently with composite and host elements, and some specific cases require us
* to crawl up the tree to a Text composite element to be able to traverse it down again. Going up the tree
* is a dangerous behaviour because we could take the risk of then traversing a sibling node to the original one.
* This test suite is designed to be able to test as many different combinations, as a safety net.
* Specific cases should still be tested within the relevant file (for instance an edge case with `within` should have
* an explicit test in the within test suite)
*/
describe('nested text handling', () => {
test('within same node', () => {
const view = render(<Text testID="subject">Hello</Text>);
expect(within(view.getByTestId('subject')).getByText('Hello')).toBeTruthy();
});

test('role with direct text children', () => {
const view = render(<Text accessibilityRole="header">About</Text>);

expect(view.getByRole('header', { name: 'About' })).toBeTruthy();
});

test('nested text with child with role', () => {
const view = render(
<Text>
<Text testID="child" accessibilityRole="header">
About
</Text>
</Text>
);

expect(view.getByRole('header', { name: 'About' }).props.testID).toBe(
'child'
);
});

test('pressable within text with label', () => {
const view = render(
<Text>
<Pressable
testID="pressable"
accessibilityRole="button"
accessibilityLabel="Save"
></Pressable>
</Text>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('pressable within text, with text child', () => {
const view = render(
<Text>
<Pressable testID="pressable" accessibilityRole="button">
<Text>Save</Text>
</Pressable>
</Text>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('pressable within text, with multiple text children', () => {
const view = render(
<Text>
<Pressable testID="pressable" accessibilityRole="button">
<Text>Save</Text>
<Text>render</Text>
</Pressable>
</Text>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('pressable within View, with text child', () => {
const view = render(
<View>
<Pressable testID="pressable" accessibilityRole="button">
<Text>Save</Text>
</Pressable>
</View>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('pressable within View, with text child within view', () => {
const view = render(
<View>
<Pressable testID="pressable" accessibilityRole="button">
<View>
<Text>Save</Text>
</View>
</Pressable>
</View>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('pressable within View within Text, with text child within view', () => {
const view = render(
<Text>
<View>
<Pressable testID="pressable" accessibilityRole="button">
<View>
<Text>Save</Text>
</View>
</Pressable>
</View>
</Text>
);

expect(view.getByRole('button', { name: 'Save' }).props.testID).toBe(
'pressable'
);
});

test('Text within pressable', () => {
const view = render(
<Pressable testID="pressable" accessibilityRole="button">
<Text testID="text">Save</Text>
</Pressable>
);

expect(view.getByText('Save').props.testID).toBe('text');
});

test('Text within view within pressable', () => {
const view = render(
<Pressable testID="pressable" accessibilityRole="button">
<View>
<Text testID="text">Save</Text>
</View>
</Pressable>
);

expect(view.getByText('Save').props.testID).toBe('text');
});

test('View with text child', () => {
const view = render(
<View testID="view">
<Text>Save</Text>
</View>
);

expect(view.getByTestId('view').props.testID).toBe('view');
});

test('Text within view', () => {
const view = render(
<View>
<Text testID="text">Save</Text>
</View>
);

expect(view.getByTestId('text').props.testID).toBe('text');
});

test('Text within view within text', () => {
const view = render(
<Text>
<View>
<Text testID="text">Save</Text>
</View>
</Text>
);

expect(view.getByTestId('text').props.testID).toBe('text');
});
});
8 changes: 4 additions & 4 deletions src/__tests__/jest-native.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ test('jest-native matchers work correctly', () => {
expect(getByText('Disabled Button')).toBeDisabled();
expect(getByText('Enabled Button')).not.toBeDisabled();

expect(getByA11yHint('Empty Text')).toBeEmpty();
expect(getByA11yHint('Empty View')).toBeEmpty();
expect(getByA11yHint('Not Empty Text')).not.toBeEmpty();
expect(getByA11yHint('Not Empty View')).not.toBeEmpty();
expect(getByA11yHint('Empty Text')).toBeEmptyElement();
expect(getByA11yHint('Empty View')).toBeEmptyElement();
expect(getByA11yHint('Not Empty Text')).not.toBeEmptyElement();
expect(getByA11yHint('Not Empty View')).not.toBeEmptyElement();

expect(getByA11yHint('Container View')).toContainElement(
// $FlowFixMe - TODO: fix @testing-library/jest-native flow typings
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/within.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,11 @@ test('within() exposes a11y queries', async () => {
test('getQueriesForElement is alias to within', () => {
expect(getQueriesForElement).toBe(within);
});

test('within allows searching for text within a composite component', () => {
const view = render(<Text testID="subject">Hello</Text>);
// view.getByTestId('subject') returns a host component, contrary to text queries returning a composite component
// we want to be sure that this doesn't interfere with the way text is searched
const hostTextQueries = within(view.getByTestId('subject'));
expect(hostTextQueries.getByText('Hello')).toBeTruthy();
});
15 changes: 15 additions & 0 deletions src/helpers/__tests__/component-tree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
getHostParent,
getHostSelves,
getHostSiblings,
getCompositeParentOfType,
isHostElement,
} from '../component-tree';

function MultipleHostChildren() {
Expand Down Expand Up @@ -200,3 +202,16 @@ test('returns host siblings for composite component', () => {
view.getByTestId('siblingAfter'),
]);
});

test('getCompositeParentOfType', () => {
const view = render(<View testID="test"></View>);
const hostComponent = view.getByTestId('test');

const compositeComponent = getCompositeParentOfType(hostComponent, View);

// We get the corresponding composite component (same testID), but not the host
expect(hostComponent.props.testID).toBe(compositeComponent?.props.testID);

expect(hostComponent).not.toBe(compositeComponent);
expect(isHostElement(compositeComponent)).toBe(false);
});
21 changes: 21 additions & 0 deletions src/helpers/component-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,24 @@ export function getHostSiblings(
(sibling) => !hostSelves.includes(sibling)
);
}

export function getCompositeParentOfType(
element: ReactTestInstance,
type: React.ComponentType
) {
let current = element.parent;

while (!isHostElement(current)) {
// We're at the root of the tree
if (!current) {
return null;
}

if (current.type === type) {
return current;
}
current = current.parent ?? null;
}

return null;
}
39 changes: 39 additions & 0 deletions src/queries/__tests__/role.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,43 @@ describe('supports name option', () => {
'target-button'
);
});

test('returns an element when the direct child is text', () => {
const { getByRole } = render(
<Text accessibilityRole="header" testID="target-header">
About
</Text>
);

// assert on the testId to be sure that the returned element is the one with the accessibilityRole
expect(getByRole('header', { name: 'About' }).props.testID).toBe(
'target-header'
);
});

test('returns an element with nested Text as children', () => {
const { getByRole } = render(
<Text accessibilityRole="header" testID="parent">
<Text testID="child">About</Text>
</Text>
);

// assert on the testId to be sure that the returned element is the one with the accessibilityRole
expect(getByRole('header', { name: 'About' }).props.testID).toBe('parent');
});

test('returns a header with an accessibilityLabel', () => {
const { getByRole } = render(
<Text
accessibilityRole="header"
testID="target-header"
accessibilityLabel="About"
></Text>
);

// assert on the testId to be sure that the returned element is the one with the accessibilityRole
expect(getByRole('header', { name: 'About' }).props.testID).toBe(
'target-header'
);
});
});
8 changes: 7 additions & 1 deletion src/queries/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Button,
TextInput,
} from 'react-native';
import { render, getDefaultNormalizer } from '../..';
import { render, getDefaultNormalizer, within } from '../..';

type MyButtonProps = {
children: React.ReactNode;
Expand Down Expand Up @@ -454,3 +454,9 @@ test('getByText and queryByText work with tabs', () => {
expect(getByText(textWithTabs)).toBeTruthy();
expect(queryByText(textWithTabs)).toBeTruthy();
});

test('getByText searches for text within itself', () => {
const { getByText } = render(<Text testID="subject">Hello</Text>);
const textNode = within(getByText('Hello'));
expect(textNode.getByText('Hello')).toBeTruthy();
});
Loading