Skip to content

Commit 1c2b751

Browse files
refactor: rename to TestInstance (#43)
1 parent 45cfa8d commit 1c2b751

11 files changed

Lines changed: 103 additions & 80 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Project Overview
44

5-
**Test Renderer for React** is a lightweight, pure JavaScript testing library for React 19. It replaces the deprecated `react-test-renderer`. Built with `react-reconciler`, it outputs to a lightweight, traversable object structure (`HostElement`) for snapshot testing and asserting component output without a browser environment (DOM) or native dependencies.
5+
**Test Renderer for React** is a lightweight, pure JavaScript testing library for React 19. It replaces the deprecated `react-test-renderer`. Built with `react-reconciler`, it outputs to a lightweight, traversable object structure (`TestInstance`) for snapshot testing and asserting component output without a browser environment (DOM) or native dependencies.
66

77
### Key Features
88

@@ -16,8 +16,8 @@
1616
- **`src/index.ts`**: The public entry point that exports `createRoot` from `renderer.ts`.
1717
- **`src/renderer.ts`**: Contains the main implementation. Exports `createRoot` which initializes the custom React reconciler.
1818
- **`src/reconciler.ts`**: Implements the `react-reconciler` host config, translating React updates into operations on the internal tree.
19-
- **`src/host-element.ts`**: Defines `HostElement`, a wrapper around the internal fiber nodes with a DOM-like API (e.g., `children`, `props`, `parent`).
20-
- **`src/render-to-json.ts`**: Handles the serialization of `HostElement` trees into JSON format for snapshots.
19+
- **`src/test-instance.ts`**: Defines `TestInstance`, a wrapper around the internal fiber nodes with a DOM-like API (e.g., `children`, `props`, `parent`).
20+
- **`src/render-to-json.ts`**: Handles the serialization of `TestInstance` trees into JSON format for snapshots.
2121

2222
## Building and Running
2323

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,26 @@ This library supports all modern React features including:
4444
- Error boundaries
4545
- Suspense boundaries
4646

47+
## Test Output Tree
48+
49+
Instead of producing a DOM tree or a native view hierarchy, the renderer builds an in-memory **Test Output Tree**:
50+
51+
- Composed of **`TestNode`s**, where each node is either:
52+
- A **`TestInstance`** — represents a host element such as `div` or `View`
53+
- A plain **`string`** — represents a text node
54+
- The root is accessible via `root.container`, a `TestInstance` whose `type` is an empty string
55+
- `TestInstance` nodes are traversable and queryable — see the [`TestInstance`](#testelement) API below
56+
57+
## JSON Output Tree
58+
59+
Calling `toJSON()` on a `TestInstance` produces a **JSON Output Tree** — a static, plain-object snapshot of the Test Output Tree at that point in time:
60+
61+
- Composed of **`JsonNode`s**, where each node is either:
62+
- A **`JsonElement`** — a plain object with `type`, `props`, and `children`
63+
- A plain **`string`** — a text node
64+
- Contains no live references, making it safe to serialize
65+
- Ideal for snapshot testing
66+
4767
## API Reference
4868

4969
### `createRoot(options?)`
@@ -58,7 +78,7 @@ Creates a new test renderer root instance.
5878

5979
- `render(element: ReactElement)`: Renders a React element into the root. Must be called within `act()`.
6080
- `unmount()`: Unmounts the root and cleans up. Must be called within `act()`.
61-
- `container`: A `HostElement` wrapper that contains the rendered element(s). Use this to query and inspect the rendered tree.
81+
- `container`: A `TestInstance` wrapper that contains the rendered element(s). Use this to query and inspect the rendered tree.
6282

6383
**Example:**
6484

@@ -84,22 +104,22 @@ Configuration options for the test renderer. Many of these options correspond to
84104
| `onUncaughtError` | `(error: unknown, errorInfo: { componentStack?: string }) => void` | Callback called when an error is thrown and not caught by an Error Boundary. Called with the error that was thrown and an errorInfo object containing the component stack. |
85105
| `onRecoverableError` | `(error: unknown, errorInfo: { componentStack?: string }) => void` | Callback called when React automatically recovers from errors. Called with an error React throws and an errorInfo object containing the component stack. Some recoverable errors may include the original error cause as `error.cause`. |
86106

87-
### `HostElement`
107+
### `TestInstance` {#test-instance}
88108

89109
A wrapper around rendered host elements with a DOM-like API for querying and inspecting the rendered tree.
90110

91111
**Properties:**
92112

93113
- `type: string`: The element type (e.g., `"View"`, `"div"`). Returns an empty string for the container element.
94-
- `props: HostElementProps`: The element's props object.
114+
- `props: Record<string, all>`: The element's props object.
95115
- `children: HostNode[]`: Array of child nodes (elements and text strings). Hidden children are excluded.
96-
- `parent: HostElement | null`: The parent element, or `null` if this is the root container.
116+
- `parent: TestInstance | null`: The parent element, or `null` if this is the root container.
97117
- `unstable_fiber: Fiber | null`: Access to the underlying React Fiber node. **Warning:** This is an unstable API that exposes internal React Reconciler structures which may change without warning in future React versions. Use with caution and only when absolutely necessary.
98118

99119
**Methods:**
100120

101121
- `toJSON(): JsonElement | null`: Converts this element to a JSON representation suitable for snapshots. Returns `null` if the element is hidden.
102-
- `queryAll(predicate: (element: HostElement) => boolean, options?: QueryOptions): HostElement[]`: Finds all descendant elements matching the predicate. See [Query Options](#query-options) below.
122+
- `queryAll(predicate: (instance: TestInstance) => boolean, options?: QueryOptions): TestInstance[]`: Finds all descendant elements matching the predicate. See [Query Options](#query-options) below.
103123

104124
**Example:**
105125

@@ -109,7 +129,7 @@ await act(async () => {
109129
renderer.render(<div className="container">Hello</div>);
110130
});
111131

112-
const root = renderer.container.children[0] as HostElement;
132+
const root = renderer.container.children[0] as TestInstance;
113133
expect(root.type).toBe("div");
114134
expect(root.props.className).toBe("container");
115135
expect(root.children).toContain("Hello");
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { beforeEach, expect, jest, test } from "@jest/globals";
22

3-
import type { HostElement } from "../host-element";
43
import { createRoot } from "../renderer";
4+
import type { TestInstance } from "../test-instance";
55
import { ReactWorkTag } from "../test-utils/react-constants";
6-
import { getRootElement, renderWithAct } from "../test-utils/render";
6+
import { getRootInstance, renderWithAct } from "../test-utils/render";
77

88
beforeEach(() => {
99
global.IS_REACT_ACT_ENVIRONMENT = true;
@@ -13,7 +13,7 @@ test("container is root's parent", async () => {
1313
const renderer = createRoot();
1414
await renderWithAct(renderer, <div>Hello!</div>);
1515

16-
const root = getRootElement(renderer);
16+
const root = getRootInstance(renderer);
1717
expect(root).toBeTruthy();
1818
expect(root.parent).toBe(renderer.container);
1919
expect(root.parent!.parent).toBeNull();
@@ -29,9 +29,9 @@ test("basic parent/child relationships", async () => {
2929
</div>,
3030
);
3131

32-
const root = getRootElement(renderer);
33-
const item1 = root.children[0] as HostElement;
34-
const item2 = root.children[1] as HostElement;
32+
const root = getRootInstance(renderer);
33+
const item1 = root.children[0] as TestInstance;
34+
const item2 = root.children[1] as TestInstance;
3535
expect(item1.props["data-testid"]).toBe("item-1");
3636
expect(item2.props["data-testid"]).toBe("item-2");
3737
expect(item1.parent).toBe(root);
@@ -42,7 +42,7 @@ test("host elements exposes fiber instance", async () => {
4242
const renderer = createRoot();
4343
await renderWithAct(renderer, <div>Hello!</div>);
4444

45-
const root = getRootElement(renderer);
45+
const root = getRootInstance(renderer);
4646
const fiber = root.unstable_fiber!;
4747
expect(fiber.tag).toBe(ReactWorkTag.HostComponent);
4848
expect(fiber.return!.tag).toBe(ReactWorkTag.HostRoot);
@@ -62,7 +62,7 @@ test("can access composite parent props", async () => {
6262
const renderer = createRoot();
6363
await renderWithAct(renderer, <TestComponent className="test-class" onChange={handleChange} />);
6464

65-
const root = getRootElement(renderer);
65+
const root = getRootInstance(renderer);
6666
expect(root.props).toEqual({ className: "test-class", children: "Hello!" });
6767

6868
const fiber = root.unstable_fiber!;
@@ -136,7 +136,7 @@ test("queryAll should not return self by default", async () => {
136136
</body>,
137137
);
138138

139-
const elements = getRootElement(renderer).queryAll(
139+
const elements = getRootInstance(renderer).queryAll(
140140
(element) => element.props.className === "yes",
141141
);
142142
expect(elements).toHaveLength(2);
@@ -155,7 +155,7 @@ test("queryAll should return self if 'includeSelf' is true", async () => {
155155
</body>,
156156
);
157157

158-
const elements = getRootElement(renderer).queryAll(
158+
const elements = getRootInstance(renderer).queryAll(
159159
(element) => element.props.className === "yes",
160160
{
161161
includeSelf: true,

src/__tests__/unmount.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ test("unmount clears the rendered content", async () => {
1111
const renderer = createRoot();
1212
await renderWithAct(renderer, <div>Hello!</div>);
1313

14-
const containerElement = renderer.container;
15-
expect(containerElement.children.length).toBe(1);
14+
const container = renderer.container;
15+
expect(container.children.length).toBe(1);
1616

1717
await unmountWithAct(renderer);
18-
expect(containerElement.children.length).toBe(0);
18+
expect(container.children.length).toBe(0);
1919

2020
expect(() => renderer.container).toThrow("Cannot access .container on unmounted test renderer");
2121
});
@@ -24,13 +24,13 @@ test("unmount can be called multiple times safely", async () => {
2424
const renderer = createRoot();
2525
await renderWithAct(renderer, <div>Hello!</div>);
2626

27-
const containerElement = renderer.container;
28-
expect(containerElement.children.length).toBe(1);
27+
const container = renderer.container;
28+
expect(container.children.length).toBe(1);
2929

3030
await unmountWithAct(renderer);
3131
await unmountWithAct(renderer);
3232
await unmountWithAct(renderer);
33-
expect(containerElement.children.length).toBe(0);
33+
expect(container.children.length).toBe(0);
3434

3535
expect(() => renderer.container).toThrow("Cannot access .container on unmounted test renderer");
3636
});

src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
export { createRoot } from "./renderer";
22

33
export type { Root, RootOptions } from "./renderer";
4-
export type { HostElement, HostElementProps, HostNode } from "./host-element";
5-
export type { JsonElement, JsonNode } from "./render-to-json";
4+
5+
// eslint-disable-next-line @typescript-eslint/no-deprecated
6+
export type { TestInstance, TestNode, HostElement } from "./test-instance";
7+
export type { JsonElement, JsonNode } from "./to-json";
68
export type { QueryOptions } from "./query-all";
79

810
/**
911
* React Fiber type from react-reconciler. Exported for advanced use cases only.
1012
* This type represents internal React structures that may change without warning.
11-
* Prefer using the stable HostElement API instead.
13+
* Prefer using the stable TestInstance API instead.
1214
*/
1315
export type { Fiber } from "react-reconciler";

src/query-all.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
import type { HostElement } from "./host-element";
1+
import type { TestInstance } from "./test-instance";
22

33
/**
44
* Options for querying elements in the rendered tree.
55
*/
66
export interface QueryOptions {
7-
/** Include the element itself in the results if it matches the predicate. Defaults to false. */
7+
/** Include the instance itself in the results if it matches the predicate. Defaults to false. */
88
includeSelf?: boolean;
99

10-
/** Exclude any ancestors of deepest matched elements even if they match the predicate. Defaults to false. */
10+
/** Exclude any ancestors of deepest matched instances even if they match the predicate. Defaults to false. */
1111
matchDeepestOnly?: boolean;
1212
}
1313

1414
/**
1515
* Find all descendant elements matching the predicate.
1616
*
17-
* @param element - Root element to search from.
17+
* @param instance - Root TestInstance to search from.
1818
* @param predicate - Function that returns true for matching elements.
1919
* @param options - Optional query configuration.
2020
* @returns Array of matching elements in tree order.
2121
*/
2222
export function queryAll(
23-
element: HostElement,
24-
predicate: (element: HostElement) => boolean,
23+
instance: TestInstance,
24+
predicate: (instance: TestInstance) => boolean,
2525
options?: QueryOptions,
26-
): HostElement[] {
26+
): TestInstance[] {
2727
const includeSelf = options?.includeSelf ?? false;
2828
const matchDeepestOnly = options?.matchDeepestOnly ?? false;
2929

30-
const results: HostElement[] = [];
30+
const results: TestInstance[] = [];
3131

3232
// Match descendants first but do not add them to results yet.
33-
const matchingDescendants: HostElement[] = [];
33+
const matchingDescendants: TestInstance[] = [];
3434

35-
element.children.forEach((child) => {
35+
instance.children.forEach((child) => {
3636
if (typeof child === "string") {
3737
return;
3838
}
@@ -44,9 +44,9 @@ export function queryAll(
4444
includeSelf &&
4545
// When matchDeepestOnly = true: add current element only if no descendants match
4646
(matchingDescendants.length === 0 || !matchDeepestOnly) &&
47-
predicate(element)
47+
predicate(instance)
4848
) {
49-
results.push(element);
49+
results.push(instance);
5050
}
5151

5252
// Add matching descendants after element to preserve original tree walk order.

src/renderer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { ReactElement } from "react";
22
import { ConcurrentRoot } from "react-reconciler/constants";
33

44
import { Tag } from "./constants";
5-
import { HostElement } from "./host-element";
65
import { measureEnd, measureStart } from "./performance";
76
import type { Container } from "./reconciler";
87
import { TestReconciler } from "./reconciler";
8+
import { TestInstance } from "./test-instance";
99

1010
// Refs:
1111
// https://github.com/facebook/react/blob/main/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
@@ -73,7 +73,7 @@ export type Root = {
7373
/** Unmount the root and clean up. Must be called within act(). */
7474
unmount: () => void;
7575
/** The root container element. */
76-
container: HostElement;
76+
container: TestInstance;
7777
};
7878

7979
/**
@@ -151,12 +151,12 @@ export function createRoot(options?: RootOptions): Root {
151151
return {
152152
render,
153153
unmount,
154-
get container(): HostElement {
154+
get container(): TestInstance {
155155
if (container == null) {
156156
throw new Error("Cannot access .container on unmounted test renderer");
157157
}
158158

159-
return HostElement.fromInstance(container);
159+
return TestInstance.fromInstance(container);
160160
},
161161
};
162162
}

0 commit comments

Comments
 (0)