Complete reference for the Plushie test framework. For a narrative introduction, see the Testing guide.

Setup

# test/test_helper.exs
Plushie.Test.setup!()
ExUnit.start()

setup!/0 starts the shared renderer session pool, configures ExUnit exclusions for backend-specific tests, and registers cleanup hooks. The pool multiplexes test sessions over a single renderer process (mock/headless) or spawns per-test renderer processes (windowed).

See Plushie.Test for setup options including :pool_name and :max_sessions (default: max(schedulers * 8, 128)).

Test cases

ModulePurpose
Plushie.Test.CaseFull app testing (starts Runtime + Bridge per test)
Plushie.Test.WidgetCaseSingle widget testing in a harness app

Plushie.Test.Case

use Plushie.Test.Case, app: MyApp

Starts a fresh app instance before each test. Imports all helpers. Tests support parallel execution. Add async: true to your test module to run concurrently (the session pool handles isolation). On teardown, checks prop validation diagnostics and raises if any are found.

Plushie.Test.WidgetCase

use Plushie.Test.WidgetCase, widget: MyWidget

setup do
  init_widget("widget-id", prop1: "value")
end

Hosts the widget in a parameterized harness app (window > column > widget). All standard helpers are available. Additional helpers:

FunctionDescription
last_event/0Most recently emitted WidgetEvent, or nil
events/0All emitted events, newest first

These are the only WidgetCase-specific helpers.

Helpers by category

All imported automatically by Plushie.Test.Case and Plushie.Test.WidgetCase. See Plushie.Test.Helpers for full specs.

Queries

FunctionDescription
find(selector)Find element, return nil if not found
find!(selector)Find element, raise if not found
find_by_role(role)Find by accessibility role
find_by_label(label)Find by accessibility label
find_focused()Currently focused element
text(element)Extract display text from an element

Interactions

FunctionWidget typesEvent produced
click(selector, opts)button, any clickable:click
type_text(selector, text, opts)text_input, text_editor:input
submit(selector, opts)text_input:submit
toggle(selector, opts)checkbox, toggler:toggle
toggle(selector, value, opts)checkbox, toggler:toggle with specific value
select(selector, value, opts)pick_list, combo_box, radio:select
slide(selector, value, opts)slider, vertical_slider:slide
scroll(selector, dx, dy, opts)scrollable:scroll
paste(selector, text, opts)text_input, text_editor:paste
canvas_press(selector, x, y, opts)canvas:press (unified pointer)
canvas_release(selector, x, y, opts)canvas:release (unified pointer)
canvas_move(selector, x, y, opts)canvas:move (unified pointer)
press(key)n/aPlushie.Event.KeyEvent
release(key)n/aPlushie.Event.KeyEvent
type_key(key)n/apress + release
move_to(x, y)n/acursor position
pane_focus_cycle(selector, opts)pane_grid:pane_focus_cycle

All interactions are synchronous. They wait for the full update cycle to complete before returning.

Multi-window interactions

Target a specific window using window-qualified selectors or the window: option:

click("settings#save")                         # window qualifier in selector
click("#save", window: "settings")             # explicit window: option
type_text("settings#name", "hello")            # qualifier works everywhere
type_text("#name", "hello", window: "settings") # equivalent

Without either, an ambiguous ID that exists in multiple windows raises an error.

Key name parsing

Key names are case-insensitive. Named keys use PascalCase internally:

  • Named keys: "Tab", "ArrowRight", "Escape", "Enter", "Backspace", "Delete", "PageUp", "PageDown", "Home", "End", "Space"
  • Single characters: lowercased ("s", "a", "1")
  • Modifier combos: "Ctrl+s", "Shift+ArrowUp", "Alt+F4"
  • Modifiers: shift, ctrl, alt, logo, command

Assertions

MacroDescription
assert_text(selector, expected)Widget displays expected text
assert_exists(selector)Widget is in the tree
assert_not_exists(selector)Widget is not in the tree
assert_model(pattern)Model matches pattern
assert_role(selector, role)Accessibility role matches
assert_a11y(selector, expected)Accessibility props match
assert_no_diagnostics()No prop validation warnings

State inspection

FunctionDescription
model()Current app model
tree()Normalized UI tree
tree_hash(name)Capture structural tree hash
screenshot(name, opts)Capture pixel screenshot
save_screenshot(name, opts)Save screenshot as PNG

Async and effects

