scripts/E2E_TESTING_PLAN.md
Azul's E2E testing system lets users design, run, and export end-to-end
tests entirely from the browser (via the debug server UI) or from the
command line (via AZUL_RUN_E2E_TESTS). The same JSON test format is
used everywhere — browser designer, CI runner, and manual curl scripts.
┌──────────────────────────────────────────────────────────────────┐
│ Browser (debugger.html) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ DOM Inspector │ │ E2E Designer │ │ Test Results │ │
│ │ (existing) │ │ + Run / RunAll │ │ + screenshots │ │
│ └─────────────────┘ └────────┬────────┘ └───────▲────────┘ │
│ │ POST /e2e/run │ JSON │
│ ▼ │ │
│ ┌──────────────────────────────────────────────────┘ │
│ │ Debug HTTP Server (port AZUL_DEBUG) │
│ │ ───────────────────────────────── │
│ │ GET / → serves debugger.html │
│ │ GET /health → health check JSON │
│ │ POST / → existing single-command API │
│ │ POST /e2e/run → run one or many E2E tests (new) │
│ └──────────────────────────────┬───────────────────────────────┘
│ │ │
│ For each test: ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. Clone current app_data (RefAny) │ │
│ │ 2. Optionally set_app_state / set_window_state │ │
│ │ 3. Create StubWindow with CpuBackend │ │
│ │ 4. Execute steps sequentially (same as curl API) │ │
│ │ 5. After each step: collect logs, optional screenshot │ │
│ │ 6. Evaluate assertions → pass / fail │ │
│ │ 7. Close StubWindow, return results │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
CLI mode (no browser):
AZUL_RUN_E2E_TESTS=tests.json ./my_app
→ Starts app headlessly
→ Runs all tests from JSON file
→ Prints cargo-test-style output
→ exit(0) on success, exit(1) on failure
→ Writes logs + screenshots to target/e2e_results/
A single E2E test is a JSON object:
{
"name": "Button click increments counter",
"description": "Verify that clicking the + button increases the counter display",
"setup": {
"window_width": 800,
"window_height": 600,
"dpi": 96,
"app_state": { "counter": 0 }
},
"steps": [
{
"op": "click",
"selector": ".increment-btn",
"screenshot": true
},
{
"op": "wait_frame"
},
{
"op": "assert_text",
"selector": ".counter-display",
"expected": "1"
},
{
"op": "take_screenshot"
}
]
}
A test file can contain a single test or an array of tests:
[
{ "name": "Test 1", "steps": [...] },
{ "name": "Test 2", "steps": [...] }
]
All existing debug API operations are valid steps (see DEBUG_API.md).
Additionally, E2E tests support assertion operations:
| Operation | Parameters | Description |
|---|---|---|
assert_text | selector, expected | Assert that the text content of a node matches |
assert_exists | selector | Assert that a node matching the selector exists |
assert_not_exists | selector | Assert that no node matches the selector |
assert_node_count | selector, expected | Assert the number of matching nodes |
assert_layout | selector, property, expected, tolerance? | Assert layout property (x, y, width, height) |
assert_css | selector, property, expected | Assert computed CSS value |
assert_screenshot | reference | Compare screenshot against a reference (base64 or filename) |
assert_app_state | path, expected | Assert a field in the serialized app state (dot-notation) |
assert_scroll | selector, x?, y?, tolerance? | Assert scroll position |
{
"step_index": 0,
"op": "click",
"status": "pass",
"duration_ms": 12,
"logs": [...],
"screenshot": "data:image/png;base64,...",
"error": null,
"response": { ... }
}
{
"name": "Button click increments counter",
"status": "pass",
"duration_ms": 156,
"step_count": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps": [ ... ],
"final_screenshot": "data:image/png;base64,..."
}
Goal: Merge debugger1.html and debugger2.html into a single
debugger.html served by the debug server.
Changes:
File: dll/src/desktop/shell2/common/debugger.html (new, merged)
Delete: debugger1.html, debugger2.html
File: dll/src/desktop/shell2/common/debug_server.rs
GET / → serve debugger.html (embedded via include_str!)GET /health → existing health JSON (unchanged)POST / → existing single-command API (unchanged)POST /e2e/run → new: run E2E test(s) and return resultsGoal: The debug server can accept E2E test JSON, create a StubWindow, execute steps, and return structured results.
Changes:
File: dll/src/desktop/shell2/common/debug_server.rs
DebugEvent::RunE2eTests { tests: Vec<E2eTest> } variantE2eTest, E2eStep, E2eTestResult, E2eStepResult structsRunE2eTests:
app_data from the running applicationrayon or sequentially):
a. Create StubWindow::new(...) with cloned state
b. Apply setup (window size, DPI, app_state)
c. For each step: dispatch as if it were a regular debug command,
collect response, logs, optional screenshot
d. Evaluate assertions
e. Close StubWindowVec<E2eTestResult>File: dll/src/desktop/shell2/stub/mod.rs
StubWindow::run_e2e_test(test: E2eTest) -> E2eTestResult
process_debug_event() logictake_screenshot_base64() for
StubWindow using tiny_skia (or a simpler "layout-only" approach
that renders rectangles + text glyphs to a Pixmap)Goal: Evaluate pass/fail conditions for E2E test steps.
Changes:
dll/src/desktop/shell2/common/e2e_assertions.rs (new)
evaluate_assertion(step, response, layout_window) -> AssertionResultassert_* operation typeassert_text: resolve selector → get node text content → compareassert_exists / assert_not_exists: resolve selector → checkassert_layout: resolve selector → get layout rect → compare with toleranceassert_css: resolve selector → get computed CSS → compareassert_app_state: deserialize app state → navigate dot-path → compareassert_screenshot: pixel-diff against reference image (optional, can be skipped)Goal: The debugger UI can display E2E test results with screenshots.
Changes:
debugger.html
/e2e/run with current test JSONassert_screenshotGoal: AZUL_RUN_E2E_TESTS=file.json ./my_app runs tests and exits.
Changes:
File: dll/src/desktop/shell2/run.rs
AZUL_RUN_E2E_TESTS env var before AZUL_HEADLESStarget/e2e_results/,
exit(0) or exit(1)Output format (stdout):
running 3 e2e tests
test Button click increments counter ... ok (156ms)
test Scroll down loads more items ... ok (342ms)
test Invalid input shows error ... FAILED (89ms)
failures:
---- Invalid input shows error ----
Step 3 (assert_text): expected "Error: invalid", got "Please enter a value"
selector: .error-message
test result: FAILED. 2 passed; 1 failed; 0 ignored
Screenshots and logs written to target/e2e_results/
File: dll/src/desktop/shell2/stub/mod.rs
StubWindow::run_e2e_tests(tests: Vec<E2eTest>) -> Vec<E2eTestResult>
Convenience wrapper that runs multiple tests and returns all results.Goal: StubWindow can produce pixel-accurate screenshots via CpuBackend.
Status: Partially implemented. CpuBackend has #[cfg(feature = "cpurender")]
field for tiny_skia::Pixmap.
Changes:
File: layout/src/cpurender.rs (new or extend existing)
render_to_pixmap(layout_result, resources, width, height, dpi) -> Pixmaptiny_skia::Pixmap:
tiny_skia path fillsab_glyph or pre-rasterized glyphsFile: dll/src/desktop/shell2/stub/mod.rs
StubWindow::take_screenshot() -> Result<String, String>
Uses CpuBackend to render current layout to Pixmap, encode as PNG,
return base64 data URI.Note: Full pixel-perfect CPU rendering is a large task. Initially,
screenshots in headless mode can return a "layout wireframe" (colored
rectangles with text labels) which is sufficient for debugging. Full
rendering can be added later behind the cpurender feature flag.
Each E2E test gets its own StubWindow with a cloned app_data.
This means:
Caveat: If the app_data doesn't implement Clone (it's a RefAny),
we use the serialize → deserialize round-trip to create a copy. If
neither is available, we fall back to the default constructor. This makes
the test non-deterministic unless the test starts with set_app_state.
The E2E test runner drives the StubWindow's event loop directly (no condvar needed). It's synchronous:
for step in test.steps:
1. Inject event into StubWindow
2. Run one iteration of the event loop (process events, tick timers)
3. If step has screenshot: render via CpuBackend → capture
4. Collect logs since last step
5. Evaluate assertion (if applicable)
6. Record step result
This is different from StubWindow::run() which blocks on a condvar.
The E2E runner calls the internal processing methods directly.
When the browser sends "Run All Tests", the server can execute tests
in parallel using std::thread::scope or similar. Each test gets its
own thread with its own StubWindow. Results are collected and returned
as a single JSON array.
The CLI runner (AZUL_RUN_E2E_TESTS) runs tests sequentially by default
but could support --parallel in the future.
| File | Purpose |
|---|---|
common/debugger.html | Unified browser UI (merged from debugger1 + debugger2) |
common/debug_server.rs | HTTP server, event routing, E2E test dispatch |
common/e2e_assertions.rs | Assertion evaluation engine |
stub/mod.rs | StubWindow + CpuBackend + E2E test runner |
run.rs | CLI runner (AZUL_RUN_E2E_TESTS) integration |
layout/src/cpurender.rs | CPU rendering to Pixmap (behind feature flag) |
scripts/DEBUG_API.md | API documentation (existing, updated) |
scripts/E2E_TESTING_PLAN.md | This document |
tests/e2e/*.sh) continue to work unchangedtests/e2e/*.jsonAZUL_RUN_E2E_TESTS)