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:
It is designed for GTK apps built with the generated GIR-based bindings.
The library supports:
GtkButton and related subclasses through
glib.signalEmitByName(widget, "clicked", [])useRobot(true))gtktest supports these selector styles:
#name
.klass
!role
button"text"
Button
Gtk prefixExamples:
gtktest.lookup("#submitBtn")
gtktest.lookupAll(".text-button")
gtktest.lookup("!button")
gtktest.lookup("\"Submit\"")
gtktest.lookup("Button")
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.
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");
Common assertion helpers:
verifyExistsverifyNotExistsverifyVisibleverifyInvisibleverifyEnabledverifyDisabledverifyHasTextverifyFocusedverifyAccessibleRoleverifyAccessiblePropertyExample:
gtktest.verifyExists("#submitBtn");
gtktest.verifyVisible("#submitBtn");
gtktest.verifyAccessibleRole("#submitBtn", AccessibleRole.button);
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.
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:
DISPLAY is setlibXtst.so.6 and libX11.so.6 are loadableGdkX11Surface (GTK is on X11 or
XWayland with X11 backend)It returns false on:
GDK_BACKEND=x11 to force the X11 backend when tests need real robot
input under a Wayland session.In robot mode, these action methods route through XTest instead of emitting GTK signals directly:
clickOn(query) - left-click at the widget's screen centerdoubleClickOn(query) - two quick left-clicksrightClickOn(query) - right-click at the widget's screen centermoveTo(query) - mouse-move to the widget's screen center (no-op in
semantic mode)push(combo) - real key press/release via XTestFakeKeyEvent with
modifier supportThe 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.
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.
rightClickOn(query) has two paths:
useRobot(true) and robotAvailable()GtkGestureClick on the widget that binds
to button 3 or "any" (button 0) and emit its pressed and released
signals with the widget-local centerIf neither path can satisfy the request, rightClickOn throws with a
clear error instead of silently falling back to a normal click.
gtktest.push(combo) maps common test shortcuts directly to GTK editable
operations and widget activation. The supported combos are:
ENTER / RETURN - activate the focused widgetESCAPE - clear focusTAB - advance focus to the next childBACK_SPACE - remove one character from the focused editableDELETE - remove the character at the cursorHOME / CONTROL+HOME - move the cursor to the startEND / CONTROL+END - move the cursor to the endCONTROL+A - select allUnknown combos are no-ops in semantic mode.
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.
robotAvailable().pump() or pumpUntil(::predicate, ms) before
asserting UI state that updates asynchronously.GDK_BACKEND=x11 over a
pure Wayland session; the frame-clock interaction under Wayland is
compositor-dependent.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.