electrobun-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseElectrobun Testing
Electrobun 测试
Tests for Electrobun APIs live in the Kitchen Sink app ().
kitchen/src/tests/针对 Electrobun API 的测试位于 Kitchen Sink 应用()中。
kitchen/src/tests/Test Definition Pattern
测试定义模式
All tests use from the test framework:
defineTest()typescript
// kitchen/src/tests/window.test.ts
import { defineTest } from "../test-framework/defineTest";
export const windowTests = [
defineTest({
id: "window-creation-with-url", // stable slug — must be unique
title: "Window creation with URL",
category: "BrowserWindow",
description: "Verifies BrowserWindow can be created with a URL",
interactive: false, // automated: no human needed
apiSurface: ["BrowserWindow"], // APIs exercised (for manifest)
async run({ assert, log }) {
const win = new BrowserWindow({
title: "Test Window",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
log("Window created, id:", win.id);
assert(win.id > 0, "Window should have valid id");
await win.close();
},
}),
];所有测试都使用测试框架中的 :
defineTest()typescript
// kitchen/src/tests/window.test.ts
import { defineTest } from "../test-framework/defineTest";
export const windowTests = [
defineTest({
id: "window-creation-with-url", // stable slug — must be unique
title: "Window creation with URL",
category: "BrowserWindow",
description: "Verifies BrowserWindow can be created with a URL",
interactive: false, // automated: no human needed
apiSurface: ["BrowserWindow"], // APIs exercised (for manifest)
async run({ assert, log }) {
const win = new BrowserWindow({
title: "Test Window",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
log("Window created, id:", win.id);
assert(win.id > 0, "Window should have valid id");
await win.close();
},
}),
];defineTest() Fields
defineTest() 字段
| Field | Type | Required | Description |
|---|---|---|---|
| string | ✅ | Stable slug (kebab-case, globally unique) |
| string | ✅ | Human-readable display name |
| string | ✅ | API grouping (e.g. "BrowserWindow", "Utils") |
| string | ✅ | One-sentence description |
| boolean | ✅ | |
| string[] | ✅ | Electrobun APIs exercised |
| async function | ✅ | Test body (see below) |
| string? | — | Playground HTML path if has UI |
| string[] | — | CSS selectors used in playground |
| string[] | — | e.g. |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | ✅ | 稳定 slug(kebab-case,全局唯一) |
| string | ✅ | 便于阅读的显示名称 |
| string | ✅ | API 分组,例如“BrowserWindow”“Utils” |
| string | ✅ | 一句话描述 |
| boolean | ✅ | 如果需要人工输入,则为 |
| string[] | ✅ | 测试调用的 Electrobun API |
| 异步函数 | ✅ | 测试主体(见下文) |
| string? | — | 如果有 UI,则为 Playground HTML 路径 |
| string[] | — | Playground 中使用的 CSS 选择器 |
| string[] | — | 例如 |
Test Body API
测试主体 API
typescript
async run({ assert, log, skip, fail }) {
// assert(condition, message) — fails test if false
assert(value === expected, "description of what was expected");
// log(message) — appears in test runner UI
log("Window id:", win.id);
// skip(reason) — marks test as skipped (not failed)
if (process.platform === "win32") skip("Not supported on Windows");
// fail(message) — immediately fail with message
fail("Should not reach here");
}typescript
async run({ assert, log, skip, fail }) {
// assert(condition, message) — fails test if false
assert(value === expected, "description of what was expected");
// log(message) — appears in test runner UI
log("Window id:", win.id);
// skip(reason) — marks test as skipped (not failed)
if (process.platform === "win32") skip("Not supported on Windows");
// fail(message) — immediately fail with message
fail("Should not reach here");
}Registering Test Suites
注册测试套件
After writing a test file, add it to the aggregator:
typescript
// kitchen/src/tests/index.ts
import { windowTests } from "./window.test";
import { myNewTests } from "./my-new-feature.test";
export const allTests = [
...windowTests,
...myNewTests, // ← add here
];Then regenerate the manifest:
bash
cd kitchen
npx tsx scripts/generate-manifest.ts编写完测试文件后,将其添加到聚合器中:
typescript
// kitchen/src/tests/index.ts
import { windowTests } from "./window.test";
import { myNewTests } from "./my-new-feature.test";
export const allTests = [
...windowTests,
...myNewTests, // ← add here
];然后重新生成 manifest:
bash
cd kitchen
npx tsx scripts/generate-manifest.tsWriting Automated Tests
编写自动化测试
Automated tests () run entirely in code — no human in the loop.
interactive: false自动化测试()完全通过代码运行,无需人工参与。
interactive: falsePattern: Test a BrowserWindow method
模式:测试 BrowserWindow 方法
typescript
defineTest({
id: "window-set-title",
title: "BrowserWindow.setTitle()",
category: "BrowserWindow",
description: "Verifies setTitle changes the window title",
interactive: false,
apiSurface: ["BrowserWindow"],
async run({ assert, log }) {
const win = new BrowserWindow({
title: "Original Title",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
win.setTitle("New Title");
// Observe via evaluateJavascript or just test the API doesn't throw
log("Title changed successfully");
assert(true, "setTitle completed without error");
await win.close();
},
}),typescript
defineTest({
id: "window-set-title",
title: "BrowserWindow.setTitle()",
category: "BrowserWindow",
description: "Verifies setTitle changes the window title",
interactive: false,
apiSurface: ["BrowserWindow"],
async run({ assert, log }) {
const win = new BrowserWindow({
title: "Original Title",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
win.setTitle("New Title");
// Observe via evaluateJavascript or just test the API doesn't throw
log("Title changed successfully");
assert(true, "setTitle completed without error");
await win.close();
},
}),Pattern: Test RPC round-trip
模式:测试 RPC 往返
typescript
defineTest({
id: "rpc-request-response",
title: "RPC request-response round trip",
category: "RPC",
description: "Verifies bun-side request handler is callable from webview",
interactive: false,
apiSurface: ["BrowserView"],
async run({ assert, log }) {
const win = new BrowserWindow({
title: "RPC Test",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
// Use evaluateJavascriptWithResponse to trigger webview-side RPC call
const result = await win.webview.rpc.request.evaluateJavascriptWithResponse({
script: `electrobun.rpc.request.someBunFunction({ a: 2, b: 3 })`,
});
assert(result === 5, `Expected 5, got ${result}`);
log("RPC round trip passed");
await win.close();
},
}),typescript
defineTest({
id: "rpc-request-response",
title: "RPC request-response round trip",
category: "RPC",
description: "Verifies bun-side request handler is callable from webview",
interactive: false,
apiSurface: ["BrowserView"],
async run({ assert, log }) {
const win = new BrowserWindow({
title: "RPC Test",
url: "views://test-harness/index.html",
hidden: true,
rpc: testRpc,
});
// Use evaluateJavascriptWithResponse to trigger webview-side RPC call
const result = await win.webview.rpc.request.evaluateJavascriptWithResponse({
script: `electrobun.rpc.request.someBunFunction({ a: 2, b: 3 })`,
});
assert(result === 5, `Expected 5, got ${result}`);
log("RPC round trip passed");
await win.close();
},
}),Pattern: Test Utils / system APIs
模式:测试 Utils / 系统 API
typescript
defineTest({
id: "utils-clipboard-text",
title: "Clipboard read/write text",
category: "Utils",
description: "Verifies clipboardWriteText and clipboardReadText round-trip",
interactive: false,
apiSurface: ["Utils"],
async run({ assert, log }) {
Utils.clipboardWriteText("electrobun-test-value");
const text = Utils.clipboardReadText();
assert(text === "electrobun-test-value", `Expected clipboard text, got: ${text}`);
Utils.clipboardClear();
log("Clipboard round-trip passed");
},
}),typescript
defineTest({
id: "utils-clipboard-text",
title: "Clipboard read/write text",
category: "Utils",
description: "Verifies clipboardWriteText and clipboardReadText round-trip",
interactive: false,
apiSurface: ["Utils"],
async run({ assert, log }) {
Utils.clipboardWriteText("electrobun-test-value");
const text = Utils.clipboardReadText();
assert(text === "electrobun-test-value", `Expected clipboard text, got: ${text}`);
Utils.clipboardClear();
log("Clipboard round-trip passed");
},
}),Pattern: Test navigation rules
模式:测试导航规则
typescript
defineTest({
id: "navigation-block-rule",
title: "setNavigationRules blocks disallowed URL",
category: "Navigation",
description: "Verifies last-match-wins navigation rules",
interactive: false,
apiSurface: ["BrowserView"],
async run({ assert, log }) {
const win = new BrowserWindow({ url: "views://test-harness/index.html", hidden: true, rpc: testRpc });
win.webview.setNavigationRules([
{ match: "*", allow: false }, // block all
{ match: "views://*", allow: true }, // allow views:// (last match wins)
]);
// Navigate to allowed URL — should succeed
await win.webview.loadURL("views://test-harness/index.html");
log("Navigation rules applied");
await win.close();
},
}),typescript
defineTest({
id: "navigation-block-rule",
title: "setNavigationRules blocks disallowed URL",
category: "Navigation",
description: "Verifies last-match-wins navigation rules",
interactive: false,
apiSurface: ["BrowserView"],
async run({ assert, log }) {
const win = new BrowserWindow({ url: "views://test-harness/index.html", hidden: true, rpc: testRpc });
win.webview.setNavigationRules([
{ match: "*", allow: false }, // block all
{ match: "views://*", allow: true }, // allow views:// (last match wins)
]);
// Navigate to allowed URL — should succeed
await win.webview.loadURL("views://test-harness/index.html");
log("Navigation rules applied");
await win.close();
},
}),Writing Interactive Tests
编写交互式测试
Interactive tests () open a playground window and walk the user through a verification flow.
interactive: truetypescript
defineTest({
id: "dialog-open-file",
title: "Open file dialog",
category: "Dialogs",
description: "Opens the native file picker and verifies a path is returned",
interactive: true,
apiSurface: ["Utils"],
playgroundRoute: "playgrounds/file-dialog/index.html",
uiSelectors: ["#openDialogBtn", "#result"],
async run({ assert, log, waitForReady, waitForVerify }) {
// Open the playground window
const playground = new BrowserWindow({
title: "File Dialog Playground",
url: "views://playgrounds/file-dialog/index.html",
rpc: playgroundRpc,
});
// Wait for user to click Start in the modal
await waitForReady();
// Instructions have been shown; user now interacts with playground window
// Wait for user to click Pass/Fail/Retest
const { action, notes } = await waitForVerify();
assert(action === "pass", `User marked ${action}: ${notes}`);
await playground.close();
},
}),交互式测试()会打开一个 Playground 窗口,并引导用户完成验证流程。
interactive: truetypescript
defineTest({
id: "dialog-open-file",
title: "Open file dialog",
category: "Dialogs",
description: "Opens the native file picker and verifies a path is returned",
interactive: true,
apiSurface: ["Utils"],
playgroundRoute: "playgrounds/file-dialog/index.html",
uiSelectors: ["#openDialogBtn", "#result"],
async run({ assert, log, waitForReady, waitForVerify }) {
// Open the playground window
const playground = new BrowserWindow({
title: "File Dialog Playground",
url: "views://playgrounds/file-dialog/index.html",
rpc: playgroundRpc,
});
// Wait for user to click Start in the modal
await waitForReady();
// Instructions have been shown; user now interacts with playground window
// Wait for user to click Pass/Fail/Retest
const { action, notes } = await waitForVerify();
assert(action === "pass", `User marked ${action}: ${notes}`);
await playground.close();
},
}),Full API Surface Tested by Kitchen Sink
Kitchen Sink 测试覆盖的完整 API 范围
BrowserWindow
BrowserWindow
new BrowserWindow(options)getById(id)webviewsetTitleclosefocusshowminimizeunminimizeisMinimizedmaximizeunmaximizeisMaximizedsetFullScreenisFullScreensetAlwaysOnTopisAlwaysOnTopsetVisibleOnAllWorkspacesisVisibleOnAllWorkspacessetPositionsetSizesetFramegetFramegetPositiongetSizesetPageZoomgetPageZoomon('close'|'move'|'resize'|'blur'|'focus')new BrowserWindow(options)getById(id)webviewsetTitleclosefocusshowminimizeunminimizeisMinimizedmaximizeunmaximizeisMaximizedsetFullScreenisFullScreensetAlwaysOnTopisAlwaysOnTopsetVisibleOnAllWorkspacesisVisibleOnAllWorkspacessetPositionsetSizesetFramegetFramegetPositiongetSizesetPageZoomgetPageZoomon('close'|'move'|'resize'|'blur'|'focus')BrowserView
BrowserView
BrowserView.defineRPC(schema)new BrowserView(options)executeJavascriptloadURLloadHTMLsetNavigationRulesstopFindInPageopenDevToolscloseDevToolstoggleDevToolssetPageZoomgetPageZoomremovegetByIdgetAllon('will-navigate'|'did-navigate'|'dom-ready')rpc.request.evaluateJavascriptWithResponseBrowserView.defineRPC(schema)new BrowserView(options)executeJavascriptloadURLloadHTMLsetNavigationRulesstopFindInPageopenDevToolscloseDevToolstoggleDevToolssetPageZoomgetPageZoomremovegetByIdgetAllon('will-navigate'|'did-navigate'|'dom-ready')rpc.request.evaluateJavascriptWithResponseTray
Tray
new Tray(options)setTitlesetImagesetMenusetVisiblegetBoundsremoveon('tray-clicked')getByIdgetAllremoveByIdnew Tray(options)setTitlesetImagesetMenusetVisiblegetBoundsremoveon('tray-clicked')getByIdgetAllremoveByIdApplicationMenu / ContextMenu
ApplicationMenu / ContextMenu
ApplicationMenu.setApplicationMenu(menu)ContextMenu.showContextMenu(menu)Electrobun.events.on('application-menu-clicked')Electrobun.events.on('context-menu-clicked')ApplicationMenu.setApplicationMenu(menu)ContextMenu.showContextMenu(menu)Electrobun.events.on('application-menu-clicked')Electrobun.events.on('context-menu-clicked')Utils
Utils
moveToTrashshowItemInFolderopenExternalopenPathsetDockIconVisibleisDockIconVisibleshowNotificationquitopenFileDialogshowMessageBoxclipboardReadTextclipboardWriteTextclipboardReadImageclipboardWriteImageclipboardClearclipboardAvailableFormatspaths.*moveToTrashshowItemInFolderopenExternalopenPathsetDockIconVisibleisDockIconVisibleshowNotificationquitopenFileDialogshowMessageBoxclipboardReadTextclipboardWriteTextclipboardReadImageclipboardWriteImageclipboardClearclipboardAvailableFormatspaths.*Session
Session
Session.fromPartition(name)Session.defaultSessionsession.cookies.set/get/remove/clearSession.fromPartition(name)Session.defaultSessionsession.cookies.set/get/remove/clearScreen
Screen
Screen.getPrimaryDisplay()Screen.getAllDisplays()Screen.getCursorScreenPoint()Screen.getPrimaryDisplay()Screen.getAllDisplays()Screen.getCursorScreenPoint()GlobalShortcut
GlobalShortcut
GlobalShortcut.register(accelerator, handler)GlobalShortcut.unregister(accelerator)GlobalShortcut.unregisterAll()GlobalShortcut.register(accelerator, handler)GlobalShortcut.unregister(accelerator)GlobalShortcut.unregisterAll()Updater
Updater
Updater.getLocalUpdater.onStatusChangeUpdater.checkForUpdateUpdater.downloadUpdateUpdater.updateInfoUpdater.applyUpdateUpdater.getStatusHistoryUpdater.clearStatusHistoryUpdater status values: , , , , ,
checkingupdate-availabledownloadingupdate-readyno-updateerrorUpdater.getLocalUpdater.onStatusChangeUpdater.checkForUpdateUpdater.downloadUpdateUpdater.updateInfoUpdater.applyUpdateUpdater.getStatusHistoryUpdater.clearStatusHistoryUpdater 状态值:、、、、、
checkingupdate-availabledownloadingupdate-readyno-updateerrorView-side (electrobun/view)
视图端(electrobun/view)
Electroview.defineRPC(schema)new Electroview({ rpc })electrobun.rpc.request.*electrobun.rpc.send.*evaluateJavascriptWithResponse({ script })Electroview.defineRPC(schema)new Electroview({ rpc })electrobun.rpc.request.*electrobun.rpc.send.*evaluateJavascriptWithResponse({ script })Platform Caveats to Document in Tests
测试中需要说明的平台注意事项
Always add a array when a test is platform-specific:
platformCaveatstypescript
platformCaveats: ["macOS only"],
platformCaveats: ["requires CEF renderer"],
platformCaveats: ["unreliable on Linux window managers"],
platformCaveats: ["ARM Windows: commented out due to VM crashes"],当测试针对特定平台时,始终添加 数组:
platformCaveatstypescript
platformCaveats: ["macOS only"],
platformCaveats: ["requires CEF renderer"],
platformCaveats: ["unreliable on Linux window managers"],
platformCaveats: ["ARM Windows: commented out due to VM crashes"],Generating and Validating After Changes
变更后的生成与验证
bash
cd kitchenbash
cd kitchenAfter adding new defineTest() calls:
After adding new defineTest() calls:
npx tsx scripts/generate-manifest.ts
npx tsx scripts/generate-manifest.ts
Verify everything is consistent:
Verify everything is consistent:
npx tsx scripts/validate-manifest.ts
npx tsx scripts/validate-manifest.ts
Checks:
Checks:
1. Every test in index.ts has manifest entries
1. Every test in index.ts has manifest entries
2. Every playground route with a test is represented
2. Every playground route with a test is represented
3. All entries have required fields
3. All entries have required fields
4. No duplicate IDs
4. No duplicate IDs
undefinedundefined