mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
622 lines
21 KiB
Markdown
Executable file
622 lines
21 KiB
Markdown
Executable file
# 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
|