mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
chore: add playwright cursor skill
This commit is contained in:
parent
25aad38ca4
commit
d52225c18d
57 changed files with 25244 additions and 0 deletions
509
.cursor/skills/playwright-testing/testing-patterns/electron.md
Normal file
509
.cursor/skills/playwright-testing/testing-patterns/electron.md
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
# Electron Testing
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Setup & Configuration](#setup--configuration)
|
||||
2. [Launching Electron Apps](#launching-electron-apps)
|
||||
3. [Main Process Testing](#main-process-testing)
|
||||
4. [Renderer Process Testing](#renderer-process-testing)
|
||||
5. [IPC Communication](#ipc-communication)
|
||||
6. [Native Features](#native-features)
|
||||
7. [Packaging & Distribution](#packaging--distribution)
|
||||
|
||||
## Setup & Configuration
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install -D @playwright/test electron
|
||||
```
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
timeout: 30000,
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Electron Test Fixture
|
||||
|
||||
```typescript
|
||||
// fixtures/electron.ts
|
||||
import {
|
||||
test as base,
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page,
|
||||
} from "@playwright/test";
|
||||
|
||||
type ElectronFixtures = {
|
||||
electronApp: ElectronApplication;
|
||||
window: Page;
|
||||
};
|
||||
|
||||
export const test = base.extend<ElectronFixtures>({
|
||||
electronApp: async ({}, use) => {
|
||||
// Launch Electron app
|
||||
const electronApp = await electron.launch({
|
||||
args: [".", "--no-sandbox"],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "test",
|
||||
},
|
||||
});
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
// Cleanup
|
||||
await electronApp.close();
|
||||
},
|
||||
|
||||
window: async ({ electronApp }, use) => {
|
||||
// Wait for first window
|
||||
const window = await electronApp.firstWindow();
|
||||
|
||||
// Wait for app to be ready
|
||||
await window.waitForLoadState("domcontentloaded");
|
||||
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
```
|
||||
|
||||
### Launch Options
|
||||
|
||||
```typescript
|
||||
// Advanced launch configuration
|
||||
const electronApp = await electron.launch({
|
||||
args: ["main.js", "--custom-flag"],
|
||||
cwd: "/path/to/app",
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_ENABLE_LOGGING: "1",
|
||||
NODE_ENV: "test",
|
||||
},
|
||||
timeout: 30000,
|
||||
// For packaged apps
|
||||
executablePath: "/path/to/MyApp.app/Contents/MacOS/MyApp",
|
||||
});
|
||||
```
|
||||
|
||||
## Launching Electron Apps
|
||||
|
||||
### Development Mode
|
||||
|
||||
```typescript
|
||||
test("launch in dev mode", async () => {
|
||||
const electronApp = await electron.launch({
|
||||
args: ["."], // Points to package.json main
|
||||
});
|
||||
|
||||
const window = await electronApp.firstWindow();
|
||||
await expect(window.locator("h1")).toContainText("My App");
|
||||
|
||||
await electronApp.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Packaged Application
|
||||
|
||||
```typescript
|
||||
test("launch packaged app", async () => {
|
||||
const appPath =
|
||||
process.platform === "darwin"
|
||||
? "/Applications/MyApp.app/Contents/MacOS/MyApp"
|
||||
: process.platform === "win32"
|
||||
? "C:\\Program Files\\MyApp\\MyApp.exe"
|
||||
: "/usr/bin/myapp";
|
||||
|
||||
const electronApp = await electron.launch({
|
||||
executablePath: appPath,
|
||||
});
|
||||
|
||||
const window = await electronApp.firstWindow();
|
||||
await expect(window).toHaveTitle(/MyApp/);
|
||||
|
||||
await electronApp.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Windows
|
||||
|
||||
```typescript
|
||||
test("handle multiple windows", async ({ electronApp }) => {
|
||||
const mainWindow = await electronApp.firstWindow();
|
||||
|
||||
// Trigger new window
|
||||
await mainWindow.getByRole("button", { name: "Open Settings" }).click();
|
||||
|
||||
// Wait for new window
|
||||
const settingsWindow = await electronApp.waitForEvent("window");
|
||||
|
||||
// Both windows are now accessible
|
||||
await expect(settingsWindow.locator("h1")).toHaveText("Settings");
|
||||
await expect(mainWindow.locator("h1")).toHaveText("Main");
|
||||
|
||||
// Get all windows
|
||||
const windows = electronApp.windows();
|
||||
expect(windows.length).toBe(2);
|
||||
});
|
||||
```
|
||||
|
||||
## Main Process Testing
|
||||
|
||||
### Evaluate in Main Process
|
||||
|
||||
```typescript
|
||||
test("access main process", async ({ electronApp }) => {
|
||||
// Evaluate in main process context
|
||||
const appPath = await electronApp.evaluate(async ({ app }) => {
|
||||
return app.getAppPath();
|
||||
});
|
||||
|
||||
expect(appPath).toContain("my-electron-app");
|
||||
});
|
||||
```
|
||||
|
||||
### Access Electron APIs
|
||||
|
||||
```typescript
|
||||
test("electron API access", async ({ electronApp }) => {
|
||||
// Get app version
|
||||
const version = await electronApp.evaluate(async ({ app }) => {
|
||||
return app.getVersion();
|
||||
});
|
||||
expect(version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
|
||||
// Get platform info
|
||||
const platform = await electronApp.evaluate(async ({ app }) => {
|
||||
return process.platform;
|
||||
});
|
||||
expect(["darwin", "win32", "linux"]).toContain(platform);
|
||||
|
||||
// Check if app is ready
|
||||
const isReady = await electronApp.evaluate(async ({ app }) => {
|
||||
return app.isReady();
|
||||
});
|
||||
expect(isReady).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
### BrowserWindow Properties
|
||||
|
||||
```typescript
|
||||
test("check window properties", async ({ electronApp, window }) => {
|
||||
// Get BrowserWindow from main process
|
||||
const windowBounds = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
return win.getBounds();
|
||||
});
|
||||
|
||||
expect(windowBounds.width).toBeGreaterThan(0);
|
||||
expect(windowBounds.height).toBeGreaterThan(0);
|
||||
|
||||
// Check window state
|
||||
const isMaximized = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
return win.isMaximized();
|
||||
});
|
||||
|
||||
// Check window title
|
||||
const title = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
return win.getTitle();
|
||||
});
|
||||
expect(title).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
## Renderer Process Testing
|
||||
|
||||
### Standard Page Testing
|
||||
|
||||
```typescript
|
||||
test("renderer interactions", async ({ window }) => {
|
||||
// Standard Playwright page interactions
|
||||
await window.getByRole("button", { name: "Click Me" }).click();
|
||||
await expect(window.getByText("Clicked!")).toBeVisible();
|
||||
|
||||
// Fill forms
|
||||
await window.getByLabel("Username").fill("testuser");
|
||||
await window.getByLabel("Password").fill("password123");
|
||||
await window.getByRole("button", { name: "Login" }).click();
|
||||
|
||||
// Verify navigation
|
||||
await expect(window).toHaveURL(/dashboard/);
|
||||
});
|
||||
```
|
||||
|
||||
### Access Node.js in Renderer
|
||||
|
||||
```typescript
|
||||
test("node integration", async ({ window }) => {
|
||||
// If nodeIntegration is enabled
|
||||
const nodeVersion = await window.evaluate(() => {
|
||||
return (window as any).process?.version;
|
||||
});
|
||||
|
||||
// Check if Node APIs are available
|
||||
const hasFs = await window.evaluate(() => {
|
||||
return typeof (window as any).require === "function";
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Context Isolation Testing
|
||||
|
||||
```typescript
|
||||
test("context isolation", async ({ window }) => {
|
||||
// Test preload script exposed APIs
|
||||
const apiAvailable = await window.evaluate(() => {
|
||||
return typeof (window as any).electronAPI !== "undefined";
|
||||
});
|
||||
expect(apiAvailable).toBe(true);
|
||||
|
||||
// Call exposed API
|
||||
const result = await window.evaluate(async () => {
|
||||
return await (window as any).electronAPI.getAppVersion();
|
||||
});
|
||||
expect(result).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
```
|
||||
|
||||
## IPC Communication
|
||||
|
||||
### Testing IPC from Renderer
|
||||
|
||||
```typescript
|
||||
test("IPC invoke", async ({ window }) => {
|
||||
// Test preload-exposed IPC call
|
||||
const result = await window.evaluate(async () => {
|
||||
return await (window as any).electronAPI.getData("user-settings");
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("theme");
|
||||
});
|
||||
```
|
||||
|
||||
### Testing IPC from Main Process
|
||||
|
||||
```typescript
|
||||
test("main to renderer IPC", async ({ electronApp, window }) => {
|
||||
// Set up listener in renderer
|
||||
await window.evaluate(() => {
|
||||
(window as any).receivedMessage = null;
|
||||
(window as any).electronAPI.onMessage((msg: string) => {
|
||||
(window as any).receivedMessage = msg;
|
||||
});
|
||||
});
|
||||
|
||||
// Send from main process
|
||||
await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win.webContents.send("message", "Hello from main!");
|
||||
});
|
||||
|
||||
// Verify receipt
|
||||
await window.waitForFunction(() => (window as any).receivedMessage !== null);
|
||||
const message = await window.evaluate(() => (window as any).receivedMessage);
|
||||
expect(message).toBe("Hello from main!");
|
||||
});
|
||||
```
|
||||
|
||||
### Mock IPC Handlers
|
||||
|
||||
```typescript
|
||||
// In test setup or fixture
|
||||
test("mock IPC handler", async ({ electronApp, window }) => {
|
||||
// Override IPC handler in main process
|
||||
await electronApp.evaluate(async ({ ipcMain }) => {
|
||||
// Remove existing handler
|
||||
ipcMain.removeHandler("fetch-data");
|
||||
|
||||
// Add mock handler
|
||||
ipcMain.handle("fetch-data", async () => {
|
||||
return { mocked: true, data: "test-data" };
|
||||
});
|
||||
});
|
||||
|
||||
// Test with mocked handler
|
||||
const result = await window.evaluate(async () => {
|
||||
return await (window as any).electronAPI.fetchData();
|
||||
});
|
||||
|
||||
expect(result.mocked).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
## Native Features
|
||||
|
||||
### File System Dialogs
|
||||
|
||||
```typescript
|
||||
test("file dialog", async ({ electronApp, window }) => {
|
||||
// Mock dialog response
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showOpenDialog = async () => ({
|
||||
canceled: false,
|
||||
filePaths: ["/mock/path/file.txt"],
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger file open
|
||||
await window.getByRole("button", { name: "Open File" }).click();
|
||||
|
||||
// Verify file was "opened"
|
||||
await expect(window.getByText("file.txt")).toBeVisible();
|
||||
});
|
||||
|
||||
test("save dialog", async ({ electronApp, window }) => {
|
||||
await electronApp.evaluate(async ({ dialog }) => {
|
||||
dialog.showSaveDialog = async () => ({
|
||||
canceled: false,
|
||||
filePath: "/mock/path/saved-file.txt",
|
||||
});
|
||||
});
|
||||
|
||||
await window.getByRole("button", { name: "Save" }).click();
|
||||
await expect(window.getByText("Saved successfully")).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Menu Testing
|
||||
|
||||
```typescript
|
||||
test("application menu", async ({ electronApp }) => {
|
||||
// Get menu structure
|
||||
const menuLabels = await electronApp.evaluate(async ({ Menu }) => {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
return menu?.items.map((item) => item.label) || [];
|
||||
});
|
||||
|
||||
expect(menuLabels).toContain("File");
|
||||
expect(menuLabels).toContain("Edit");
|
||||
|
||||
// Trigger menu action
|
||||
await electronApp.evaluate(async ({ Menu }) => {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
const fileMenu = menu?.items.find((item) => item.label === "File");
|
||||
const newItem = fileMenu?.submenu?.items.find(
|
||||
(item) => item.label === "New",
|
||||
);
|
||||
newItem?.click();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Native Notifications
|
||||
|
||||
```typescript
|
||||
test("notifications", async ({ electronApp, window }) => {
|
||||
// Mock Notification
|
||||
let notificationShown = false;
|
||||
await electronApp.evaluate(async ({ Notification }) => {
|
||||
const OriginalNotification = Notification;
|
||||
(global as any).Notification = class extends OriginalNotification {
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
(global as any).lastNotification = options;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Trigger notification
|
||||
await window.getByRole("button", { name: "Notify" }).click();
|
||||
|
||||
// Verify notification was created
|
||||
const notification = await electronApp.evaluate(async () => {
|
||||
return (global as any).lastNotification;
|
||||
});
|
||||
|
||||
expect(notification.title).toBe("New Message");
|
||||
});
|
||||
```
|
||||
|
||||
### Clipboard
|
||||
|
||||
```typescript
|
||||
test("clipboard operations", async ({ electronApp, window }) => {
|
||||
// Write to clipboard
|
||||
await electronApp.evaluate(async ({ clipboard }) => {
|
||||
clipboard.writeText("Test clipboard content");
|
||||
});
|
||||
|
||||
// Paste in app
|
||||
await window.getByRole("textbox").focus();
|
||||
await window.keyboard.press("ControlOrMeta+v");
|
||||
|
||||
// Read clipboard
|
||||
const clipboardContent = await electronApp.evaluate(async ({ clipboard }) => {
|
||||
return clipboard.readText();
|
||||
});
|
||||
|
||||
expect(clipboardContent).toBe("Test clipboard content");
|
||||
});
|
||||
```
|
||||
|
||||
## Packaging & Distribution
|
||||
|
||||
### Testing Packaged Apps
|
||||
|
||||
```typescript
|
||||
// fixtures/packaged-electron.ts
|
||||
import { test as base, _electron as electron } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
export const test = base.extend({
|
||||
electronApp: async ({}, use) => {
|
||||
// Build the app first (or use pre-built)
|
||||
const distPath = path.join(__dirname, "../dist");
|
||||
|
||||
let executablePath: string;
|
||||
if (process.platform === "darwin") {
|
||||
executablePath = path.join(
|
||||
distPath,
|
||||
"mac",
|
||||
"MyApp.app",
|
||||
"Contents",
|
||||
"MacOS",
|
||||
"MyApp",
|
||||
);
|
||||
} else if (process.platform === "win32") {
|
||||
executablePath = path.join(distPath, "win-unpacked", "MyApp.exe");
|
||||
} else {
|
||||
executablePath = path.join(distPath, "linux-unpacked", "myapp");
|
||||
}
|
||||
|
||||
const electronApp = await electron.launch({ executablePath });
|
||||
await use(electronApp);
|
||||
await electronApp.close();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
| ------------------------------------- | ---------------------------- | -------------------------------------------- |
|
||||
| Not closing ElectronApplication | Resource leaks | Always call `electronApp.close()` in cleanup |
|
||||
| Hardcoded executable paths | Breaks cross-platform | Use platform detection |
|
||||
| Testing packaged app without building | Outdated code | Build before testing or test dev mode |
|
||||
| Ignoring IPC in tests | Missing coverage | Test IPC communication explicitly |
|
||||
| Not mocking native dialogs | Tests hang waiting for input | Mock dialog responses |
|
||||
|
||||
## Related References
|
||||
|
||||
- **Fixtures**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for custom fixture patterns
|
||||
- **Component Testing**: See [component-testing.md](component-testing.md) for renderer testing patterns
|
||||
- **Debugging**: See [debugging.md](../debugging/debugging.md) for troubleshooting
|
||||
Loading…
Add table
Add a link
Reference in a new issue