Tutorials
Headless Mode
Dolphin can run a command on a hidden Windows desktop with dolphin-run.
dolphin-run pytest tests/ -v
The wrapper creates a desktop named DolphinHidden, sets DOLPHIN_HEADLESS=1 for the child process, starts the command there, waits for it to finish, and closes its desktop handle.
When To Use It
Use dolphin-run for test subsets that have been checked on a hidden desktop, especially smoke tests that mostly launch, inspect, read state, and collect artifacts.
For many end-to-end suites, the safer default is a normal interactive Windows runner:
pytest tests/ -v
GitHub Actions windows-latest and many self-hosted Windows agents already provide an interactive session. Start there before adding hidden-desktop execution.
Activation Paths
Run the whole test command on the hidden desktop:
dolphin-run pytest tests/ -v --dolphin-backend=uia
Force a Desktop instance to use hidden mode:
from dolphin_desktop import Desktop
desktop = Desktop(hidden=True)
Use visible mode even when DOLPHIN_HEADLESS is set:
desktop = Desktop(hidden=False)
Important Limitations
Hidden desktops are a Windows isolation primitive. They are not the same as a browser's headless mode.
Known limitations:
- MSIX, UWP, Microsoft Store apps, and some system apps may launch through broker processes on the default desktop instead of
DolphinHidden. - Mouse-based actions can fail when Windows refuses to move the physical cursor on a non-input desktop.
- UAC prompts appear on the secure desktop and cannot be automated by Dolphin.
- Clipboard behavior can be surprising because the window station and desktop focus both matter.
If a test uses many physical mouse actions, run it on a visible interactive desktop unless you have verified that the target app and actions work under dolphin-run.
Headless-Safe API — invoke() and Friends
Under the hood, Locator.click(), .hover(), .drag_to(), and other
"input-simulation" actions call pywinauto's click_input(), which
uses win32api.SetCursorPos to move the physical cursor. That
requires an input desktop — a desktop currently attached to the
hardware mouse/keyboard — which DolphinHidden is not. Under
dolphin-run those calls explode with pywintypes.error: (2, 'SetCursorPos', ...).
Dolphin exposes a parallel family of programmatic actions that dispatch the same UI event without ever moving the mouse. They route through Windows UI Automation patterns (or the equivalent Java Access Bridge actions on Swing) — the same API a screen reader uses.
| Physical (needs input desktop) | Programmatic (headless-safe) | UIA pattern |
|---|---|---|
.click() on a button |
.invoke() |
InvokePattern |
.click() on a checkbox |
.toggle() |
TogglePattern |
.click() on a radio button |
.select() |
SelectionItemPattern |
.click() on a list item |
.select() |
SelectionItemPattern |
.click() on a tab |
.select() |
SelectionItemPattern |
.click() on a tree expander |
.expand() / .collapse() |
ExpandCollapsePattern |
.click() on a combo arrow |
.expand() / .collapse() |
ExpandCollapsePattern |
.type_text("hello") |
.set_value("hello") |
ValuePattern |
Same method names on JABLocator (Java Swing) — they route through
JAB's doAccessibleActions("click" / "toggle" / "expand" / …) and
setTextContents.
When to pick which
click() / type_text() — input simulation
- Fires real hover/mouseover handlers, keyboard-down handlers, focus-lost events. What a human would trigger.
- Better for E2E — catches bugs where hover state matters, real- time input validation runs on every keystroke, focus behaviour is under test.
- Requires a real desktop (dev box, headed CI runner, RDP session
that stays connected). Fails under
dolphin-run.
invoke() / toggle() / set_value() — programmatic
- Fires only the "action" event — the button's
OnClick, the checkbox's state-change, the text field'sTextproperty. No hover, no keystrokes, no focus dance. - Better for smoke tests + state assertions — runs anywhere,
including
dolphin-runand CI hosts without a display. - Fails with
UnsupportedPatternErrorif the element does not implement the required UIA pattern (no silent fallback to mouse — the difference between "programmatic worked" and "we moused instead" MUST stay visible in the test).
Choosing at the test level
Concrete guidance for the common cases:
# Smoke test — assertion-only, needs to run under dolphin-run
def test_sign_in_updates_status(desktop):
app = desktop.launch("myapp.exe")
win = app.window(title="Login")
win.edit(auto_id="txtUser").set_value("alice") # no keystrokes
win.edit(auto_id="txtPassword").set_value("secret")
win.button(name="Sign In").invoke() # no mouse
# Auto-waits — no sleep(), no assumption about handler timing.
win.get_by_automation_id("status").wait_for_text("Signed in as alice")
# Full E2E — hover effects and keystroke handlers under test
def test_sign_in_full_ux(desktop):
app = desktop.launch("myapp.exe")
win = app.window(title="Login")
user = win.edit(auto_id="txtUser")
user.click() # focus via mouse
user.type_text("alice") # real keystrokes
win.edit(auto_id="txtPassword").type_text("secret")
win.button(name="Sign In").click() # real mouse
win.get_by_automation_id("status").wait_for_text("Signed in as alice")
Two suites for one workflow is fine — the E2E flavour catches "our
button's hover effect broke", the smoke flavour catches "the login
API stopped working" and runs on every commit under dolphin-run.
Auto-waiting counterparts — no sleep()
Programmatic actions still race the app's event queue: .invoke()
returns as soon as the UIA dispatch is queued, not when the action
handler has finished. The naive fix is sleep(N); dolphin's
supported fix is the wait_for_* family — polls the same state a
manual assertion would, returns the moment it matches, times out
with the current value embedded in the error:
| Assertion pattern | Replace with |
|---|---|
sleep(0.5); assert "X" in loc.text() |
loc.wait_for_text("X") (substring, default) |
sleep(0.5); assert loc.text() == "X" |
loc.wait_for_text("X", contains=False) (exact) |
sleep(0.5); assert re.match(pat, loc.text()) |
loc.wait_for_text(text_re=pat) |
sleep(0.5); assert loc.is_checked() |
loc.wait_for_checked() |
sleep(0.5); assert not loc.is_checked() |
loc.wait_for_checked(checked=False) |
Same methods on JABLocator — plus an extra source="description"
kwarg for the Swing-specific case where a JLabel publishes its
dynamic value through AccessibleContext.setAccessibleDescription
(as the bundled Swing demo does):
status = win.get_by_role("label", name="Status")
win.get_by_role("push button", name="Save").invoke()
status.wait_for_text("Saved", source="description", timeout=5)
sleep() is not banned outright — it stays available for real-time
delays (rate-limited APIs, animations that must run for a fixed
duration). But if the test is waiting for a UI state change, the
wait_for_* primitive is the right tool.
Error surface — UnsupportedPatternError
Every programmatic method raises
dolphin_desktop.UnsupportedPatternError when the target
does not implement the required pattern. The error message names
the pattern, the alternative primitive to try, and the physical
fallback — no need to look anything up:
UnsupportedPatternError: invoke() called on an element that does not
expose InvokePattern (wrapper 'UIAWrapper' has no 'invoke' method)
hint: for checkboxes use .toggle(); for radio buttons / list items
use .select(); for tree nodes use .expand() / .collapse(); for headed
E2E fallback use .click()
Same shape on JABLocator when the JVM does not expose the required
AccessibleAction.
pytest Flag
The pytest plugin also exposes --dolphin-headless:
pytest tests/ -v --dolphin-headless
This creates Desktop(hidden=True) through the fixture. For full UIA visibility of hidden-desktop windows, prefer running the entire command through dolphin-run.
Skipping Tests In Headless Mode
import os
import pytest
pytestmark = pytest.mark.skipif(
os.getenv("DOLPHIN_HEADLESS") == "1",
reason="This test requires the visible desktop",
)
Artifacts
Artifacts still use the normal pytest options:
dolphin-run pytest tests/ -v --dolphin-trace=always --dolphin-video=keepfailedonly
Traces go to dolphin-traces/, videos to dolphin-videos/, and screenshots to dolphin-screenshots/ when enabled.
Next: CI Setup.