Basics

Guides

API Reference

Menu

Basics

Guides

API Reference

gtktest Usage

What gtktest is

gtktest is the GTK4 UI testing library for Aussom.

It fills the same role for GTK apps that testfx fills for JavaFX apps. Tests can:

  • look up widgets by selector
  • focus widgets
  • write text into editable controls
  • pump the GTK event loop
  • verify text, visibility, enabled state, and accessibility role
  • capture widget or window PNGs
  • record frame snapshots during a test

It is designed for GTK apps built with the generated GIR-based bindings.

Current status

The library supports:

  • selector lookup by id, css class, role, visible text, or type name
  • focus and text entry
  • event-loop pumping
  • common assertions
  • widget, window, and screen capture
  • frame-snapshot recording with optional ffmpeg stitching
  • semantic click on GtkButton and related subclasses through glib.signalEmitByName(widget, "clicked", [])
  • real mouse and keyboard injection on Linux X11 / XWayland through the X11 XTest extension (toggle with useRobot(true))

Selectors

gtktest supports these selector styles:

  • #name
    • widget buildable id or widget name
  • .klass
    • CSS class on the widget
  • !role
    • accessible role name such as button
  • "text"
    • visible widget text
  • Button
    • native GTK type name, with or without the Gtk prefix

Examples:

gtktest.lookup("#submitBtn")
gtktest.lookupAll(".text-button")
gtktest.lookup("!button")
gtktest.lookup("\"Submit\"")
gtktest.lookup("Button")

Basic example

include aunit;
include gtktest;
include gtk.gtk.functions;
include gtk.gtk.window;
include gtk.gtk.box;
include gtk.gtk.label;
include gtk.gtk.entry;
include gtk.gtk.button;
include gtk.gtk.editable;
include gtk.gtk.enums;

@Test(name = "gtktest basic")
class basicGtkTest : test {
    public gtkApi = null;
    public window = null;
    public label = null;
    public entry = null;

    @Before
    public runBefore() {
        this.gtkApi = new GtkApi();
        this.gtkApi.init();

        this.window = new Window();
        this.window.set_title("gtktest example");
        this.window.set_default_size(360, 160);

        this.label = new Label("initial");
        this.label.asWidget().set_name("resultLabel");

        this.entry = new Entry();
        this.entry.asWidget().set_name("inputField");

        btn = new Button("Submit");
        btn.asWidget().set_name("submitBtn");
        btn.setOnClicked(::onSubmit);

        box = new Box(Orientation.vertical, 10);
        box.append(this.label);
        box.append(this.entry);
        box.append(btn);
        this.window.set_child(box);
        this.window.present();

        gtktest.setup(this.window);
        gtktest.waitForDraw(this.window);
        return null;
    }

    @After
    public runAfter() {
        gtktest.cleanup();
        if (this.window != null) {
            this.window.close();
        }
        return null;
    }

    /**
     * Self is a Button wrapper delivered by the signal trampoline.
     */
    public onSubmit(object Self) {
        this.label.set_text(new Editable(this.entry).get_text());
        return null;
    }

    @Test(name = "typing updates entry")
    public typeText() {
        gtktest.focus("#inputField");
        gtktest.write("hello");
        gtktest.verifyHasText("#inputField", "hello");
        return this.expectBool(true);
    }
}