FunctionDescription
await_async(tag, timeout)Wait for tagged async task to complete
register_effect_stub(kind, response)Stub a platform effect by kind atom (e.g. :file_open)
unregister_effect_stub(kind)Remove an effect stub
reset()Re-initialise the app from scratch (stops and restarts the full supervision tree)

Effect stubs intercept effects at the renderer and return controlled responses. They register by kind (the operation type atom like :file_open, :clipboard_write), not by tag. A stub applies to all effects of that kind regardless of which tag they use. Stubs are scoped to the test session and auto-cleaned on teardown.

reset/0 is expensive. It stops the entire Plushie supervisor tree and starts a fresh instance. Use it when you need a guaranteed clean slate mid-test. For most tests, the per-test setup from Plushie.Test.Case is sufficient.

Selector syntax

FormMatches
"#widget_id"Local widget ID (# prefix required)
"#scope/path/id"Exact scoped path
"window_id#widget_id"Widget in a specific window
"window_id#scope/path/id"Scoped path in a specific window
{:text, "Save"}Widget displaying this text (depth-first)
{:role, :button}Widget with accessibility role
{:label, "Name"}Widget with accessibility label
:focusedCurrently focused widget

The window_id#path form scopes the selector to a specific window. "main#save" finds widget "save" only in window "main". "main#form/save" finds the scoped widget "form/save" in window "main". The window qualifier works with all ID-based helpers (find, click, assert_text, etc.).

Bare strings without a # prefix are not valid selectors and raise ArgumentError. Use {:text, "..."} for text content matching.

Backend capabilities

Tests run against one of three backends. Selection: PLUSHIE_TEST_BACKEND env var or config :plushie, :test_backend.

BackendSpeedRenderingScreenshotsEffects
:mock~msProtocol onlyHash onlyStubs only
:headless~100msSoftware renderingPixel-accurateStubs only
:windowed~secondsGPU renderingPixel-accurateReal

The mock backend uses focus + space for click simulation and synthetic events for canvas/select. All backends use the real renderer binary and real wire protocol.

Tests are backend-agnostic by default. The same assertions work on all three. Use tags to restrict tests to specific backends when they depend on rendering capabilities:

@tag backend: :headless    # runs in headless + windowed, skipped in mock
@tag backend: :windowed    # runs only in windowed

Backend capability is hierarchical: mock < headless < windowed. A test tagged :headless runs in both headless and windowed mode but is excluded from mock mode. Untagged tests run on all backends.

Screenshots and tree hashes

assert_tree_hash("initial-state")     # structural tree comparison
assert_screenshot("styled-view")       # pixel comparison

Golden files are stored in test/snapshots/ (tree hashes) and test/screenshots/ (pixel hashes). First run creates the golden file; subsequent runs compare against it.

Update golden files when the UI intentionally changes:

PLUSHIE_UPDATE_SNAPSHOTS=1 mix test     # tree hashes
PLUSHIE_UPDATE_SCREENSHOTS=1 mix test   # pixel screenshots

These are separate environment variables.

Animation testing

The mock backend resolves renderer-side transitions instantly. Props snap to their target values without interpolation. The headless backend runs real interpolation; use Command.advance_frame/1 to step through frames deterministically. The skip_transitions helper fast-forwards all in-flight transitions to completion in a single call.

.plushie scripting format

Declarative test scripts with a header and instruction list:

app: MyApp
viewport: 800x600
theme: dark
backend: mock
-----
click "#save"
type_text "#editor" "Hello"
expect "Hello"
screenshot "after-hello"
wait 500
assert_text "#count" "3"

Header fields: app: (required), viewport: (default 800x600), theme:, backend: (default mock).

Instructions:

InstructionDescription
click SELECTORClick a widget
type_text SELECTOR TEXTType into a widget
type_key KEYPress and release a key
press KEYKey down
release KEYKey up
move_to X YMove cursor
toggle SELECTOR [true|false]Toggle checkbox
select SELECTOR VALUESelect from list
slide SELECTOR VALUEMove slider
expect TEXTAssert text appears in tree
screenshot NAMECapture screenshot
assert_text SELECTOR TEXTAssert widget text
assert_model EXPRAssert model matches
wait MSPause for milliseconds
mix plushie.script                           # run all in test/scripts/
mix plushie.script path/to/test.plushie      # run specific script
mix plushie.replay path/to/test.plushie      # replay with real windows

See also