@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
# Electron Testing
|
|
2
|
+
|
|
3
|
+
> **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.
|
|
4
|
+
> **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { _electron as electron } from 'playwright';
|
|
10
|
+
|
|
11
|
+
// Launch the Electron app
|
|
12
|
+
const app = await electron.launch({ args: ['./main.js'] });
|
|
13
|
+
|
|
14
|
+
// Get the first window (renderer process)
|
|
15
|
+
const window = await app.firstWindow();
|
|
16
|
+
|
|
17
|
+
// Access the main process for evaluation
|
|
18
|
+
const appPath = await app.evaluate(async ({ app }) => {
|
|
19
|
+
return app.getPath('userData');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Close the app
|
|
23
|
+
await app.close();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Patterns
|
|
27
|
+
|
|
28
|
+
### Basic Electron App Setup
|
|
29
|
+
|
|
30
|
+
**Use when**: Starting to test an Electron app with Playwright for the first time.
|
|
31
|
+
**Avoid when**: Your app is a web app, not an Electron app.
|
|
32
|
+
|
|
33
|
+
**TypeScript**
|
|
34
|
+
```typescript
|
|
35
|
+
import { test, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test';
|
|
36
|
+
|
|
37
|
+
let app: ElectronApplication;
|
|
38
|
+
let window: Page;
|
|
39
|
+
|
|
40
|
+
test.beforeAll(async () => {
|
|
41
|
+
// Launch the Electron app from your project directory
|
|
42
|
+
app = await electron.launch({
|
|
43
|
+
args: ['./dist/main.js'],
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
NODE_ENV: 'test',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Wait for the first BrowserWindow to open
|
|
51
|
+
window = await app.firstWindow();
|
|
52
|
+
|
|
53
|
+
// Optional: wait for the app to be fully loaded
|
|
54
|
+
await window.waitForLoadState('domcontentloaded');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test.afterAll(async () => {
|
|
58
|
+
await app.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('app window has correct title', async () => {
|
|
62
|
+
const title = await window.title();
|
|
63
|
+
expect(title).toBe('My Electron App');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('main page renders', async () => {
|
|
67
|
+
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**JavaScript**
|
|
72
|
+
```javascript
|
|
73
|
+
const { test, expect, _electron: electron } = require('@playwright/test');
|
|
74
|
+
|
|
75
|
+
let app;
|
|
76
|
+
let window;
|
|
77
|
+
|
|
78
|
+
test.beforeAll(async () => {
|
|
79
|
+
app = await electron.launch({
|
|
80
|
+
args: ['./dist/main.js'],
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
NODE_ENV: 'test',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
window = await app.firstWindow();
|
|
87
|
+
await window.waitForLoadState('domcontentloaded');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test.afterAll(async () => {
|
|
91
|
+
await app.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('app window has correct title', async () => {
|
|
95
|
+
const title = await window.title();
|
|
96
|
+
expect(title).toBe('My Electron App');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('main page renders', async () => {
|
|
100
|
+
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Electron App Fixture (Recommended)
|
|
105
|
+
|
|
106
|
+
**Use when**: You want isolated, reusable Electron app instances across test files.
|
|
107
|
+
**Avoid when**: All tests can share a single app instance (rare in practice).
|
|
108
|
+
|
|
109
|
+
**TypeScript**
|
|
110
|
+
```typescript
|
|
111
|
+
// fixtures.ts
|
|
112
|
+
import { test as base, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test';
|
|
113
|
+
|
|
114
|
+
type ElectronFixtures = {
|
|
115
|
+
electronApp: ElectronApplication;
|
|
116
|
+
window: Page;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const test = base.extend<ElectronFixtures>({
|
|
120
|
+
electronApp: async ({}, use) => {
|
|
121
|
+
const app = await electron.launch({
|
|
122
|
+
args: ['./dist/main.js'],
|
|
123
|
+
env: { ...process.env, NODE_ENV: 'test' },
|
|
124
|
+
});
|
|
125
|
+
await use(app);
|
|
126
|
+
await app.close();
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
window: async ({ electronApp }, use) => {
|
|
130
|
+
const window = await electronApp.firstWindow();
|
|
131
|
+
await window.waitForLoadState('domcontentloaded');
|
|
132
|
+
await use(window);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export { expect };
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// app.spec.ts
|
|
141
|
+
import { test, expect } from './fixtures';
|
|
142
|
+
|
|
143
|
+
test('navigate to settings', async ({ window }) => {
|
|
144
|
+
await window.getByRole('link', { name: 'Settings' }).click();
|
|
145
|
+
await expect(window.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**JavaScript**
|
|
150
|
+
```javascript
|
|
151
|
+
// fixtures.js
|
|
152
|
+
const { test: base, expect, _electron: electron } = require('@playwright/test');
|
|
153
|
+
|
|
154
|
+
const test = base.extend({
|
|
155
|
+
electronApp: async ({}, use) => {
|
|
156
|
+
const app = await electron.launch({
|
|
157
|
+
args: ['./dist/main.js'],
|
|
158
|
+
env: { ...process.env, NODE_ENV: 'test' },
|
|
159
|
+
});
|
|
160
|
+
await use(app);
|
|
161
|
+
await app.close();
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
window: async ({ electronApp }, use) => {
|
|
165
|
+
const window = await electronApp.firstWindow();
|
|
166
|
+
await window.waitForLoadState('domcontentloaded');
|
|
167
|
+
await use(window);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
module.exports = { test, expect };
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Accessing the Main Process
|
|
175
|
+
|
|
176
|
+
**Use when**: You need to read Electron app state, check paths, get app version, or verify main process behavior.
|
|
177
|
+
**Avoid when**: Everything you need is in the renderer (UI). Prefer testing through the UI.
|
|
178
|
+
|
|
179
|
+
`app.evaluate()` runs code in the main process with access to all Electron APIs.
|
|
180
|
+
|
|
181
|
+
**TypeScript**
|
|
182
|
+
```typescript
|
|
183
|
+
import { test, expect } from './fixtures';
|
|
184
|
+
|
|
185
|
+
test('verify app version and paths', async ({ electronApp }) => {
|
|
186
|
+
// Evaluate in the main process — receives the Electron module
|
|
187
|
+
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
|
188
|
+
return {
|
|
189
|
+
version: app.getVersion(),
|
|
190
|
+
name: app.getName(),
|
|
191
|
+
userData: app.getPath('userData'),
|
|
192
|
+
locale: app.getLocale(),
|
|
193
|
+
isPackaged: app.isPackaged,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(appInfo.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
198
|
+
expect(appInfo.name).toBe('my-electron-app');
|
|
199
|
+
expect(appInfo.userData).toBeTruthy();
|
|
200
|
+
expect(appInfo.isPackaged).toBe(false); // false during development
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('main process environment variables are set', async ({ electronApp }) => {
|
|
204
|
+
const nodeEnv = await electronApp.evaluate(async () => {
|
|
205
|
+
return process.env.NODE_ENV;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(nodeEnv).toBe('test');
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**JavaScript**
|
|
213
|
+
```javascript
|
|
214
|
+
const { test, expect } = require('./fixtures');
|
|
215
|
+
|
|
216
|
+
test('verify app version and paths', async ({ electronApp }) => {
|
|
217
|
+
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
|
218
|
+
return {
|
|
219
|
+
version: app.getVersion(),
|
|
220
|
+
name: app.getName(),
|
|
221
|
+
userData: app.getPath('userData'),
|
|
222
|
+
isPackaged: app.isPackaged,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(appInfo.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
227
|
+
expect(appInfo.name).toBe('my-electron-app');
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Testing IPC Communication
|
|
232
|
+
|
|
233
|
+
**Use when**: Your app uses `ipcMain` / `ipcRenderer` for communication between the main and renderer processes.
|
|
234
|
+
**Avoid when**: IPC is an implementation detail and the behavior is fully testable through the UI.
|
|
235
|
+
|
|
236
|
+
**TypeScript**
|
|
237
|
+
```typescript
|
|
238
|
+
import { test, expect } from './fixtures';
|
|
239
|
+
|
|
240
|
+
test('renderer sends IPC message and gets response', async ({ electronApp, window }) => {
|
|
241
|
+
// Trigger an IPC call from the renderer
|
|
242
|
+
const result = await window.evaluate(async () => {
|
|
243
|
+
// Assumes your preload script exposes ipcRenderer via contextBridge
|
|
244
|
+
return await (window as any).electronAPI.getSystemInfo();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result).toHaveProperty('platform');
|
|
248
|
+
expect(result).toHaveProperty('arch');
|
|
249
|
+
expect(result.platform).toBeTruthy();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('main process handles IPC file-read request', async ({ electronApp, window }) => {
|
|
253
|
+
// Set up a listener in the main process first
|
|
254
|
+
await electronApp.evaluate(async ({ ipcMain }) => {
|
|
255
|
+
ipcMain.handle('test-ping', async () => {
|
|
256
|
+
return { pong: true, timestamp: Date.now() };
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Send from renderer
|
|
261
|
+
const response = await window.evaluate(async () => {
|
|
262
|
+
return await (window as any).electronAPI.invoke('test-ping');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(response.pong).toBe(true);
|
|
266
|
+
expect(response.timestamp).toBeGreaterThan(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('IPC event triggers UI update', async ({ window }) => {
|
|
270
|
+
// Simulate the main process sending an event to the renderer
|
|
271
|
+
await window.evaluate(() => {
|
|
272
|
+
// Trigger a custom event that the app listens for
|
|
273
|
+
window.dispatchEvent(new CustomEvent('app:notification', {
|
|
274
|
+
detail: { message: 'Update available', version: '2.0.0' },
|
|
275
|
+
}));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await expect(window.getByText('Update available')).toBeVisible();
|
|
279
|
+
await expect(window.getByText('Version 2.0.0')).toBeVisible();
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**JavaScript**
|
|
284
|
+
```javascript
|
|
285
|
+
const { test, expect } = require('./fixtures');
|
|
286
|
+
|
|
287
|
+
test('renderer sends IPC message and gets response', async ({ electronApp, window }) => {
|
|
288
|
+
const result = await window.evaluate(async () => {
|
|
289
|
+
return await window.electronAPI.getSystemInfo();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(result).toHaveProperty('platform');
|
|
293
|
+
expect(result).toHaveProperty('arch');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('IPC event triggers UI update', async ({ window }) => {
|
|
297
|
+
await window.evaluate(() => {
|
|
298
|
+
window.dispatchEvent(new CustomEvent('app:notification', {
|
|
299
|
+
detail: { message: 'Update available', version: '2.0.0' },
|
|
300
|
+
}));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await expect(window.getByText('Update available')).toBeVisible();
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### File System Dialogs
|
|
308
|
+
|
|
309
|
+
**Use when**: Your app uses Electron's `dialog.showOpenDialog`, `dialog.showSaveDialog`, or similar native file dialogs.
|
|
310
|
+
**Avoid when**: File selection is handled by a web input (`<input type="file">`). Use standard Playwright file chooser for that.
|
|
311
|
+
|
|
312
|
+
Native dialogs cannot be interacted with directly. Mock them in the main process.
|
|
313
|
+
|
|
314
|
+
**TypeScript**
|
|
315
|
+
```typescript
|
|
316
|
+
import { test, expect } from './fixtures';
|
|
317
|
+
|
|
318
|
+
test('open file dialog and load a document', async ({ electronApp, window }) => {
|
|
319
|
+
// Mock the dialog to return a specific file path
|
|
320
|
+
await electronApp.evaluate(async ({ dialog }) => {
|
|
321
|
+
dialog.showOpenDialog = async () => ({
|
|
322
|
+
canceled: false,
|
|
323
|
+
filePaths: ['/tmp/test-document.txt'],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Click the "Open File" button in the renderer
|
|
328
|
+
await window.getByRole('button', { name: 'Open File' }).click();
|
|
329
|
+
|
|
330
|
+
// Verify the app loaded the file
|
|
331
|
+
await expect(window.getByTestId('file-name')).toHaveText('test-document.txt');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('save file dialog returns selected path', async ({ electronApp, window }) => {
|
|
335
|
+
await electronApp.evaluate(async ({ dialog }) => {
|
|
336
|
+
dialog.showSaveDialog = async () => ({
|
|
337
|
+
canceled: false,
|
|
338
|
+
filePath: '/tmp/exported-report.pdf',
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await window.getByRole('button', { name: 'Export PDF' }).click();
|
|
343
|
+
await expect(window.getByText('Saved to /tmp/exported-report.pdf')).toBeVisible();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('handle canceled file dialog', async ({ electronApp, window }) => {
|
|
347
|
+
await electronApp.evaluate(async ({ dialog }) => {
|
|
348
|
+
dialog.showOpenDialog = async () => ({
|
|
349
|
+
canceled: true,
|
|
350
|
+
filePaths: [],
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await window.getByRole('button', { name: 'Open File' }).click();
|
|
355
|
+
|
|
356
|
+
// App should not crash or change state
|
|
357
|
+
await expect(window.getByTestId('file-name')).toHaveText('No file selected');
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**JavaScript**
|
|
362
|
+
```javascript
|
|
363
|
+
const { test, expect } = require('./fixtures');
|
|
364
|
+
|
|
365
|
+
test('open file dialog and load a document', async ({ electronApp, window }) => {
|
|
366
|
+
await electronApp.evaluate(async ({ dialog }) => {
|
|
367
|
+
dialog.showOpenDialog = async () => ({
|
|
368
|
+
canceled: false,
|
|
369
|
+
filePaths: ['/tmp/test-document.txt'],
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await window.getByRole('button', { name: 'Open File' }).click();
|
|
374
|
+
await expect(window.getByTestId('file-name')).toHaveText('test-document.txt');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('handle canceled file dialog', async ({ electronApp, window }) => {
|
|
378
|
+
await electronApp.evaluate(async ({ dialog }) => {
|
|
379
|
+
dialog.showOpenDialog = async () => ({
|
|
380
|
+
canceled: true,
|
|
381
|
+
filePaths: [],
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await window.getByRole('button', { name: 'Open File' }).click();
|
|
386
|
+
await expect(window.getByTestId('file-name')).toHaveText('No file selected');
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### System Tray Testing
|
|
391
|
+
|
|
392
|
+
**Use when**: Your app has a system tray icon with context menus or status indicators.
|
|
393
|
+
**Avoid when**: Your app has no tray functionality.
|
|
394
|
+
|
|
395
|
+
Playwright cannot directly click system tray icons. Test the tray logic by evaluating in the main process.
|
|
396
|
+
|
|
397
|
+
**TypeScript**
|
|
398
|
+
```typescript
|
|
399
|
+
import { test, expect } from './fixtures';
|
|
400
|
+
|
|
401
|
+
test('tray icon is created on app launch', async ({ electronApp }) => {
|
|
402
|
+
const hasTray = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
|
403
|
+
// Access the tray via a reference your app stores
|
|
404
|
+
const { tray } = require('./tray-manager');
|
|
405
|
+
return tray !== null && !tray.isDestroyed();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(hasTray).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('tray tooltip shows unread count', async ({ electronApp }) => {
|
|
412
|
+
const tooltip = await electronApp.evaluate(async () => {
|
|
413
|
+
const { tray } = require('./tray-manager');
|
|
414
|
+
return tray.getToolTip();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(tooltip).toMatch(/\d+ unread messages?/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('clicking tray "Show" menu item opens the window', async ({ electronApp }) => {
|
|
421
|
+
// Simulate clicking a tray menu item by invoking its callback
|
|
422
|
+
await electronApp.evaluate(async ({ BrowserWindow }) => {
|
|
423
|
+
const { trayMenu } = require('./tray-manager');
|
|
424
|
+
// Find the "Show" menu item and invoke its click handler
|
|
425
|
+
const showItem = trayMenu.items.find((item: any) => item.label === 'Show');
|
|
426
|
+
if (showItem && showItem.click) {
|
|
427
|
+
showItem.click();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// The main window should now be visible
|
|
432
|
+
const window = await electronApp.firstWindow();
|
|
433
|
+
const isVisible = await window.evaluate(() => {
|
|
434
|
+
return document.visibilityState === 'visible';
|
|
435
|
+
});
|
|
436
|
+
expect(isVisible).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**JavaScript**
|
|
441
|
+
```javascript
|
|
442
|
+
const { test, expect } = require('./fixtures');
|
|
443
|
+
|
|
444
|
+
test('tray icon is created on app launch', async ({ electronApp }) => {
|
|
445
|
+
const hasTray = await electronApp.evaluate(async () => {
|
|
446
|
+
const { tray } = require('./tray-manager');
|
|
447
|
+
return tray !== null && !tray.isDestroyed();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(hasTray).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Multiple Windows
|
|
455
|
+
|
|
456
|
+
**Use when**: Your Electron app opens multiple windows (preferences, about, detached panels).
|
|
457
|
+
**Avoid when**: Your app uses a single window.
|
|
458
|
+
|
|
459
|
+
**TypeScript**
|
|
460
|
+
```typescript
|
|
461
|
+
import { test, expect } from './fixtures';
|
|
462
|
+
|
|
463
|
+
test('open and interact with preferences window', async ({ electronApp, window }) => {
|
|
464
|
+
// Click the menu item or button that opens the preferences window
|
|
465
|
+
await window.getByRole('menuitem', { name: 'Preferences' }).click();
|
|
466
|
+
|
|
467
|
+
// Wait for the new window to appear
|
|
468
|
+
const prefsWindow = await electronApp.waitForEvent('window');
|
|
469
|
+
await prefsWindow.waitForLoadState('domcontentloaded');
|
|
470
|
+
|
|
471
|
+
// Interact with the preferences window
|
|
472
|
+
await prefsWindow.getByLabel('Theme').selectOption('dark');
|
|
473
|
+
await prefsWindow.getByRole('button', { name: 'Save' }).click();
|
|
474
|
+
|
|
475
|
+
// Verify the main window reflects the change
|
|
476
|
+
await expect(window.locator('html')).toHaveAttribute('data-theme', 'dark');
|
|
477
|
+
|
|
478
|
+
// Close the preferences window
|
|
479
|
+
await prefsWindow.close();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('get all open windows', async ({ electronApp, window }) => {
|
|
483
|
+
// Open a second window
|
|
484
|
+
await window.getByRole('button', { name: 'New Window' }).click();
|
|
485
|
+
|
|
486
|
+
// Get all windows
|
|
487
|
+
const allWindows = electronApp.windows();
|
|
488
|
+
expect(allWindows.length).toBe(2);
|
|
489
|
+
|
|
490
|
+
// Find the new window (not the main one)
|
|
491
|
+
const newWindow = allWindows.find((w) => w !== window)!;
|
|
492
|
+
await expect(newWindow.getByRole('heading')).toBeVisible();
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**JavaScript**
|
|
497
|
+
```javascript
|
|
498
|
+
const { test, expect } = require('./fixtures');
|
|
499
|
+
|
|
500
|
+
test('open and interact with preferences window', async ({ electronApp, window }) => {
|
|
501
|
+
await window.getByRole('menuitem', { name: 'Preferences' }).click();
|
|
502
|
+
|
|
503
|
+
const prefsWindow = await electronApp.waitForEvent('window');
|
|
504
|
+
await prefsWindow.waitForLoadState('domcontentloaded');
|
|
505
|
+
|
|
506
|
+
await prefsWindow.getByLabel('Theme').selectOption('dark');
|
|
507
|
+
await prefsWindow.getByRole('button', { name: 'Save' }).click();
|
|
508
|
+
|
|
509
|
+
await expect(window.locator('html')).toHaveAttribute('data-theme', 'dark');
|
|
510
|
+
await prefsWindow.close();
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Testing Packaged/Built Apps
|
|
515
|
+
|
|
516
|
+
**Use when**: You want to test the production build of your Electron app (after `electron-builder`, `electron-forge`, etc.).
|
|
517
|
+
**Avoid when**: Development mode testing is sufficient for your CI pipeline.
|
|
518
|
+
|
|
519
|
+
**TypeScript**
|
|
520
|
+
```typescript
|
|
521
|
+
import { test, expect, _electron as electron } from '@playwright/test';
|
|
522
|
+
import path from 'path';
|
|
523
|
+
|
|
524
|
+
test('packaged app launches and works', async () => {
|
|
525
|
+
// Path to the packaged app executable
|
|
526
|
+
const appPath = process.platform === 'darwin'
|
|
527
|
+
? path.join(__dirname, '../dist/mac/MyApp.app/Contents/MacOS/MyApp')
|
|
528
|
+
: process.platform === 'win32'
|
|
529
|
+
? path.join(__dirname, '../dist/win-unpacked/MyApp.exe')
|
|
530
|
+
: path.join(__dirname, '../dist/linux-unpacked/my-app');
|
|
531
|
+
|
|
532
|
+
const app = await electron.launch({
|
|
533
|
+
executablePath: appPath,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const window = await app.firstWindow();
|
|
537
|
+
await window.waitForLoadState('domcontentloaded');
|
|
538
|
+
|
|
539
|
+
// Verify the packaged app works correctly
|
|
540
|
+
const title = await window.title();
|
|
541
|
+
expect(title).toBe('My Electron App');
|
|
542
|
+
|
|
543
|
+
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
|
544
|
+
|
|
545
|
+
// Verify it reports as packaged
|
|
546
|
+
const isPackaged = await app.evaluate(async ({ app }) => app.isPackaged);
|
|
547
|
+
expect(isPackaged).toBe(true);
|
|
548
|
+
|
|
549
|
+
await app.close();
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**JavaScript**
|
|
554
|
+
```javascript
|
|
555
|
+
const { test, expect, _electron: electron } = require('@playwright/test');
|
|
556
|
+
const path = require('path');
|
|
557
|
+
|
|
558
|
+
test('packaged app launches and works', async () => {
|
|
559
|
+
const appPath = process.platform === 'darwin'
|
|
560
|
+
? path.join(__dirname, '../dist/mac/MyApp.app/Contents/MacOS/MyApp')
|
|
561
|
+
: process.platform === 'win32'
|
|
562
|
+
? path.join(__dirname, '../dist/win-unpacked/MyApp.exe')
|
|
563
|
+
: path.join(__dirname, '../dist/linux-unpacked/my-app');
|
|
564
|
+
|
|
565
|
+
const app = await electron.launch({
|
|
566
|
+
executablePath: appPath,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const window = await app.firstWindow();
|
|
570
|
+
await window.waitForLoadState('domcontentloaded');
|
|
571
|
+
|
|
572
|
+
const title = await window.title();
|
|
573
|
+
expect(title).toBe('My Electron App');
|
|
574
|
+
|
|
575
|
+
await expect(window.getByRole('heading', { name: 'Welcome' })).toBeVisible();
|
|
576
|
+
await app.close();
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Decision Guide
|
|
581
|
+
|
|
582
|
+
| Scenario | Approach | Why |
|
|
583
|
+
|---|---|---|
|
|
584
|
+
| Launch Electron app for testing | `_electron.launch({ args: ['./main.js'] })` | Playwright's built-in Electron support |
|
|
585
|
+
| Get the main window | `app.firstWindow()` | Returns the first `BrowserWindow` as a Playwright `Page` |
|
|
586
|
+
| Read main process state | `app.evaluate(({ app }) => ...)` | Runs code in the main process with Electron APIs |
|
|
587
|
+
| Test IPC round-trips | `window.evaluate` (renderer) + `app.evaluate` (main) | Cover both sides of the IPC bridge |
|
|
588
|
+
| Mock native file dialogs | Override `dialog.showOpenDialog` via `app.evaluate` | Native dialogs cannot be automated directly |
|
|
589
|
+
| Test system tray | `app.evaluate` to invoke tray callbacks | Tray icons are OS-native; not clickable via Playwright |
|
|
590
|
+
| Multiple windows | `app.waitForEvent('window')` | Captures new `BrowserWindow` instances as they open |
|
|
591
|
+
| Test packaged builds | `electron.launch({ executablePath })` | Points to the built binary instead of source |
|
|
592
|
+
|
|
593
|
+
## Anti-Patterns
|
|
594
|
+
|
|
595
|
+
| Don't Do This | Problem | Do This Instead |
|
|
596
|
+
|---|---|---|
|
|
597
|
+
| `const { app } = require('electron')` in test files | Electron APIs are not available in Playwright's Node process | Use `electronApp.evaluate(({ app }) => ...)` |
|
|
598
|
+
| Directly import renderer code into tests | Bypasses the actual app lifecycle and IPC | Test through the UI via the `window` (Page) object |
|
|
599
|
+
| Skip `waitForLoadState` after `firstWindow()` | Window may not be fully rendered | Always `await window.waitForLoadState('domcontentloaded')` |
|
|
600
|
+
| Test tray by clicking system-level UI | Playwright cannot interact with native OS chrome | Mock tray menu callbacks via `app.evaluate` |
|
|
601
|
+
| Share a single `ElectronApplication` across all tests without cleanup | State leaks between tests | Use fixtures with `app.close()` in teardown |
|
|
602
|
+
| Forget to close the app in `afterAll` | Leaves Electron processes running, eating CI resources | Always `await app.close()` in teardown |
|
|
603
|
+
|
|
604
|
+
## Troubleshooting
|
|
605
|
+
|
|
606
|
+
| Symptom | Cause | Fix |
|
|
607
|
+
|---|---|---|
|
|
608
|
+
| `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 |
|
|
609
|
+
| `firstWindow()` times out | App does not open a `BrowserWindow` in time | Check that the app creates a window on startup; increase timeout |
|
|
610
|
+
| `app.evaluate` cannot access Electron modules | Destructuring the wrong parameter | Destructure correctly: `evaluate(async ({ app, dialog, BrowserWindow }) => ...)` |
|
|
611
|
+
| 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 |
|
|
612
|
+
| Second window not captured | `waitForEvent('window')` registered after the window opened | Register the event listener before triggering the action that opens the window |
|
|
613
|
+
| Tests hang after `app.close()` | Child processes spawned by the app are still running | Ensure your Electron app cleans up child processes on quit |
|
|
614
|
+
| Packaged app test fails with path error | Executable path varies by OS and build tool | Use `process.platform` to compute the correct path |
|
|
615
|
+
| `window.evaluate` throws context destroyed | Window was closed or navigated during evaluation | Ensure the window is stable before evaluating |
|
|
616
|
+
|
|
617
|
+
## Related
|
|
618
|
+
|
|
619
|
+
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrapping Electron app launch in fixtures
|
|
620
|
+
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- all standard assertions work on Electron windows
|
|
621
|
+
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- Electron apps often embed web components or iframes
|
|
622
|
+
- [core/browser-apis.md](browser-apis.md) -- localStorage, IndexedDB, and other APIs work the same in Electron
|