mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
plaint text support
This commit is contained in:
@@ -318,6 +318,8 @@ await render(<MyComponent />);
|
||||
const element = screen.getByText('banana');
|
||||
```
|
||||
|
||||
Besides RN's `<Text>`, this query also matches `<PlainText>` from [`react-native-plain-text`](https://github.com/mdjastrzebski/react-native-plain-text), which holds its content in the `text` prop instead of string children.
|
||||
|
||||
### `*ByHintText`
|
||||
|
||||
> getByA11yHint, getAllByA11yHint, queryByA11yHint, queryAllByA11yHint, findByA11yHint, findAllByA11yHint
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ module.exports = {
|
||||
setupFilesAfterEnv: ['./jest-setup.ts'],
|
||||
testPathIgnorePatterns: ['dist/', 'examples/', 'experiments-app/', 'codemods/'],
|
||||
testTimeout: 60000,
|
||||
transformIgnorePatterns: ['/node_modules/(?!(@react-native|react-native)/).*/'],
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(@react-native|react-native|react-native-plain-text)/).*/',
|
||||
],
|
||||
snapshotSerializers: ['@relmify/jest-serializer-strip-ansi/always'],
|
||||
clearMocks: true,
|
||||
collectCoverageFrom: [
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"oxfmt": "^0.52.0",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-plain-text": "^0.8.2",
|
||||
"release-it": "^20.0.1",
|
||||
"test-renderer": "1.2.0",
|
||||
"tsx": "^4.22.3",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Image, Modal, ScrollView, Switch, Text, TextInput } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { render, screen } from '..';
|
||||
import {
|
||||
isHostImage,
|
||||
isHostModal,
|
||||
isHostPlainText,
|
||||
isHostScrollView,
|
||||
isHostSwitch,
|
||||
isHostText,
|
||||
@@ -23,6 +25,19 @@ test('detects raw RCTText component', async () => {
|
||||
expect(isHostText(screen.root)).toBe(true);
|
||||
});
|
||||
|
||||
// Plain text components, e.g. `<PlainText>` from `react-native-plain-text`,
|
||||
// hold their content in the `text` prop instead of string children.
|
||||
test('detects host plain text component', async () => {
|
||||
await render(<PlainText>Hello</PlainText>);
|
||||
expect(isHostText(screen.root)).toBe(true);
|
||||
expect(isHostPlainText(screen.root)).toBe(true);
|
||||
});
|
||||
|
||||
test('does not detect host Text component as plain text', async () => {
|
||||
await render(<Text>Hello</Text>);
|
||||
expect(isHostPlainText(screen.root)).toBe(false);
|
||||
});
|
||||
|
||||
test('detects host TextInput component', async () => {
|
||||
await render(<TextInput />);
|
||||
expect(isHostTextInput(screen.root)).toBe(true);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Image, Pressable, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../..';
|
||||
import {
|
||||
@@ -915,3 +916,44 @@ describe('computeAccessibleName', () => {
|
||||
expect(computeAccessibleName(screen.getByTestId('parent-no-text'))).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plain text elements', () => {
|
||||
test('isAccessibilityElement() returns true', async () => {
|
||||
await render(<PlainText testID="text">Hello</PlainText>);
|
||||
expect(isAccessibilityElement(screen.getByTestId('text'))).toBe(true);
|
||||
});
|
||||
|
||||
test('getRole() returns "text"', async () => {
|
||||
await render(<PlainText testID="text">Hello</PlainText>);
|
||||
expect(getRole(screen.getByTestId('text'))).toBe('text');
|
||||
});
|
||||
|
||||
test('computeAccessibleName() uses the text content', async () => {
|
||||
await render(<PlainText testID="text">Hello</PlainText>);
|
||||
expect(computeAccessibleName(screen.getByTestId('text'))).toBe('Hello');
|
||||
});
|
||||
|
||||
test('computeAccessibleName() prefers explicit accessibility label', async () => {
|
||||
await render(
|
||||
<PlainText testID="text" accessibilityLabel="Label">
|
||||
Hello
|
||||
</PlainText>,
|
||||
);
|
||||
expect(computeAccessibleName(screen.getByTestId('text'))).toBe('Label');
|
||||
});
|
||||
|
||||
test('computeAccessibleName() returns empty string for empty text', async () => {
|
||||
await render(<PlainText testID="text" />);
|
||||
expect(computeAccessibleName(screen.getByTestId('text'))).toBe('');
|
||||
});
|
||||
|
||||
test('computeAccessibleName() includes plain text children', async () => {
|
||||
await render(
|
||||
<View testID="view" accessible>
|
||||
<PlainText>Hello</PlainText>
|
||||
<PlainText text="World" />
|
||||
</View>,
|
||||
);
|
||||
expect(computeAccessibleName(screen.getByTestId('view'))).toBe('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { render, screen } from '../..';
|
||||
import { getTextContent } from '../text-content';
|
||||
@@ -48,3 +49,28 @@ test('getTextContent with multiple boolean content', async () => {
|
||||
);
|
||||
expect(getTextContent(screen.root)).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('getTextContent with plain text content', async () => {
|
||||
await render(<PlainText>Hello world</PlainText>);
|
||||
expect(getTextContent(screen.root)).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('getTextContent with plain text `text` prop', async () => {
|
||||
await render(<PlainText text="Hello world" />);
|
||||
expect(getTextContent(screen.root)).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('getTextContent with empty plain text', async () => {
|
||||
await render(<PlainText />);
|
||||
expect(getTextContent(screen.root)).toBe('');
|
||||
});
|
||||
|
||||
test('getTextContent with nested plain text content', async () => {
|
||||
await render(
|
||||
<View>
|
||||
<PlainText>Hello</PlainText>
|
||||
<Text> world</Text>
|
||||
</View>,
|
||||
);
|
||||
expect(getTextContent(screen.root)).toBe('Hello world');
|
||||
});
|
||||
|
||||
@@ -4,7 +4,13 @@ import type { TestInstance } from 'test-renderer';
|
||||
|
||||
import { getContainerInstance, getInstanceSiblings, isTestInstance } from './component-tree';
|
||||
import { findAll } from './find-all';
|
||||
import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names';
|
||||
import {
|
||||
getHostPlainTextValue,
|
||||
isHostImage,
|
||||
isHostSwitch,
|
||||
isHostText,
|
||||
isHostTextInput,
|
||||
} from './host-component-names';
|
||||
import { getTextContent } from './text-content';
|
||||
import { isEditableTextInput } from './text-input';
|
||||
|
||||
@@ -286,6 +292,12 @@ export function computeAccessibleName(
|
||||
return instance.props.placeholder;
|
||||
}
|
||||
|
||||
// Plain text host elements have no children, their content is in `text` prop.
|
||||
const plainText = getHostPlainTextValue(instance);
|
||||
if (plainText !== undefined) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
const parts: AccessibleNamePart[] = [];
|
||||
for (const child of instance.children) {
|
||||
if (typeof child === 'string') {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { TestInstance } from 'test-renderer';
|
||||
|
||||
// Host components that render RN `<Text>`, i.e. text elements holding their
|
||||
// content as string children.
|
||||
export const HOST_TEXT_NAMES = ['Text', 'RCTText'];
|
||||
|
||||
// Host components that hold their text content in the `text` prop instead of
|
||||
// string children, e.g. `<PlainText>` from `react-native-plain-text`.
|
||||
const HOST_PLAIN_TEXT_NAMES = ['RNPlainText'];
|
||||
const HOST_TEXT_INPUT_NAMES = ['TextInput'];
|
||||
const HOST_IMAGE_NAMES = ['Image'];
|
||||
const HOST_SWITCH_NAMES = ['RCTSwitch'];
|
||||
@@ -8,11 +14,40 @@ const HOST_SCROLL_VIEW_NAMES = ['RCTScrollView'];
|
||||
const HOST_MODAL_NAMES = ['Modal'];
|
||||
|
||||
/**
|
||||
* Checks if the given element is a host Text element.
|
||||
* Checks if the given element is a host plain text element, i.e. one that holds
|
||||
* its text content in the `text` prop, e.g. `<PlainText>` from
|
||||
* `react-native-plain-text`.
|
||||
* @param instance The instance to check.
|
||||
*/
|
||||
export function isHostPlainText(instance: TestInstance | null) {
|
||||
return typeof instance?.type === 'string' && HOST_PLAIN_TEXT_NAMES.includes(instance.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given element is a host text element: either a RN `<Text>`,
|
||||
* holding its content as string children, or a plain text one, holding it in
|
||||
* the `text` prop.
|
||||
* @param instance The instance to check.
|
||||
*/
|
||||
export function isHostText(instance: TestInstance | null) {
|
||||
return typeof instance?.type === 'string' && HOST_TEXT_NAMES.includes(instance.type);
|
||||
if (typeof instance?.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return HOST_TEXT_NAMES.includes(instance.type) || HOST_PLAIN_TEXT_NAMES.includes(instance.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text held directly by a host plain text element, if any.
|
||||
* @param instance The instance to read.
|
||||
*/
|
||||
export function getHostPlainTextValue(instance: TestInstance | null): string | undefined {
|
||||
if (instance == null || !isHostPlainText(instance)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { text } = instance.props;
|
||||
return typeof text === 'string' || typeof text === 'number' ? String(text) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { TestInstance } from 'test-renderer';
|
||||
|
||||
import { getHostPlainTextValue } from './host-component-names';
|
||||
|
||||
export function getTextContent(instance: TestInstance | string | null): string {
|
||||
if (!instance) {
|
||||
return '';
|
||||
@@ -9,6 +11,13 @@ export function getTextContent(instance: TestInstance | string | null): string {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Plain text host elements, e.g. `<PlainText>` from `react-native-plain-text`,
|
||||
// hold their content in the `text` prop rather than as string children.
|
||||
const plainText = getHostPlainTextValue(instance);
|
||||
if (plainText !== undefined) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
instance.children?.forEach((child) => {
|
||||
result.push(getTextContent(child));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Image, Text, TextInput, View } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { render, screen } from '../..';
|
||||
|
||||
@@ -137,3 +138,11 @@ it('toHaveAccessibleName() rejects non-host element', () => {
|
||||
Received has value: "This is not a TestInstance""
|
||||
`);
|
||||
});
|
||||
|
||||
test('toHaveAccessibleName() handles plain text element', async () => {
|
||||
await render(<PlainText testID="text">Hello</PlainText>);
|
||||
const element = screen.getByTestId('text');
|
||||
expect(element).toHaveAccessibleName('Hello');
|
||||
expect(element).toHaveAccessibleName();
|
||||
expect(element).not.toHaveAccessibleName('World');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { render, screen } from '../..';
|
||||
|
||||
@@ -80,3 +81,15 @@ test('toHaveTextContent() on null element', () => {
|
||||
Received has value: null"
|
||||
`);
|
||||
});
|
||||
|
||||
test('toHaveTextContent() supports plain text', async () => {
|
||||
await render(
|
||||
<View testID="view">
|
||||
<PlainText>Hello</PlainText>
|
||||
<PlainText text=" World" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('view')).toHaveTextContent('Hello World');
|
||||
expect(screen.getByTestId('view')).not.toHaveTextContent('Hello there');
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { render, screen } from '../..';
|
||||
|
||||
@@ -994,3 +995,11 @@ test('error message renders the element tree, preserving only helpful props', as
|
||||
/>"
|
||||
`);
|
||||
});
|
||||
|
||||
test('supports plain text elements', async () => {
|
||||
await render(<PlainText testID="text">Hello</PlainText>);
|
||||
|
||||
expect(screen.getByRole('text').props.testID).toBe('text');
|
||||
expect(screen.getByRole('text', { name: 'Hello' }).props.testID).toBe('text');
|
||||
expect(screen.queryByRole('text', { name: 'World' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Image, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { PlainText } from 'react-native-plain-text';
|
||||
|
||||
import { getDefaultNormalizer, render, screen, within } from '../..';
|
||||
|
||||
@@ -540,3 +541,25 @@ test('byText should return host component', async () => {
|
||||
await render(<Text>hello</Text>);
|
||||
expect(screen.getByText('hello').type).toBe('Text');
|
||||
});
|
||||
|
||||
test('byText matches plain text', async () => {
|
||||
await render(<PlainText testID="text">Hello World</PlainText>);
|
||||
expect(screen.getByText('Hello World').props.testID).toBe('text');
|
||||
});
|
||||
|
||||
test('byText matches plain text passed by `text` prop', async () => {
|
||||
await render(<PlainText testID="text" text="Hello World" />);
|
||||
expect(screen.getByText('Hello World').props.testID).toBe('text');
|
||||
});
|
||||
|
||||
test('byText supports text match options for plain text', async () => {
|
||||
await render(<PlainText testID="text">Hello World</PlainText>);
|
||||
expect(screen.getByText('hello world', { exact: false }).props.testID).toBe('text');
|
||||
expect(screen.getByText(/hello/i).props.testID).toBe('text');
|
||||
expect(screen.queryByText('Hello')).toBeNull();
|
||||
});
|
||||
|
||||
test('byText does not match plain text without content', async () => {
|
||||
await render(<PlainText testID="text" />);
|
||||
expect(screen.queryByText('Hello World')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -322,6 +322,8 @@ await render(<MyComponent />);
|
||||
const element = screen.getByText('banana');
|
||||
```
|
||||
|
||||
Besides RN's `<Text>`, this query also matches `<PlainText>` from [`react-native-plain-text`](https://github.com/mdjastrzebski/react-native-plain-text), which holds its content in the `text` prop instead of string children.
|
||||
|
||||
### `*ByHintText` {#by-hint-text}
|
||||
|
||||
> getByA11yHint, getAllByA11yHint, queryByA11yHint, queryAllByA11yHint, findByA11yHint, findAllByA11yHint
|
||||
|
||||
@@ -3259,6 +3259,7 @@ __metadata:
|
||||
pretty-format: "npm:^30.4.1"
|
||||
react: "npm:19.2.3"
|
||||
react-native: "npm:0.85.3"
|
||||
react-native-plain-text: "npm:^0.8.2"
|
||||
redent: "npm:^3.0.0"
|
||||
release-it: "npm:^20.0.1"
|
||||
test-renderer: "npm:1.2.0"
|
||||
@@ -9454,6 +9455,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-plain-text@npm:^0.8.2":
|
||||
version: 0.8.2
|
||||
resolution: "react-native-plain-text@npm:0.8.2"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10c0/1be4bbd466fa6326b87c7605a3def1ab88748e79fb0f6c66a670166b18770f6d0c7d069afd86dfab1bcc2772d3de99644a99f06421f3d8c3f52a846fe84d1db2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native@npm:0.85.3":
|
||||
version: 0.85.3
|
||||
resolution: "react-native@npm:0.85.3"
|
||||
|
||||
Reference in New Issue
Block a user