E2E capture harness (Playwright web/mock)
June 14, 2026 · View on GitHub
Automated E2E scenario runs that save PNG evidence captures of the UI. This is the web-only harness from issue #1609: it runs the React frontend in a plain Chromium browser with mocked Tauri IPC and events, so no Rust backend, live hardware data, or Tauri runtime is required.
Scope
- In scope: deterministic scenario runs + PNG capture artifacts.
- Out of scope: visual snapshot regression / baseline comparison is intentionally not part of this harness. Captures are evidence for human review, not compared against golden images.
- The native Tauri smoke path (tauri-driver/WebDriver) is tracked separately in issue #1610. Note that tauri-driver only supports Linux and Windows; macOS has no WKWebView driver.
Running locally
npm run test:e2e
Playwright starts its own Vite dev server on port 1521 with
vite dev --mode e2e (a regular npm run dev server on 1520 is never
reused). Captures are written to:
test-results/captures/<scenario>.png
The first page load pays Vite's cold module-transform cost (babel + react-compiler) and can take >10 seconds; specs use an extended timeout for the initial render assertion.
How the mocks work
vite --mode e2erewritesindex.htmlto usesrc/main.e2e.tsx. The regularsrc/main.tsxkeeps the production/Tauri app entry static, while the E2E entry installs mocks before dynamically importing the app.src/e2e/mocks/installTauriMocks.tsis the single mock entry point:mockIPC(..., { shouldMockEvents: true })from@tauri-apps/api/mocksintercepts everyinvoke()(generated tauri-specta commands andplugin:<name>|<command>plugin calls) and implementsplugin:event|listen/emit/unlisten.mockWindows("main")fakes the current window label.window.__TAURI_OS_PLUGIN_INTERNALS__is set directly becauseplatform()from@tauri-apps/plugin-osis a synchronous global read, not an IPC call.- The Tauri store plugin is backed by an in-memory
Map, seeded fromsrc/e2e/fixtures/store.ts. - Unhandled commands throw
[e2e-mock] Unhandled invoke: <cmd>so coverage gaps surface immediately when new screens are added to scenarios.
- Fixture data lives in
src/e2e/fixtures/(settings, hardware info, process list, storage health, and a deterministichardware-monitor-updateseries built from fixed sine waves). - Tests push hardware events through
window.__E2E__.emitHardwareUpdate/emitHardwareUpdateSeries, exposed by the mock installer.
Covered scenarios
| Spec | Captures |
|---|---|
e2e/dashboard.spec.ts | dashboard, dashboard-gpu-secondary (GPU tab switch) |
e2e/usage.spec.ts | usage (mixed CPU/RAM/GPU chart) |
e2e/cpu-detail.spec.ts | cpu-detail (info table + per-core charts) |
e2e/insights.spec.ts | insights-main, insights-process |
e2e/settings.spec.ts | settings (General/About sections) |
Insights pins the clock with page.clock.setFixedTime(...) because its
archive query ranges derive from Date.now(); the mocked archive commands
synthesize records from the requested start/end range
(src/e2e/fixtures/archive.ts), so charts stay deterministic.
Pass criteria are DOM-level: fixture content visible via accessible selectors, interactions reflected in ARIA state, and captures saved. Pixels are not compared (no baselines). One targeted style guard exists: the usage spec compares the computed stroke of each chart series against the fixture colors, catching invalid color plumbing that would render series black while remaining "visible" to all other assertions.
Writing a scenario
Specs live in e2e/*.spec.ts (outside src/, so Vitest does not pick them
up). Shared helpers live in e2e/helpers.ts. The shape of a scenario:
await gotoApp(page)— loads/and waits for fixture content (first load can take >10s, see above).- Seed chart history:
await seedHardwareHistory(page). - Navigate with
await navigateTo(page, "<screen>")(clicks the side menu's accessibleopen <type>buttons) and interact via accessible selectors (getByRole("tab", ...), aria-labels, headings). - Save the capture:
await saveCapture(page, "<name>")— writes a full-page PNG (the whole scrollable page, not just the viewport) intotest-results/captures/.
Determinism rules:
- Locked viewport (1280x800),
deviceScaleFactor: 1, dark color scheme,en-USlocale, UTC timezone, reduced motion (seeplaywright.config.ts). - Use fixture data only — never live hardware values.
Render performance smoke
Lightweight render performance checks reuse the same web/mock harness, but run as a separate suite:
npm run test:perf:render
The suite writes a JSON report and Playwright artifacts under:
test-results/render-perf/
These checks target coarse, CI-stable signals such as DOM element count,
Long Task count, and generous interaction timing. They intentionally avoid
strict FPS, CPU, and memory gates. Thresholds are moderate rather than
extremely loose because this suite is meant to catch obvious render regressions
even while it starts as an observation signal. In CI the test-render-perf job
runs only for frontend pull requests, uploads artifacts, and stays outside the
merge gate.
Render memory smoke
Frontend memory-growth checks also reuse the web/mock harness, but they measure Dashboard behavior while mocked Tauri IPC events continue to arrive over time:
npm run test:perf:render-memory
This suite starts a deterministic hardware-monitor-update stream through the
mocked Tauri event path, then samples Chromium through CDP:
Runtime.getHeapUsagePerformance.getMetricsMemory.getDOMCounters
The report is written under:
test-results/render-memory/
The check watches JS heap growth, DOM document/node counters, and JavaScript
listener growth from a warmup baseline plus the later-window heap slope. This is
a frontend memory-growth signal, not a Windows WebView2 process-memory check;
the existing full performance workflow remains responsible for real Tauri
process-level CPU/RSS monitoring. In CI the test-render-memory-perf job runs
only for frontend pull requests, uploads artifacts, and stays non-blocking.
CI
The test-e2e-web job in .github/workflows/ci.yml runs the suite on
ubuntu-latest and uploads test-results/captures/ as the e2e-captures
artifact with if: always(), so captures survive failed runs. The Playwright
HTML report and per-test output are uploaded only on failure.
For same-repo pull requests the job also posts the captures inline as a
sticky PR comment (.github/scripts/comment-e2e-captures.sh): images are
force-pushed to the single-commit e2e-captures branch under
pr-<number>/ and embedded via raw.githubusercontent.com URLs. The branch
is disposable — it can be deleted at any time and will be recreated by the
next run. Fork PRs are skipped (their token is read-only).
Native Tauri smoke
Issue #1610 adds a separate native smoke path that launches the real Tauri
desktop application through tauri-driver and WebDriver. This is intentionally
not another deterministic fixture harness: it uses the normal frontend build,
real Tauri IPC/plugin calls, and the real backend startup path.
Run it locally with:
npm run test:e2e:native
The native runner is named by screen and scenario:
e2e-native/settings/native-first-run-smoke.ts
Shared non-scenario code lives under e2e-native/support/.
Type-check the native runner without launching the app:
npm run typecheck:e2e:native
The command builds a debug, no-bundle app with the dedicated E2E config:
npm run tauri build -- --debug --no-bundle --config src-tauri/tauri.e2e.conf.json
The native smoke sets HARDVIZ_E2E=1, which switches the runtime identity to
HardwareVisualizerE2E. Before each run the HardwareVisualizerE2E app data
directory is deleted after a path-safety check. This deliberately exercises the
first-run path without touching normal release or development data.
The smoke also sets HARDVIZ_E2E_DEFAULT_LANGUAGE=en, which is honored only
when HARDVIZ_E2E=1. This keeps first-run settings and text assertions
independent from the runner's OS locale.
It also sets HARDVIZ_E2E_FORCE_CLOSE_TO_TRAY_PROMPT=1. This only makes the
frontend is_close_to_tray_available command return true, so the first-run
prompt is deterministic under headless Linux CI. Tray setup and the normal
window-close lifecycle still run through the real app path; this smoke does not
assert OS tray/status-notifier integration.
Current native scenario:
- Launch the real Tauri app binary through
tauri-driver. - Wait for the first-run Close-to-Tray prompt.
- Dismiss the prompt with the close button.
- Open Settings through the side menu.
- Assert that
GeneralandAboutrender. - Assert that About shows a non-empty
Version:value from the real Tauri app plugin. - Save
test-results/captures/settings/native-first-run-smoke.png.
If the scenario fails after the WebDriver session starts, the runner tries to
save test-results/captures/settings/native-first-run-smoke-failure.png.
Native capture paths use screen/scenario naming. These PNGs are evidence for
debugging and review only; they are not compared against visual baselines.
Native prerequisites:
tauri-driver, installed withcargo install tauri-driver --locked.- Linux:
webkit2gtk-driver,xvfb, and appindicator dependencies such aslibayatana-appindicator3-dev. - Windows: a compatible EdgeDriver/WebView2 WebDriver setup is required for
local experimentation, but Windows CI is not part of the initial native smoke.
If
tauri-driverfinds an incompatible native driver first, setTAURI_NATIVE_DRIVER_BINto the matching driver path. - macOS: desktop
tauri-drivercoverage is not practical because WKWebView has no corresponding desktop WebDriver path.
The test-e2e-native CI job runs only on ubuntu-22.04. It uploads
test-results/captures/ as the e2e-native-captures artifact with
if: always(), so evidence survives failures. Native captures are not
published through .github/scripts/comment-e2e-captures.sh; that PR comment
remains dedicated to the Playwright web/mock review captures.