源码
tests/desktop/history.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 { create, select } from "../helpers/composer.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";
import { seedHistory } from "../history-fixture.ts";
test("packaged ten complete Agent calls, upward paging, reading position and restart; title hover is complete", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-history-")),
cwd = join(dir, "workspace");
await mkdir(cwd);
const seed = seedHistory(dir, cwd, 30);
const server = await fakeServer((_, res) => {
send(res, chunk({ content: "reply" }));
done(res);
});
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: server.url, packaged: true });
let page = await app.firstWindow();
// Wide conversations preload the navigator directory; verify incremental paging in a narrow pane.
await page.setViewportSize({ width: 700, height: 800 });
await expect(page.getByTestId("run")).toHaveCount(10);
const first = page.getByTestId("run").filter({ hasText: "用户上下文 20" });
const before = await page.locator(".conversation").evaluate((el) => {
el.scrollTop = 0;
return el.querySelector('[data-run-id="run-20"]')?.getBoundingClientRect().y;
});
await expect(page.getByTestId("run")).toHaveCount(20);
await expect
.poll(async () => Math.abs(((await first.boundingBox())?.y ?? 0) - (before ?? 0)))
.toBeLessThan(4);
const scroll = await page.locator(".conversation").evaluate((el) => el.scrollTop);
await create(page);
await select(page, seed.id);
await expect(page.getByTestId("run")).toHaveCount(20);
await expect
.poll(() => page.locator(".conversation").evaluate((el) => el.scrollTop))
.toBeCloseTo(scroll, 0);
await app.close();
app = await launchDesktop({ dir, url: server.url, packaged: true });
page = await app.firstWindow();
await page.setViewportSize({ width: 700, height: 800 });
await expect(page.getByTestId("run")).toHaveCount(20);
await expect
.poll(() => page.locator(".conversation").evaluate((el) => el.scrollTop))
.toBeCloseTo(scroll, 0);
await page.screenshot({ path: "test-results/desktop-history-paged.png" });
const latest = page.getByRole("button", { name: "回到最新" });
await expect(latest).toHaveText("");
const background = await latest.evaluate((el) => getComputedStyle(el).backgroundColor);
expect(background).not.toContain("rgba");
await latest.hover();
expect(await latest.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(background);
await page.screenshot({ path: "test-results/desktop-latest-circle.png" });
await latest.click();
await expect(latest).toBeHidden();
const title = "这是一个保存完整内容、仅在窄侧边栏和顶部视觉省略的会话标题";
await page.evaluate(
async ({ id, title }) => {
const r = await window.zpi.renameSession(id, title);
if (!r.ok) throw Error(r.error.message);
},
{ id: seed.id, title },
);
await expect(page.locator(".topbar-title")).toHaveText(title);
await expect(page.locator(".topbar-title")).toHaveAttribute("title", title);
const name = page.locator(`.recent-sessions .session-row[data-session-id="${seed.id}"] .session-name`);
await expect(name.locator('[data-task-title-copy="original"]')).toHaveText(title);
await page.emulateMedia({ reducedMotion: "no-preference" });
await name.hover();
await expect
.poll(() =>
name
.locator("[data-task-title-marquee-track]")
.evaluate((node) => node.getAnimations().some((animation) => animation.playState === "running")),
)
.toBe(true);
} finally {
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});