Skip to content
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
44 changes: 22 additions & 22 deletions skills/react-native-testing/references/api-reference-v14.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Complete API reference for `@testing-library/react-native` v14.x (React 19+).

**Test renderer:** `test-renderer` (not `react-test-renderer`)
**Element type:** `HostElement` (not `ReactTestInstance`)
**Element type:** `TestInstance` (not `ReactTestInstance`)

## Table of Contents

Expand Down Expand Up @@ -106,8 +106,8 @@ let screen: {
unmount(): Promise<void>; // async
debug(options?: { message?: string; mapProps?: MapPropsFunction }): void;
toJSON(): RendererJSON | null;
container: HostElement; // safe root host element
root: HostElement; // root host element
container: TestInstance; // safe root host element
root: TestInstance; // root host element
};
```

Expand All @@ -132,14 +132,14 @@ Each query = **variant** + **predicate** (e.g., `getByRole` = `getBy` + `ByRole`

### Query Variants

| Variant | Assertion | Return Type | Async |
| ------------- | ------------------ | ------------------------------------ | ----- |
| `getBy*` | Exactly one match | `HostElement` (throws if 0 or >1) | No |
| `getAllBy*` | At least one match | `HostElement[]` (throws if 0) | No |
| `queryBy*` | Zero or one match | `HostElement \| null` (throws if >1) | No |
| `queryAllBy*` | No assertion | `HostElement[]` (empty if 0) | No |
| `findBy*` | Exactly one match | `Promise<HostElement>` | Yes |
| `findAllBy*` | At least one match | `Promise<HostElement[]>` | Yes |
| Variant | Assertion | Return Type | Async |
| ------------- | ------------------ | ------------------------------------- | ----- |
| `getBy*` | Exactly one match | `TestInstance` (throws if 0 or >1) | No |
| `getAllBy*` | At least one match | `TestInstance[]` (throws if 0) | No |
| `queryBy*` | Zero or one match | `TestInstance \| null` (throws if >1) | No |
| `queryAllBy*` | No assertion | `TestInstance[]` (empty if 0) | No |
| `findBy*` | Exactly one match | `Promise<TestInstance>` | Yes |
| `findAllBy*` | At least one match | `Promise<TestInstance[]>` | Yes |

`findBy*` / `findAllBy*` accept optional `waitForOptions: { timeout?, interval?, onTimeout? }`.

Expand All @@ -157,7 +157,7 @@ getByRole(role: TextMatch, options?: {
expanded?: boolean;
value?: { min?: number; max?: number; now?: number; text?: TextMatch };
includeHiddenElements?: boolean;
}): HostElement;
}): TestInstance;
```

Matches elements by `role` or `accessibilityRole`. Element must be an accessibility element:
Expand All @@ -177,47 +177,47 @@ screen.getByRole('slider', { value: { now: 50, min: 0, max: 100 } });
#### `*ByLabelText`

```ts
getByLabelText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByLabelText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches by `aria-label`/`accessibilityLabel` or text content of element referenced by `aria-labelledby`/`accessibilityLabelledBy`.

#### `*ByPlaceholderText`

```ts
getByPlaceholderText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByPlaceholderText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches `TextInput` by `placeholder` prop.

#### `*ByText`

```ts
getByText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByText(text: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches by text content. Joins `<Text>` siblings to find matches (like RN runtime).

#### `*ByDisplayValue`

```ts
getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByDisplayValue(value: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches `TextInput` by current display value.

#### `*ByHintText`

```ts
getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByHintText(hint: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches by `accessibilityHint` prop. Also available as `getByA11yHint` / `getByAccessibilityHint`.

#### `*ByTestId` (last resort)

```ts
getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): HostElement;
getByTestId(testId: TextMatch, options?: { exact?: boolean; normalizer?: Function; includeHiddenElements?: boolean }): TestInstance;
```

Matches by `testID` prop. Use only when other queries don't work.
Expand Down Expand Up @@ -326,7 +326,7 @@ Use when `userEvent` doesn't support the event or when triggering events on comp

```ts
async function fireEvent(
element: HostElement,
instance: TestInstance,
eventName: string,
...data: unknown[]
): Promise<void>;
Expand Down Expand Up @@ -361,7 +361,7 @@ Available automatically with any `@testing-library/react-native` import. No setu
| Matcher | Signature | Description |
| --------------------- | ------------------------------------------------------------- | --------------------------- |
| `toHaveTextContent()` | `(text: string \| RegExp, options?: { exact?, normalizer? })` | Text content match |
| `toContainElement()` | `(element: HostElement \| null)` | Contains child element |
| `toContainElement()` | `(instance: TestInstance \| null)` | Contains child element |
| `toBeEmptyElement()` | — | No children or text content |

