Basics

Guides

API Reference

Menu

Basics

Guides

API Reference

TestFx Usage Guide

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.

Getting Started

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:

  1. Optionally call testfx.setHeadless(true) for headless mode.
  2. Create your UI with fx.fxApp() and show it with app.show(false).
  3. Call testfx.setup(app) to initialize the test robot.
  4. Interact and assert.
  5. Call testfx.cleanup(), close the app, and shut down JavaFX.

Node Query Strings

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

Headed vs. Headless Mode

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.

Simple Example

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

Intermediate Example

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);
    }
}

Advanced Example

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);
    }
}

API Reference

Setup and Teardown

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.

Mouse Actions

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.

Keyboard Actions

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").

Drag and Drop

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");

Scroll

Method Description
scrollUp(int Amount = 1) Scroll up by Amount units.
scrollDown(int Amount = 1) Scroll down by Amount units.

Node Lookup

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");

Assertions

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.

Utility

Method Description
sleep(int Millis) Pause execution for Millis milliseconds.

Tips

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