Peek Test Framework#
Architecture#
Tests are organized by platform capability:
tests/
├── desktop/ # Tests for desktop backends (Electron, Tauri)
│ └── smoke.spec.ts # Main smoke tests
├── fixtures/ # Playwright fixtures for app launching
│ └── desktop-app.ts # Desktop app launcher abstraction
├── helpers/ # Shared utilities
│ └── window-utils.ts
└── README.md # This file
Backend-specific tests live in their respective directories:
backend/electron/tests/- Electron-only featuresbackend/tauri/tests/- Tauri-only features (includes smoke.rs Rust unit tests)
Running Tests#
# Run all tests (Electron Playwright + Tauri Rust)
yarn test
# Run specific backend
yarn test:electron # Electron Playwright tests (headless)
yarn test:tauri # Tauri Rust unit tests
# Run with visible windows (Electron only)
yarn test:visible
# Run specific test by name
yarn test:grep "Settings"
# Debug mode (visible + Playwright inspector)
yarn test:debug
Backend Status#
| Backend | Status | Test Framework | Command |
|---|---|---|---|
| Electron | ✅ Working | Playwright | yarn test:electron |
| Tauri | ✅ Working | Frontend Mock + Rust | yarn test:tauri |
Tauri Testing Notes#
Tauri testing uses a two-part approach:
- Frontend Tests (
yarn test:tauri:frontend) - Runs the same Playwright tests against a mocked backend served via HTTP. Tests frontend UI behavior. - Rust Unit Tests (
yarn test:tauri:rust) - Tests backend datastore and API functionality.
Known Limitations of Frontend Mock:
- Each page has its own mock instance - data doesn't share across pages (e.g., Groups Navigation test fails)
- Data doesn't persist across app restarts (Data Persistence tests fail)
- 14/17 tests pass with the mock approach
The Rust unit tests in backend/tauri/src-tauri/tests/smoke.rs cover the backend functionality that the mock doesn't test.
Why Can't We Test Tauri Directly on macOS?#
Tauri on macOS uses WKWebView (Apple's native WebKit wrapper), which has no automation support:
| Protocol | Electron (Chromium) | Tauri macOS (WKWebView) | Tauri Linux (WebKitGTK) | Tauri Windows (WebView2) |
|---|---|---|---|---|
| CDP (Chrome DevTools Protocol) | ✅ | ❌ | ❌ | ✅ |
| WebDriver | ✅ | ❌ | ✅ | ✅ |
| Playwright support | ✅ | ❌ | ❌ | ✅ |
The core problem: Apple provides safaridriver for Safari, but no equivalent driver for WKWebView. They simply never built one.
Relevant issues and documentation:
- tauri-apps/tauri#7068 - Feature request for macOS support in tauri-driver
- Tauri WebDriver docs - Official docs confirming macOS limitation
- Apple Developer Forums - "How to test WKWebView" with no good answer
- appium/appium#5839 - Appium's historical struggles with WKWebView
- Example workarounds repo
Alternative Approaches Considered#
| Approach | Reuses Playwright Tests | Tests Real App | Complexity | Why Not |
|---|---|---|---|---|
| Frontend Mock (chosen) | ✅ Yes | ❌ Mock only | Low | Best tradeoff |
| Appium Mac2 Driver | ❌ Different API | ✅ Yes | Medium | Can't reuse tests, clunky |
| tauri-driver | ✅ WebDriver | ✅ Yes | Low | Doesn't work on macOS |
| TestDriver.ai | ❌ Different API | ✅ Yes | Medium | AI-based, non-deterministic |
| CrabNebula Cloud | ✅ Partial | ✅ Yes | Low | Paid service |
| Run Linux VM | ✅ Yes | ✅ Yes | High | Slow CI, complex setup |
How the Frontend Mock Works#
Since the frontend code (app/) is identical between Electron and Tauri, we can test it separately:
Electron Tests: Tauri Frontend Tests:
┌─────────────────┐ ┌─────────────────┐
│ Playwright │ │ Playwright │
│ (CDP to │ │ (HTTP to │
│ Electron) │ │ localhost) │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Electron App │ │ HTTP Server │
│ (real backend) │ │ + Mock API │
└─────────────────┘ └─────────────────┘
Implementation:
tests/mocks/tauri-backend.js- Mocks thewindow.appAPI (datastore, shortcuts, windows, etc.)tests/fixtures/desktop-app.ts-launchTauriFrontend()starts HTTP server and injects mock- HTTP server serves
app/directory onlocalhost:5199 - Playwright runs against Chromium, same selectors and assertions as Electron tests
What gets tested:
- ✅ Frontend UI (identical code, same tests)
- ✅ Component interactions
- ✅ Datastore API calls (mocked responses)
- ❌ Real Tauri↔Frontend IPC
- ❌ Real multi-window behavior
- ❌ Data persistence across restarts
The gap is filled by Rust unit tests in backend/tauri/src-tauri/tests/smoke.rs which test real datastore operations, persistence, and backend logic.
Hybrid Extension Mode Tests#
The test suite includes specific tests for the hybrid extension architecture in the Hybrid Extension Mode @desktop describe block:
| Test | What it verifies |
|---|---|
extension host window exists |
Extension host window loads at peek://app/extension-host.html |
built-in extensions load as iframes |
cmd, groups, peeks, slides are iframes in extension host |
example extension loads as separate window |
External extensions get separate BrowserWindows |
commands work from both consolidated and external |
Commands from both loading modes are accessible |
pubsub works between consolidated and external |
Cross-extension messaging works across loading modes |
correct window count for hybrid mode |
Expected window count: 1 core + 1 host + 1 external |
The test fixture (tests/fixtures/desktop-app.ts) handles hybrid mode by:
- Waiting for the extension host window to load
- Waiting for at least one external extension window (e.g.,
example) - Providing
getExtensionWindows()that returns separate window extensions only
Coverage Matrix#
Both test frameworks cover equivalent functionality:
| Feature | Playwright (Electron) | Rust (Tauri) |
|---|---|---|
| Database initialization | Core Functionality |
test_database_init |
| Address CRUD | Core Functionality |
test_address_operations |
| Visit tracking | Core Functionality |
test_visit_tracking |
| Tag operations | Core Functionality |
test_tag_operations |
| Stats retrieval | Core Functionality |
test_stats |
| Table operations | Core Functionality |
test_table_operations |
| URL normalization | Core Functionality |
test_url_normalization |
| Extension CRUD | Extension Lifecycle |
test_extension_operations |
| Extension settings | Extension Lifecycle |
test_extension_settings |
| Extension errors | Extension Lifecycle |
test_extension_error_tracking |
| Data persistence | Data Persistence |
test_data_persistence |
| Settings UI | Settings |
N/A (UI only) |
| Cmd Palette | Cmd Palette |
N/A (UI only) |
| Extension windows | Peeks, Slides, Groups |
N/A (UI only) |
Note: UI-specific tests (Settings, Cmd Palette, extension windows) only run via Playwright. Tauri Rust tests focus on backend/datastore functionality.
Writing Tests#
Cross-Backend Tests (tests/desktop/)#
Use the desktopApp fixture - it works with any desktop backend:
import { test, expect } from '../fixtures/desktop-app';
test('datastore works', async ({ desktopApp }) => {
const bg = await desktopApp.getBackgroundWindow();
const result = await bg.evaluate(async () => {
return await window.app.datastore.getStats();
});
expect(result.success).toBe(true);
});
Using launchDesktopApp Directly#
For tests that need manual app lifecycle control:
import { launchDesktopApp, DesktopApp } from '../fixtures/desktop-app';
test.describe('My Feature @desktop', () => {
let app: DesktopApp;
test.beforeAll(async () => {
app = await launchDesktopApp('my-test-profile');
});
test.afterAll(async () => {
if (app) await app.close();
});
test('works', async () => {
const bg = await app.getBackgroundWindow();
// ... test code
});
});
Backend-Specific Tests#
Place in backend/{name}/tests/ and use backend-specific APIs:
// backend/electron/tests/tray.spec.ts
import { test, _electron as electron } from '@playwright/test';
test('tray icon works', async () => {
// Electron-specific tray testing
});
Environment Variables#
| Variable | Values | Default | Purpose |
|---|---|---|---|
| BACKEND | electron, tauri | electron | Which backend to test |
| PROFILE | string | auto-generated | Data isolation directory |
| HEADLESS | 1 or empty | 1 (in scripts) | Run without visible windows (unified across backends) |
| DEBUG | 1 or empty | empty | Enable debug logging |
Note: The HEADLESS env var is unified across backends. The test fixture translates it to
backend-specific vars (e.g., PEEK_HEADLESS for Electron). Always use HEADLESS in test commands.
Fixtures#
desktopApp#
Provides a launched desktop app instance per test:
interface DesktopApp {
backend: 'electron' | 'tauri';
windows(): Page[];
getWindow(pattern: string | RegExp, timeout?: number): Promise<Page>;
getBackgroundWindow(): Promise<Page>;
getExtensionWindows(): Page[];
close(): Promise<void>;
}
Test Isolation#
Each test suite should use a unique profile to avoid data conflicts. The fixture auto-generates profiles based on test names.
For manual control:
const app = await launchDesktopApp('my-unique-profile-name');
Adding New Platforms#
- Create fixture in
tests/fixtures/{platform}-app.ts - Create test directory
tests/{platform}/ - Add scripts to package.json
- Document in this README
IMPORTANT: For Claude Code#
When working on tests:
- Cross-backend tests go in
tests/desktop/ - Use
desktopAppfixture, never_electrondirectly in cross-backend tests - Backend-specific tests go in
backend/{name}/tests/ - Always use PROFILE for data isolation
- Test both backends before marking complete:
yarn test - Follow existing patterns - look at smoke.spec.ts for examples
- Use @desktop tag in test.describe for desktop-only tests
Test Tags#
Tests use @desktop, @mobile, @extension tags for filtering:
test.describe('Settings @desktop', () => {
// Runs on desktop backends only
});
Test Environment Behavior#
Dock and Menubar#
During tests (when PROFILE starts with test), the macOS dock icon is always hidden.
This prevents the dock from appearing during test runs and interfering with test execution
or causing visual distractions. The updateDockVisibility() function in
backend/electron/windows.ts checks isTestProfile() and always hides the dock.
DevTools#
DevTools are automatically disabled in test profiles to prevent them from stealing focus or slowing down tests.
Troubleshooting#
Tests hang or timeout#
- Check if previous test left app running:
pkill -f "peek-tauri\|electron" - Increase timeout:
yarn test:desktop --timeout=120000
Window not found#
- Increase wait time in test
- Check URL patterns match actual window URLs
- Use
app.windows()to debug available windows
Data conflicts between tests#
- Ensure unique PROFILE per test suite
- Use
launchDesktopApp('unique-name')for isolation
Playwright keyboard.press('Escape') doesn't work#
Playwright's keyboard.press('Escape') doesn't reliably trigger Electron's before-input-event
handler on webContents. This affects escape-based navigation in modal windows.
Workaround: Expose the navigation function on window and call it directly from the test:
// In the page code:
window.showGroups = showGroups;
// In the test:
await page.evaluate(async () => {
await (window as any).showGroups();
});
This tests the navigation logic without relying on the escape key mechanism.