mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
feat: add e2e test cursor skill
This commit is contained in:
parent
2e1b9b5582
commit
4ac994792b
45 changed files with 39848 additions and 0 deletions
622
.cursor/skills/playwright-testing/electron-testing.md
Executable file
622
.cursor/skills/playwright-testing/electron-testing.md
Executable file
|
|
@ -0,0 +1,622 @@
|
|||
# Electron Testing
|
||||
|
||||
> **When to use**: When your application is an Electron desktop app and you need end-to-end tests covering the renderer process, main process, IPC communication, native dialogs, system tray, and multi-window workflows.
|
||||
> **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```typescript
|
||||
import { _electron as electron } from 'playwright';
|
||||
|
||||
// Launch the Electron app
|
||||
const app = await electron.launch({ args: ['./main.js'] });
|
||||
|
||||
// Get the first window (renderer process)
|
||||
const window = await app.firstWindow();
|
||||
|
||||
// Access the main process for evaluation
|
||||
const appPath = await app.evaluate(async ({ app }) => {
|
||||
return app.getPath('userData');
|
||||
});
|
||||
|
||||
// Close the app
|
||||
await app.close();
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Basic Electron App Setup
|
||||
|
||||
**Use when**: Starting to test an Electron app with Playwright for the first time.
|
||||
**Avoid when**: Your app is a web app, not an Electron app.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test';
|
||||
|
||||
let app: ElectronApplication;
|
||||
let window: Page;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// Launch the Electron app from your project directory
|
||||
app = await electron.launch({
|
||||
args: ['./dist/main.js'],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for the first BrowserWindow to open
|
||||
window = await app.firstWindow();
|
||||
|
||||
// Optional: wait for the app to be fully loaded
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('app window has correct title', async () => {
|
||||
const title = await window.title();
|
||||
expect(title).toBe('My Electron App');
|
||||
});
|
||||
|
||||
test('main page renders', async () => {
|
||||
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect, _electron: electron } = require('@playwright/test');
|
||||
|
||||
let app;
|
||||
let window;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
app = await electron.launch({
|
||||
args: ['./dist/main.js'],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
});
|
||||
window = await app.firstWindow();
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('app window has correct title', async () => {
|
||||
const title = await window.title();
|
||||
expect(title).toBe('My Electron App');
|
||||
});
|
||||
|
||||
test('main page renders', async () => {
|
||||
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Electron App Fixture (Recommended)
|
||||
|
||||
**Use when**: You want isolated, reusable Electron app instances across test files.
|
||||
**Avoid when**: All tests can share a single app instance (rare in practice).
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
// fixtures.ts
|
||||
import { test as base, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test';
|
||||
|
||||
type ElectronFixtures = {
|
||||
electronApp: ElectronApplication;
|
||||
window: Page;
|
||||
};
|
||||
|
||||
export const test = base.extend<ElectronFixtures>({
|
||||
electronApp: async ({}, use) => {
|
||||
const app = await electron.launch({
|
||||
args: ['./dist/main.js'],
|
||||
env: { ...process.env, NODE_ENV: 'test' },
|
||||
});
|
||||
await use(app);
|
||||
await app.close();
|
||||
},
|
||||
|
||||
window: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app.spec.ts
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('navigate to settings', async ({ window }) => {
|
||||
await window.getByRole('link', { name: 'Settings' }).click();
|
||||
await expect(window.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
// fixtures.js
|
||||
const { test: base, expect, _electron: electron } = require('@playwright/test');
|
||||
|
||||
const test = base.extend({
|
||||
electronApp: async ({}, use) => {
|
||||
const app = await electron.launch({
|
||||
args: ['./dist/main.js'],
|
||||
env: { ...process.env, NODE_ENV: 'test' },
|
||||
});
|
||||
await use(app);
|
||||
await app.close();
|
||||
},
|
||||
|
||||
window: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { test, expect };
|
||||
```
|
||||
|
||||
### Accessing the Main Process
|
||||
|
||||
**Use when**: You need to read Electron app state, check paths, get app version, or verify main process behavior.
|
||||
**Avoid when**: Everything you need is in the renderer (UI). Prefer testing through the UI.
|
||||
|
||||
`app.evaluate()` runs code in the main process with access to all Electron APIs.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('verify app version and paths', async ({ electronApp }) => {
|
||||
// Evaluate in the main process — receives the Electron module
|
||||
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
name: app.getName(),
|
||||
userData: app.getPath('userData'),
|
||||
locale: app.getLocale(),
|
||||
isPackaged: app.isPackaged,
|
||||
};
|
||||
});
|
||||
|
||||
expect(appInfo.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(appInfo.name).toBe('my-electron-app');
|
||||
expect(appInfo.userData).toBeTruthy();
|
||||
expect(appInfo.isPackaged).toBe(false); // false during development
|
||||
});
|
||||
|
||||
test('main process environment variables are set', async ({ electronApp }) => {
|
||||
const nodeEnv = await electronApp.evaluate(async () => {
|
||||
return process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
expect(nodeEnv).toBe('test');
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect } = require('./fixtures');
|
||||
|
||||
test('verify app version and paths', async ({ electronApp }) => {
|
||||
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
name: app.getName(),
|
||||
userData: app.getPath('userData'),
|
||||
isPackaged: app.isPackaged,
|
||||
};
|
||||
});
|
||||
|
||||
expect(appInfo.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(appInfo.name).toBe('my-electron-app');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing IPC Communication
|
||||
|
||||
**Use when**: Your app uses `ipcMain` / `ipcRenderer` for communication between the main and renderer processes.
|
||||
**Avoid when**: IPC is an implementation detail and the behavior is fully testable through the UI.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('renderer sends IPC message and gets response', async ({ electronApp, window }) => {
|
||||
// Trigger an IPC call from the renderer
|
||||
const result = await window.evaluate(async () => {
|
||||
// Assumes your preload script exposes ipcRenderer via contextBridge
|
||||
return await (window as any).electronAPI.getSystemInfo();
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('platform');
|
||||
expect(result).toHaveProperty('arch');
|
||||
expect(result.platform).toBeTruthy();
|
||||
});
|
||||
|
||||
test('main process handles IPC file-read request', async ({ electronApp, window }) => {
|
||||
// Set up a listener in the main process first
|
||||
await electronApp.evaluate(async ({ ipcMain }) => {
|
||||
ipcMain.handle('test-ping', async () => {
|
||||
return { pong: true, timestamp: Date.now() };
|
||||
});
|
||||
});
|
||||
|
||||
// Send from renderer
|
||||
const response = await window.evaluate(async () => {
|
||||
return await (window as any).electronAPI.invoke('test-ping');
|
||||
});
|
||||
|
||||
expect(response.pong).toBe(true);
|
||||
expect(response.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('IPC event triggers UI update', async ({ window }) => {
|
||||
// Simulate the main process sending an event to the renderer
|
||||
await window.evaluate(() => {
|
||||
// Trigger a custom event that the app listens for
|
||||
window.dispatchEvent(new CustomEvent('app:notification', {
|
||||
detail: { message: 'Update available', version: '2.0.0' },
|
||||
}));
|
||||
});
|
||||
|
||||
await expect(window.getByText('Update available')).toBeVisible();
|
||||
await expect(window.getByText('Version 2.0.0')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect } = require('./fixtures');
|
||||
|
||||
test('renderer sends IPC message and gets response', async ({ electronApp, window }) => {
|
||||
const result = await window.evaluate(async () => {
|
||||
return await window.electronAPI.getSystemInfo();
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('platform');
|
||||
expect(result).toHaveProperty('arch');
|
||||
});
|
||||
|
||||
test('IPC event triggers UI update', async ({ window }) => {
|
||||
await window.evaluate(() => {
|
||||
window.dispatchEvent(new CustomEvent('app:notification', {
|
||||
detail: { message: 'Update available', version: '2.0.0' },
|
||||
}));
|
||||
});
|
||||
|
||||
await expect(window.getByText('Update available')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### File System Dialogs
|
||||
|
||||
**Use when**: Your app uses Electron's `dialog.showOpenDialog`, `dialog.showSaveDialog`, or similar native file dialogs.
|
||||
**Avoid when**: File selection is handled by a web input (`<input type="file">`). Use standard Playwright file chooser for that.
|
||||
|
||||
Native dialogs cannot be interacted with directly. Mock them in the main process.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('open file dialog and load a document', async ({ electronApp, window }) => {
|
||||
// Mock the dialog to return a specific file path
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: false,
|
||||
filePaths: ['/tmp/test-document.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
// Click the "Open File" button in the renderer
|
||||
await window.getByRole('button', { name: 'Open File' }).click();
|
||||
|
||||
// Verify the app loaded the file
|
||||
await expect(window.getByTestId('file-name')).toHaveText('test-document.txt');
|
||||
});
|
||||
|
||||
test('save file dialog returns selected path', async ({ electronApp, window }) => {
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showSaveDialog = async () => ({
|
||||
canceled: false,
|
||||
filePath: '/tmp/exported-report.pdf',
|
||||
});
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Export PDF' }).click();
|
||||
await expect(window.getByText('Saved to /tmp/exported-report.pdf')).toBeVisible();
|
||||
});
|
||||
|
||||
test('handle canceled file dialog', async ({ electronApp, window }) => {
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: true,
|
||||
filePaths: [],
|
||||
});
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Open File' }).click();
|
||||
|
||||
// App should not crash or change state
|
||||
await expect(window.getByTestId('file-name')).toHaveText('No file selected');
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect } = require('./fixtures');
|
||||
|
||||
test('open file dialog and load a document', async ({ electronApp, window }) => {
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: false,
|
||||
filePaths: ['/tmp/test-document.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Open File' }).click();
|
||||
await expect(window.getByTestId('file-name')).toHaveText('test-document.txt');
|
||||
});
|
||||
|
||||
test('handle canceled file dialog', async ({ electronApp, window }) => {
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: true,
|
||||
filePaths: [],
|
||||
});
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Open File' }).click();
|
||||
await expect(window.getByTestId('file-name')).toHaveText('No file selected');
|
||||
});
|
||||
```
|
||||
|
||||
### System Tray Testing
|
||||
|
||||
**Use when**: Your app has a system tray icon with context menus or status indicators.
|
||||
**Avoid when**: Your app has no tray functionality.
|
||||
|
||||
Playwright cannot directly click system tray icons. Test the tray logic by evaluating in the main process.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('tray icon is created on app launch', async ({ electronApp }) => {
|
||||
const hasTray = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
// Access the tray via a reference your app stores
|
||||
const { tray } = require('./tray-manager');
|
||||
return tray !== null && !tray.isDestroyed();
|
||||
});
|
||||
|
||||
expect(hasTray).toBe(true);
|
||||
});
|
||||
|
||||
test('tray tooltip shows unread count', async ({ electronApp }) => {
|
||||
const tooltip = await electronApp.evaluate(async () => {
|
||||
const { tray } = require('./tray-manager');
|
||||
return tray.getToolTip();
|
||||
});
|
||||
|
||||
expect(tooltip).toMatch(/\d+ unread messages?/);
|
||||
});
|
||||
|
||||
test('clicking tray "Show" menu item opens the window', async ({ electronApp }) => {
|
||||
// Simulate clicking a tray menu item by invoking its callback
|
||||
await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
const { trayMenu } = require('./tray-manager');
|
||||
// Find the "Show" menu item and invoke its click handler
|
||||
const showItem = trayMenu.items.find((item: any) => item.label === 'Show');
|
||||
if (showItem && showItem.click) {
|
||||
showItem.click();
|
||||
}
|
||||
});
|
||||
|
||||
// The main window should now be visible
|
||||
const window = await electronApp.firstWindow();
|
||||
const isVisible = await window.evaluate(() => {
|
||||
return document.visibilityState === 'visible';
|
||||
});
|
||||
expect(isVisible).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect } = require('./fixtures');
|
||||
|
||||
test('tray icon is created on app launch', async ({ electronApp }) => {
|
||||
const hasTray = await electronApp.evaluate(async () => {
|
||||
const { tray } = require('./tray-manager');
|
||||
return tray !== null && !tray.isDestroyed();
|
||||
});
|
||||
|
||||
expect(hasTray).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Windows
|
||||
|
||||
**Use when**: Your Electron app opens multiple windows (preferences, about, detached panels).
|
||||
**Avoid when**: Your app uses a single window.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('open and interact with preferences window', async ({ electronApp, window }) => {
|
||||
// Click the menu item or button that opens the preferences window
|
||||
await window.getByRole('menuitem', { name: 'Preferences' }).click();
|
||||
|
||||
// Wait for the new window to appear
|
||||
const prefsWindow = await electronApp.waitForEvent('window');
|
||||
await prefsWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Interact with the preferences window
|
||||
await prefsWindow.getByLabel('Theme').selectOption('dark');
|
||||
await prefsWindow.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify the main window reflects the change
|
||||
await expect(window.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
|
||||
// Close the preferences window
|
||||
await prefsWindow.close();
|
||||
});
|
||||
|
||||
test('get all open windows', async ({ electronApp, window }) => {
|
||||
// Open a second window
|
||||
await window.getByRole('button', { name: 'New Window' }).click();
|
||||
|
||||
// Get all windows
|
||||
const allWindows = electronApp.windows();
|
||||
expect(allWindows.length).toBe(2);
|
||||
|
||||
// Find the new window (not the main one)
|
||||
const newWindow = allWindows.find((w) => w !== window)!;
|
||||
await expect(newWindow.getByRole('heading')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect } = require('./fixtures');
|
||||
|
||||
test('open and interact with preferences window', async ({ electronApp, window }) => {
|
||||
await window.getByRole('menuitem', { name: 'Preferences' }).click();
|
||||
|
||||
const prefsWindow = await electronApp.waitForEvent('window');
|
||||
await prefsWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
await prefsWindow.getByLabel('Theme').selectOption('dark');
|
||||
await prefsWindow.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(window.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
await prefsWindow.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Packaged/Built Apps
|
||||
|
||||
**Use when**: You want to test the production build of your Electron app (after `electron-builder`, `electron-forge`, etc.).
|
||||
**Avoid when**: Development mode testing is sufficient for your CI pipeline.
|
||||
|
||||
**TypeScript**
|
||||
```typescript
|
||||
import { test, expect, _electron as electron } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
test('packaged app launches and works', async () => {
|
||||
// Path to the packaged app executable
|
||||
const appPath = process.platform === 'darwin'
|
||||
? path.join(__dirname, '../dist/mac/MyApp.app/Contents/MacOS/MyApp')
|
||||
: process.platform === 'win32'
|
||||
? path.join(__dirname, '../dist/win-unpacked/MyApp.exe')
|
||||
: path.join(__dirname, '../dist/linux-unpacked/my-app');
|
||||
|
||||
const app = await electron.launch({
|
||||
executablePath: appPath,
|
||||
});
|
||||
|
||||
const window = await app.firstWindow();
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify the packaged app works correctly
|
||||
const title = await window.title();
|
||||
expect(title).toBe('My Electron App');
|
||||
|
||||
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
||||
|
||||
// Verify it reports as packaged
|
||||
const isPackaged = await app.evaluate(async ({ app }) => app.isPackaged);
|
||||
expect(isPackaged).toBe(true);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```javascript
|
||||
const { test, expect, _electron: electron } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
test('packaged app launches and works', async () => {
|
||||
const appPath = process.platform === 'darwin'
|
||||
? path.join(__dirname, '../dist/mac/MyApp.app/Contents/MacOS/MyApp')
|
||||
: process.platform === 'win32'
|
||||
? path.join(__dirname, '../dist/win-unpacked/MyApp.exe')
|
||||
: path.join(__dirname, '../dist/linux-unpacked/my-app');
|
||||
|
||||
const app = await electron.launch({
|
||||
executablePath: appPath,
|
||||
});
|
||||
|
||||
const window = await app.firstWindow();
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
|
||||
const title = await window.title();
|
||||
expect(title).toBe('My Electron App');
|
||||
|
||||
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
||||
await app.close();
|
||||
});
|
||||
```
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Scenario | Approach | Why |
|
||||
|---|---|---|
|
||||
| Launch Electron app for testing | `_electron.launch({ args: ['./main.js'] })` | Playwright's built-in Electron support |
|
||||
| Get the main window | `app.firstWindow()` | Returns the first `BrowserWindow` as a Playwright `Page` |
|
||||
| Read main process state | `app.evaluate(({ app }) => ...)` | Runs code in the main process with Electron APIs |
|
||||
| Test IPC round-trips | `window.evaluate` (renderer) + `app.evaluate` (main) | Cover both sides of the IPC bridge |
|
||||
| Mock native file dialogs | Override `dialog.showOpenDialog` via `app.evaluate` | Native dialogs cannot be automated directly |
|
||||
| Test system tray | `app.evaluate` to invoke tray callbacks | Tray icons are OS-native; not clickable via Playwright |
|
||||
| Multiple windows | `app.waitForEvent('window')` | Captures new `BrowserWindow` instances as they open |
|
||||
| Test packaged builds | `electron.launch({ executablePath })` | Points to the built binary instead of source |
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Don't Do This | Problem | Do This Instead |
|
||||
|---|---|---|
|
||||
| `const { app } = require('electron')` in test files | Electron APIs are not available in Playwright's Node process | Use `electronApp.evaluate(({ app }) => ...)` |
|
||||
| Directly import renderer code into tests | Bypasses the actual app lifecycle and IPC | Test through the UI via the `window` (Page) object |
|
||||
| Skip `waitForLoadState` after `firstWindow()` | Window may not be fully rendered | Always `await window.waitForLoadState('domcontentloaded')` |
|
||||
| Test tray by clicking system-level UI | Playwright cannot interact with native OS chrome | Mock tray menu callbacks via `app.evaluate` |
|
||||
| Share a single `ElectronApplication` across all tests without cleanup | State leaks between tests | Use fixtures with `app.close()` in teardown |
|
||||
| Forget to close the app in `afterAll` | Leaves Electron processes running, eating CI resources | Always `await app.close()` in teardown |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `electron.launch()` throws "cannot find module" | `args` path does not point to your main entry file | Verify the path: `args: ['./dist/main.js']` relative to the working directory |
|
||||
| `firstWindow()` times out | App does not open a `BrowserWindow` in time | Check that the app creates a window on startup; increase timeout |
|
||||
| `app.evaluate` cannot access Electron modules | Destructuring the wrong parameter | Destructure correctly: `evaluate(async ({ app, dialog, BrowserWindow }) => ...)` |
|
||||
| Dialog mock does not take effect | Mock applied after the dialog was already called | Set up mocks before triggering the UI action that opens the dialog |
|
||||
| Second window not captured | `waitForEvent('window')` registered after the window opened | Register the event listener before triggering the action that opens the window |
|
||||
| Tests hang after `app.close()` | Child processes spawned by the app are still running | Ensure your Electron app cleans up child processes on quit |
|
||||
| Packaged app test fails with path error | Executable path varies by OS and build tool | Use `process.platform` to compute the correct path |
|
||||
| `window.evaluate` throws context destroyed | Window was closed or navigated during evaluation | Ensure the window is stable before evaluating |
|
||||
|
||||
## Related
|
||||
|
||||
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrapping Electron app launch in fixtures
|
||||
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- all standard assertions work on Electron windows
|
||||
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- Electron apps often embed web components or iframes
|
||||
- [core/browser-apis.md](browser-apis.md) -- localStorage, IndexedDB, and other APIs work the same in Electron
|
||||
Loading…
Add table
Add a link
Reference in a new issue