The example above uses the idiomatic GTK4 binding shape: primary constructors (new Button("Submit")), setOn<Name>(...) signal setters that deliver wrapped objects to your callback, enum values as strings (Orientation.vertical is the string "vertical"), and "re-wrap" downcasts (new Editable(this.entry) wraps the Entry's native handle as a GtkEditable interface). See gtk4-usage.md for a full explanation of these patterns.

Event loop helpers

GTK changes often need one or more main-loop turns before assertions see the final state.

Use:

  • gtktest.pump()
  • gtktest.pumpUntil(::predicate, 5000)
  • gtktest.sleep(200)
  • gtktest.waitForDraw(widget)

Example:

gtktest.focus("#inputField");
gtktest.write("updated");
gtktest.pump();
gtktest.verifyHasText("#inputField", "updated");

Assertions

Common assertion helpers:

  • verifyExists
  • verifyNotExists
  • verifyVisible
  • verifyInvisible
  • verifyEnabled
  • verifyDisabled
  • verifyHasText
  • verifyFocused
  • verifyAccessibleRole
  • verifyAccessibleProperty

Example:

gtktest.verifyExists("#submitBtn");
gtktest.verifyVisible("#submitBtn");
gtktest.verifyAccessibleRole("#submitBtn", AccessibleRole.button);

Capture and recording

You can capture widgets, windows, or the current screen target:

btn = gtktest.lookup("#submitBtn");
gtktest.capture(btn, "/tmp/submit-btn.png");
gtktest.captureWindow(this.window, "/tmp/window.png");
gtktest.captureScreen("/tmp/screen.png");

You can also record frame snapshots:

gtktest.startRecording("/tmp/session", 30);
gtktest.focus("#inputField");
gtktest.write("hello");
outPath = gtktest.stopRecording();

When ffmpeg is available on PATH, stopRecording() tries to stitch the frames into out.mp4. Otherwise it returns the frame directory.

Robot mode

gtktest can inject real mouse and keyboard events through the X11 XTest extension when the backend supports it. Call useRobot(true) to switch to the robot path, and always gate robot-only tests with robotAvailable() so tests that need real input still pass on compositors where XTest is not available.

gtktest.useRobot(true);
if (!gtktest.robotAvailable()) {
    return this.expectBool(true);
}

gtktest.focus("#inputField");
gtktest.write("robot text");
gtktest.clickOn("#submitBtn");

robotAvailable() returns true when:

  • The OS is Linux
  • DISPLAY is set
  • libXtst.so.6 and libX11.so.6 are loadable
  • The target window's GdkSurface is GdkX11Surface (GTK is on X11 or XWayland with X11 backend)

It returns false on:

  • headless environments
  • GTK running on the Wayland backend (even with XWayland present). Set GDK_BACKEND=x11 to force the X11 backend when tests need real robot input under a Wayland session.
  • macOS and Windows in v1 (tracked as v2 work)

In robot mode, these action methods route through XTest instead of emitting GTK signals directly:

  • clickOn(query) - left-click at the widget's screen center
  • doubleClickOn(query) - two quick left-clicks
  • rightClickOn(query) - right-click at the widget's screen center
  • moveTo(query) - mouse-move to the widget's screen center (no-op in semantic mode)
  • push(combo) - real key press/release via XTestFakeKeyEvent with modifier support

The robot path computes widget screen coordinates by combining gtk_widget_compute_point (widget to native-window) with XTranslateCoordinates (native-window to root) and the surface scale factor. Note that graphene_point_t is a pair of floats, so the computation uses explicit 4-byte float IO rather than the 8-byte double layout the default PointLayout wrapper assumes.

Why XTest and not AWT Robot

java.awt.Robot on Linux calls sun.awt.UNIXToolkit.load_gtk() during construction, which loads a GTK2 or GTK3 shared library into the same JVM. When GTK4 is already loaded for the app under test, the AWT GTK loader deadlocks. gtktest sidesteps this by binding libXtst.so.6 directly through Panama and opening a dedicated X11 display connection for input injection, independent of GTK's own X connection.

Right-click

rightClickOn(query) has two paths:

  • Robot mode - real secondary-button click at the widget's screen center when useRobot(true) and robotAvailable()
  • Semantic mode - find a GtkGestureClick on the widget that binds to button 3 or "any" (button 0) and emit its pressed and released signals with the widget-local center

If neither path can satisfy the request, rightClickOn throws with a clear error instead of silently falling back to a normal click.

Push combo vocabulary

gtktest.push(combo) maps common test shortcuts directly to GTK editable operations and widget activation. The supported combos are:

  • ENTER / RETURN - activate the focused widget
  • ESCAPE - clear focus
  • TAB - advance focus to the next child
  • BACK_SPACE - remove one character from the focused editable
  • DELETE - remove the character at the cursor
  • HOME / CONTROL+HOME - move the cursor to the start
  • END / CONTROL+END - move the cursor to the end
  • CONTROL+A - select all

Unknown combos are no-ops in semantic mode.

Running under Wayland

GTK4's frame clock only advances when the compositor delivers frame events, and many Wayland compositors skip frame events for windows that do not currently hold keyboard focus. A window launched from a terminal for a test run is usually in that state, so calls that wait on the frame clock -- gtk_test_widget_wait_for_draw, the paintable-cache snapshot behind capture, captureWindow, and startRecording -- can stall indefinitely under a pure Wayland backend. The standard workaround is to force GTK onto XWayland with the X11 backend:

GDK_BACKEND=x11 aussom -t tests/myGtkTest.aus

gtktest.waitForDraw is also bounded by a wall-clock deadline (2 s by default) so even under a misbehaving compositor the call returns rather than hanging forever. Capture-dependent tests stay sensitive to compositor behavior; the X11 backend is the safest default for CI.

Practical advice

  • Prefer focus, write, verify, and pump helpers today.
  • Use selector IDs or widget names consistently in your fixtures.
  • Gate robot-only tests with robotAvailable().
  • Drain pending work with pump() or pumpUntil(::predicate, ms) before asserting UI state that updates asynchronously.
  • When running capture or record tests, prefer GDK_BACKEND=x11 over a pure Wayland session; the frame-clock interaction under Wayland is compositor-dependent.
  • If you need a modal popup on top of a main window, prefer a plain GtkWindow with set_modal(true) + set_transient_for(parent) over GtkDialog. GtkDialog is deprecated in GTK4.10+ and its content area does not always map under modern compositors.