eguidev is an automation library for egui apps. It is like Playwright, but it runs inside your app process and reads real widget state instead of screen pixels. You tag each widget with a string id. Luau scripts then use that id to inspect the widget, click, type, wait, and take screenshots. Each action runs in step with the event loop of the app.
Write these scripts as a checked-in smoketest suite, or run one script at a time during development. You can also give the same surface to an AI agent through MCP.
Join the Discord server for discussion and release updates.
A script refers to each widget by the id you gave it. Each action waits until the widget is ready for input, and then settles the UI before it returns. Scripts therefore need no sleeps. This example is part of a checked-in smoketest for the demo app:
eguidev.fixture("basic.empty")
eguidev.widget("basic.name"):type_text("Luau", { clear = true })
eguidev.widget("basic.enabled"):set_value(true)
eguidev.widget("basic.intensity"):set_value(73.25)
eguidev.widget("basic.submit"):click()
local status = eguidev.widget("basic.status")
:expect({ value_text_contains = "Saved Luau" })
assert(status ~= nil, "submit should update status")Scripts are strict Luau. eguidev checks each script before it runs, and the
sandbox has no filesystem, network, or module imports. eguidev adds one global,
eguidev. You can run the same script in three ways:
edev eval script.luauruns one script and prints its structured result.edev smokeruns your full suite against one live app.script_evalover MCP lets an agent drive the app with its own scripts.
- Widget state -- the role, label, value, geometry, and hierarchy of each tagged widget, captured at every frame boundary.
- Real input -- clicks, typing, drags, and scrolls, injected into the event loop of the app.
- Waits -- typed conditions and predicates over live state, plus
settle()for queued work. A bad wait is the main cause of flaky UI automation, so eguidev supplies one shared condition model. - Assertions --
Widget:expect(...)checks state, relative geometry, hierarchy, text fit, and painter output. eguidev records each call. - Screenshots and pixels -- capture one viewport or one widget. Sample exact pixels when a visual claim needs proof.
- Tree dumps -- canonical widget-tree text for one viewport, also available
from the CLI as
edev dump. - Layout diagnostics -- overlap and clipping defects, text measurement, and the identity warnings of egui.
- Fixtures -- named states that your app registers. Each script starts from
a declared baseline.
edev fixtureslists them, andedev fixture NAMElaunches the app into one and stays up for manual work.
edev smoke runs a directory of independent .luau scripts against one managed
app process. These scripts are regression tests, and they are also executable
documentation of your UI:
$ edev smoke
[PASS] 10_basic_form.luau (412ms)
[PASS] 20_menu_and_scroll.luau (508ms)
[FAIL] 30_input_and_events.luau (1203ms): Widget(basic.status) expectation failedEach script sets its own start state with a fixture, and asserts visible
behavior instead of app internals. No script depends on another script.
edev smoke ignores return values: assertions decide pass or fail, and
eguidev.log(...) adds evidence to the result.
Use these flags while you write a suite:
--list [--json]shows the selected scripts and does not launch the app.--only GLOBnarrows the run.--fail-faststops at the first failure.--repeat Nand--until-fail Nfind intermittent failures across rounds.--bundlewrites a failure bundle for each failure. A bundle holds the tree dump, diagnostics, screenshots, script logs, and app output.
Each script result includes the egui id_clash and rect_changed_id warnings
that no script dismissed. edev smoke fails on these warnings by default, so an
intermittent identity bug cannot pass unnoticed. To keep the warnings without a
failure, set [smoke] fail_on_egui_diagnostics = false. A script that causes a
warning on purpose can consume it with Viewport:egui_diagnostics().
edev record OUT.mov runs the same suite and records the app window. This
command works only on macOS. It uses ScreenCaptureKit directly, not an external
recorder program, and writes H.264 video at the native pixel resolution of the
window. The process that runs edev must have Screen Recording permission. The
title of the root viewport selects the window. Use --window-title to select a
different window.
Register the edev launcher as an MCP server. For example, with Claude Code:
claude mcp add eguidev -- edev mcpThe launcher supplies four tools: start, stop, restart, and status. A
successful lifecycle result includes the direct endpoint of the app, and the app
serves script_api and script_eval at that endpoint. The agent surface is a
complete script, not fine-grained RPC, so one call can set up, interact, assert,
and report in one round trip.
This repository also includes an agent skill at
skills/SKILL.md that teaches agents this workflow.
eguidev has three crates, and you add them in this order:
eguidevis the instrumentation library that your app depends on. It compiles for native andwasm32targets.eguidev_runtimeis the native-only embedded runtime. It supplies script evaluation, screenshots, and the in-process MCP server.edevis the CLI and lifecycle launcher. It runs scripts, suites, and fixtures against a managed app process.
1. Instrument your app. Put a DevMcp handle in your app state. Wrap each
viewport frame with frame_scope. Tag each widget with a dev_* helper.
eguidev::frame_scope(&self.devmcp, ui, "root", |ui| {
ui.dev_text_edit("app.name", &mut self.name);
if ui.dev_button("app.submit", "Submit").clicked() { /* ... */ }
});2. Attach the runtime in one bootstrap location.
[dependencies]
eguidev_runtime = "0.1"let devmcp = eguidev_runtime::attach(devmcp);attach returns the inert handle unless Edev supplies EGUIDEV_MCP_ADDR. A
usual cargo run therefore starts a usual app, with no server and no change to
presentation.
3. Configure the launcher. Install the CLI with cargo install edev. Then
put a .edev.toml file in your project root:
[app]
command = ["cargo", "run", "--locked", "-p", "myapp"]
[smoke]
suite_dir = "smoketest"On macOS, the default background presentation continues to render covered
windows. It adds no Dock item and does not take the focus. Set
presentation = "foreground" for manual sessions. See
examples/edev.toml for a commented reference of every
option.
Run edev with no arguments to see the full command list.
- Luau scripting: the API reference is
eguidev.d.luau, whichedev docsand thescript_apiMCP tool return without change. For the language itself, see luau.org. - Worked example: the
eguidev_demoapp and its suite insmoketest/. Seedocs/examples/README.md.
MIT