e2e-spec
Write, run and debug the WebdriverIO + tauri-driver end-to-end specs of EasyTask (e2e/), and measure UI lag in the real app. Use when a user flow is added or changed, when an e2e test fails, or to mea
SKILL.md.claude/skills/e2e-spec/SKILL.md
GitHubWebdriverIO E2E spec (EasyTask)
e2e/wdio.conf.ts builds the debug app (tauri build --debug --no-bundle), creates a temporary EASYTASK_DATA_DIR (real data is never touched), starts tauri-driver (port 4444) per session and drives the real app: WebView2 + msedgedriver on Windows, WebKitGTK + WebKitWebDriver under xvfb on Linux. Mocha bdd, timeout 120 s, waitforTimeout 10 s, maxInstances: 1. Specs are e2e/specs/**/*.e2e.ts. Failure screenshots and logs go to e2e/output/.
Running it
npm run e2e # build + all specs
E2E_SKIP_BUILD=1 npm run e2e # reuse the existing debug binary
npm run e2e:typecheck # tsc -p e2e/tsconfig.json (also run by CI, "frontend" job)
npm run ui-tour # screenshot tour (e2e/ui-tour), not part of the regular run
- Other env vars read by
wdio.conf.ts:E2E_APP=<exe>(custom binary),E2E_NATIVE_DRIVER=<path>,E2E_EDGE_ARGS(extra WebView2 args),E2E_DRIVER_LOG=1(verbose msedgedriver log). - Windows: run from a NON-elevated terminal. In an Administrator shell WebView2 does not open its debug port ("DevToolsActivePort file doesn't exist").
e2e/run-as-basic-user.cmdruns the suite with a limited token. For the same reason the Windows e2e CI job runs only onworkflow_dispatch; Linux runs on every push. The user parked that issue: do not reopen it unless asked. - Linux CI e2e can flake: rerun the failed jobs before investigating.
- CI e2e job is
continue-on-error; it uploadse2e/outputase2e-output-<os>. - Known: locally "empties the trash" fails on the Windows dev build (passes on Linux CI); there is no e2e for export/import yet (native dialogs, see the manual checklist in
e2e/README.md). - Keep the current @wdio major (9): the 10 upgrade was left out on purpose (needs expect-webdriverio 8 and rewriting e2e code:
executeAsyncis gone).
Helpers (e2e/helpers.ts)
currentLang()/trIn(lang, key, vars)/tr(key, vars): read<html lang>and look labels up insrc/i18n/locales(never hard-code UI strings).byLabel(label)/allByLabel(label): element(s) byaria-label.byText(text, scope?): button/radio/menuitem/tab whose visible text is exactlytext.addTaskButton(scope): the "+" add-task button of a section card.groupCard(label)/newSectionInGroup(group, title)/directTasks(group)/groupAddTaskButton(group)/addTaskToGroup(group, text): a group card ([data-group-card]), a new section from the group menu (there is no section button), and the direct tasks area with its "Nuovo task" button.menuButtonPredicate()/menuButton(scope): the "..." menu button ("Apri menu" or "Menu: <name>").topDialog(): last opendialog/alertdialog.waitForApp(): waits until#roothas rendered.typeInto(input, value): click (retried, reports what covers the field) + setValue, with a native-setter fallback for WebKitWebDriver dropping Shift.createWorkspace(name)/openWorkspace(name): home page flow (create retries the click until the dialog opens).createFromSidebar(buttonKey, submitKey, name): folder/note dialogs with anamefield.treeRow(name)/sectionCard(title): sidebartreeitem/[data-section-card]by name.openContextMenu(target): right click, returns the[role=menu].openSubmenu(menu, triggerText, item): Radix submenu, with ArrowRight fallback (hover is unreliable on WebKitGTK).domClick(element): click through the DOM when a tooltip covers the target on WebKitGTK.cssColor,compose,contrastRatio,backgroundOf,inTheme(theme, fn): computed colors and WCAG contrast in light/dark.dataDir()/dbUrl()/sql(kind, query, values): app data folder and direct SQLite access through the page's sql plugin (assert what was saved).
Writing a spec
- New file
e2e/specs/<topic>.e2e.ts; imports from@wdio/globals($,$$,browser,expect) and../helpers. - Specs run in separate sessions, one at a time, on the SAME data folder: use unique names (
"Feat Workspace","Audio Note"...) and do not depend on file order. Inside a spec,its run in order and build on each other (create workspace, open it, act, assert). - Start with
waitForApp(), thencreateWorkspace/openWorkspace. - Select by role/
aria-label/visible text viatr(key); use xpath only for structure (treeRow,sectionCard). PreferwaitForDisplayed/waitUntil({ timeoutMsg })overpause; always give atimeoutMsg. - Check persistence with
sql(...)(seesavedOrderinfeatures.e2e.ts) and wait for the write before undo/reload. - Toasts:
[data-sonner-toast]; the automations spec waits for old toasts to disappear first (noToasts) so they cannot cover targets or be confused with new ones. - Run
npm run e2e:typecheck, then the suite (non-elevated terminal on Windows).
Pitfalls
- Opening submenus, hovering and tooltips behave differently on WebKitGTK: use
openSubmenu/domClick. - Right after start the home page can still redraw and swallow a click: the helpers retry, do the same in new helpers.
- The driver does not share the webview profile across sessions: localStorage (theme) is lost on restart.
- Unit tests are separate (vitest + jsdom,
npm run test). jsdom matches:focus-visibleon every focus, so focus-visible behaviour needs the real app (e2e) or a stub.
Measuring UI lag in the real app
- Scratch spec (do not commit) with a wdio config extending
e2e/wdio.conf.ts(seee2e/ui-tour/wdio.ui-tour.conf.ts:{ ...base, specs: [...] }). EASYTASK_DATA_DIRpointing to a COPY of the user's db (never the original inDocuments\EasyTask\easytask.db). NoteonPrepareoverrides it with a fresh temp dir: set it so the copy is used (or adapt the scratch config); verify before trusting numbers.E2E_APP=<exe>andE2E_SKIP_BUILD=1.- Drag with
browser.action("pointer")in ~40 steps; in the page recordrequestAnimationFramedeltas and aPerformanceObserver("longtask"). Compare frame mean/p95 and long task count before/after the change. - Pitfall:
src-tauri/target/debug/EasyTask.exemay be thetauri devbuild (Vite dev server, React dev mode, much slower). For production-like numbers:CARGO_TARGET_DIR=<short path> npm run tauri -- build --debug --no-bundle(long paths fail with LNK1104), then pointE2E_APPat that exe.