diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs
index 88bfc62a..74a1c28e 100644
--- a/apps/desktop/src-tauri/build.rs
+++ b/apps/desktop/src-tauri/build.rs
@@ -24,7 +24,11 @@ fn main() {
"identifier": "playwright",
"description": "Playwright E2E testing plugin permissions (auto-generated by build.rs)",
"windows": ["main", "settings", "viewer-*"],
- "permissions": ["playwright:default"]
+ "permissions": [
+ "playwright:default",
+ "core:window:allow-title",
+ "core:window:allow-set-title"
+ ]
}
"#,
)
diff --git a/apps/desktop/test/e2e-playwright/CLAUDE.md b/apps/desktop/test/e2e-playwright/CLAUDE.md
index e79bcc30..664fbd40 100644
--- a/apps/desktop/test/e2e-playwright/CLAUDE.md
+++ b/apps/desktop/test/e2e-playwright/CLAUDE.md
@@ -16,6 +16,15 @@ Playwright (Node.js) --Unix socket--> tauri-plugin-playwright (Rust)
Same tests run on macOS and Linux. Platform differences (Ctrl vs Meta) are handled by a single `CTRL_OR_META` constant
in `helpers.ts`.
+## Window-title decoration
+
+`fixtures.ts` wraps each test in `beforeEach`/`afterEach` hooks that update the main window's OS title via the standard
+Tauri window plugin (`plugin:window|set_title`). While a test runs the title becomes ` (Running: )`; after
+it returns, `(FINISHED)` is appended. The base title is captured once per worker on the first hook call so suffixes
+don't accumulate. The required permissions (`core:window:allow-title`, `core:window:allow-set-title`) are added to the
+feature-gated `playwright.json` capability generated by `build.rs`, so they're absent from release builds. Use this in
+Cmd-Tab / Mission Control / Linux title bars to spot which spec is in flight or stuck without tailing the log.
+
## Running on macOS
**Via the checker (recommended):** The checker handles the full lifecycle automatically: build, fixture creation, app
diff --git a/apps/desktop/test/e2e-playwright/fixtures.ts b/apps/desktop/test/e2e-playwright/fixtures.ts
index 030d4cd3..fde8159b 100644
--- a/apps/desktop/test/e2e-playwright/fixtures.ts
+++ b/apps/desktop/test/e2e-playwright/fixtures.ts
@@ -9,9 +9,16 @@
* - globalSetup: creates the fixture directory tree (~170 MB)
* - beforeEach: recreates small text files (keeps bulk .dat files)
* - globalTeardown: deletes the fixture directory
+ *
+ * Window-title decoration:
+ * - `beforeEach` sets the main window's OS title to " (Running: )"
+ * - `afterEach` updates it to " (Running: ) (FINISHED)"
+ * so you can glance at the dock / Cmd-Tab / Linux title bar to see which
+ * spec is in flight (or stuck) without tailing the log.
*/
import { createTauriTest } from '@srsholmes/tauri-playwright'
+import type { TestInfo } from '@playwright/test'
// Each parallel E2E shard spawns its own Tauri instance bound to a distinct
// Unix socket. The Go check runner sets CMDR_PLAYWRIGHT_SOCKET per shard.
@@ -26,3 +33,47 @@ export const { test, expect } = createTauriTest({
// Tauri mode config
mcpSocket: socketPath,
})
+
+// Captured once per worker on the first beforeEach so suffixes don't accumulate
+// across tests. Each shard owns its own Tauri instance + its own worker process,
+// so this lives correctly per-shard.
+let baseTitle: string | null = null
+
+type EvaluatablePage = { evaluate: (js: string) => Promise }
+
+/** Joins describe blocks + test title into "Section > test name" style. */
+function formatTestName(info: TestInfo): string {
+ const parts = info.titlePath
+ const fileIdx = parts.findIndex((p) => /\.spec\.[tj]s$/.test(p))
+ const tail = fileIdx >= 0 ? parts.slice(fileIdx + 1) : [info.title]
+ return tail.filter((p) => p.length > 0).join(' › ')
+}
+
+async function readMainTitle(tauriPage: EvaluatablePage): Promise {
+ const result = await tauriPage.evaluate(`window.__TAURI_INTERNALS__.invoke('plugin:window|title', { label: 'main' })`)
+ return typeof result === 'string' ? result : ''
+}
+
+async function setMainTitle(tauriPage: EvaluatablePage, title: string): Promise {
+ await tauriPage.evaluate(
+ `window.__TAURI_INTERNALS__.invoke('plugin:window|set_title', { label: 'main', value: ${JSON.stringify(title)} })`,
+ )
+}
+
+test.beforeEach(async ({ tauriPage }, testInfo) => {
+ try {
+ if (baseTitle === null) baseTitle = await readMainTitle(tauriPage)
+ await setMainTitle(tauriPage, `${baseTitle} (Running: ${formatTestName(testInfo)})`)
+ } catch {
+ // Title decoration is purely for human eyeballs — never block a test on it.
+ }
+})
+
+test.afterEach(async ({ tauriPage }, testInfo) => {
+ try {
+ if (baseTitle === null) baseTitle = await readMainTitle(tauriPage)
+ await setMainTitle(tauriPage, `${baseTitle} (Running: ${formatTestName(testInfo)}) (FINISHED)`)
+ } catch {
+ // See beforeEach.
+ }
+})