Expand description
NOTE: This library is an internal crate of the Slint project.
This crate should not be used directly by applications using Slint.
You should use the slint crate instead.
WARNING: This crate does not follow the semver convention for versioning and can
only be used with version = "=x.y.z" in Cargo.toml.
§Preliminary Slint Testing API
This crate provides the preliminary API that we’re developing to enable different user interface (UI) testing scenarios for Slint applications.
To use this functionality, you need to be cautious when importing dependencies since
this crate does not adhere to semver and may introduce breaking changes in any patch release.
Additionally, the version of this crate must match the version of Slint.
To indicate that you specifically want this version, include the = symbol in the version string.
[dependencies]
slint = { version = "x.y.z", ... }
i-slint-backend-testing = "=x.y.z"§Testing Backend
By default, Slint applications will select a backend and renderer suitable for application display on the screen, by means of utilizing a windowing system - if present - or directly rendering to the framebuffer.
For automated testing in CI environments without a windowing system / display, it might still be desirable to run tests. The Slint Testing Backend simulates a windowing system without requiring one: No pixels are rendered and text is measured by a fixed font size.
Use init_integration_test_with_system_time() for integration tests
where your test code requires Slint to provide an event loop, for example when spawning threads
and calling slint::invoke_from_event_loop(). If you want to not only simulate the windowing system but
also the system time, use init_integration_test_with_mock_time() to initialize the backend and then
call mock_elapsed_time() to advance animations and move timers closer to their next timeout.
Use init_no_event_loop() for unit tests when your test
code does not require an event loop. Note that system time is also mocked in this scenario, so use
mock_elapsed_time() to advance animations and timers.
§Preliminary User Interface Testing API
We’re developing APIs to facilitate the creation of automated tests for Slint based UIs. A building block is the ability to introspect and modify the state from within what would be a regular application process.
A regular application might have a main() entry-point like so:
fn main() -> Result<(), slint::PlatformError>
{
let app = App::new()?;
// ... set up state, callbacks, models, ...
app.run()
}In addition, it may be desirable to create an integration test that verifies how the application behaves when simulating user input.
The objective of the user interface testing API provided in this crate is to facilitate locating, mutation, and verifying state of
elements in your UI. ElementHandle provides a view for these elements.
The example below assumes that somewhere in the UI you have declared a Button with the text “Submit” and you may want to verify
how the application behaves when simulation the activation. This is done by locating and triggering it via its accessibility interface,
that every Button implements.
import { Button } from "std-widgets.slint";
component Form {
callback submit();
VerticalLayout {
// ...
Button {
text: "Submit";
clicked => { root.submit(); }
}
}
}
export component App {
callback submit <=> form.submit;
// ...
form := Form {
// ...
}
}#[test]
fn test_basic_user_interface()
{
i_slint_backend_testing::init_no_event_loop();
let app = App::new().unwrap();
// ... set up state, callbacks, models, ...
let submitted = std::rc::Rc::new(std::cell::RefCell::new(false));
app.on_submit({
let submitted = submitted.clone();
move || { *submitted.borrow_mut() = true; }
});
let buttons: Vec<_> = i_slint_backend_testing::ElementHandle::find_by_accessible_label(&app, "Submit").collect();
assert_eq!(buttons.len(), 1);
let button = &buttons[0];
button.invoke_accessible_default_action();
assert!(*submitted.borrow());
}§Simulating events / Asynchronous testing
When testing user interfaces it may be desirable to not only invoke accessible actions on elements, but it may also be useful to simulate touch or mouse input. For example a mouse click on a button is a sequence:
- An initial mouse move event to a location over the button
- A mouse press event.
- In real life, a certain amount of time would elapse now.
- Finally, the user lifts the finger again from the mouse and a mouse release event is triggered.
To simulate this behavior, ElementHandle provides functions such as ElementHandle::single_click() and ElementHandle::double_click().
Since these functions simulate a sequence of events with a period of idle time between the events, these functions are async
and return a std::future::Future, which resolves when the last event in the sequence was sent.
Calling these functions requires running the test function itself as a future and running an event loop in the background.
This can be accomplished using slint::spawn_local(), slint::run_event_loop(), and slint::quit_event_loop(). The following
example wraps the core functions for testing in an async closure:
use slint::platform::PointerEventButton;
use i_slint_backend_testing::ElementHandle;
#[test]
fn test_click()
{
i_slint_backend_testing::init_integration_test_with_system_time();
slint::spawn_local(async move {
slint::slint! {
export component App inherits Window {
out property <int> click-count: 0;
ta := TouchArea {
clicked => { root.click-count += 1; }
}
}
}
let app = App::new().unwrap();
let mut it = ElementHandle::find_by_element_id(&app, "App::ta");
let elem = it.next().unwrap();
assert!(it.next().is_none());
assert_eq!(app.get_click_count(), 0);
elem.single_click(PointerEventButton::Left).await;
assert_eq!(app.get_click_count(), 1);
slint::quit_event_loop().unwrap();
})
.unwrap();
slint::run_event_loop().unwrap();
}After initializing the testing backend with support for using the system time, an async
closure is spawned, which does the actual testing. In the subsequent run_event_loop() call,
the event loop is started, and that will start polling the async closure passed to spawn_local().
In this closure we can now call .await on the future ElementHandle::single_click() returns, which
will keep running the event loop until the click is complete, and then continue with the test function.
§Embedded MCP Server
The testing backend includes an embedded MCP (Model Context Protocol) server that allows AI agents to inspect and interact with a running Slint application in real time. The server provides tools for exploring the UI tree, taking screenshots, clicking elements, dragging, typing, and more. Agents discover the available tools and usage instructions automatically via the MCP protocol — no additional configuration is needed beyond enabling the server.
§Enabling the MCP Server
Set the following environment variables when running your application, and pass
--features slint/mcp to enable the server:
SLINT_EMIT_DEBUG_INFO=1 SLINT_MCP_PORT=8080 cargo run -p my-slint-app --features slint/mcpSLINT_EMIT_DEBUG_INFO=1 is required for element introspection to work (it embeds element
metadata into the compiled UI). SLINT_MCP_PORT controls which port the MCP server listens on.
If SLINT_MCP_PORT is not set, no server is started and there is no runtime overhead.
Do not add mcp to the [features] section of your Cargo.toml — use the --features
flag on the command line instead.
§Running Without a Display
On a machine with no display server (CI, container, agent sandbox) the regular
backend can’t open a window, so take_screenshot and the other tools won’t
work. Set SLINT_BACKEND=headless to run the app under a windowless,
software-rasterized backend instead:
SLINT_EMIT_DEBUG_INFO=1 SLINT_MCP_PORT=8080 SLINT_BACKEND=headless \
cargo run -p my-slint-app --features slint/mcpThe headless backend uses Skia’s software rasterizer when slint/renderer-skia
is enabled, otherwise the built-in software renderer. Suffix the value
(headless-software, headless-skia) to force a specific rasterizer. If
SLINT_BACKEND is unset and the configured graphical backend fails to
initialize (for example because no display is available), Slint falls back to
the headless backend automatically.
This is an unstable, MCP-oriented entry point — the exact value of
SLINT_BACKEND may change between Slint releases. Use it from automation, not
from production code.
§Usage with AI Agents
The simplest approach is to tell the agent to run the application with both environment variables set and then interact with it. For example, in Claude Code:
“Run
SLINT_EMIT_DEBUG_INFO=1 SLINT_MCP_PORT=8080 cargo run -p my-app --features slint/mcpin the background. The app includes a built-in MCP server. Connect to it and toggle the dark mode switch.”
The agent will discover the MCP endpoint, connect, and use the tools to accomplish the task.
When scripting or testing from the command line, use curl to call tools directly — it is
the most reliable way to send JSON-RPC requests to the server:
# Initialize (confirms the server is up and prints available tools)
curl -s -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# List windows
curl -s -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_windows","arguments":{}}}'
# Take a screenshot (response contains a base64-encoded PNG in the "data" field)
curl -s -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"take_screenshot","arguments":{"windowHandle":{"index":"1","generation":"1"}}}}'You can also register the server in your MCP client’s configuration if you prefer:
{
"mcpServers": {
"my-slint-app": {
"type": "streamable-http",
"url": "http://localhost:8080/mcp"
}
}
}For architecture and internals, see docs/development/mcp-server.md.
Re-exports§
pub use testing_backend::get_mocked_time;pub use testing_backend::TestingBackend;pub use testing_backend::TestingBackendOptions;
Modules§
Structs§
- Element
Handle ElementHandlewraps an existing element in a Slint UI. An ElementHandle does not keep the corresponding element in the UI alive. UseSelf::is_valid()to verify that it is still alive.- Element
Query - Use ElementQuery to form a query into the tree of UI elements and then locate one or multiple matching elements.
Enums§
- Accessible
Liveness - This enum represents the different values of the
accessible-live-regionproperty. It indicates that an element is a live region whose content changes should be announced by assistive technologies. - Accessible
Role - This enum represents the different values for the
accessible-roleproperty, used to describe the role of an element in the context of assistive technology such as screen readers. - Layout
Kind - Describes the kind of layout an element represents.
- Orientation
- Represents the orientation of an element or widget such as the
Slider.
Traits§
- Element
Root - Trait for type that can be searched for element. This is implemented for everything that implements
ComponentHandle
Functions§
- init_
integration_ test_ with_ mock_ time - Initialize the testing backend with support for simple event loop.
This function can only be called once per process, so make sure to use integration
tests with only one
#[test]function. (Or in a doc test) Must be called before any call that would otherwise initialize the rendering backend. Calling it when the rendering backend is already initialized will panic. - init_
integration_ test_ with_ system_ time - Initialize the testing backend with support for simple event loop.
This function can only be called once per process, so make sure to use integration
tests with only one
#[test]function. (Or in a doc test) Must be called before any call that would otherwise initialize the rendering backend. Calling it when the rendering backend is already initialized will panic. - init_
no_ event_ loop - Initialize the testing backend without support for event loop.
This means that each test thread can use its own backend, but global functions that needs
an event loop such as
slint::invoke_from_event_looporTimers won’t work. Must be called before any call that would otherwise initialize the rendering backend. Calling it when the rendering backend is already initialized will panic. - mock_
elapsed_ time - Advance the simulated mock time by the specified duration. Use in combination with
init_integration_test_with_mock_time()orinit_no_event_loop().