### Element State
Expand Down Expand Up @@ -430,7 +430,7 @@ Waits until the queried element is removed. Element must be initially present.
### `within`

```ts
function within(element: HostElement): Queries;
function within(instance: TestInstance): Queries;
```

Scoped queries on a subtree. Useful for querying within a single `FlatList` item or a specific screen.
Expand Down Expand Up @@ -533,7 +533,7 @@ Note: `concurrentRoot` option is removed (always on). `unstable_validateStringsR
### `isHiddenFromAccessibility`

```ts
function isHiddenFromAccessibility(element: HostElement | null): boolean;
function isHiddenFromAccessibility(instance: TestInstance | null): boolean;
```

Also available as `isInaccessible()` alias.
Expand Down
18 changes: 9 additions & 9 deletions src/__tests__/fire-event.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('fireEvent.changeText', () => {
const input = screen.getByTestId('input');
await fireEvent.changeText(input, 'new text');
expect(onChangeText).toHaveBeenCalledWith('new text');
expect(nativeState.valueForElement.get(input)).toBe('new text');
expect(nativeState.valueForInstance.get(input)).toBe('new text');
});

test('does not fire on non-editable TextInput', async () => {
Expand All @@ -170,7 +170,7 @@ describe('fireEvent.changeText', () => {
const input = screen.getByTestId('input');
await fireEvent.changeText(input, 'new text');
expect(onChangeText).not.toHaveBeenCalled();
expect(nativeState.valueForElement.get(input)).toBeUndefined();
expect(nativeState.valueForInstance.get(input)).toBeUndefined();
});
});

Expand Down Expand Up @@ -286,7 +286,7 @@ describe('fireEvent.scroll', () => {
const scrollView = screen.getByTestId('scroll');
await fireEvent.scroll(scrollView, verticalScrollEvent);
expect(onScroll.mock.calls[0][0]).toMatchObject(verticalScrollEvent);
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 0, y: 200 });
});

test.each([
Expand All @@ -301,7 +301,7 @@ describe('fireEvent.scroll', () => {
const scrollView = screen.getByTestId('scroll');
await fireEvent(scrollView, eventName, verticalScrollEvent);
expect(handler).toHaveBeenCalledWith(verticalScrollEvent);
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 0, y: 200 });
});

test('without contentOffset scrolls to (0, 0)', async () => {
Expand All @@ -316,7 +316,7 @@ describe('fireEvent.scroll', () => {
expect(onScroll.mock.calls[0][0]).toMatchObject({
nativeEvent: { contentOffset: { x: 0, y: 0 } },
});
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 0, y: 0 });
});

test('with non-finite contentOffset values uses 0', async () => {
Expand All @@ -331,7 +331,7 @@ describe('fireEvent.scroll', () => {
nativeEvent: { contentOffset: { y: Infinity } },
});
expect(onScroll).toHaveBeenCalled();
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 0, y: 0 });
});

test('with horizontal scroll updates native state', async () => {
Expand All @@ -344,7 +344,7 @@ describe('fireEvent.scroll', () => {
const scrollView = screen.getByTestId('scroll');
await fireEvent.scroll(scrollView, horizontalScrollEvent);
expect(onScroll.mock.calls[0][0]).toMatchObject(horizontalScrollEvent);
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 0 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 50, y: 0 });
});

test('without contentOffset via fireEvent() does not update native state', async () => {
Expand All @@ -357,7 +357,7 @@ describe('fireEvent.scroll', () => {
const scrollView = screen.getByTestId('scroll');
await fireEvent(scrollView, 'scroll', { nativeEvent: {} });
expect(onScroll).toHaveBeenCalled();
expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined();
expect(nativeState.contentOffsetForInstance.get(scrollView)).toBeUndefined();
});

test('with non-finite x contentOffset value uses 0', async () => {
Expand All @@ -372,7 +372,7 @@ describe('fireEvent.scroll', () => {
nativeEvent: { contentOffset: { x: Infinity } },
});
expect(onScroll).toHaveBeenCalled();
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({ x: 0, y: 0 });
});
});

Expand Down
60 changes: 30 additions & 30 deletions src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import type {
TextProps,
ViewProps,
} from 'react-native';
import type { Fiber, HostElement } from 'test-renderer';
import type { Fiber, TestInstance } from 'test-renderer';

import { act } from './act';
import { buildScrollEvent, buildTouchEvent } from './event-builder';
import type { EventHandler } from './event-handler';
import { getEventHandlerFromProps } from './event-handler';
import { isElementMounted } from './helpers/component-tree';
import { isInstanceMounted } from './helpers/component-tree';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isEditableTextInput } from './helpers/text-input';
import { nativeState } from './native-state';
import type { Point, StringWithAutocomplete } from './types';

function isTouchResponder(element: HostElement) {
return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element);
function isTouchResponder(instance: TestInstance) {
return Boolean(instance.props.onStartShouldSetResponder) || isHostTextInput(instance);
}

/**
Expand All @@ -46,9 +46,9 @@ const textInputEventsIgnoringEditableProp = new Set([
]);

function isEventEnabled(
element: HostElement,
instance: TestInstance,
eventName: string,
nearestTouchResponder?: HostElement,
nearestTouchResponder?: TestInstance,
) {
if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) {
return (
Expand All @@ -57,7 +57,7 @@ function isEventEnabled(
);
}

if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(element)) {
if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(instance)) {
return false;
}

Expand All @@ -71,24 +71,24 @@ function isEventEnabled(
}

function findEventHandler(
element: HostElement,
instance: TestInstance,
eventName: string,
nearestTouchResponder?: HostElement,
nearestTouchResponder?: TestInstance,
): EventHandler | null {
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
const touchResponder = isTouchResponder(instance) ? instance : nearestTouchResponder;

const handler =
getEventHandlerFromProps(element.props, eventName, { loose: true }) ??
findEventHandlerFromFiber(element.unstable_fiber, eventName);
if (handler && isEventEnabled(element, eventName, touchResponder)) {
getEventHandlerFromProps(instance.props, eventName, { loose: true }) ??
findEventHandlerFromFiber(instance.unstable_fiber, eventName);
if (handler && isEventEnabled(instance, eventName, touchResponder)) {
return handler;
}

if (element.parent === null) {
if (instance.parent === null) {
return null;
}

return findEventHandler(element.parent, eventName, touchResponder);
return findEventHandler(instance.parent, eventName, touchResponder);
}

function findEventHandlerFromFiber(fiber: Fiber | null, eventName: string): EventHandler | null {
Expand Down Expand Up @@ -123,14 +123,14 @@ type EventName = StringWithAutocomplete<
| EventNameExtractor<ScrollViewProps>
>;

async function fireEvent(element: HostElement, eventName: EventName, ...data: unknown[]) {
if (!isElementMounted(element)) {
async function fireEvent(instance: TestInstance, eventName: EventName, ...data: unknown[]) {
if (!isInstanceMounted(instance)) {
return;
}

setNativeStateIfNeeded(element, eventName, data[0]);
setNativeStateIfNeeded(instance, eventName, data[0]);

const handler = findEventHandler(element, eventName);
const handler = findEventHandler(instance, eventName);
if (!handler) {
return;
}
Expand All @@ -145,25 +145,25 @@ async function fireEvent(element: HostElement, eventName: EventName, ...data: un

type EventProps = Record<string, unknown>;

fireEvent.changeText = async (element: HostElement, text: string) =>
await fireEvent(element, 'changeText', text);
fireEvent.changeText = async (instance: TestInstance, text: string) =>
await fireEvent(instance, 'changeText', text);

fireEvent.press = async (element: HostElement, eventProps?: EventProps) => {
fireEvent.press = async (instance: TestInstance, eventProps?: EventProps) => {
const event = buildTouchEvent();
if (eventProps) {
mergeEventProps(event, eventProps);
}

await fireEvent(element, 'press', event);
await fireEvent(instance, 'press', event);
};

fireEvent.scroll = async (element: HostElement, eventProps?: EventProps) => {
fireEvent.scroll = async (instance: TestInstance, eventProps?: EventProps) => {
const event = buildScrollEvent();
if (eventProps) {
mergeEventProps(event, eventProps);
}

await fireEvent(element, 'scroll', event);
await fireEvent(instance, 'scroll', event);
};

export { fireEvent };
Expand All @@ -176,15 +176,15 @@ const scrollEventNames = new Set([
'momentumScrollEnd',
]);

function setNativeStateIfNeeded(element: HostElement, eventName: string, value: unknown) {
if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(element)) {
nativeState.valueForElement.set(element, value);
function setNativeStateIfNeeded(instance: TestInstance, eventName: string, value: unknown) {
if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(instance)) {
nativeState.valueForInstance.set(instance, value);
}

if (scrollEventNames.has(eventName) && isHostScrollView(element)) {
if (scrollEventNames.has(eventName) && isHostScrollView(instance)) {
const contentOffset = tryGetContentOffset(value);
if (contentOffset) {
nativeState.contentOffsetForElement.set(element, contentOffset);
nativeState.contentOffsetForInstance.set(instance, contentOffset);
}
}
}
Expand Down
Loading
Loading