API reference
Everything below is exported from @ng-native/testing, except the runner integrations, which have
entry points of their own: @ng-native/testing/vitest and @ng-native/testing/register. The
signatures are the declarations in the source, shortened only where a type is spelled out
elsewhere on this page.
render()
function render<T>(
component: Type<T> | string,
options?: RenderOptions<T>,
): Promise<RenderResult<T>>;Mounts a component class, or a template string, onto a fresh fake Fabric with mount() from
@ng-native/platform, and resolves once the first frame has been committed and one more task has
passed. Each call gets its own fake, and becomes what screen queries.
A string is compiled just in time as the template of a host component, so the template form needs
@angular/compiler installed; a class is used as compiled. Anything the component throws while
it is created or first checked, such as NG0950 for a required input nobody set, rejects the
promise.
RenderOptions<T>
| Option | Type | Default | What it does |
|---|---|---|---|
inputs |
Record<string, unknown> |
none | Inputs, set with setInput before the first change detection pass. The only way to give an input.required() a value. Class form only. |
on |
Record<string, (value) => void> |
none | Output handlers, by output name, subscribed after the component is created. Class form only. |
providers |
(Provider | EnvironmentProviders)[] |
none | Providers for the app the component is mounted in: provideNativeRouter, provideNativeHttpClient, a stand-in for a service. |
imports |
unknown[] |
none | What the template may use. Template form only. |
componentProperties |
Partial<T> & Record<string, unknown> |
none | Fields assigned to the instance. On the template form's host, before its first pass; on a class, after it, followed by another pass. |
globalStyles |
StyleSheet | null |
none | A compiled global stylesheet, as compileCss from @ng-native/metro/css/compile.cjs returns it. What Tailwind classes resolve against. |
conditions |
Conditions |
none | What @media queries resolve against: width, height, colour scheme. With none, every media query is false. |
tokens |
Record<string, TokenValue> |
none | Values only a device knows, such as the hairline width. |
processColor |
(value: string | number) => unknown |
none | React Native's processColor. With none, colours reach the props as the strings the stylesheet wrote, which is what a test usually wants to assert. |
resolveAssetSource |
(value: unknown) => unknown |
none | React Native's Image.resolveAssetSource. |
dev, now and onDirty are the engine's own options too, and pass through unchanged.
RenderResult<T>
Every query in the matrix, bound to this render, plus:
| Member | Type | What it does |
|---|---|---|
fabric |
FakeFabric |
The fake this render committed to. See createFakeFabric(). |
componentRef |
ComponentRef<T> |
The rendered component. componentRef.injector reaches anything the app provides. |
instance |
T |
The component instance, or the template form's host. |
fixture |
{ componentRef, componentInstance, detectChanges() } |
The part of ComponentFixture that has a meaning here. |
rerender(changes) |
(changes?: { inputs?, componentProperties? }) => Promise<void> |
Sets each input given with setInput, assigns each property, and runs change detection. Inputs not named keep their values. |
detectChanges() |
() => Promise<void> |
Marks the component for check, runs change detection, and waits a task for the commit. For a plain field changed from the test. |
debug(node?) |
(node?: FakeFabricNode) => void |
Prints the committed tree, or one node's subtree, as screen.debug does. |
unmount() |
() => void |
Destroys the component and the app it was mounted in. screen falls back to the render before it. Calling it twice does nothing. |
A render's own queries keep querying that render after a later one; screen moves to the latest.
screen
const screen: BoundQueries & { debug(node?: FakeFabricNode): void };Every query, over whatever was rendered most recently and has not been unmounted. A query on
screen with nothing rendered throws Nothing is rendered: call render() first.
screen.debug(node?) prints the tree with console.log: one line per node, its Fabric view name,
the text of a RawText, and whichever of testID, nativeID, accessibilityRole,
accessibilityLabel, placeholder and a text field's text (printed as value) it has. That
is the same tree a failing getBy* query puts in its error.
within()
function within(node: FakeFabricNode): BoundQueries;Every query, scoped to node and everything under it, in whichever commit is current. The node
itself is included, so within(screen.getByRole('alert')).getByText('Email is required') finds the
alert's own text. The scope follows the node by its tag, so it stays valid across commits, and
matches nothing once the node has gone.
Queries
Each of the six queries comes in six variants. ...args is the query's own arguments, below.
| Variant | Returns | No match | One match | More than one |
|---|---|---|---|---|
getBy*(...args) |
FakeFabricNode |
throws | the node | throws |
getAllBy*(...args) |
FakeFabricNode[] |
throws | [node] |
every node |
queryBy*(...args) |
FakeFabricNode | null |
null |
the node | throws |
queryAllBy*(...args) |
FakeFabricNode[] |
[] |
[node] |
every node |
findBy*(...args, waitForOptions?) |
Promise<FakeFabricNode> |
retries, then rejects | the node | retries, then rejects |
findAllBy*(...args, waitForOptions?) |
Promise<FakeFabricNode[]> |
retries, then rejects | [node] |
every node |
findBy* is getBy* inside waitFor: it retries until the query would succeed and
rejects with the query's own error once the timeout passes. Its waitForOptions is the third
argument, after the query's options, so a query that takes none passes undefined:
screen.findByText('Going soon', undefined, { timeout: 60, interval: 10 }).
Every error says what was looked for and prints the tree. With none:
Unable to find a node with text "Bananas".. With more than one:
Found 2 nodes with role "listitem", and expected one. Use getAllByRole if more than one is expected.
| Query | Arguments | Matches a node when |
|---|---|---|
ByRole |
(role, { name?, exact? }) |
accessibilityRole is role; and, given name, the node's accessible name matches it: its accessibilityLabel, or its text content when it has none, as a screen reader reads it. |
ByText |
(text, { exact? }) |
It is a Paragraph and its text content matches. |
ByTestId |
(testId, { exact? }) |
testID or nativeID matches. |
ByLabelText |
(label, { exact? }) |
accessibilityLabel matches. |
ByPlaceholderText |
(placeholder, { exact? }) |
placeholder matches. |
ByDisplayValue |
(value, { exact? }) |
It is a text field and its text, the value <text-input> binds its model to, matches. |
Every query reads the props on the committed nodes, which are the props native was sent: what a
component writes for testID, id (as nativeID), accessibilityRole or role (as
accessibilityRole), accessibilityLabel and placeholder. A control that sets its own role
by default commits one without you writing it, the way <switch> commits accessibilityRole: 'switch'; anything else takes [accessibilityRole] directly.
Text content is every RawText under the node, in order, joined with nothing between them.
<text>and <text>plums</text></text> commits as one Paragraph holding a RawText and a nested
Text span, and its text content is and plums. Only the Paragraph is a ByText match; the
span is part of its text, not a match of its own.
Matching follows Testing Library's rules:
- Both sides are normalised first: whitespace runs collapse to one space, and the ends are trimmed.
getByText(' Pears ')finds a paragraph holdingPears. - A string matches the whole value, case-sensitively, by default.
{ exact: false }matches a case-insensitive substring instead:getByText('apple', { exact: false })findsApples.- A
RegExpis tested against the normalised value as given, so/pears/iis case-insensitive and/^and plums$/is anchored. - In
ByRole, the role itself is always an exact match, andexactapplies toname.
Hidden elements are left out, as React Native Testing Library leaves them out by default: a view
with display: none, accessibilityElementsHidden, aria-hidden or importantForAccessibility: 'no-hide-descendants' hides itself and everything under it. { includeHiddenElements: true } finds
them as well. A node scrolled off screen is not hidden; the fake has no layout to say it is. A
<virtual-list> keeps a few recycled rows hidden this way, still showing their last item, so a
query does not find those either.
Interactions
Every interaction delivers its events through the handler the renderer registered with Fabric, the
one the C++ side calls on a device, so they take the same route a real event does: a touch goes
through the engine's responder negotiation and every other event bubbles from the target up. Every
event payload carries target, the node's React tag. Each interaction resolves after the tree has
had a task to commit.
An interaction on a node found by a query goes to the render that node came from. A node held from an earlier commit still works: events find their target by its tag, which does not change.
fireEvent
function fireEvent(node: FakeFabricNode, name: string, payload?: unknown): Promise<void>;
fireEvent.press(node: FakeFabricNode): Promise<void>;
fireEvent.changeText(node: FakeFabricNode, text: string): Promise<void>;
fireEvent.scroll(node: FakeFabricNode, payload?: EventPayload): Promise<void>;
fireEvent.focus(node: FakeFabricNode): Promise<void>;
fireEvent.blur(node: FakeFabricNode): Promise<void>;One event, or the one pair a press is, then a task for the commit. fireEvent(node, name) with
press, changeText, scroll, focus or blur is the matching method; any other name is sent
as a top-level event, with top and a capital letter put in front unless the name has them:
fireEvent(field, 'submitEditing', { text: 'x' }) sends topSubmitEditing. A payload is the native
event itself, or an event object with it under nativeEvent, as React Native Testing Library
passes one: fireEvent.scroll(node, { nativeEvent: { contentOffset: { x: 0, y: 120 } } }).
fireEvent.changeText goes to the first TextInput at or under node, so a query for the view
wrapping a field reaches the field. The navigation bar's search field (RNSSearchBar, from a
<native-header-item type="searchBar">) counts as one too, for userEvent.type as well. With
neither there it rejects with No TextInput or search bar at or under this <view name>.
userEvent
const userEvent: UserEvent & { setup(): UserEvent };
interface UserEvent {
press(node: FakeFabricNode): Promise<void>;
longPress(node: FakeFabricNode, options?: { duration?: number }): Promise<void>;
type(node: FakeFabricNode, text: string, options?: TypeOptions): Promise<void>;
clear(node: FakeFabricNode): Promise<void>;
}
interface TypeOptions {
skipBlur?: boolean;
submitEditing?: boolean;
}What a person does, as the several events the platform sends, a task apart. userEvent.setup()
returns the same four methods, and they can be called on userEvent directly too. longPress
holds for duration milliseconds, 500 by default, which is React Native's long-press delay.
type and clear go to the text field at or under node, and do nothing at all if it has
editable set to false: a disabled or read-only field takes no input.
Event sequences
touch below is { identifier: 0, target, pageX: 0, pageY: 0, locationX: 0, locationY: 0, timestamp }.
A "task" is one setTimeout(0), which is when change detection runs and commits.
| Call | Events, in order |
|---|---|
fireEvent.press(node) |
topTouchStart { ...touch, touches: [touch], changedTouches: [touch] }, topTouchEnd { ...touch, touches: [], changedTouches: [touch] }, both in the same task; a task. |
fireEvent.changeText(node, text) |
topChange { text, eventCount } to the field; a task. eventCount is one more than the field's committed mostRecentEventCount, which is what native would send next. |
fireEvent.scroll(node, payload) |
topScroll with the payload; a task. |
fireEvent.focus(node) |
topFocus; a task. |
fireEvent.blur(node) |
topBlur; a task. |
fireEvent(node, name, payload) |
top + Name with the payload; a task. |
userEvent.press(node) |
topTouchStart; a task; topTouchEnd; a task. |
userEvent.longPress(node, { duration }) |
topTouchStart; a task; duration ms; topTouchEnd; a task. |
userEvent.type(node, text, options) |
topFocus; a task; then for each character, topKeyPress { key } and topChange { text, eventCount } with the text so far, and a task; then topSubmitEditing { text } if submitEditing; then, unless skipBlur, topEndEditing { text } and topBlur; a task. |
userEvent.clear(node) |
topFocus, topChange { text: '', eventCount }; a task; topEndEditing { text: '' }, topBlur; a task. |
Typing starts from the field's committed text, so it appends to what is already there. A field
with maxLength stops emitting past it, the way native truncates before onChangeText ever fires
rather than delivering the extra characters for a handler to reject. The topBlur at the end of
type is what marks a Signal Forms field touched.
gestureOf
gestureOf(node: FakeFabricNode, kind?: string): TestGestureThe gesture a view was given with [gesture], or, with kind ('Pan', 'Tap', 'LongPress'
and the other builder names), the gesture of that kind inside it, however deeply it is composed
with Gesture.Race, Gesture.Exclusive or Gesture.Simultaneous. Its callbacks are the ones
the component gave it, by name, to call in place of a finger:
gestureOf(row, 'Pan').callbacks['onEnd']!({ translationX: -200 }). Throws when the view has no
gesture, or none of that kind.
Waiting
waitFor
function waitFor<T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T>;
interface WaitForOptions {
timeout?: number; // 1000
interval?: number; // 50
}Calls callback until it returns without throwing, and resolves to what it returned. Between
attempts it waits interval milliseconds on a real timer, which is also what lets change detection
commit. Once timeout has passed, it rejects with the callback's last error, so a failed waitFor
reads like the assertion inside it. A fake clock stops it: advance the clock yourself instead.
waitForElementToBeRemoved
function waitForElementToBeRemoved(
target:
| FakeFabricNode
| readonly FakeFabricNode[]
| (() => FakeFabricNode | readonly FakeFabricNode[] | null | undefined),
options?: WaitForOptions,
): Promise<void>;Resolves once the node, the nodes, or whatever the callback returns have left the committed tree.
A callback that throws or returns null counts as removed. It rejects at once if the target is
already gone when it starts, as Testing Library's does, because a wait for something that is
already absent passes for the wrong reason.
settle
function settle(): Promise<void>;One task: long enough for zoneless change detection to run and commit whatever is pending. Every
interaction above ends with it. It is exported for a test that changes state some other way, a
signal set on instance for example, and then wants to read the tree.
cleanup
function cleanup(): void;Unmounts everything rendered so far. It runs after every test by itself when afterEach is a
global, which it is in Vitest with globals: true; with node:test or Vitest's default, call it
from an afterEach of your own if a test should not leave its render mounted. Every render has its
own fake, so a test that skips cleanup still cannot see another test's tree.
createFakeFabric()
function createFakeFabric(): FakeFabric;The fake nativeFabricUIManager itself, for a test that mounts with mount() directly rather than
through render(). It records every call instead of drawing, and adds what a test needs:
| Member | What it is |
|---|---|
committed |
The root nodes of the most recent completeRoot. Each FakeFabricNode has viewName, props, children, reactTag, handle and instanceHandle. |
render(options?) |
The committed tree as an indented string of view names and text. { props: true } adds every node's full prop payload. |
find(viewName) |
The first committed node with that view name. |
emit(node, type, nativeEvent?) |
Calls the registered event handler, as the C++ side does. Everything in Interactions is built on it. |
frames |
measureInWindow's answers, by nativeID and then by view name. A node with no entry is not measured, as on a platform that has not laid it out. |
responderCalls |
Every setIsJSResponder call, in order. |
commands |
Every dispatchCommand, in order: focus, blur, setTextAndSelection and the rest. |
calls |
How many of each node operation the renderer made. reset() zeroes them. |
@ng-native/testing/vitest
function ngNative(options?: { inline?: (string | RegExp)[] }): Plugin;The Vite plugin for Vitest. It compiles every decorated .ts file with @ng-native/metro's AOT
transform and links every partial-compiled package; tells Vitest to process @angular/*,
@ng-native/* and @ng-icons/* itself rather than hand them to Node, plus anything in inline;
adds a setup file that installs the animation globals; and turns off Vitest's injectCjsGlobals.
Setup says why each is needed.
@ng-native/testing/register
node --import @ng-native/testing/register --testInstalls the animation globals and registers a Node module hook that does what the Vitest plugin
does, for node:test or any other script run under Node. It also strips types from every .ts
file under node_modules, which Node refuses to. @ng-native/testing/loader is the hook on its
own, for a suite that registers it with module.register() and passes
{ data: { skip: string[] } }: URL fragments whose TypeScript is left to Node untouched.