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 1 commit
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
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 composite component, contrary to most queries returning host 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();
});
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();
});
63 changes: 46 additions & 17 deletions src/queries/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer';
import * as React from 'react';
import { createLibraryNotSupportedError } from '../helpers/errors';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { isHostElement } from '../helpers/component-tree';
import { matches, TextMatch } from '../matches';
import type { NormalizerFn } from '../matches';
import { makeQueries } from './makeQueries';
Expand Down Expand Up @@ -58,37 +59,65 @@ const getChildrenAsText = (
const getNodeByText = (
node: ReactTestInstance,
text: TextMatch,
TextComponent: React.ComponentType,
options: TextMatchOptions = {}
) => {
try {
const { Text } = require('react-native');
const isTextComponent = filterNodeByType(node, Text);
if (isTextComponent) {
const textChildren = getChildrenAsText(node.props.children, Text);
if (textChildren) {
const textToTest = textChildren.join('');
const { exact, normalizer } = options;
return matches(text, textToTest, normalizer, exact);
}
const isTextComponent = filterNodeByType(node, TextComponent);
if (isTextComponent) {
const textChildren = getChildrenAsText(node.props.children, TextComponent);
if (textChildren) {
const textToTest = textChildren.join('');
const { exact, normalizer } = options;
return matches(text, textToTest, normalizer, exact);
}
return false;
} catch (error) {
throw createLibraryNotSupportedError(error);
}
return false;
};

function getCompositeParent(
element: ReactTestInstance,
compositeType: React.ComponentType
) {
if (!isHostElement(element)) return null;

let current = element.parent;
while (!isHostElement(current)) {
// We're at the top of the tree
if (!current) {
return null;
}

if (filterNodeByType(current, compositeType)) {
return current;
}
current = current.parent ?? null;
}

return null;
}

const queryAllByText = (
instance: ReactTestInstance
): ((
text: TextMatch,
options?: TextMatchOptions
) => Array<ReactTestInstance>) =>
function queryAllByTextFn(text, options) {
const results = instance.findAll((node) =>
getNodeByText(node, text, options)
);
try {
const { Text } = require('react-native');
const rootInstance = isHostElement(instance)
? getCompositeParent(instance, Text) ?? instance
: instance;

return results;
if (!rootInstance) return [];
const results = rootInstance.findAll((node) =>
getNodeByText(node, text, Text, options)
);

return results;
} catch (error) {
throw createLibraryNotSupportedError(error);
}
};

const getMultipleError = (text: TextMatch) =>
Expand Down