源码
tests/desktop/settings-appearance.spec.ts
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expectZCodeSystemFont } from "../helpers/rendered-fonts.ts";
import { expect, test } from "../helpers/test.ts";
import { seedHistory } from "../history-fixture.ts";
test("settings replace the workspace on the first frame without changing its layout or state", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-settings-first-frame-"));
const session = seedHistory(dir, dir, 1);
let app: Awaited<ReturnType<typeof launchDesktop>>;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.locator(`[data-session-id="${session.id}"] .session-name`).click();
await page.getByLabel("展开右侧栏", { exact: true }).click();
await expect(page.locator(".right-pane")).toHaveCSS("opacity", "1");
for (const theme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme: theme });
const firstFrame = await page.evaluate(async () => {
const panels = [".sidebar", ".main", ".right-pane"].map((selector) => {
const node = document.querySelector<HTMLElement>(selector);
if (!node) throw Error(`Missing ${selector}`);
return node;
});
const before = panels.map((node) => node.getBoundingClientRect().toJSON());
const button = document.querySelector<HTMLButtonElement>(".sidebar-footer button");
if (!button) throw Error("Missing settings button");
button.click();
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
return {
settingsPresent: Boolean(document.querySelector(".settings-screen")),
before,
after: panels.map((node) => node.getBoundingClientRect().toJSON()),
panels: panels.map((node) => {
let painted = true;
for (let parent: HTMLElement | null = node; parent; parent = parent.parentElement) {
const style = getComputedStyle(parent);
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
painted = false;
}
return { painted, inert: Boolean(node.closest("[inert]")) };
}),
};
});
expect(firstFrame.settingsPresent).toBe(true);
expect(firstFrame.panels).toEqual(Array(3).fill({ painted: false, inert: true }));
expect(firstFrame.after).toEqual(firstFrame.before);
await page.screenshot({ path: `test-results/settings-first-frame-${theme}.png` });
await page.locator(".mention-editor").evaluate((node: HTMLElement) => node.focus());
await expect(page.getByLabel("关闭设置", { exact: true })).toBeFocused();
await page.getByLabel("关闭设置", { exact: true }).click();
await expect(page.locator(".topbar-title")).toHaveText("历史标题");
await expect(page.locator(".right-pane")).toBeVisible();
await expect(page.getByRole("button", { name: "设置", exact: true })).toBeVisible();
}
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});
for (const timeline of [false, true]) {
test(`settings preserve ${timeline ? "timeline" : "project"} task pagination and scroll while closing hints`, async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-settings-sidebar-state-"));
for (let index = 0; index < 25; index++) seedHistory(dir, dir, 1);
let app: Awaited<ReturnType<typeof launchDesktop>>;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
if (timeline) {
await page.getByLabel("筛选视图", { exact: true }).click();
await page.getByRole("menuitemradio", { name: "最近更新", exact: true }).click();
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(page.getByLabel("筛选视图", { exact: true })).toBeFocused();
}
const list = page.locator(timeline ? ".timeline-tasks" : ".recent-sessions");
const pageSize = timeline ? 20 : 5;
await expect(list.getByTestId("session-row")).toHaveCount(pageSize);
for (let limit = pageSize; limit < 25; limit += pageSize) {
await list.getByRole("button", { name: "显示更多", exact: true }).click();
await expect(list.getByTestId("session-row")).toHaveCount(Math.min(limit + pageSize, 25));
}
const settingsButton = page.getByRole("button", { name: "设置", exact: true });
await settingsButton.focus();
const scroll = await page.locator(".sidebar-sections").evaluate((element) => {
element.scrollTop = 200;
return element.scrollTop;
});
expect(scroll).toBeGreaterThan(0);
const hint = page.getByRole("tooltip").filter({ hasText: /^设置$/ });
for (let attempt = 0; attempt < 2; attempt++) {
await settingsButton.hover();
await expect(hint).toBeVisible();
await settingsButton.click();
const settings = page.getByRole("region", { name: "设置", exact: true });
await expect(settings).toBeVisible();
await expect(hint).toHaveCount(0);
await settings.getByLabel("关闭设置").click();
await expect(list.getByTestId("session-row")).toHaveCount(25);
await expect
.poll(() => page.locator(".sidebar-sections").evaluate((element) => element.scrollTop))
.toBe(scroll);
}
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});
}
test("settings hide pinned task controls and restore the task sidebar on return", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-settings-pinned-"));
const session = seedHistory(dir, dir, 1);
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
const row = page.locator(`.sidebar [data-session-id="${session.id}"]`);
await row.hover();
await row.getByLabel("置顶任务 历史标题", { exact: true }).click();
const pin = row.getByLabel("取消置顶任务 历史标题", { exact: true });
await expect(pin).toBeVisible();
await row.getByRole("button", { name: "历史标题", exact: true }).click();
await expect(page.locator(".topbar-title")).toHaveText("历史标题");
await page.getByRole("button", { name: "设置", exact: true }).click();
const settings = page.getByRole("region", { name: "设置", exact: true });
for (const name of ["常规", "界面设置", "系统提示词", "工具", "技能", "模型", "已归档任务"]) {
const tab = settings.getByRole("button", { name, exact: true });
await tab.click();
await expect(tab).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("button", { name: "取消置顶任务 历史标题", exact: true })).toHaveCount(0);
await expect(page.locator(".workspace-surface")).toHaveCSS("opacity", "0");
await expect(page.locator(".workspace-surface")).toHaveAttribute("inert", "");
await expect(pin).toHaveCount(1);
}
await settings.getByLabel("关闭设置").click();
await expect(settings).toHaveCount(0);
await expect(page.locator(".topbar-title")).toHaveText("历史标题");
await expect(page.locator(".sidebar-resizer")).toBeVisible();
await expect(pin).toBeVisible();
await pin.click();
await expect(page.locator(".pinned-tasks")).toHaveCount(0);
await row.hover();
await expect(row.getByLabel("置顶任务 历史标题", { exact: true })).toBeVisible();
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});
test("settings use ZCode typography and keep titlebar dragging separate from controls", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-settings-appearance-"));
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
await page.getByRole("button", { name: "设置", exact: true }).click();
const settings = page.getByRole("region", { name: "设置", exact: true });
const tab = settings.getByRole("button", { name: "系统提示词", exact: true });
const title = settings.locator("h1");
const heading = settings.locator(".interface-settings h2").first();
const label = settings.locator(".settings-row-copy strong").first();
await expect(settings.locator(".settings-row-copy small")).toHaveCount(0);
await expectZCodeSystemFont(page, [
".settings-back",
".settings-tabs button",
".settings-content h1",
".interface-settings h2",
".settings-row-copy strong",
".settings-select-trigger .settings-select-value",
]);
await expect(tab.locator("svg")).toHaveCSS("width", "16px");
await expect(settings.locator(".settings-back svg")).toHaveCSS("width", "16px");
for (const icon of await settings.locator("svg.lucide").all()) {
await expect(icon).toHaveCSS("stroke-width", "1.5px");
}
for (const name of ["常规", "系统提示词", "工具", "技能", "模型", "已归档任务"]) {
await settings.getByRole("button", { name, exact: true }).click();
await expectZCodeSystemFont(page, [".settings-content h1"]);
}
await settings.getByRole("button", { name: "界面设置", exact: true }).click();
// Reference: ZCode SettingsPage, SettingsRow and theme-zai-light/theme-zai-dark.
for (const [theme, color] of [
["浅色", /oklch\(0\.269 0 (?:0|none)\)/],
["深色", /oklch\(0\.87 0 (?:0|none)\)/],
] as const) {
await settings.getByRole("combobox", { name: "界面主题", exact: true }).click();
for (const icon of await page.locator(".settings-select-menu svg.lucide").all()) {
await expect(icon).toHaveCSS("stroke-width", "1.5px");
}
await page.getByRole("option", { name: theme, exact: true }).click();
await expect(tab).toHaveCSS("color", color);
await expect(label).toHaveCSS("color", color);
await expect(title).toHaveCSS("color", color);
await expect(tab).toHaveCSS("font-size", "14px");
await expect(tab).toHaveCSS("font-weight", "400");
await expect(heading).toHaveCSS("font-size", "16px");
await expect(heading).toHaveCSS("font-weight", "600");
await expect(label).toHaveCSS("font-size", "14px");
await expect(label).toHaveCSS("font-weight", "500");
expect(await label.evaluate((el) => getComputedStyle(el).fontFamily)).toMatch(
/^ui-sans-serif, system-ui, sans-serif,/,
);
await page.screenshot({ path: `test-results/settings-${theme === "浅色" ? "light" : "dark"}.png` });
}
await expect(title).toHaveCSS("font-size", "30px");
await expect(settings.locator(".settings-titlebar")).toHaveCSS("app-region", "drag");
await expect(settings.locator(".settings-sidebar-drag-space")).toHaveCSS("app-region", "drag");
await expect(tab).toHaveCSS("app-region", "no-drag");
await expect(settings.getByLabel("关闭设置")).toHaveCSS("app-region", "no-drag");
await tab.click();
await expect(title).toHaveText("系统提示词");
await settings.getByRole("button", { name: "界面设置", exact: true }).click();
const initialRow = await settings.locator(".settings-row").first().boundingBox();
const fontSize = settings.getByRole("spinbutton", { name: "界面字号", exact: true });
await expect(fontSize).toHaveValue("14");
await expect(fontSize).toHaveCSS("height", "28px");
await expect(fontSize).toHaveCSS("border-top-width", "1px");
await expect(settings.locator(".settings-breadcrumb")).toHaveCount(0);
await expect(settings.locator(".settings-main")).toHaveCSS("border-radius", "12px");
await expect(settings.locator(".settings-main")).toHaveCSS("overflow", "hidden");
for (const size of [16, 12, 15, 20, 14]) {
await fontSize.fill(String(size));
await fontSize.press("Enter");
await expect(label).toHaveCSS("font-size", `${size}px`);
await expect(page.locator("html")).toHaveCSS("font-size", "16px");
await expect(settings.locator(".settings-select-trigger").first()).toHaveCSS("height", "32px");
await expect(title).toHaveCSS("font-size", "30px");
expect((await settings.locator(".settings-row").first().boundingBox())?.width).toBe(initialRow?.width);
}
// ZCode clamps/rounds on commit and restores empty or cancelled edits.
for (const [input, result] of [
["11", "12"],
["21", "20"],
["15.6", "16"],
["", "16"],
]) {
await fontSize.fill(input);
await fontSize.press("Tab");
await expect(fontSize).toHaveValue(result);
}
await fontSize.fill("18");
await fontSize.press("Escape");
await expect(fontSize).toHaveValue("16");
await expect(settings).toBeVisible();
await fontSize.fill("15");
await fontSize.press("Enter");
await page.reload();
await page.getByRole("button", { name: "设置", exact: true }).click();
await expect(fontSize).toHaveValue("15");
await expect(tab).toHaveCSS("font-size", "15px");
await fontSize.fill("14");
await fontSize.press("Enter");
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.setSize(900, 740));
await expect(title).toHaveCSS("font-size", "24px");
await expect(settings.locator(".settings-titlebar")).toHaveCSS("app-region", "drag");
await settings.getByLabel("关闭设置").click();
await expect(settings).toHaveCount(0);
await expect(page.locator(".topbar")).toHaveCSS("font-size", "14px");
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});