源码
tests/desktop/shortcut-settings-parity.spec.ts
import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication, Page } from "@playwright/test";
import { shortcutCommands } from "../../packages/desktop/src/shared/shortcuts.ts";
import { done, fakeServer } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";
const primary = process.platform === "darwin" ? "Meta" : "Control";
async function settings(page: Page) {
await page.getByRole("button", { name: "设置", exact: true }).click();
await page.getByRole("button", { name: "快捷键", exact: true }).click();
}
async function held(page: Page, key: string, code: string) {
await page.evaluate(
({ key, code }) => {
for (let i = 0; i < 3; i++)
document.activeElement?.dispatchEvent(
new KeyboardEvent("keydown", {
key,
code,
metaKey: window.zpi.platform === "darwin",
ctrlKey: window.zpi.platform !== "darwin",
repeat: true,
bubbles: true,
cancelable: true,
}),
);
},
{ key, code },
);
}
test("shortcut search, capture, cancel, clear, reset confirmation and failure feedback", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-shortcut-parity-"));
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
await settings(page);
const rows = page.locator("[data-shortcut-command]");
await expect(rows).toHaveCount(shortcutCommands.length);
const search = page.getByRole("textbox", { name: "搜索命令或快捷键" });
await search.fill("选择模型");
await expect(rows).toHaveCount(1);
await search.fill(process.platform === "darwin" ? "cmd+l" : "ctrl+l");
await expect(rows).toHaveAttribute("data-shortcut-command", "select-model");
await search.fill("不存在的命令");
await expect(page.getByText("没有匹配的命令或快捷键")).toBeVisible();
await page.getByRole("button", { name: "清空搜索", exact: true }).click();
const keySearch = page.getByRole("button", { name: "按组合键搜索", exact: true });
await keySearch.click();
await expect(keySearch).toHaveAttribute("aria-pressed", "true");
await held(page, "k", "KeyK");
await expect(keySearch).toHaveAttribute("aria-pressed", "true");
await page.keyboard.press(`${primary}+k`);
await expect(rows).toHaveCount(1);
await expect(rows).toHaveAttribute("data-shortcut-command", "search");
await expect(page.locator(".task-search-dialog")).toHaveCount(0);
await keySearch.click();
await expect(keySearch).toHaveAttribute("aria-pressed", "true");
await page.keyboard.press("Backspace");
await page.keyboard.press("Escape");
await expect(keySearch).toHaveAttribute("aria-pressed", "false");
await expect(rows).toHaveCount(shortcutCommands.length);
const model = page.locator('[data-shortcut-command="select-model"]');
const old = await model.locator(".shortcut-recorder").textContent();
await model.getByLabel("编辑选择模型快捷键", { exact: true }).click();
await expect(model.locator(".shortcut-recorder")).toHaveAttribute("aria-pressed", "true");
await page.keyboard.press(`${primary}+u`);
await expect(model.getByRole("alert")).toContainText("冲突");
await page.keyboard.press("Alt+b");
await expect(model.getByRole("alert")).toContainText("Ctrl 或 Command");
await page.keyboard.press("Escape");
await expect(model.locator(".shortcut-recorder")).toHaveText(old ?? "");
await model.getByLabel("清除选择模型快捷键", { exact: true }).click();
await expect(model.locator(".shortcut-recorder")).toHaveText("未设置");
await expect(model.getByLabel("清除选择模型快捷键", { exact: true })).toBeDisabled();
await page.getByLabel("启用快捷键", { exact: true }).uncheck();
await page.getByRole("button", { name: "全部恢复默认", exact: true }).click();
await page.getByRole("button", { name: "取消", exact: true }).click();
await expect(model.locator(".shortcut-recorder")).toHaveText("未设置");
await page.getByRole("button", { name: "全部恢复默认", exact: true }).click();
await page.getByRole("button", { name: "恢复默认", exact: true }).click();
await expect(model.locator(".shortcut-recorder")).toHaveText(old ?? "");
await expect(page.getByLabel("启用快捷键", { exact: true })).not.toBeChecked();
// Inject the controller's registration-error response at IPC, without touching OS global keys.
await app.evaluate(({ ipcMain }) => {
const handlers = (
ipcMain as unknown as { _invokeHandlers: Map<string, (...args: unknown[]) => unknown> }
)._invokeHandlers;
const original = handlers.get("zpi:call");
if (!original) throw new Error("Missing IPC handler");
let fail = true;
ipcMain.removeHandler("zpi:call");
ipcMain.handle("zpi:call", (event, method, args) => {
if (method === "updateShortcuts" && fail) {
fail = false;
return {
ok: false,
error: {
code: "configuration",
message: "全局快捷键注册失败,可能已被其他应用或系统占用,请更换组合",
},
};
}
return original(event, method, args);
});
});
await page.getByRole("button", { name: "保存快捷键", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("注册失败");
expect(
await page.evaluate(async () => {
const result = await window.zpi.getSettings();
return result.ok && result.value.shortcuts.enabled;
}),
).toBe(true);
await page.getByRole("button", { name: "保存快捷键", exact: true }).click();
await expect(page.getByRole("status")).toHaveText("快捷键已保存。");
await app.close();
app = await launchDesktop({ dir, url: "" });
const reopened = await app.firstWindow();
await settings(reopened);
await expect(reopened.getByLabel("启用快捷键", { exact: true })).not.toBeChecked();
await expect(reopened.locator('[data-shortcut-command="select-model"] .shortcut-recorder')).toHaveText(
old ?? "",
);
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});
test("model, search, find and task menu toggle from their own focused layer and ignore repeats", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-overlay-shortcuts-"));
const cwd = join(dir, "workspace");
await mkdir(cwd);
const server = await fakeServer((_, response) => done(response));
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: server.url });
const page = await app.firstWindow();
await page.getByLabel("消息", { exact: true }).fill("task menu fixture");
await page.getByLabel("消息", { exact: true }).press("Enter");
await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
const editor = page.getByLabel("消息", { exact: true });
for (const [key, code, selector] of [
["l", "KeyL", '.model-menu[data-state="open"]'],
["k", "KeyK", '.task-search-dialog[data-state="open"]'],
["f", "KeyF", ".task-find-bar"],
["u", "KeyU", '.task-menu[data-state="open"]'],
]) {
await editor.focus();
await page.keyboard.press(`${primary}+${key}`);
await expect(page.locator(selector)).toBeVisible();
await expect
.poll(() => page.locator(selector).evaluate((layer) => layer.contains(document.activeElement)))
.toBe(true);
await held(page, key, code);
await expect(page.locator(selector)).toBeVisible();
await page.keyboard.press(`${primary}+${key}`);
await expect(page.locator(selector.replace('[data-state="open"]', ""))).toHaveCount(0);
await expect(editor).toBeFocused();
await page.keyboard.press(`${primary}+${key}`);
await expect(page.locator(selector)).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator(selector.replace('[data-state="open"]', ""))).toHaveCount(0);
}
await editor.focus();
await page.keyboard.press(`${primary}+u`);
await expect(page.getByRole("menuitem", { name: "置顶", exact: true })).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(page.getByRole("menuitem", { name: "重命名任务", exact: true })).toBeFocused();
await page.keyboard.press("ArrowUp");
await expect(page.getByRole("menuitem", { name: "置顶", exact: true })).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.locator(".pinned-tasks [data-session-id]")).toHaveCount(1);
await expect(page.locator(".task-menu")).toHaveCount(0);
await editor.focus();
await page.keyboard.press(`${primary}+u`);
await expect(page.getByRole("menuitem", { name: "取消置顶", exact: true })).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(page.getByRole("menuitem", { name: "重命名任务", exact: true })).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.getByRole("dialog", { name: "重命名任务", exact: true })).toBeVisible();
await expect(page.locator(".task-menu")).toHaveCount(0);
await expect(page.getByRole("textbox", { name: "名称", exact: true })).toBeFocused();
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});
test("environment shortcut closes from the nested branch search and restores its composer", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-environment-toggle-"));
const cwd = join(dir, "workspace");
await mkdir(cwd);
execFileSync("git", ["init", cwd]);
execFileSync("git", [
"-C",
cwd,
"-c",
"user.name=Test",
"-c",
"user.email=test@example.invalid",
"commit",
"--allow-empty",
"-m",
"Initial",
]);
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: "", project: cwd });
const page = await app.firstWindow();
await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
const editor = page.getByLabel("消息", { exact: true });
await expect(page.getByRole("button", { name: "配置本地环境", exact: true })).toBeVisible();
await editor.focus();
await expect(editor).toBeFocused();
await page.keyboard.press(`${primary}+j`);
await expect(page.locator('.environment-menu[data-state="open"]').first()).toBeVisible();
await page.getByRole("menuitemcheckbox", { name: "新工作树" }).click();
await page.getByRole("menuitem", { name: /起始分支/ }).hover();
const input = page.locator(".worktree-branch-menu input");
await expect(input).toBeVisible();
await input.focus();
await held(page, "j", "KeyJ");
await expect(input).toBeVisible();
await page.keyboard.press(`${primary}+j`);
await expect(page.locator('.environment-menu[data-state="open"]')).toHaveCount(0);
await expect(editor).toBeFocused();
await page.keyboard.press(`${primary}+j`);
await expect(page.locator('.environment-menu[data-state="open"]').first()).toBeVisible();
await page.keyboard.press("Escape");
await expect(editor).toBeFocused();
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});