RSG testing library
The @roku-sdk/testing-library package is a testing utility library for Roku SceneGraph TypeScript components.
To use the library, you render a component with the render() function, query the resulting test object tree, simulate events, and assert on the results.
The library is built with SolidJS. Its approach is similar to the React Testing Library, but it is tailored to Roku's RSG architecture.
The library includes the following features:
- Test object tree, which is a reflection of your component structure that includes the properties and functionality needed for testing; it is not a real Document Object Model (DOM), but it mirrors real-world component behavior
- Component rendering utilities with SolidJS integration
- Querying functions (
getByTag,getByProp, and so on) to find test objects in the rendered tree - Event simulation with
fireEvent - Reactive update handling with
transaction() - TsComponent wrapper for testing declarative components
- Test data generation for parameterized testing
Installation
An app scaffolded with rk create already declares @roku-sdk/testing-library in its dependencies, alongside a jest.config.mjs and a passing example test; therefore, you do not need to install anything.
To add the library to a project that predates the scaffold, run the following command:
pnpm add @roku-sdk/testing-libraryThe library is a runtime dependency rather than a development dependency, which is how the scaffold declares it. The library lists @roku-sdk/common, @roku-sdk/interfaces, @roku-sdk/solid, and @roku-sdk/ts-core as its own dependencies; therefore, you do not need to install them yourself.
Quickstart
The following example renders a label component and checks its text prop:
import { describe, expect, test } from "@jest/globals";
import { Label } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
test("renders a label component", async () => {
const { getByTag } = await render(() => <label text="Hello World" />);
const label = getByTag<typeof Label>("label");
expect(label).toBeDefined();
expect(label.props.text).toBe("Hello World");
});
ImportantYou must import SolidJS primitives from
@roku-sdk/solid(not fromsolid-js). See Solid reactivity for more information.
API reference
This section describes the render() function and the other functions and helpers that the testing library provides.
render(component)
The render() function renders a component in the test environment and returns utilities for working with the rendered tree.
Description
The render() function is the core function for rendering components in the test environment. It has the following signature:
async function render(component: () => DomNode | JSX.Element): Promise<RenderResult>;Parameters
The function takes the following parameter:
| Property | Type | Description |
|---|---|---|
component | Function | A function that returns a JSX element or DomNode |
Return value
This function returns a RenderResult object with the following utilities:
| Property | Type | Description |
|---|---|---|
dispose | () => void | Cleanup function to unmount the component |
getByTag | <T>(tag: string) => TestNode<T> | Find single node by tag (throws if not found) |
getAllByTag | <T>(tag: string) => TestNode<T>[] | Find all nodes by tag (throws if none) |
queryByTag | <T>(tag: string) => TestNode<T> | null | Find single node by tag (returns null if not found) |
queryAllByTag | <T>(tag: string) => TestNode<T>[] | Find all nodes by tag (returns empty array if none) |
getByProp | <T>(prop: string, value: unknown) => TestNode<T> | Find node by prop value (throws if not found) |
queryByProp | <T>(prop: string, value: unknown) => TestNode<T> | null | Find node by prop value (returns null if not found) |
props | <T>(node: TestNode<T>) => T | Extract props from a test node |
children | (node: TestNode) => TestNode[] | Get children of a test node |
tag | (node: TestNode) => string | Get tag name of a test node |
getRoot | () => TestNode | Get the root test node |
toString | (indent?: number) => string | Serialize the test object tree to a JSON string |
transaction | async () => Promise<void> | Wait for pending reactive updates |
fireEvent | async (node: TestNode, eventName: string, eventValue: unknown) => Promise<void> | Fire an event on a node |
Query functions
You can use query functions to search through the entire test object tree generated by the render() function. Query functions traverse all nodes to find matching components based on the tag name or property values. All query functions come in four variants:
| Pattern | Behavior |
|---|---|
getBy* | Returns single element; throws error if not found. |
getAllBy* | Returns array of elements; throws error if empty. |
queryBy* | Returns single element or null if not found. |
queryAllBy* | Returns array (the array can be empty). |
Querying by tag name
The getByTag, getAllByTag, queryByTag, and queryAllByTag functions find test nodes by tag name.
Parameters
The functions take the following parameters:
| Parameter | Type | Description |
|---|---|---|
tag | string | The tag name to search for (for example, "button", "label", "rectangle") |
T (generic) | typeof ComponentName or Component<P> | Optional type parameter for typed props access; use typeof ComponentName for SDK components or Component<P> for custom components (see Typed queries for more information) |
Example
The following example finds nodes by tag name:
import { Button, Panel } from "@roku-sdk/components";
// Find single element by tag name (throws if not found)
const button = getByTag<typeof Button>("button");
// Find all elements with tag (throws if none found)
const allButtons = getAllByTag<typeof Button>("button");
// Find single element (returns null if not found)
const maybePanel = queryByTag<typeof Panel>("panel");
// Find all elements (returns empty array if none)
const groups = queryAllByTag("group");Querying by property value
The getByProp and queryByProp functions find test nodes by the value of a prop.
Parameters
The functions take the following parameters:
| Parameter | Type | Description |
|---|---|---|
prop | string | The property name to match (for example, "id", "text", "disabled") |
value | unknown | The expected value of the property |
T (generic) | Component<P> | Optional type parameter for typed props access (see Typed queries for more information) |
Example
The following example finds nodes by prop value:
import { Button, Label } from "@roku-sdk/components";
// Find by prop value (throws if not found)
const submitBtn = getByProp<typeof Button>("id", "submit-button");
// Find by prop value (returns null if not found)
const maybeLabel = queryByProp<typeof Label>("text", "Submit");Typed queries
You can pass typeof ComponentName as the generic parameter for SDK components from @roku-sdk/components to get typed props. This provides type checking and autocomplete in your integrated development environment (IDE):
import { Button } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
test("typed query example", async () => {
const { getByTag } = await render(() => <button text="Click me" />);
const button = getByTag<typeof Button>("button");
button.props.text // string | undefined
button.props.disabled // boolean | undefined
});Custom components
For custom components (typically with lowercase tag names), you can use Component<P>, where P is an interface describing the component's properties:
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
interface ICustomTagProps {
text?: string;
value?: number;
onClick?: (event: number) => void;
}
test("custom component typed query", async () => {
const { getByTag } = await render(() => <custom-tag text="Hello" />);
const tag = getByTag<Component<ICustomTagProps>>("custom-tag");
tag.props.text // string | undefined
tag.props.value // number | undefined
});See @roku-sdk/components types for more information about the Component<P> and EventHandler<T> type definitions and the available component exports.
Tree utility functions
The following functions read the test object tree, serialize it, and apply updates to it.
children(node) as Object
The children() function reads the child nodes of a test node.
Description
Returns the direct children of a test node in the rendered tree.
Parameters
The function takes the following parameter:
| Parameter | Type | Description |
|---|---|---|
node | TestNode | The node whose children you want to read |
Return value
A TestNode array that contains the child nodes (the array is empty if the node does not have any direct children).
tag(node) as Object
The tag() function reads the tag name of a test node.
Description
Retrieves the tag name of a test node.
Parameters
The function takes the following parameter:
| Parameter | Type | Description |
|---|---|---|
node | TestNode | The node whose tag name you want to read |
Return value
A string that contains the tag name of the TestNode (for example, "button", "label", or "rectangle").
getRoot() as Object
The getRoot() function reads the root of the test object tree.
Description
Returns the root test node for the rendered component tree.
Return value
The root TestNode that the render() function created.
toString(indent As number) as String
The toString() function serializes the test object tree.
Description
Serializes the entire test object tree to a JSON string for debugging or snapshot testing.
Parameters
The function takes the following parameter:
| Parameter | Type | Description |
|---|---|---|
indent | number (optional) | Indentation spaces for pretty-printing (for example, 2 or 4) |
Return value
A JSON string representation of the tree.
See Testing timer-based components and Parameterized testing with
generateCombinationsfor more information about snapshot workflows.
transaction() as Promise<void>
The transaction() function applies queued updates to the test object tree.
Description
Executes all pending RSG transaction updates and applies them to the test object tree. You can call this function after signal changes (or timer advances) when you need to apply the queued updates before you assert.
Return value
Resolves after the function applies the updates.
fireEvent(node, eventName, eventValue) as Promise<void>
The fireEvent() function simulates an event on a test node.
Description
Simulates an event on a test node, invoking the corresponding handler and applying any resulting tree updates.
The
fireEventfunction callstransaction()internally; therefore, you do not need to make an extra call totransaction()afterfireEvent.
Parameters
The function takes the following parameters:
| Parameter | Type | Description |
|---|---|---|
node | TestNode | The node whose event handler you want to trigger |
eventName | string | The event name with "on" prefix (for example, "onClick", "onLoadStatus") |
eventValue | unknown | The value to pass to the handler (the payload) |
Return value
Resolves after the function applies the updates.
For more information
See Event handling with fireEvent function for more information about usage patterns and automatic transaction behavior.
generateCombinations
The generateCombinations() function has the following signature:
function generateCombinations<T extends { [I: string]: PropValue | undefined }>(obj: { [K in keyof T]: T[K][] }): T[];=Description
The generateCombinations() function generates all possible combinations of property values for use with Jest's test.each. You can use it to render a component with every combination of props.
Parameters
The function takes the following parameter:
| Parameter | Type | Description |
|---|---|---|
obj | Map | A map of prop keys to arrays of candidate values. Each key represents a prop name you want to vary (its array contains the values to test for that prop). You can include undefined to represent "prop not provided" when you use WrapRecord for optional props. |
Return value
An array of objects containing every combination of the input values.
For more information
See Parameterized testing with generateCombinations for more information, including usage examples with test.each and snapshot testing.
Example
The following example generates combinations for the size and disabled props of a component:
import { generateCombinations, WrapRecord } from "@roku-sdk/testing-library";
interface IMyComponentProps {
size?: "small" | "medium" | "large";
disabled?: boolean;
}
const combinations = generateCombinations<WrapRecord<IMyComponentProps>>({
size: ["small", "medium", "large"],
disabled: [true, false]
});
// Results in 6 combinations:
// { size: "small", disabled: true }
// { size: "small", disabled: false }
// { size: "medium", disabled: true }
// ... and so onTestAnchorNode(component)
The TestAnchorNode() function wraps an AnchorNode component so that you can test it with the render() function. It has the following signature:
function TestAnchorNode<D extends AnchorNodeDescriptor>(
component: readonly [D, AnchorNodeImplementation<D>]
): (props: TestProps<D>) => JSX.ElementThe
TestAnchorNode()function is for testing anchor nodes only. You can use therender()function directly to test regular SolidJS components.
Description
The entry point from BrightScript to TypeScript is a special component type called AnchorNode. Unlike regular SolidJS components, AnchorNode has two separate property sets (inputFields and outputFields) that a descriptor object defines.
The TestAnchorNode function is a testing wrapper that lets you test AnchorNode components in the SolidJS testing environment. It converts inputFields and outputFields into standard SolidJS props; as a result, you can do the following:
- Set or update
inputFieldsas component props - Attach handler functions to
outputFieldsto verify component behavior
Parameters
The function takes the following parameter:
| Parameter | Type | Description |
|---|---|---|
component | AnchorNode | The array that defineAnchorNode() returns, which contains the descriptor and its implementation |
Return value
A JSX-compatible component function that accepts props that match the descriptor's fields.
Example
The following example defines an AnchorNode component with conditional visibility and output events:
// view-one.tsx
import { Button, Group, Label, Rectangle } from "@roku-sdk/components";
import { defineAnchorNode, FieldType, type AnchorNodeImplementation } from "@roku-sdk/rsg-ts";
import { createSignal } from "@roku-sdk/solid";
const ViewOneDescriptor = {
componentName: "ViewOne",
extends: "Group" as const,
inputFields: {
changeVisibility: FieldType.Boolean(false),
assignFocus: FieldType.Boolean(false)
},
outputFields: {
sendData: FieldType.Object()
}
};
const ViewOne: AnchorNodeImplementation<typeof ViewOneDescriptor> = (inputs, outputs) => {
const [count, setCount] = createSignal(0);
return (
inputs.changeVisibility && (
<group>
<rectangle width={400} height={400} color="0xFF0000FF" />
<label text={`Clicked: ${count()} times.`} />
<button
text="Click Me!"
onButtonSelected={() => {
setCount(count() + 1);
outputs.sendData({ message: `Count: ${count()}!` });
}}
/>
</group>
)
);
};
export default defineAnchorNode(ViewOneDescriptor, ViewOne);Testing AnchorNode components
The following test file renders the ViewOne component with TestAnchorNode() and checks its visibility, input updates, and output callbacks:
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { render, TestAnchorNode } from "@roku-sdk/testing-library";
import { createSignal } from "@roku-sdk/solid";
import ViewOne from "./view-one";
describe("ViewOne AnchorNode", () => {
const TestViewOne = TestAnchorNode(ViewOne);
it("renders when visibility input is true", async () => {
const sendDataMock = jest.fn();
const { getByTag, dispose } = await render(() => (
<TestViewOne changeVisibility={true} assignFocus={false} sendData={sendDataMock} />
));
// Component renders when changeVisibility is true
expect(getByTag("rectangle")).toBeDefined();
expect(getByTag("label")).toBeDefined();
dispose();
});
it("hides when visibility input is false", async () => {
const { queryByTag, dispose } = await render(() => (
<TestViewOne changeVisibility={false} assignFocus={false} sendData={jest.fn()} />
));
// Component does not render
expect(queryByTag("rectangle")).toBeNull();
dispose();
});
it("updates reactively when input fields change", async () => {
const [changeVisibility, setChangeVisibility] = createSignal(false);
const { getByTag, queryByTag, transaction, dispose } = await render(() => (
<TestViewOne changeVisibility={changeVisibility()} assignFocus={false} sendData={jest.fn()} />
));
// Initially hidden
expect(queryByTag("rectangle")).toBeNull();
// Update input field to show component
setChangeVisibility(true);
await transaction();
// Component now renders
expect(getByTag("rectangle")).toBeDefined();
dispose();
});
it("triggers output field callbacks", async () => {
const sendDataMock = jest.fn();
const { getByTag, fireEvent, dispose } = await render(() => (
<TestViewOne changeVisibility={true} assignFocus={false} sendData={sendDataMock} />
));
const button = getByTag<typeof Button>("button");
const label = getByTag<typeof Label>("label");
expect(label.props.text).toBe("Clicked: 0 times.");
// Trigger button action
await fireEvent(button, "onButtonSelected", {});
// Verify state update and output field callback
expect(label.props.text).toBe("Clicked: 1 times.");
expect(sendDataMock).toHaveBeenCalledWith({ message: "Count: 1!" });
dispose();
}Examples
The following sections show how to use the testing library for common testing tasks.
Basic rendering and assertions
You can use the render() function as the entry point for testing Roku SceneGraph components. It creates a test object tree that mirrors the structure and behavior of your actual component hierarchy. As a result, you can make assertions about rendered output, component properties, and tree structure.
Key concepts
The testing library provides the following features designed to make component testing intuitive and robust:
-
Test object tree: The
render()function produces a representation of your component tree that preserves all properties and relationships. Unlike a real DOM, this tree is built for testing, and it mirrors the behavior of actual components at runtime. -
Query functions: You can use
getByTagto find components by their tag name. Theget*variants throw an error if they do not find an element (you should use these variants in most tests), while thequery*variants returnnulland are useful for testing conditional rendering. -
Type safety: You can pass
typeof ComponentNamefor SDK components orComponent<P>for custom components as a generic parameter to get typed access to component props. As a result, you get IDE autocomplete, and TypeScript reports type errors at compile time. -
Async rendering: The
render()function is asynchronous because it needs to set up the SolidJS reactive context and wait for initial effects to complete. You must useawaitwhen you call it.
Common patterns
The following tests render a component and collect nodes by tag:
import { describe, expect, test } from "@jest/globals";
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
interface ITagProps {
prop?: string;
children?: JSX.Element;
}
test("should render a component", async () => {
const { getByTag } = await render(() => <tag prop="test" />);
const tag = getByTag<Component<ITagProps>>("tag");
expect(tag).toBeDefined();
expect(tag.props.prop).toBe("test");
});
test("should collect nodes by tag", async () => {
const { getByTag, getAllByTag } = await render(() => (
<tag>
<tag />
<tag />
</tag>
));
const tag = getByTag("tag");
const tags = getAllByTag("tag");
expect(tag).toBeDefined();
expect(tags.length).toBe(3);
});Snapshot testing
Snapshot testing captures the entire test object tree structure as a serialized JSON string and compares it against a saved baseline. You can use snapshot testing for regression testing of complex component hierarchies, where manual assertions are tedious or incomplete.
Using the toString() method
The toString(indent?) method serializes your rendered component tree into a human-readable JSON format. The optional indent parameter controls formatting; higher values produce more readable output with deeper indentation, which makes diffs easier to review when snapshots change.
When to use snapshots
You can use snapshot testing in the following scenarios:
| Use case | Description |
|---|---|
| Component structure verification | Ensure complex nested hierarchies remain stable across refactors |
| Props propagation | Verify that props flow correctly through multiple component layers |
| Regression prevention | Catch unintended changes to component output |
| Large component surfaces | Test components with many child elements or properties |
Best practices
The following practices help keep your snapshot tests effective and maintainable:
- You should review snapshot diffs carefully during pull request reviews because they represent actual output changes.
- You should call the
dispose()function after you create a snapshot to clean up reactive contexts. - You can set an appropriate indent level (for example,
2or4) for readable diffs. - You should combine snapshots with targeted assertions for critical properties.
The following example serializes the test object tree with toString() for snapshot testing:
import { render } from "@roku-sdk/testing-library";
test("matches snapshot", async () => {
const { toString, dispose } = await render(() => (
<ComplexComponent data={mockData} />
));
// Pass indent level for readable snapshots of the test object tree
expect(toString(2)).toMatchSnapshot();
dispose();
});Reactive updates with signals
SolidJS uses fine-grained reactivity with signals to manage component state. While SolidJS itself propagates signal changes immediately, the RSG 2.0 SDK implementation uses a transaction-based update model. When reactive changes occur, the RSG 2.0 SDK collects all update commands in a list rather than applying them directly to the component tree. The testing library mocks this behavior; as a result, tests reflect real runtime conditions.
Using the transaction() function
The transaction() function executes all collected update commands and applies them to the test object tree. It mirrors the transaction-based update mechanism used in the actual RSG runtime. Without calling transaction(), update commands remain queued and your test assertions run against the old tree state.
How the transaction() function works
The transaction() function processes component updates in the following steps:
- Signal update: You call a setter (for example,
setText("new value")). - Command collection: The RSG 2.0 SDK collects the resulting update commands in a queue.
- Transaction call: Calling
await transaction()executes all queued commands and updates the test tree. - Assertion: After the call, you can assert on the updated props and structure.
Best practices for using the transaction() function
The following guidelines help keep your tests reliable:
- You must
awaitthetransaction()call because it is asynchronous. - You must call
transaction()after signal updates and before assertions. Otherwise, the tree can be out of date, and tests can fail intermittently. - You can batch multiple signal updates before a single
transaction()call because the library collects and executes all commands together.
Using the transaction() function to wait for SolidJS reactive updates
The following example uses the transaction() function to wait for SolidJS reactive updates to complete:
import { createSignal } from "@roku-sdk/solid";
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
interface ITagProps {
prop?: string;
}
test("should update property", async () => {
const [text, setText] = createSignal("test");
const { getByTag, transaction } = await render(() => <tag prop={text()} />);
const tag = getByTag<Component<ITagProps>>("tag");
expect(tag.props.prop).toBe("test");
// Update the signal
setText("test2");
// Props have not updated yet (SolidJS batches updates)
expect(tag.props.prop).toBe("test");
// Wait for reactive updates
await transaction();
// Now props are updated
expect(tag.props.prop).toBe("test2");
});Conditional rendering with the Show component
SolidJS provides control flow components such as Show, For, and Switch for conditional rendering. These components work with the testing library; as a result, you can verify that components appear and disappear from the tree based on reactive conditions.
Testing pattern
The example in this section demonstrates how to test components that use SolidJS's Show component for conditional rendering. The pattern combines signal updates, transaction() calls to apply tree changes, and query functions to verify element presence or absence.
Key testing strategies
The following patterns verify show and hide behavior:
- You can use
getByTag()wrapped inexpect(() => ...).toThrow()to assert that an element is not present. - You can use
getByTag()directly to assert that an element is present (it throws if it does not find the element). - You must call
transaction()after signal updates that affect conditional rendering. - The test object tree reflects show and hide behavior; therefore, you can test conditional logic.
Example
The following example shows and hides a tag with the Show component:
import { createSignal, Show } from "@roku-sdk/solid";
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
test("should insert/remove tag", async () => {
const [insert, setInsert] = createSignal(false);
const InsComponent: Component<{ insert: boolean }> = props => (
<tag>
<Show when={props.insert}>
<inserted-tag />
</Show>
</tag>
);
const { getByTag, transaction } = await render(() => <InsComponent insert={insert()} />);
// Initially not rendered
expect(() => getByTag("inserted-tag")).toThrow(
"No node found with tag name: inserted-tag"
);
// Show the element
setInsert(true);
await transaction();
const insertedTag = getByTag("inserted-tag");
expect(insertedTag).toBeDefined();
// Hide the element
setInsert(false);
await transaction();
expect(() => getByTag("inserted-tag")).toThrow(
"No node found with tag name: inserted-tag"
);
});Event handling with fireEvent function
The fireEvent function simulates user interactions and system events on test nodes. You can use it to verify that event handlers are called with the correct arguments and trigger the expected behavior.
How fireEvent works
The fireEvent function takes three parameters: the test node, the event name (the name must start with "on"), and the event value to pass to the handler. It invokes the handler function directly and automatically calls the transaction() function internally to apply any resulting tree updates.
In the example in this section, the event handler is defined outside of the testing component as a Jest mock function; therefore, you can verify that the handler received the correct arguments. The fireEvent function works the same way for event handlers that components define internally as part of their business logic. You can query for any component with an event handler anywhere in the tree, fire the event with specific arguments, and then test the resulting updates to the component tree.
Key points
The fireEvent() function has the following characteristics:
- Automatic transaction: Unlike signal updates, you do not need to call
transaction()afterfireEventbecausefireEventcalls it automatically. - Event naming: Event names must use the
"on"prefix (for example,"onClick","onButtonSelected","onFocusedChild"). - Type safety: You should pass an event value of the type that your handler expects (numbers, objects, and so on).
- Async: You must
awaitthefireEventcall because it needs to process the transaction. - Handler types: The
fireEventfunction works with both external mock handlers and internal component handlers.
When to use fireEvent
You can use fireEvent() in the following testing scenarios:
- Testing click handlers, button selections, and user interactions
- Verifying event handlers receive correct parameters
- Testing components that respond to SceneGraph field observations
- Simulating focus changes, item selections, or custom events
Example
The following example fires an onClick event on a custom tag and checks the handler call:
import { jest } from "@jest/globals";
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
interface IClickableTag {
onClick?: (event: number) => void;
id?: string;
}
test("should fire event", async () => {
const eventHandler = jest.fn();
const { getByTag, fireEvent } = await render(() => (
<clickable-tag id="100" onClick={eventHandler} />
));
const tag = getByTag<Component<IClickableTag>>("clickable-tag");
expect(tag).toBeDefined();
// Fire the event
await fireEvent(tag, "onClick", 1);
expect(eventHandler).toHaveBeenCalledWith(1);
});Events that update state
In real-world components, event handlers often trigger internal state updates through signals. The testing library supports testing these state transitions; as a result, you can verify that user interactions produce the correct component state changes.
Testing pattern
The example in this section demonstrates a pattern in which an event handler updates a signal, which in turn updates component properties through SolidJS reactivity. Because fireEvent automatically executes the transaction, the test object tree immediately reflects any state changes that the event handler triggers (you do not need to call transaction() again).
In this example, the test component itself defines the signal and event handler to illustrate the pattern. The same behavior applies when signals and handlers are part of your component's internal business logic; the testing library handles both cases the same way.
Key concepts
The following concepts describe how fireEvent() manages state changes:
- Event-driven state updates: Event handlers can call signal setters to update component state.
- Automatic propagation: The
fireEventfunction handles the transaction internally; therefore, reactive updates apply immediately, and unlike direct signal updates, you do not need to calltransaction()afterfireEvent. - Query after update: After
fireEventcompletes, you can query for elements using their new prop values.
Use cases
You can use fireEvent() to test the following UI interaction patterns:
- Button clicks that update counters or toggle states
- Selection events that update active item indicators
- Input events that update displayed text or validation states
- Navigation events that update current route or screen state
Example
The following example fires an event that updates a signal, and then queries for the element by its new prop value:
import { createSignal } from "@roku-sdk/solid";
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
interface IClickableTag {
onClick?: (num: number) => void;
id?: string;
}
interface ITagProps {
prop?: string;
}
test("should update property on event", async () => {
const TestComponent: Component = () => {
const [text, setText] = createSignal("text1");
return (
<clickable-tag onClick={num => setText(`text-${num}`)} id="100">
<tag prop={text()} />
</clickable-tag>
);
};
const { getByProp, fireEvent } = await render(() => <TestComponent />);
const clickableTag = getByProp<Component<IClickableTag>>("id", "100");
expect(clickableTag).toBeDefined();
// Fire event that updates state
await fireEvent(clickableTag, "onClick", 1);
// Find element by new prop value
const tag = getByProp<Component<ITagProps>>("prop", "text-1");
expect(tag).toBeDefined();
});Context provider patterns
SolidJS contexts enable you to pass data through the component tree without manually passing props at every level. When you test components that read a context, you must wrap them with the appropriate context providers and supply mock context values.
Mocking context
The example in this section demonstrates how to create a reusable test helper function (renderWithContext) that wraps your component with a context provider. This pattern enables you to do the following:
- Control context values: You can provide mock implementations of context APIs with Jest mocks.
- Isolate component logic: You can test components independently from their actual context dependencies.
- Reuse setup code: You can create helper functions that reduce boilerplate in multiple tests.
- Verify context usage: You can assert that components interact correctly with context values.
Example
The following example defines a renderWithContext helper and uses it in a test:
import { Component } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
import { ApiContext } from "./contexts";
const mockApiContext = {
fetchData: jest.fn(),
isLoading: false
};
const renderWithContext = async (props = {}) =>
render(() => (
<ApiContext.Provider value={mockApiContext}>
<MyComponent {...props} />
</ApiContext.Provider>
));
test("renders with context", async () => {
const { getByTag } = await renderWithContext({ title: "Test" });
expect(getByTag("my-component")).toBeDefined();
});Testing context consumption
To verify that a context provider supplies the correct values to child components, you can use a test component that reads the context and captures its value. You can then make assertions about the captured value.
Verification strategy
You can create a simple test component that reads the context with useContext and assigns it to a variable in the test scope. As a result, you can do the following:
- Verify context availability: You can confirm that the context provider works correctly.
- Check context values: You can assert that the provided values match your expectations.
- Test context shape: You can verify that all expected properties and methods are present.
- Validate mock setup: You can confirm that the Jest mocks in the context are configured correctly.
Example
The following example tests context consumption by capturing the context value in a test component. You can use this approach when you debug the setup of a context provider or check that context flows through complex component hierarchies.
import { useContext } from "@roku-sdk/solid";
import { Rectangle } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
import { ApiContext, IApiContext } from "./contexts";
test("should provide context to children", async () => {
let capturedContext: IApiContext | undefined;
const mockApiContext: IApiContext = {
fetchGalleries: jest.fn().mockResolvedValue([]),
fetchItems: jest.fn().mockResolvedValue([]),
isLoading: false
};
const TestConsumer = () => {
capturedContext = useContext(ApiContext);
return <rectangle id="test" />;
};
await render(() => (
<ApiContext.Provider value={mockApiContext}>
<TestConsumer />
</ApiContext.Provider>
));
expect(capturedContext).toBeDefined();
expect(capturedContext).toBe(mockApiContext);
expect(typeof capturedContext!.fetchGalleries).toBe("function");
});Parameterized testing with generateCombinations
You can use parameterized testing with generateCombinations for scenarios where different sets of properties produce different component tree structures. This approach lets you test all meaningful prop combinations and capture the resulting structures in snapshots.
Use cases
You can use parameterized testing with generateCombinations when component props affect the rendered structure, such as in the following scenarios:
- Different sizes that change layout or add or remove elements
- Disabled states that hide or show interactive elements
- Variants that render different child components
- Conditional features controlled by boolean flags
Avoiding test expansion
The number of test cases grows exponentially with each property you add (for example, testing three different sizes, two disabled states, and three labels equals 18 separate tests).
To minimize the number of tests and snapshots, you can include only properties that affect the component tree structure. For properties that do not affect the structure (such as API tokens, configuration prefixes, or other business logic properties that do not appear in the rendered tree), you can use a single representative value instead of testing all variations.
Example
The following example compares a set of combinations that produces too many tests with a set that produces fewer tests:
// Too many tests (3 × 2 × 5 tokens = 30 tests)
generateCombinations({
size: ["small", "medium", "large"],
disabled: [true, false],
apiToken: ["token1", "token2", "token3", ...] // Does not change tree structure
})
// Efficient (3 × 2 = 6 tests)
generateCombinations({
size: ["small", "medium", "large"], // Changes structure
disabled: [true, false], // Changes structure
apiToken: ["test-token"] // Single value (business logic only)
})Comprehensive prop testing example
To test every combination of props, you can use test.each with the generated combinations:
import { generateCombinations, render, WrapRecord } from "@roku-sdk/testing-library";
interface IComponentProps {
size?: "small" | "medium" | "large";
disabled?: boolean;
label?: string;
}
test.each(
generateCombinations<WrapRecord<IComponentProps>>({
size: ["small", "medium", "large"],
disabled: [true, false],
label: ["Submit", "Cancel"]
})
)("renders with size=$size, disabled=$disabled, label=$label", async (props) => {
const { toString, dispose } = await render(() => <MyComponent {...props} />);
expect(toString(2)).toMatchSnapshot();
dispose();
});Testing timer-based components
Components that rely on setTimeout, setInterval, or other timer-based asynchronous behavior require you to control time in tests. Without controlling time, tests must wait for real time to pass, which makes them slow and potentially unreliable. You can use Jest's fake timers to manually advance time and control timer execution.
Common time-based testing scenarios
You can use timer controls to test the following time-dependent behaviors:
- Animations: Crossfades, transitions, and other time-based visual effects
- Auto-hide timers: UI elements that disappear after inactivity
- Polling mechanisms: Periodic data refreshes or status checks
- Debouncing and throttling: User input handlers with time delays
- Retry logic: Exponential backoff or timed retry attempts
How fake timers work
Jest's fake timers intercept all timer-related functions (setTimeout, setInterval, clearTimeout, and so on) and give you manual control over time progression. You can use jest.advanceTimersByTimeAsync() to instantly "fast-forward" to a specific point in time instead of waiting for the actual time period to elapse.
Testing time-based logic
When testing components that use setTimeout or setInterval internally (such as animations, auto-hide timers, and polling), you can use Jest's fake timers to control time progression:
- Enable fake timers with
jest.useFakeTimers()before your tests. - Use
jest.advanceTimersByTimeAsync()to move time forward. - Call
transaction()after advancing timers to apply any resulting tree updates. - Restore real timers with
jest.useRealTimers()after your tests.
You must use the
Asyncvariant (advanceTimersByTimeAsync) to resolve promises.
Example
The following example tests a crossfade animation with fake timers:
import { jest } from "@jest/globals";
import { render } from "@roku-sdk/testing-library";
import { CrossFade } from "./cross-fade";
beforeAll(() => jest.useFakeTimers());
afterAll(() => jest.useRealTimers());
test("should animate crossfade", async () => {
const { getByProp, props, fireEvent, transaction } = await render(() => (
<CrossFade imageUri="image1.png" fadeTime={1000} />
));
const poster = getByProp("id", "crossfade-poster");
const animation = getByProp("id", "crossfade-animation");
// Initial state
expect(props(poster).opacity).toBe(0);
expect(props(animation).control).toBe("stop");
// Advance timers
await jest.advanceTimersByTimeAsync(2000);
await transaction();
// Simulate load ready event
await fireEvent(poster, "onLoadStatus", "ready");
expect(props(animation).control).toBe("start");
// Simulate animation complete
await fireEvent(animation, "onState", "stopped");
expect(props(poster).opacity).toBe(1);
});Testing hooks with wrapper components
Custom hooks in SolidJS cannot be tested in isolation because you must call them within a reactive context (inside a component). To test a hook, you create a minimal wrapper component that uses the hook and exposes its behavior through the component tree. As a result, you can trigger the hook logic with events and observe the results in assertions.
Wrapper component test pattern
To test custom hooks in isolation, you must create a simple wrapper component that connects the hook's logic to the testing library. This wrapper component does the following:
- Calls the hook: Invokes your custom hook with the necessary parameters
- Exposes hook methods: Provides buttons or other elements that trigger hook functions when you call
fireEvent - Makes state observable: Renders hook state values as component props or passes them to mock callbacks
- Accepts test props: Takes props that the test controls (for example, mock callbacks)
Testing strategy
Once you have created a wrapper component, you can apply the following techniques to verify hook behavior:
- You can use
fireEventto trigger wrapper component buttons that call hook methods. - You can pass mock functions as props to observe hook callbacks.
- You can use fake timers if the hook uses
setTimeoutorsetInterval. - You can assert on mock function calls or component tree state changes.
Use cases
You can apply the wrapper component pattern to test hook behavior without duplicating the same setup across multiple component tests. You can test the following:
- Hooks that manage timers (such as auto-hide, debounce, and polling)
- Hooks that handle complex state transitions
- Hooks that integrate with external APIs or contexts
- Reusable logic that needs isolated verification
Example
The following example tests an auto-hide timer hook through a wrapper component:
import { createSignal } from "@roku-sdk/solid";
import { Component, Button, Group } from "@roku-sdk/components";
import { render } from "@roku-sdk/testing-library";
import { useAutoHideTimer } from "./use-auto-hide-timer";
interface ITestProps {
onHide: () => void;
timeoutSeconds: number;
}
const TestComponent: Component<ITestProps> = props => {
const [isVisible, setIsVisible] = createSignal(false);
const { restartTimer, clearTimer } = useAutoHideTimer({
isVisible,
onHide: props.onHide,
timeoutSeconds: props.timeoutSeconds
});
return (
<group>
<button id="show-button" onKeyPress={() => setIsVisible(true)} />
<button id="restart-button" onKeyPress={() => restartTimer()} />
<button id="clear-button" onKeyPress={() => clearTimer()} />
</group>
);
};
beforeAll(() => jest.useFakeTimers());
test("should call onHide after timeout", async () => {
const onHide = jest.fn();
const { getByProp, fireEvent } = await render(() => (
<TestComponent onHide={onHide} timeoutSeconds={60} />
));
// Trigger visibility
await fireEvent(getByProp("id", "show-button"), "onKeyPress");
// Advance past timeout
jest.advanceTimersByTime(60001);
expect(onHide).toHaveBeenCalledTimes(1);
});Reference
This section describes the types that the testing library provides.
TestNode<P>
The TestNode type represents a test object in the rendered component tree. A TestNode is not a real DOM node; it reflects a real-world component, including the properties and functionality needed for testing. The type has the following definition:
type TestNode<P = DefaultProps> = {
tag: string; // The component tag name
props: ExtractComponentProps<P>; // Component props (typed)
children: TestNode[]; // Child test nodes
s: symbol; // Internal symbol marker
r: object; // Node reference
};WrapRecord<T>
The WrapRecord type is a helper type for generateCombinations. It has the following definition:
type WrapRecord<T> = T & { [key: string]: PropValue | undefined };ExtractComponentProps<U>
The ExtractComponentProps type extracts the props type from a Component type. It has the following definition:
type ExtractComponentProps<U> = U extends Component<infer P> ? P : DefaultProps;FireEvent
The FireEvent type defines the signature of the function that fires events:
type FireEvent = (node: TestNode, eventName: string, eventValue: unknown) => void;Best practices
The following sections describe practices for writing reliable tests with the testing library.
Disposing after tests
To clean up after a test, you should call the dispose() function that render() returns:
test("my test", async () => {
const { dispose, getByTag } = await render(() => <Component />);
// ... assertions
dispose(); // Clean up
});Alternatively, you can call dispose() from an afterEach hook:
let dispose: () => void;
afterEach(() => dispose?.());
test("my test", async () => {
const result = await render(() => <Component />);
dispose = result.dispose;
// ... assertions
});Using transaction() after signal updates
You must call transaction() after updating signals to apply the SolidJS reactive updates before your assertions:
const [value, setValue] = createSignal("initial");
const { transaction } = await render(() => <Component value={value()} />);
setValue("updated");
await transaction(); // Required: wait for reactive updates to complete
// Now assertions see the updated stateUsing typed queries
You should use typed queries to get typed props. The following example compares typed and untyped queries:
import { Button } from "@roku-sdk/components";
// Typed props with SDK component
const button = getByTag<typeof Button>("button");
button.props.text; // TypeScript knows this is string | undefined
// Typed props with custom component
import { Component } from "@roku-sdk/components";
interface ICustomProps {
text?: string;
}
const custom = getByTag<Component<ICustomProps>>("custom-tag");
custom.props.text; // TypeScript knows this is string | undefined
// Untyped props
const button = getByTag("button");
button.props.text; // anyUsing queryBy* for optional elements
You should use the queryBy* functions for elements that might not be in the tree, instead of wrapping a getBy* function in a try block:
// Use queryBy* for optional elements
const maybeElement = queryByTag("optional-element");
if (maybeElement) {
// element exists
}
// Do not use try/catch with getBy*
try {
const element = getByTag("optional-element");
} catch {
// element does not exist
}Using getByProp for identifying elements
You can use getByProp to find a specific element (for example, by its id) when finding by tag might return the wrong element:
// Find specific element by ID
const submitButton = getByProp("id", "submit-btn");
// Compare with finding by tag, which may return the wrong element
const button = getByTag("button"); // Which button?Avoiding implementation details in tests
You should test component behavior, not internal structure:
// Test behavior
await fireEvent(button, "onClick", 1);
expect(mockHandler).toHaveBeenCalledWith(1);
// Do not test internal state directly
expect(component.internalState).toBe("something");Running tests from monorepo root
To run the tests, run the following pnpm command from the monorepo root:
# Run all tests
pnpm testRelated documentation
- Solid reactivity: Describes SolidJS patterns and behaviors.
- Knowledge Base: Describes pitfalls to avoid when working with RSG TypeScript to improve app performance.
- Release Notes: Lists SDK release history, including testing-library changes.
Updated about 5 hours ago