name: e2e description: Write and maintain termic's automated end-to-end tests (WebdriverIO driving the real macOS window). Use whenever you develop a NEW feature (add a spec that exercises its flow), CHANGE an existing feature (update its spec), or need to verify a UI flow before declaring done. This is how termic avoids UI regressions. Replaces the old automation-bridge driving skill.
Authoring & maintaining termic e2e tests
Automated, repeatable tests that launch the real Termic window, click through real flows, read real app state, and screenshot. The point is regression safety: every new feature with a UI/flow surface gets a spec, and every change to an existing feature updates its spec. Don't skip it.
Full architecture + prod-safety rationale: docs/e2e-tests.md.
The workflow (do this every time)
New feature → add e2e/specs/<feature>.e2e.ts covering its main
user-observable outcome(s). Changed feature → open that feature's spec,
adjust the assertion/selectors to the new behavior (keep asserting the
outcome, not incidental markup), and re-run. Before declaring done on any
UI-affecting change → make e2e must be green.
Run
make e2e # build the --features e2e binary + run the whole suite
Iterating on spec files only? Skip the rebuild:
npm run test:e2e # just runs wdio against the last-built binary
Rebuild (npm run e2e:build, or make e2e) after any Rust or frontend
change — the frontend is embedded in the e2e binary. Screenshots land in
.e2e/artifacts/ (gitignored). Local Mac only; there is no CI job by design
(the tests launch a GUI window).
Never build the e2e binary with a bare cargo build — that produces a binary
that points at the (unrunning) dev server and the window comes up blank. Always
go through npm run e2e:build / make e2e (it runs tauri build, which
embeds the frontend). If a run shows url: about:blank / a white window, this
is the cause.
Writing a spec
Use the shared helpers in e2e/helpers.ts so specs stay short and a UI change is a one-place fix. Reference example: e2e/specs/smoke.e2e.ts.
import { waitForAppShell, clickByText, waitForText } from "../helpers.js";
describe("archive a task", () => {
it("moves the task to History", async () => {
await waitForAppShell();
await clickByText("Archive");
await waitForText("No archived tasks."); // auto-retries; no sleep
});
});
One it = one user-observable outcome. All its in a file share ONE launched
window (boot once, assert many) — order them so earlier tests don't leave state
that breaks later ones, or reset between them.
Reading real app state (prefer this over DOM scraping)
The e2e binary exposes window.__termic (stores + ipc + invoke — same handle
the dev bridge uses; enabled via VITE_E2E=1, stripped from real release
builds). Read state or drive real IPC through browser.execute:
// Read store state
const names = await browser.execute(() =>
window.__termic!.useApp.getState().workspaces.map((w: any) => w.name));
// Set up state fast by driving the app's own IPC (no clicking through wizards)
const wsId = await browser.execute(async () => {
const t = window.__termic!;
const proj = t.useApp.getState().projects.find((p: any) => p.name === "fixture-repo");
const ws = await t.invoke("workspace_open_repo",
{ projectId: proj.id, cli: "fakeagent", name: null });
await t.useApp.getState().loadAll();
t.useApp.getState().setActiveWorkspace(ws.id);
return ws.id;
});
requireTermicApi() asserts the handle is present (fails loudly if you ran an
old/non-e2e binary).
Stability rules (non-negotiable — this is what keeps the suite non-fuzzy)
- Never sleep. No
setTimeout/ fixed waits. Usebrowser.waitUntil, thewaitFor*helpers, or auto-retryingexpect. Every wait is a condition. - Assert on state / DOM text, not pixels. Screenshots are for humans to eyeball, never for assertions.
- Terminal content is NOT in the DOM. xterm renders to a WebGL canvas, so
innerTextnever contains PTY output no matter how long you wait. Assert terminal activity via store state — e.g.tab.lastOutputAt(bytes flowed) ortab.liveTitle(the agent's OSC title) read throughwindow.__termic. All OTHER UI (sidebar, tabs, dialogs, Git panel) is normal DOM.scripts/fake-agent.shmimics claude: it drives the OSC title with claude's glyphs (✳idle / Braille spinner working). NOTE:tab.workState === "working"won't flip from a rawipc.ptyWrite— termic gates the working indicator on a real submit through its input path, so assertliveTitlefor OSC-title checks, notworkState. Seee2e/specs/task-spawn.e2e.ts. - Semantic selectors. Match by role / visible text (
clickByText). Add adata-testidonly where text is ambiguous or localized. Never depend on generated class names. - Deterministic fixtures. Runs use the isolated
.e2e/profile(welcomed+ thefixture-repoproject + the zero-tokenfakeagent). Agent flows usefakeagent(scripts/fake-agent.sh, real PTY, zero tokens). Don't depend on state a previous test left behind.
Fixtures / isolation
wdio.conf.ts launches the app against TERMIC_DATA_DIR=.e2e/profile, a
throwaway profile seeded once (the same one the ad-hoc bridge used), so a run
never touches your real termic_dev data. Paths round-trip canonicalized on
projectAdd (symlinks resolved), so match projects by name, not by the path
you passed in.
Debugging a failing spec
- See what's actually on screen: the spec should
saveScreenshotinto.e2e/artifacts/; open it. A blank white window ⇒ the about:blank build issue above. - Wrong webview / empty DOM: enumerate handles —
await browser.getWindowHandles()thenswitchToWindow(h)and loglocation.hrefper handle. The app content is themainhandle at atauri://URL, notabout:blank. - Occluded window: if the window is on another Space / behind others,
document.visibilityStateishiddenand rAF is frozen.browser.execute, IPC, and store reads still work; only rAF-driven visual updates stall. window.__termicundefined: you're running a non-e2e binary —make e2e.
Ad-hoc / exploratory driving (not a written test)
For one-off manual poking where you don't (yet) want a spec, the dev automation
bridge still exists (src-tauri/src/automation.rs, TERMIC_AUTOMATION=1 under
tauri dev) — see docs/automation.md. But
anything meant to prevent a regression belongs in a spec here, not a throwaway
eval.
Maturity caveat
@wdio/tauri-service + tauri-plugin-wdio-webdriver are young (1.x, 2026) and
maintained by the WebdriverIO org. package.json pins @wdio/native-utils to
2.5.0 via overrides to work around a broken pin in tauri-service 1.2.0. If a
future upgrade breaks, pin the @wdio/* packages and tauri-plugin-wdio-webdriver
in lockstep to the last-known-good set.
TDD Red-Green-Refactor
Testing
Skill that guides Claude through the complete TDD cycle.
Web Accessibility Audit
Testing
Performs a comprehensive web accessibility audit following WCAG standards.
UAT Test Case Generator
Testing
Generates structured and comprehensive user acceptance test cases.