The testfx library provides JavaFX UI testing support for Aussom. It wraps
the TestFX library and lets you simulate
user interactions like clicking buttons, typing text, dragging nodes, and
scrolling. It also provides assertion methods for verifying the state of
your UI.
Include the testfx library along with aunit and any fx components your
test needs.
include aunit;
include fx;
include fx.Label;
include fx.Button;
include testfx;
Every test follows the same lifecycle:
testfx.setHeadless(true) for headless mode.fx.fxApp() and show it with app.show(false).testfx.setup(app) to initialize the test robot.testfx.cleanup(), close the app, and shut down JavaFX.Most testfx methods accept a query string to find nodes. There are three
ways to target a node:
| Syntax | Matches By | Example |
|---|---|---|
#id |
Node ID | "#submitBtn" |
.class |
CSS class | ".button" |
text |
Text content | "Submit" |
Set a node ID in your UI code with setId():
btn = new Button("Save");
btn.setId("saveBtn");
Then reference it in tests as "#saveBtn".
By default, tests run in headed mode and you will see the application window on screen as the robot interacts with it. This is useful during development because you can watch the test play out.
For CI pipelines or environments without a display, use headless mode. Headless mode uses the Monocle Glass platform to run JavaFX with a virtual framebuffer. No window appears, but all interactions and assertions work the same way.
To enable headless mode, call testfx.setHeadless(true) before
fx.fxApp():
testfx.setHeadless(true);
app = fx.fxApp("My App", 400, 300);
The call order matters because the JavaFX toolkit reads the system properties
when it first initializes. Setting headless after fx.fxApp() has no effect.
This example creates a label and a button, then verifies the label text updates when the button is clicked.
include aunit;
include fx;
include fx.Label;
include fx.Button;
include fx.VBox;
include testfx;
@Test(name = "Simple TestFx Example")
class simpleTest : test {
private app = null;
private lbl = null;
private count = 0;
@Before
public runBefore() {
this.app = fx.fxApp("Counter App", 300, 200);
this.lbl = new Label("Count: 0");
this.lbl.setId("counterLabel");
btn = new Button("Increment");
btn.setId("incBtn");
btn.onClick(::onIncrement);
vbox = new VBox();
vbox.add([this.lbl, btn]);
this.app.setLayout(vbox);
this.app.show(false);
testfx.setup(this.app);
}
@After
public runAfter() {
testfx.cleanup();
fx.runLater(::closeApp);
fx.shutdown();
}
public closeApp() { this.app.close(); }
public onIncrement() {
this.count++;
this.lbl.setText("Count: " + this.count);
}
@Test(name = "Label shows initial count.")
public initialCount() {
testfx.verifyHasText("#counterLabel", "Count: 0");
return this.expectBool(true);
}
@Test(name = "Click increments the count.")
public clickIncrement() {
testfx.clickOn("#incBtn");
testfx.verifyHasText("#counterLabel", "Count: 1");
return this.expectBool(true);
}
@Test(name = "Button exists and is enabled.")
public buttonState() {
testfx.verifyExists("#incBtn");
testfx.verifyEnabled("#incBtn");
testfx.verifyVisible("#incBtn");
return this.expectBool(true);
}
}
Run it with:
aussom -t simpleTest.aus
This example tests a login form with text input, field clearing, and multiple assertions. It also shows headless mode.
include aunit;
include fx;
include fx.Label;
include fx.TextField;
include fx.PasswordField;
include fx.Button;
include fx.VBox;
include testfx;
@Test(name = "Login Form Tests")
class loginFormTest : test {
private app = null;
private statusLabel = null;
private userField = null;
private passField = null;
@Before
public runBefore() {
// Run without a visible window.
testfx.setHeadless(true);
this.app = fx.fxApp("Login", 300, 250);
this.statusLabel = new Label("Enter credentials");
this.statusLabel.setId("status");
this.userField = new TextField();
this.userField.setId("username");
this.passField = new PasswordField();
this.passField.setId("password");
loginBtn = new Button("Login");
loginBtn.setId("loginBtn");
loginBtn.onClick(::onLogin);
clearBtn = new Button("Clear");
clearBtn.setId("clearBtn");
clearBtn.onClick(::onClear);
vbox = new VBox();
vbox.add([
this.statusLabel,
this.userField,
this.passField,
loginBtn,
clearBtn
]);
this.app.setLayout(vbox);
this.app.show(false);
testfx.setup(this.app);
}
@After
public runAfter() {
testfx.cleanup();
fx.runLater(::closeApp);
fx.shutdown();
}
public closeApp() { this.app.close(); }
public onLogin() {
user = this.userField.getText();
pass = this.passField.getText();
if (user == "admin" && pass == "secret") {
this.statusLabel.setText("Login successful");
} else {
this.statusLabel.setText("Invalid credentials");
}
}
public onClear() {
this.userField.setText("");
this.passField.setText("");
this.statusLabel.setText("Enter credentials");
}
@Test(name = "Initial status message is correct.")
public initialStatus() {
testfx.verifyHasText("#status", "Enter credentials");
return this.expectBool(true);
}
@Test(name = "Valid login shows success message.")
public validLogin() {
testfx.clickOn("#username");
testfx.write("admin");
testfx.clickOn("#password");
testfx.write("secret");
testfx.clickOn("#loginBtn");
testfx.verifyHasText("#status", "Login successful");
return this.expectBool(true);
}
@Test(name = "Invalid login shows error message.")
public invalidLogin() {
// Clear fields from the previous test.
testfx.clickOn("#clearBtn");
testfx.clickOn("#username");
testfx.write("user");
testfx.clickOn("#password");
testfx.write("wrong");
testfx.clickOn("#loginBtn");
testfx.verifyHasText("#status", "Invalid credentials");
return this.expectBool(true);
}
@Test(name = "Clear button resets the form.")
public clearForm() {
testfx.clickOn("#username");
testfx.write("something");
testfx.clickOn("#clearBtn");
testfx.verifyHasText("#status", "Enter credentials");
return this.expectBool(true);
}
@Test(name = "All form controls exist.")
public controlsExist() {
testfx.verifyExists("#username");
testfx.verifyExists("#password");
testfx.verifyExists("#loginBtn");
testfx.verifyExists("#clearBtn");
testfx.verifyExists("#status");
return this.expectBool(true);
}
}
This example demonstrates keyboard shortcuts, node lookup with AJI inspection, CSS class queries, drag and drop, and scrolling.
include aunit;
include fx;
include fx.Label;
include fx.TextField;
include fx.Button;
include fx.ListView;
include fx.VBox;
include fx.HBox;
include testfx;
@Test(name = "Advanced TestFx Examples")
class advancedTest : test {
private app = null;
private outputLabel = null;
private inputField = null;
@Before
public runBefore() {
testfx.setHeadless(true);
this.app = fx.fxApp("Advanced Test", 500, 400);
this.inputField = new TextField();
this.inputField.setId("input");
this.outputLabel = new Label("");
this.outputLabel.setId("output");
copyBtn = new Button("Copy");
copyBtn.setId("copyBtn");
copyBtn.onClick(::onCopy);
vbox = new VBox();
vbox.add([this.inputField, copyBtn, this.outputLabel]);
this.app.setLayout(vbox);
this.app.show(false);
testfx.setup(this.app);
}
@After
public runAfter() {
testfx.cleanup();
fx.runLater(::closeApp);
fx.shutdown();
}
public closeApp() { this.app.close(); }
public onCopy() {
this.outputLabel.setText(this.inputField.getText());
}
/*
* Keyboard shortcuts
*/
@Test(name = "Select all and overwrite text.")
public selectAllOverwrite() {
testfx.clickOn("#input");
testfx.write("first");
// Select all text then type to replace it.
testfx.push("CONTROL+A");
testfx.write("second");
testfx.clickOn("#copyBtn");
testfx.verifyHasText("#output", "second");
return this.expectBool(true);
}
@Test(name = "Erase text with backspace.")
public eraseWithBackspace() {
testfx.clickOn("#input");
testfx.push("CONTROL+A");
testfx.write("abcdef");
// Erase last 3 characters.
testfx.eraseText(3);
testfx.clickOn("#copyBtn");
testfx.verifyHasText("#output", "abc");
return this.expectBool(true);
}
/*
* Node lookup and AJI inspection
*/
@Test(name = "Lookup node and read properties with AJI.")
public lookupAndInspect() {
// lookup() returns an AussomJavaObject wrapping
// the JavaFX Node. You can call JavaFX methods on
// it using AJI's invoke().
node = testfx.lookup("#copyBtn");
text = node.invoke("getText");
return this.expect(text, "Copy");
}
@Test(name = "Lookup all buttons by CSS class.")
public lookupByClass() {
// The .button CSS class matches all Button instances.
buttons = testfx.lookupAll(".button");
return this.expectBool(buttons.size() >= 1);
}
@Test(name = "Check a non-existent node returns false.")
public nonExistentNode() {
return this.expect(testfx.exists("#phantom"), false);
}
/*
* Multiple assertions on the same node
*/
@Test(name = "Verify multiple properties on a node.")
public multipleAssertions() {
testfx.verifyExists("#copyBtn");
testfx.verifyVisible("#copyBtn");
testfx.verifyEnabled("#copyBtn");
testfx.verifyHasText("#copyBtn", "Copy");
return this.expectBool(true);
}
/*
* Timing control
*/
@Test(name = "Sleep between actions for timing.")
public sleepBetweenActions() {
testfx.clickOn("#input");
testfx.write("delayed");
// Pause for 100ms to simulate a delay.
testfx.sleep(100);
testfx.clickOn("#copyBtn");
testfx.verifyHasText("#output", "delayed");
return this.expectBool(true);
}
}
| Method | Description |
|---|---|
setHeadless(bool Headless) |
Enable/disable headless mode. Call before fx.fxApp(). |
setup(object FxAppObj) |
Initialize the robot and target the app window. |
cleanup() |
Release held keys and mouse buttons. |
| Method | Description |
|---|---|
clickOn(string Query) |
Click on a node. |
doubleClickOn(string Query) |
Double-click on a node. |
rightClickOn(string Query) |
Right-click on a node. |
moveTo(string Query) |
Move the mouse to a node. |
moveBy(double X, double Y) |
Move the mouse by a relative offset. |
| Method | Description |
|---|---|
write(string Text) |
Type text into the focused control. |
push(string KeyCombo) |
Press a key combination (e.g. "CONTROL+S"). |
eraseText(int Count) |
Press backspace Count times. |
Key names in push() match JavaFX KeyCode values. Common keys include
CONTROL, SHIFT, ALT, ENTER, TAB, ESCAPE, BACK_SPACE,
DELETE, UP, DOWN, LEFT, RIGHT, and all letter and number keys
(A through Z, DIGIT0 through DIGIT9). Combine keys with +
(e.g. "CONTROL+SHIFT+N").
| Method | Description |
|---|---|
drag(string Query) |
Begin a drag from the node. |
dropTo(string Query) |
Drop at the node (after a drag). |
Usage: testfx.drag("#source"); testfx.dropTo("#target");
| Method | Description |
|---|---|
scrollUp(int Amount = 1) |
Scroll up by Amount units. |
scrollDown(int Amount = 1) |
Scroll down by Amount units. |
| Method | Description |
|---|---|
lookup(string Query) |
Find a node, returns an AussomJavaObject. |
lookupAll(string Query) |
Find all matching nodes, returns a list. |
exists(string Query) |
Returns true if a matching node exists. |
The AussomJavaObject returned by lookup() wraps the underlying JavaFX
Node. You can call JavaFX methods on it using AJI:
node = testfx.lookup("#myLabel");
text = node.invoke("getText");
node.invoke("setText", "new text");
All assertion methods throw an exception on failure, which causes the
test to fail in the aunit framework.
| Method | Description |
|---|---|
verifyVisible(string Query) |
Assert the node is visible. |
verifyInvisible(string Query) |
Assert the node is not visible. |
verifyEnabled(string Query) |
Assert the node is enabled. |
verifyDisabled(string Query) |
Assert the node is disabled. |
verifyHasText(string Query, string Text) |
Assert a labeled node has text. |
verifyExists(string Query) |
Assert a node exists. |
verifyNotExists(string Query) |
Assert no node matches. |
verifyFocused(string Query) |
Assert the node has focus. |
The verifyHasText() method works with any labeled control including
Label, Button, CheckBox, RadioButton, Hyperlink, and
ToggleButton.
| Method | Description |
|---|---|
sleep(int Millis) |
Pause execution for Millis milliseconds. |
Always set node IDs on controls you want to test. The #id query
is the most reliable way to target nodes.
Call show(false) on the app so the test thread is not blocked.
The false argument tells show() not to block the calling thread.
Close the app on the FX thread. Use fx.runLater(::closeApp) in
@After to avoid threading errors.
Use headless mode in CI. Headless tests run faster and do not require a display server.
Add testfx.sleep() for timing-sensitive tests. If a test depends
on an animation or async operation completing, insert a sleep to give
it time to finish.
Use exists() for conditional logic. Unlike verifyExists() which
throws on failure, exists() returns a bool that you can use in
if-statements.