651 lines
21 KiB
Markdown
651 lines
21 KiB
Markdown
# Electron 测试
|
||
|
||
> **使用场景**:当你的应用是 Electron 桌面应用,并且需要对渲染进程、主进程、IPC 通信、原生对话框、系统托盘和多窗口工作流进行端到端测试时。
|
||
> **前置条件**:[core/configuration.md](configuration.md)、[core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
||
|
||
## 快速参考
|
||
|
||
```typescript
|
||
import { _electron as electron } from "playwright"
|
||
|
||
// 启动 Electron 应用
|
||
const app = await electron.launch({ args: ["./main.js"] })
|
||
|
||
// 获取第一个窗口(渲染进程)
|
||
const window = await app.firstWindow()
|
||
|
||
// 访问主进程进行评估
|
||
const appPath = await app.evaluate(async ({ app }) => {
|
||
return app.getPath("userData")
|
||
})
|
||
|
||
// 关闭应用
|
||
await app.close()
|
||
```
|
||
|
||
## 模式
|
||
|
||
### 基础 Electron 应用设置
|
||
|
||
**使用场景**:首次使用 Playwright 测试 Electron 应用时。
|
||
**避免场景**:你的应用是 Web 应用,而非 Electron 应用。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect, _electron as electron, ElectronApplication, Page } from "@playwright/test"
|
||
|
||
let app: ElectronApplication
|
||
let window: Page
|
||
|
||
test.beforeAll(async () => {
|
||
// 从项目目录启动 Electron 应用
|
||
app = await electron.launch({
|
||
args: ["./dist/main.js"],
|
||
env: {
|
||
...process.env,
|
||
NODE_ENV: "test",
|
||
},
|
||
})
|
||
|
||
// 等待第一个 BrowserWindow 打开
|
||
window = await app.firstWindow()
|
||
|
||
// 可选:等待应用完全加载
|
||
await window.waitForLoadState("domcontentloaded")
|
||
})
|
||
|
||
test.afterAll(async () => {
|
||
await app.close()
|
||
})
|
||
|
||
test("应用窗口标题正确", async () => {
|
||
const title = await window.title()
|
||
expect(title).toBe("My Electron App")
|
||
})
|
||
|
||
test("主页面渲染正常", 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("应用窗口标题正确", async () => {
|
||
const title = await window.title()
|
||
expect(title).toBe("My Electron App")
|
||
})
|
||
|
||
test("主页面渲染正常", async () => {
|
||
await expect(window.getByRole("heading", { name: "Welcome" })).toBeVisible()
|
||
})
|
||
```
|
||
|
||
### Electron 应用 Fixture(推荐)
|
||
|
||
**使用场景**:你希望跨测试文件获得隔离的、可复用的 Electron 应用实例。
|
||
**避免场景**:所有测试可以共享同一个应用实例(实际中很少见)。
|
||
|
||
**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("导航到设置页面", 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 }
|
||
```
|
||
|
||
### 访问主进程
|
||
|
||
**使用场景**:你需要读取 Electron 应用状态、检查路径、获取应用版本,或验证主进程行为。
|
||
**避免场景**:你需要的一切都在渲染进程(UI)中。优先通过 UI 进行测试。
|
||
|
||
`app.evaluate()` 在主进程中运行代码,可访问所有 Electron API。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect } from "./fixtures"
|
||
|
||
test("验证应用版本和路径", async ({ electronApp }) => {
|
||
// 在主进程中执行评估——接收 Electron 模块
|
||
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
|
||
})
|
||
|
||
test("主进程环境变量已设置", 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("验证应用版本和路径", 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")
|
||
})
|
||
```
|
||
|
||
### 测试 IPC 通信
|
||
|
||
**使用场景**:你的应用使用 `ipcMain` / `ipcRenderer` 在主进程和渲染进程之间进行通信。
|
||
**避免场景**:IPC 是实现细节,且其行为完全可以通过 UI 进行测试。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect } from "./fixtures"
|
||
|
||
test("渲染进程发送 IPC 消息并接收响应", async ({ electronApp, window }) => {
|
||
// 从渲染进程触发 IPC 调用
|
||
const result = await window.evaluate(async () => {
|
||
// 假设你的预加载脚本通过 contextBridge 暴露了 ipcRenderer
|
||
return await (window as any).electronAPI.getSystemInfo()
|
||
})
|
||
|
||
expect(result).toHaveProperty("platform")
|
||
expect(result).toHaveProperty("arch")
|
||
expect(result.platform).toBeTruthy()
|
||
})
|
||
|
||
test("主进程处理 IPC 文件读取请求", async ({ electronApp, window }) => {
|
||
// 首先在主进程中设置监听器
|
||
await electronApp.evaluate(async ({ ipcMain }) => {
|
||
ipcMain.handle("test-ping", async () => {
|
||
return { pong: true, timestamp: Date.now() }
|
||
})
|
||
})
|
||
|
||
// 从渲染进程发送消息
|
||
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 事件触发 UI 更新", 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()
|
||
await expect(window.getByText("Version 2.0.0")).toBeVisible()
|
||
})
|
||
```
|
||
|
||
**JavaScript**
|
||
|
||
```javascript
|
||
const { test, expect } = require("./fixtures")
|
||
|
||
test("渲染进程发送 IPC 消息并接收响应", async ({ electronApp, window }) => {
|
||
const result = await window.evaluate(async () => {
|
||
return await window.electronAPI.getSystemInfo()
|
||
})
|
||
|
||
expect(result).toHaveProperty("platform")
|
||
expect(result).toHaveProperty("arch")
|
||
})
|
||
|
||
test("IPC 事件触发 UI 更新", 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()
|
||
})
|
||
```
|
||
|
||
### 文件系统对话框
|
||
|
||
**使用场景**:你的应用使用 Electron 的 `dialog.showOpenDialog`、`dialog.showSaveDialog` 或类似的原生文件对话框。
|
||
**避免场景**:文件选择由 Web 输入控件(`<input type="file">`)处理。这种情况应使用标准 Playwright 文件选择器。
|
||
|
||
原生对话框无法直接交互。在主进程中对它们进行模拟。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect } from "./fixtures"
|
||
|
||
test("打开文件对话框并加载文档", 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("保存文件对话框返回所选路径", 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("处理取消的文件对话框", 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")
|
||
})
|
||
```
|
||
|
||
**JavaScript**
|
||
|
||
```javascript
|
||
const { test, expect } = require("./fixtures")
|
||
|
||
test("打开文件对话框并加载文档", 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("处理取消的文件对话框", 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")
|
||
})
|
||
```
|
||
|
||
### 系统托盘测试
|
||
|
||
**使用场景**:你的应用带有包含上下文菜单或状态指示器的系统托盘图标。
|
||
**避免场景**:你的应用没有托盘功能。
|
||
|
||
Playwright 无法直接点击系统托盘图标。通过在主进程中执行评估来测试托盘逻辑。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect } from "./fixtures"
|
||
|
||
test("应用启动时托盘图标已创建", async ({ electronApp }) => {
|
||
const hasTray = await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||
// 通过应用存储的引用来访问托盘
|
||
const { tray } = require("./tray-manager")
|
||
return tray !== null && !tray.isDestroyed()
|
||
})
|
||
|
||
expect(hasTray).toBe(true)
|
||
})
|
||
|
||
test("托盘工具提示显示未读计数", async ({ electronApp }) => {
|
||
const tooltip = await electronApp.evaluate(async () => {
|
||
const { tray } = require("./tray-manager")
|
||
return tray.getToolTip()
|
||
})
|
||
|
||
expect(tooltip).toMatch(/\d+ unread messages?/)
|
||
})
|
||
|
||
test('点击托盘"显示"菜单项可打开窗口', async ({ electronApp }) => {
|
||
// 通过调用其回调来模拟点击托盘菜单项
|
||
await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||
const { trayMenu } = require("./tray-manager")
|
||
// 找到"显示"菜单项并调用其点击处理函数
|
||
const showItem = trayMenu.items.find((item: any) => item.label === "Show")
|
||
if (showItem && showItem.click) {
|
||
showItem.click()
|
||
}
|
||
})
|
||
|
||
// 主窗口现在应该可见
|
||
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("应用启动时托盘图标已创建", async ({ electronApp }) => {
|
||
const hasTray = await electronApp.evaluate(async () => {
|
||
const { tray } = require("./tray-manager")
|
||
return tray !== null && !tray.isDestroyed()
|
||
})
|
||
|
||
expect(hasTray).toBe(true)
|
||
})
|
||
```
|
||
|
||
### 多窗口
|
||
|
||
**使用场景**:你的 Electron 应用打开了多个窗口(偏好设置、关于页面、分离面板)。
|
||
**避免场景**:你的应用使用单个窗口。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect } from "./fixtures"
|
||
|
||
test("打开偏好设置窗口并进行交互", 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()
|
||
})
|
||
|
||
test("获取所有打开的窗口", async ({ electronApp, window }) => {
|
||
// 打开第二个窗口
|
||
await window.getByRole("button", { name: "New Window" }).click()
|
||
|
||
// 获取所有窗口
|
||
const allWindows = electronApp.windows()
|
||
expect(allWindows.length).toBe(2)
|
||
|
||
// 找到新窗口(非主窗口)
|
||
const newWindow = allWindows.find((w) => w !== window)!
|
||
await expect(newWindow.getByRole("heading")).toBeVisible()
|
||
})
|
||
```
|
||
|
||
**JavaScript**
|
||
|
||
```javascript
|
||
const { test, expect } = require("./fixtures")
|
||
|
||
test("打开偏好设置窗口并进行交互", 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()
|
||
})
|
||
```
|
||
|
||
### 测试打包/构建后的应用
|
||
|
||
**使用场景**:你希望测试 Electron 应用的生产构建版本(经过 `electron-builder`、`electron-forge` 等工具打包后)。
|
||
**避免场景**:开发模式测试对你的 CI 管道来说已足够。
|
||
|
||
**TypeScript**
|
||
|
||
```typescript
|
||
import { test, expect, _electron as electron } from "@playwright/test"
|
||
import path from "path"
|
||
|
||
test("打包后的应用启动并正常运行", 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()
|
||
|
||
// 验证应用报告为已打包状态
|
||
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("打包后的应用启动并正常运行", 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()
|
||
})
|
||
```
|
||
|
||
## 决策指南
|
||
|
||
| 场景 | 方法 | 原因 |
|
||
| --- | --- | --- |
|
||
| 启动 Electron 应用进行测试 | `_electron.launch({ args: ['./main.js'] })` | Playwright 内置的 Electron 支持 |
|
||
| 获取主窗口 | `app.firstWindow()` | 将第一个 `BrowserWindow` 以 Playwright `Page` 的形式返回 |
|
||
| 读取主进程状态 | `app.evaluate(({ app }) => ...)` | 在主进程中运行代码,可使用 Electron API |
|
||
| 测试 IPC 往返通信 | `window.evaluate`(渲染进程)+ `app.evaluate`(主进程) | 覆盖 IPC 桥的两端 |
|
||
| 模拟原生文件对话框 | 通过 `app.evaluate` 覆盖 `dialog.showOpenDialog` | 原生对话框无法直接自动化 |
|
||
| 测试系统托盘 | 通过 `app.evaluate` 调用托盘回调 | 托盘图标是操作系统原生的,无法通过 Playwright 点击 |
|
||
| 多窗口 | `app.waitForEvent('window')` | 在新 `BrowserWindow` 实例打开时捕获它们 |
|
||
| 测试打包后的构建版本 | `electron.launch({ executablePath })` | 指向构建后的二进制文件而非源代码 |
|
||
|
||
## 反模式
|
||
|
||
| 不要这样做 | 问题 | 应这样做 |
|
||
| --- | --- | --- |
|
||
| 在测试文件中使用 `const { app } = require('electron')` | Electron API 在 Playwright 的 Node 进程中不可用 | 使用 `electronApp.evaluate(({ app }) => ...)` |
|
||
| 将渲染进程代码直接导入测试中 | 绕过了实际的应用生命周期和 IPC | 通过 `window`(Page)对象在 UI 层面进行测试 |
|
||
| 在 `firstWindow()` 之后跳过 `waitForLoadState` | 窗口可能尚未完全渲染 | 始终使用 `await window.waitForLoadState('domcontentloaded')` |
|
||
| 通过点击系统级 UI 来测试托盘 | Playwright 无法与原生操作系统 UI 交互 | 通过 `app.evaluate` 模拟托盘菜单回调 |
|
||
| 在所有测试中共享同一个 `ElectronApplication` 实例而不做清理 | 状态在测试之间泄漏 | 使用 fixture,并在拆卸中调用 `app.close()` |
|
||
| 忘记在 `afterAll` 中关闭应用 | 留下 Electron 进程持续运行,消耗 CI 资源 | 始终在拆卸中执行 `await app.close()` |
|
||
|
||
## 故障排除
|
||
|
||
| 症状 | 原因 | 修复方法 |
|
||
| --- | --- | --- |
|
||
| `electron.launch()` 抛出"cannot find module" | `args` 路径未指向你的主入口文件 | 验证路径:`args: ['./dist/main.js']` 相对于工作目录 |
|
||
| `firstWindow()` 超时 | 应用未及时打开 `BrowserWindow` | 检查应用是否在启动时创建窗口;增加超时时间 |
|
||
| `app.evaluate` 无法访问 Electron 模块 | 解构了错误的参数 | 正确解构:`evaluate(async ({ app, dialog, BrowserWindow }) => ...)` |
|
||
| 对话框模拟未生效 | 模拟在对话框已被调用之后才生效 | 在触发打开对话框的 UI 操作之前设置好模拟 |
|
||
| 第二个窗口未被捕获 | `waitForEvent('window')` 在窗口打开后才注册 | 在触发打开窗口的操作之前注册事件监听器 |
|
||
| `app.close()` 后测试挂起 | 应用产生的子进程仍在运行 | 确保你的 Electron 应用在退出时清理子进程 |
|
||
| 打包应用测试因路径错误而失败 | 可执行文件路径因操作系统和构建工具而异 | 使用 `process.platform` 计算正确的路径 |
|
||
| `window.evaluate` 抛出"context destroyed"错误 | 评估期间窗口被关闭或发生了导航 | 确保窗口在评估前处于稳定状态 |
|
||
|
||
## 相关文档
|
||
|
||
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) —— 将 Electron 应用启动封装到 fixture 中
|
||
- [core/assertions-and-waiting.md](assertions-and-waiting.md) —— 所有标准断言均适用于 Electron 窗口
|
||
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) —— Electron 应用常嵌入 Web Components 或 iframe
|
||
- [core/browser-apis.md](browser-apis.md) —— localStorage、IndexedDB 等 API 在 Electron 中的工作方式相同
|