Testing
@gtkx/testing renders real GTKX components and provides queries, interactions, and assertions for their native widgets. Its API follows React Testing Library conventions.
Setup
Projects created with npm create gtkx@1.6.0 can include testing setup. To add it to an existing project, install the test dependencies:
npm install -D @gtkx/testing@1.6.0 vitestAdd a test script that runs vitest run, and create vitest.config.ts:
import gtkx from "@gtkx/cli/vitest-plugin";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [gtkx()],
test: {
include: ["tests/**/*.test.{ts,tsx}"],
},
});The plugin gives each worker a private headless display and session bus. Its default compositor is Sway; it also needs dbus-daemon and setpriv. On Fedora 44, install these with:
sudo dnf install sway dbus-daemon util-linuxCheck that the commands are available, then run the tests:
command -v sway dbus-daemon setpriv
npm testThis setup was checked in the Fedora 44 environment described in Getting Started. See the plugin reference for display size and compositor options.
Importing @gtkx/testing registers widget matchers and automatic cleanup. No additional setup file is needed.
Sway in a restricted container
Fedora may install Sway with the CAP_SYS_NICE file capability. A rootless container that excludes that capability can fail to start Sway with exit status 126, before a test runs. Check the container capability policy. For a disposable test container, removing the capability with sudo setcap -r "$(command -v sway)" allows Sway to run without it. This is a container workaround, not a desktop setup step.
Rendering a component
Await render before querying or interacting with widgets. This example tests an application's SettingsPanel:
import * as Gtk from "@gtkx/gi/gtk";
import { render, screen, userEvent } from "@gtkx/testing";
import { expect, it } from "vitest";
import { SettingsPanel } from "../src/settings-panel.js";
it("saves the display name", async () => {
await render(<SettingsPanel />);
const name = screen.getByRole(Gtk.AccessibleRole.TEXT_BOX, { name: "Display name" });
await userEvent.type(name, "Ada");
await userEvent.click(screen.getByRole(Gtk.AccessibleRole.BUTTON, { name: "Save" }));
expect(await screen.findByText("Settings saved")).toHaveTextContent("Settings saved");
});Ordinary components render inside a harness window. An application component that creates its own window needs rootElement instead:
import { rootElement } from "@gtkx/react";
await render(<App />, { container: rootElement });Use the wrapper option for context providers. rerender updates the rendered component; unmount removes it. Animations are disabled by default, and automatic cleanup removes rendered trees between tests. For hooks, use renderHook and assert on result.current.
Finding widgets
Prefer screen.getByRole with a Gtk.AccessibleRole enum and an accessible name. screen searches open toplevel windows, including dialogs and popovers. Use within(container) to restrict a query to a subtree.
GTKX follows Testing Library's query conventions. Other query families match label text, placeholders, display values, or the widget's name prop; see the query reference.
Queries exclude widgets that are not mapped, including content on inactive stack pages. The hidden option on role queries only includes widgets excluded from the accessibility tree; it does not include unmapped widgets.
A text button's child label takes precedence over its accessibleLabel prop. Query the visible label and use within when several buttons share it.
Interacting and waiting
Await every userEvent call. Helpers wait for the target to become actionable and flush React updates before resolving. Use userEvent.setup() when a sequence needs to retain held keyboard modifiers or pointer buttons.
const user = userEvent.setup();
await user.keyboard(entry, "{Control>}a{/Control}");
await user.type(entry, "Replacement");
await user.click(saveButton);Use slide(range, value) for a slider. drag drives authored drag gestures and cannot drive a range's native slider. The interaction reference covers the available helpers.
fireEvent(object, signalName, ...args) emits a GObject signal directly. It returns a promise, so await it. Use it when testing a signal handler or a tree without a visible window. Wrap state changes made outside these helpers in act.
For asynchronous assertions, use waitFor; for a widget leaving the tree, use waitForElementToBeRemoved. Their timeout defaults to one second and can be changed per call or through configure.
Assertions and debugging
Use widget matchers such as toHaveTextContent, toHaveAccessibleName, toBeChecked, and toHaveDisplayValue. Accessible states and properties have their own matchers, toHaveAccessibleState and toHaveAccessibleProperty. See the matcher reference for the complete set.
screen.debug() prints the widget tree, and screen.logRoles() groups widgets by accessible role. Capture the active window with await screen.screenshot({ path: "test.png" }), or pass a widget to screenshot to capture a subtree. The MCP server provides the same inspection tools during development.
GLib criticals and addon panics fail the running test as uncaught exceptions. Warnings alone do not fail tests. See Error Handling for the failure channels.
Next
The tutorial's testing chapter applies these tools to a complete application.