electrobun-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Electrobun 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
defineTest()
from the test framework:
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() 字段

FieldTypeRequiredDescription
id
string✅Stable slug (kebab-case, globally unique)
title
string✅Human-readable display name
category
string✅API grouping (e.g. "BrowserWindow", "Utils")
description
string✅One-sentence description
interactive
boolean✅
true
if requires human input
apiSurface
string[]✅Electrobun APIs exercised
run
async function✅Test body (see below)
playgroundRoute
string?—Playground HTML path if has UI
uiSelectors
string[]—CSS selectors used in playground
platformCaveats
string[]—e.g.
["macOS only"]
字段类型必填说明
id
string✅稳定 slug(kebab-case,全局唯一)
title
string✅便于阅读的显示名称
category
string✅API 分组,例如“BrowserWindow”“Utils”
description
string✅一句话描述
interactive
boolean✅如果需要人工输入,则为
true
apiSurface
string[]✅测试调用的 Electrobun API
run
异步函数✅测试主体(见下文)
playgroundRoute
string?—如果有 UI,则为 Playground HTML 路径
uiSelectors
string[]—Playground 中使用的 CSS 选择器
platformCaveats
string[]—例如
["macOS only"]

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.ts

Writing Automated Tests

编写自动化测试

Automated tests (
interactive: false
) run entirely in code — no human in the loop.
自动化测试(
interactive: false
)完全通过代码运行,无需人工参与。

Pattern: 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 (
interactive: true
) open a playground window and walk the user through a verification flow.
typescript
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();
  },
}),
交互式测试(
interactive: true
)会打开一个 Playground 窗口,并引导用户完成验证流程。
typescript
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)
,
webview
,
setTitle
,
close
,
focus
,
show
,
minimize
,
unminimize
,
isMinimized
,
maximize
,
unmaximize
,
isMaximized
,
setFullScreen
,
isFullScreen
,
setAlwaysOnTop
,
isAlwaysOnTop
,
setVisibleOnAllWorkspaces
,
isVisibleOnAllWorkspaces
,
setPosition
,
setSize
,
setFrame
,
getFrame
,
getPosition
,
getSize
,
setPageZoom
,
getPageZoom
,
on('close'|'move'|'resize'|'blur'|'focus')
new BrowserWindow(options)
、
getById(id)
、
webview
、
setTitle
、
close
、
focus
、
show
、
minimize
、
unminimize
、
isMinimized
、
maximize
、
unmaximize
、
isMaximized
、
setFullScreen
、
isFullScreen
、
setAlwaysOnTop
、
isAlwaysOnTop
、
setVisibleOnAllWorkspaces
、
isVisibleOnAllWorkspaces
、
setPosition
、
setSize
、
setFrame
、
getFrame
、
getPosition
、
getSize
、
setPageZoom
、
getPageZoom
、
on('close'|'move'|'resize'|'blur'|'focus')

BrowserView

BrowserView

BrowserView.defineRPC(schema)
,
new BrowserView(options)
,
executeJavascript
,
loadURL
,
loadHTML
,
setNavigationRules
,
stopFindInPage
,
openDevTools
,
closeDevTools
,
toggleDevTools
,
setPageZoom
,
getPageZoom
,
remove
,
getById
,
getAll
,
on('will-navigate'|'did-navigate'|'dom-ready')
,
rpc.request.evaluateJavascriptWithResponse
BrowserView.defineRPC(schema)
、
new BrowserView(options)
、
executeJavascript
、
loadURL
、
loadHTML
、
setNavigationRules
、
stopFindInPage
、
openDevTools
、
closeDevTools
、
toggleDevTools
、
setPageZoom
、
getPageZoom
、
remove
、
getById
、
getAll
、
on('will-navigate'|'did-navigate'|'dom-ready')
、
rpc.request.evaluateJavascriptWithResponse

Tray

Tray

new Tray(options)
,
setTitle
,
setImage
,
setMenu
,
setVisible
,
getBounds
,
remove
,
on('tray-clicked')
,
getById
,
getAll
,
removeById
new Tray(options)
、
setTitle
、
setImage
、
setMenu
、
setVisible
、
getBounds
、
remove
、
on('tray-clicked')
、
getById
、
getAll
、
removeById

ApplicationMenu / 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

moveToTrash
,
showItemInFolder
,
openExternal
,
openPath
,
setDockIconVisible
,
isDockIconVisible
,
showNotification
,
quit
,
openFileDialog
,
showMessageBox
,
clipboardReadText
,
clipboardWriteText
,
clipboardReadImage
,
clipboardWriteImage
,
clipboardClear
,
clipboardAvailableFormats
,
paths.*
moveToTrash
、
showItemInFolder
、
openExternal
、
openPath
、
setDockIconVisible
、
isDockIconVisible
、
showNotification
、
quit
、
openFileDialog
、
showMessageBox
、
clipboardReadText
、
clipboardWriteText
、
clipboardReadImage
、
clipboardWriteImage
、
clipboardClear
、
clipboardAvailableFormats
、
paths.*

Session

Session

Session.fromPartition(name)
,
Session.defaultSession
,
session.cookies.set/get/remove/clear
Session.fromPartition(name)
、
Session.defaultSession
、
session.cookies.set/get/remove/clear

Screen

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.getLocal
,
Updater.onStatusChange
,
Updater.checkForUpdate
,
Updater.downloadUpdate
,
Updater.updateInfo
,
Updater.applyUpdate
,
Updater.getStatusHistory
,
Updater.clearStatusHistory
Updater status values:
checking
,
update-available
,
downloading
,
update-ready
,
no-update
,
error
Updater.getLocal
、
Updater.onStatusChange
、
Updater.checkForUpdate
、
Updater.downloadUpdate
、
Updater.updateInfo
、
Updater.applyUpdate
、
Updater.getStatusHistory
、
Updater.clearStatusHistory
Updater 状态值:
checking
、
update-available
、
downloading
、
update-ready
、
no-update
、
error

View-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
platformCaveats
array when a test is platform-specific:
typescript
platformCaveats: ["macOS only"],
platformCaveats: ["requires CEF renderer"],
platformCaveats: ["unreliable on Linux window managers"],
platformCaveats: ["ARM Windows: commented out due to VM crashes"],
当测试针对特定平台时,始终添加
platformCaveats
数组:
typescript
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 kitchen
bash
cd kitchen

After 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

undefined
undefined