源码
tests/desktop/sidebar-empty.spec.ts
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { chunk, done, fakeServer, send } from "../fake-server.ts";
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";
test("empty project and independent task lists match ZCode spacing, fonts and theme colors", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-sidebar-empty-"));
const project = join(dir, "empty-project");
await mkdir(project);
const server = await fakeServer((_, response) => {
send(response, chunk({ content: "ok" }));
done(response);
});
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, project, url: server.url });
const page = await app.firstWindow();
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1200, 800));
await page.getByLabel("添加项目", { exact: true }).click();
const projectEmpty = page.locator(".projects .task-list-empty");
const taskEmpty = page.locator('[data-sidebar-section="tasks"] .task-list-empty');
for (const [theme, size] of [
["light", 14],
["dark", 18],
] as const) {
await page.evaluate(({ theme, fontSize }) => window.zpi.updatePreferences({ theme, fontSize }), {
theme,
fontSize: size,
});
await page.reload();
await expect(page.locator(".sidebar")).toHaveCSS(
"width",
await page
.locator(".sidebar")
.evaluate((el) => getComputedStyle(el).getPropertyValue("--sidebar-content-width")),
);
await expect(projectEmpty).toHaveText("暂无任务");
await expect(taskEmpty).toHaveText("暂无任务");
for (const empty of [projectEmpty, taskEmpty]) {
await expect(empty).toHaveCSS("font-size", `${size}px`);
await expect(empty).toHaveCSS("font-weight", "400");
await expect(empty).toHaveCSS("line-height", `${size * 1.5}px`);
await expect(empty).toHaveCSS("padding-top", "8px");
await expect(empty).toHaveCSS("padding-bottom", "8px");
}
await expect(projectEmpty).toHaveCSS("padding-left", "34px");
await expect(taskEmpty).toHaveCSS("padding-left", "34px");
await expectZCodeSystemFont(page, [
".projects .task-list-empty",
'[data-sidebar-section="tasks"] .task-list-empty',
]);
const metrics = await page.evaluate(() => {
const projectEmpty = document.querySelector(".projects .task-list-empty") as HTMLElement;
const taskEmpty = document.querySelector(
'[data-sidebar-section="tasks"] .task-list-empty',
) as HTMLElement;
const projectTitle = document.querySelector(".project-title") as HTMLElement;
const taskHeading = document.querySelector(
'[data-sidebar-section="tasks"] .sidebar-heading',
) as HTMLElement;
const textRect = (el: HTMLElement) => {
const range = document.createRange();
range.selectNodeContents(el);
return range.getBoundingClientRect();
};
const probe = document.createElement("span");
document.body.append(probe);
const color = (token: string) => {
probe.style.color = `var(${token})`;
return getComputedStyle(probe).color;
};
const result = {
projectColor: getComputedStyle(projectEmpty).color,
taskColor: getComputedStyle(taskEmpty).color,
subtlest: color("--color-text-subtlest"),
subtle: color("--color-text-subtle"),
projectTextX: textRect(projectEmpty).x,
taskTextX: textRect(taskEmpty).x,
headingTextX: taskHeading.querySelector("span")?.getBoundingClientRect().x,
folderX: projectTitle.querySelector("svg")?.getBoundingClientRect().x,
contentWidth: document.querySelector(".sidebar-sections")?.getBoundingClientRect().width,
sidebarWidth: document.querySelector(".sidebar")?.getBoundingClientRect().width,
projectGap: projectEmpty.getBoundingClientRect().top - projectTitle.getBoundingClientRect().bottom,
sectionGap: taskHeading.getBoundingClientRect().top - projectEmpty.getBoundingClientRect().bottom,
taskGap: taskEmpty.getBoundingClientRect().top - taskHeading.getBoundingClientRect().bottom,
};
probe.remove();
return result;
});
expect(metrics.projectColor).toBe(metrics.subtlest);
expect(metrics.taskColor).toBe(metrics.subtlest);
expect(metrics.projectTextX).toBe(42);
expect(metrics.taskTextX).toBe(42);
expect(metrics.headingTextX).toBe(18);
expect(metrics.folderX).toBe(18);
expect(metrics.contentWidth).toBe(metrics.sidebarWidth);
expect(metrics.projectGap).toBe(4);
expect(metrics.sectionGap).toBe(12);
expect(metrics.taskGap).toBe(0);
await page.locator(".sidebar").screenshot({ path: `test-results/sidebar-empty-${theme}.png` });
}
await page.getByLabel("收起项目 empty-project", { exact: true }).click();
await expect(projectEmpty).toHaveCount(0);
await expect(taskEmpty).toBeVisible();
await page.getByLabel("展开项目 empty-project", { exact: true }).click();
await expect(projectEmpty).toBeVisible();
await page.getByLabel("收起任务列表", { exact: true }).click();
await expect(taskEmpty).toHaveCount(0);
await page.getByLabel("展开任务列表", { exact: true }).click();
await expect(taskEmpty).toBeVisible();
const editor = page.getByLabel("消息", { exact: true });
await editor.fill("项目任务");
await editor.press("Enter");
await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
await expect(page.locator(".projects .session-row")).toHaveCount(1);
await expect(projectEmpty).toHaveCount(0);
await expect(taskEmpty).toBeVisible();
const row = page.locator(".projects .session-row");
const rowBounds = await row.boundingBox();
const sidebarBounds = await page.locator(".sidebar").boundingBox();
expect(rowBounds?.x).toBe(8);
expect((sidebarBounds?.width ?? 0) - (rowBounds?.x ?? 0) - (rowBounds?.width ?? 0)).toBe(8);
await row.hover();
await row.getByLabel("归档任务 项目任务", { exact: true }).click();
await expect(projectEmpty).toBeVisible();
await page.reload();
await expect(projectEmpty).toBeVisible();
await page.locator('[data-sidebar-section="tasks"]').getByLabel("新建任务", { exact: true }).click();
await editor.fill("独立任务");
await editor.press("Enter");
await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
await expect(taskEmpty).toHaveCount(0);
await expect(projectEmpty).toBeVisible();
const independent = page.locator(".recent-sessions .session-row");
await independent.hover();
await independent.getByLabel("归档任务 独立任务", { exact: true }).click();
await expect(taskEmpty).toHaveText("暂无任务");
await page.getByLabel("筛选视图", { exact: true }).click();
await page.getByRole("menuitemradio", { name: "最近更新" }).click();
await expect(page.locator(".task-list-empty")).toHaveText("暂无任务");
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});