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-library

The 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");
});
📘

Important

You must import SolidJS primitives from @roku-sdk/solid (not from solid-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:

PropertyTypeDescription
componentFunctionA function that returns a JSX element or DomNode

Return value

This function returns a RenderResult object with the following utilities:

PropertyTypeDescription
dispose() => voidCleanup 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> | nullFind 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> | nullFind node by prop value (returns null if not found)
props<T>(node: TestNode<T>) => TExtract props from a test node
children(node: TestNode) => TestNode[]Get children of a test node
tag(node: TestNode) => stringGet tag name of a test node
getRoot() => TestNodeGet the root test node
toString(indent?: number) => stringSerialize the test object tree to a JSON string
transactionasync () => Promise<void>Wait for pending reactive updates
fireEventasync (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:

PatternBehavior
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:

ParameterTypeDescription
tagstringThe 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:

ParameterTypeDescription
propstringThe property name to match (for example, "id", "text", "disabled")
valueunknownThe 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:

ParameterTypeDescription
nodeTestNodeThe 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:

ParameterTypeDescription
nodeTestNodeThe 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:

ParameterTypeDescription
indentnumber (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 generateCombinations for 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 fireEvent function calls transaction() internally; therefore, you do not need to make an extra call to transaction() after fireEvent.

Parameters

The function takes the following parameters:

ParameterTypeDescription
nodeTestNodeThe node whose event handler you want to trigger
eventNamestringThe event name with "on" prefix (for example, "onClick", "onLoadStatus")
eventValueunknownThe 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:

ParameterTypeDescription
objMapA 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 on

TestAnchorNode(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.Element

The TestAnchorNode() function is for testing anchor nodes only. You can use the render() 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 inputFields as component props
  • Attach handler functions to outputFields to verify component behavior

Parameters

The function takes the following parameter:

ParameterTypeDescription
componentAnchorNodeThe 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 getByTag to find components by their tag name. The get* variants throw an error if they do not find an element (you should use these variants in most tests), while the query* variants return null and are useful for testing conditional rendering.

  • Type safety: You can pass typeof ComponentName for SDK components or Component<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 use await when 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 caseDescription
Component structure verificationEnsure complex nested hierarchies remain stable across refactors
Props propagationVerify that props flow correctly through multiple component layers
Regression preventionCatch unintended changes to component output
Large component surfacesTest 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, 2 or 4) 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:

  1. Signal update: You call a setter (for example, setText("new value")).
  2. Command collection: The RSG 2.0 SDK collects the resulting update commands in a queue.
  3. Transaction call: Calling await transaction() executes all queued commands and updates the test tree.
  4. 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 await the transaction() 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 in expect(() => ...).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() after fireEvent because fireEvent calls 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 await the fireEvent call because it needs to process the transaction.
  • Handler types: The fireEvent function 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 fireEvent function handles the transaction internally; therefore, reactive updates apply immediately, and unlike direct signal updates, you do not need to call transaction() after fireEvent.
  • Query after update: After fireEvent completes, 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:

  1. Enable fake timers with jest.useFakeTimers() before your tests.
  2. Use jest.advanceTimersByTimeAsync() to move time forward.
  3. Call transaction() after advancing timers to apply any resulting tree updates.
  4. Restore real timers with jest.useRealTimers() after your tests.

You must use the Async variant (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 fireEvent to 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 setTimeout or setInterval.
  • 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 state

Using 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; // any

Using 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 test

Related 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.

Did this page help you?