Skip to content
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

fix: improve text matching #1222

Merged
merged 5 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion src/__tests__/react-native-api.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test('React Native API assumption: nested <Text> renders single host element', (
</Text>
</Text>
);
expect(getHostSelf(view.getByText('Hello'))).toBe(view.getByTestId('test'));
expect(getHostSelf(view.getByText(/Hello/))).toBe(view.getByTestId('test'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test relied of nested Text not being matched, so I needed to change whole string match to partial match. This was a limitation on previous text matching and not a feature per se. IMO that is not a breaking change as test required a minor tweak without affecting the essence of the test.

expect(getHostSelf(view.getByText('Before'))).toBe(
view.getByTestId('before')
);
Expand Down
49 changes: 49 additions & 0 deletions src/helpers/__tests__/getTextContent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';
import { Text } from 'react-native';
import render from '../../render';
import { getTextContent } from '../getTextContent';

test('getTextContent with simple content', () => {
const view = render(<Text>Hello world</Text>);
expect(getTextContent(view.container)).toBe('Hello world');
});

test('getTextContent with null element', () => {
expect(getTextContent(null)).toBe('');
});

test('getTextContent with single nested content', () => {
const view = render(
<Text>
<Text>Hello world</Text>
</Text>
);
expect(getTextContent(view.container)).toBe('Hello world');
});

test('getTextContent with multiple nested content', () => {
const view = render(
<Text>
<Text>Hello</Text> <Text>world</Text>
</Text>
);
expect(getTextContent(view.container)).toBe('Hello world');
});

test('getTextContent with multiple number content', () => {
const view = render(
<Text>
<Text>Hello</Text> <Text>world</Text> <Text>{100}</Text>
</Text>
);
expect(getTextContent(view.container)).toBe('Hello world 100');
});

test('getTextContent with multiple boolean content', () => {
const view = render(
<Text>
<Text>Hello{false}</Text> <Text>{true}world</Text>
</Text>
);
expect(getTextContent(view.container)).toBe('Hello world');
});
41 changes: 39 additions & 2 deletions src/helpers/findAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { getConfig } from '../config';
import { isHiddenFromAccessibility } from './accessiblity';

interface FindAllOptions {
/** Match elements hidden from accessibility */
includeHiddenElements?: boolean;

/** RTL-compatible alias to `includeHiddenElements` */
hidden?: boolean;

/* Exclude any ancestors of deepest matched elements even if they match the predicate */
matchDeepestOnly?: boolean;
}

export function findAll(
root: ReactTestInstance,
predicate: (node: ReactTestInstance) => boolean,
predicate: (element: ReactTestInstance) => boolean,
options?: FindAllOptions
) {
const results = root.findAll(predicate);
const results = findAllInternal(root, predicate, options);

const includeHiddenElements =
options?.includeHiddenElements ??
Expand All @@ -29,3 +34,35 @@ export function findAll(
(element) => !isHiddenFromAccessibility(element, { cache })
);
}

// Extracted from React Test Renderer
// src: https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-test-renderer/src/ReactTestRenderer.js#L402
function findAllInternal(
root: ReactTestInstance,
predicate: (element: ReactTestInstance) => boolean,
options?: FindAllOptions
): Array<ReactTestInstance> {
const results: ReactTestInstance[] = [];

// Match descendants first but do not add them to results yet.
const matchingDescendants: ReactTestInstance[] = [];
root.children.forEach((child) => {
if (typeof child === 'string') {
return;
}
matchingDescendants.push(...findAllInternal(child, predicate, options));
});

if (
// When matchDeepestOnly = true: add current element only if no descendants match
(!options?.matchDeepestOnly || matchingDescendants.length === 0) &&
predicate(root)
) {
results.push(root);
}

// Add matching descendants after element to preserve original tree walk order.
results.push(...matchingDescendants);

return results;
}
20 changes: 20 additions & 0 deletions src/helpers/getTextContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactTestInstance } from 'react-test-renderer';

export function getTextContent(
element: ReactTestInstance | string | null
): string {
if (!element) {
return '';
}

if (typeof element === 'string') {
return element;
}

const result: string[] = [];
element.children?.forEach((child) => {
result.push(getTextContent(child));
});

return result.join('');
}
19 changes: 19 additions & 0 deletions src/helpers/matchers/matchTextContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Text } from 'react-native';
import type { ReactTestInstance } from 'react-test-renderer';
import { matches, TextMatch, TextMatchOptions } from '../../matches';
import { filterNodeByType } from '../filterNodeByType';
import { getTextContent } from '../getTextContent';

export function matchTextContent(
node: ReactTestInstance,
text: TextMatch,
options: TextMatchOptions = {}
) {
if (!filterNodeByType(node, Text)) {
return false;
}

const textContent = getTextContent(node);
const { exact, normalizer } = options;
return matches(text, textContent, normalizer, exact);
}
5 changes: 5 additions & 0 deletions src/matches.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export type NormalizerFn = (textToNormalize: string) => string;

export type TextMatch = string | RegExp;
export type TextMatchOptions = {
mdjastrzebski marked this conversation as resolved.
Show resolved Hide resolved
exact?: boolean;
normalizer?: NormalizerFn;
};

export function matches(
matcher: TextMatch,
Expand Down
33 changes: 27 additions & 6 deletions src/queries/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import {
} from 'react-native';
import { render, getDefaultNormalizer, within } from '../..';

test('byText matches simple text', () => {
const { getByText } = render(<Text testID="text">Hello World</Text>);
expect(getByText('Hello World').props.testID).toBe('text');
});

test('byText matches inner nested text', () => {
const { getByText } = render(
<Text testID="outer">
<Text testID="inner">Hello World</Text>
</Text>
);
expect(getByText('Hello World').props.testID).toBe('inner');
});

test('byText matches accross multiple texts', () => {
const { getByText } = render(
<Text testID="outer">
<Text testID="inner-1">Hello</Text> <Text testID="inner-2">World</Text>
</Text>
);
expect(getByText('Hello World').props.testID).toBe('outer');
});

type MyButtonProps = {
children: React.ReactNode;
onPress: () => void;
Expand Down Expand Up @@ -193,7 +216,7 @@ test('queryByText not found', () => {
});

test('queryByText does not match nested text across multiple <Text> in <Text>', () => {
mdjastrzebski marked this conversation as resolved.
Show resolved Hide resolved
const { queryByText } = render(
const { getByText } = render(
<Text nativeID="1">
Hello{' '}
<Text nativeID="2">
Expand All @@ -203,7 +226,7 @@ test('queryByText does not match nested text across multiple <Text> in <Text>',
</Text>
);

expect(queryByText('Hello World!')).toBe(null);
expect(getByText('Hello World!')).toBeTruthy();
});

test('queryByText with nested Text components return the closest Text', () => {
Expand Down Expand Up @@ -241,8 +264,8 @@ test('queryByText nested deep <CustomText> in <Text>', () => {
<Text>
<CustomText>Hello</CustomText> <CustomText>World!</CustomText>
</Text>
).queryByText('Hello World!')
).toBe(null);
).getByText('Hello World!')
).toBeTruthy();
});

test('queryByText with nested Text components: not-exact text match returns the most deeply nested common component', () => {
Expand Down Expand Up @@ -365,7 +388,6 @@ describe('Supports normalization', () => {
<View>
<Text>{` Text and


whitespace`}</Text>
</View>
);
Expand All @@ -376,7 +398,6 @@ describe('Supports normalization', () => {
test('trim and collapseWhitespace is customizable by getDefaultNormalizer param', () => {
const testTextWithWhitespace = ` Text and


whitespace`;
const { getByText } = render(
<View>
Expand Down
4 changes: 2 additions & 2 deletions src/queries/displayValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer';
import { TextInput } from 'react-native';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { findAll } from '../helpers/findAll';
import { matches, TextMatch } from '../matches';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -12,7 +12,7 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import type { CommonQueryOptions, TextMatchOptions } from './options';
import type { CommonQueryOptions } from './options';

type ByDisplayValueOptions = CommonQueryOptions & TextMatchOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/queries/hintText.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { findAll } from '../helpers/findAll';
import { matches, TextMatch } from '../matches';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -10,7 +10,7 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import { CommonQueryOptions, TextMatchOptions } from './options';
import { CommonQueryOptions } from './options';

type ByHintTextOptions = CommonQueryOptions & TextMatchOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/queries/labelText.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { findAll } from '../helpers/findAll';
import { matches, TextMatch } from '../matches';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -10,7 +10,7 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import { CommonQueryOptions, TextMatchOptions } from './options';
import { CommonQueryOptions } from './options';

type ByLabelTextOptions = CommonQueryOptions & TextMatchOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/queries/placeholderText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer';
import { TextInput } from 'react-native';
import { findAll } from '../helpers/findAll';
import { filterNodeByType } from '../helpers/filterNodeByType';
import { matches, TextMatch } from '../matches';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -12,7 +12,7 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import type { CommonQueryOptions, TextMatchOptions } from './options';
import type { CommonQueryOptions } from './options';

type ByPlaceholderTextOptions = CommonQueryOptions & TextMatchOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/queries/testId.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { findAll } from '../helpers/findAll';
import { matches, TextMatch } from '../matches';
import { matches, TextMatch, TextMatchOptions } from '../matches';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -10,7 +10,7 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import type { CommonQueryOptions, TextMatchOptions } from './options';
import type { CommonQueryOptions } from './options';

type ByTestIdOptions = CommonQueryOptions & TextMatchOptions;

Expand Down
Loading