Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/hooks.spec.ts

typescript194 行查看原文
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication, Page } 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 { expect, test } from "../helpers/test.ts";

async function openHooks(page: Page) {
  await page.getByRole("button", { name: "设置", exact: true }).click();
  await page.getByRole("button", { name: "钩子", exact: true }).click();
  await expect(page.getByRole("button", { name: "刷新钩子", exact: true })).toBeEnabled();
}
const script = (text: string) =>
  `process.stdout.write(JSON.stringify({additionalContext:${JSON.stringify(text)}}))`;
async function fillHook(page: Page, text: string) {
  await page.getByRole("combobox", { name: "事件", exact: true }).click();
  await page.getByRole("option", { name: "UserPromptSubmit", exact: true }).click();
  await page.getByRole("textbox", { name: "命令", exact: true }).fill(process.execPath);
  await page.getByLabel("参数", { exact: true }).fill(`-e\n${script(text)}`);
}

for (const colorScheme of ["light", "dark"] as const) {
  test(`hooks settings create, execute, edit, disable and delete persisted hooks in ${colorScheme} mode`, async () => {
    const dir = await mkdtemp(join(tmpdir(), "zpi-hook-settings-"));
    const server = await fakeServer((_, response) => {
      send(response, chunk({ content: "Hook configuration accepted." }));
      done(response);
    });
    let app: ElectronApplication | undefined;
    try {
      app = await launchDesktop({ dir, url: server.url });
      let page = await app.firstWindow();
      await page.emulateMedia({ colorScheme });
      await openHooks(page);
      const installed = () => page.getByRole("region", { name: "已安装钩子", exact: true });
      await expect(installed().getByRole("heading")).toHaveText("已安装 0");
      await expect(installed().getByText("尚未安装钩子", { exact: true })).toBeVisible();
      await expect(page.getByRole("region", { name: "会话标题钩子", exact: true })).toContainText(
        "InputAccepted",
      );
      await expect(page.locator(".hook-plugin-state").first()).toContainText("系统内置插件");
      for (const width of [1280, 800, 640]) {
        await page.setViewportSize({ width, height: 900 });
        expect(
          await page.locator(".hooks-settings").evaluate((panel) => panel.scrollWidth > panel.clientWidth),
        ).toBe(false);
        await expect(page.getByLabel("搜索钩子", { exact: true })).toBeVisible();
      }
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.screenshot({
        path: test.info().outputPath(`hooks-${colorScheme}.png`),
        animations: "disabled",
      });
      await page.getByLabel("搜索钩子", { exact: true }).fill("记忆策略");
      await expect(page.getByRole("region", { name: "记忆策略钩子", exact: true })).toBeVisible();
      await expect(installed()).toHaveCount(0);
      await expect(page.getByRole("region", { name: "会话标题钩子", exact: true })).toHaveCount(0);
      await page.getByLabel("搜索钩子", { exact: true }).fill("not-a-hook");
      await expect(installed()).toContainText("未找到匹配的钩子");
      await page.getByRole("button", { name: "新建", exact: true }).click();
      const breadcrumb = page.getByRole("navigation", { name: "钩子导航", exact: true });
      await expect(breadcrumb).toBeVisible();
      await expect(breadcrumb.locator('[aria-current="page"]')).toHaveText("新建钩子");
      await expect(page.getByRole("heading", { name: "钩子", exact: true })).toHaveCount(0);
      const geometry = await page.getByRole("form", { name: "新建钩子", exact: true }).evaluate((form) => {
        const eventInput = form.querySelector("#hook-event");
        const typeInput = form.querySelector("#hook-type");
        const scopeInput = form.querySelector(".hook-form-scope");
        const argsInput = form.querySelector("#hook-args");
        if (!eventInput || !typeInput || !scopeInput || !argsInput) throw new Error("Missing hook fields");
        const event = eventInput.getBoundingClientRect();
        const type = typeInput.getBoundingClientRect();
        const scope = scopeInput.getBoundingClientRect();
        return {
          eventWidth: event.width,
          typeWidth: type.width,
          inputHeight: event.height,
          scopeRight: scope.x > type.right,
          scopeAboveInput: scope.y < event.y,
          argsHeight: argsInput.getBoundingClientRect().height,
        };
      });
      expect(geometry).toMatchObject({
        eventWidth: 192,
        typeWidth: 192,
        inputHeight: 32,
        scopeRight: true,
        scopeAboveInput: true,
        argsHeight: 64,
      });
      for (const width of [800, 640]) {
        await page.setViewportSize({ width, height: 900 });
        expect(await page.locator(".hook-form").evaluate((form) => form.scrollWidth > form.clientWidth)).toBe(
          false,
        );
        await expect(breadcrumb).toBeVisible();
      }
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.screenshot({
        path: test.info().outputPath(`hook-new-layout-${colorScheme}.png`),
        animations: "disabled",
      });
      await breadcrumb.getByRole("button", { name: "返回钩子列表", exact: true }).click();
      await expect(breadcrumb).toHaveCount(0);
      await expect(page.getByRole("heading", { name: "钩子", exact: true })).toBeVisible();
      await page.getByRole("button", { name: "新建", exact: true }).click();
      await page.getByRole("button", { name: "常规", exact: true }).click();
      await expect(breadcrumb).toHaveCount(0);
      await expect(page.getByRole("heading", { name: "常规", exact: true })).toBeVisible();
      await page.getByRole("button", { name: "钩子", exact: true }).click();
      await expect(page.getByRole("button", { name: "刷新钩子", exact: true })).toBeEnabled();
      await page.getByRole("button", { name: "新建", exact: true }).click();
      await fillHook(page, "HOOK_UI_CONTEXT");
      await page.getByText("高级", { exact: true }).click();
      await page.getByLabel("超时(秒)", { exact: true }).fill("5");
      await page.screenshot({
        path: test.info().outputPath(`hook-form-${colorScheme}.png`),
        animations: "disabled",
      });
      await page.getByRole("button", { name: "保存", exact: true }).click();
      const toggle = () => page.getByRole("switch", { name: "启用 UserPromptSubmit 钩子", exact: true });
      await expect(toggle()).toBeChecked();
      await page.getByRole("button", { name: "关闭设置", exact: true }).click();
      await page.getByLabel("消息", { exact: true }).fill("Use the configured hook");
      await page.getByLabel("发送", { exact: true }).click();
      await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
      expect(JSON.stringify(server.requests.at(-1)?.messages)).toContain("HOOK_UI_CONTEXT");
      await openHooks(page);
      await page.getByRole("button", { name: "编辑 UserPromptSubmit 钩子", exact: true }).click();
      await expect(breadcrumb.locator('[aria-current="page"]')).toHaveText("UserPromptSubmit");
      await page.getByLabel("参数", { exact: true }).fill(`-e\n${script("UPDATED_HOOK")}`);
      await page.getByRole("button", { name: "保存", exact: true }).click();
      await expect(installed()).toContainText("UPDATED_HOOK");
      await toggle().uncheck();
      await expect(toggle()).toBeEnabled();
      await expect(toggle()).not.toBeChecked();
      await app.close();
      app = await launchDesktop({ dir, url: server.url });
      page = await app.firstWindow();
      await page.emulateMedia({ colorScheme });
      await openHooks(page);
      await expect(toggle()).not.toBeChecked();
      await expect(installed()).toContainText("UPDATED_HOOK");
      await page.getByRole("button", { name: "编辑 UserPromptSubmit 钩子", exact: true }).click();
      await page.getByRole("button", { name: "删除", exact: true }).click();
      const confirmation = page.getByRole("dialog", { name: "删除钩子?", exact: true });
      await expect(confirmation.getByRole("button", { name: "取消", exact: true })).toBeFocused();
      await confirmation.getByRole("button", { name: "取消", exact: true }).click();
      await expect(page.getByRole("form", { name: "编辑钩子", exact: true })).toBeVisible();
      await page.getByRole("button", { name: "删除", exact: true }).click();
      await confirmation.getByRole("button", { name: "删除", exact: true }).click();
      await expect(installed()).toContainText("尚未安装钩子");
      expect(server.requests).toHaveLength(1);
    } finally {
      await runCleanup([
        () => rm(dir, { recursive: true, force: true }),
        () => server.close(),
        () => app?.close(),
      ]);
    }
  });
}

test("hook scope targets the selected project and leaves its directory clean", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-hooks-scope-"));
  const project = join(dir, "hook-project");
  await mkdir(project);
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: "" });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await openHooks(page);
    const scope = page.getByRole("combobox", { name: "钩子范围", exact: true });
    await scope.click();
    await page.getByRole("option", { name: "本机 · hook-project", exact: true }).click();
    await expect(page.getByRole("button", { name: "刷新钩子", exact: true })).toBeEnabled();
    await page.getByRole("button", { name: "新建钩子", exact: true }).click();
    await fillHook(page, "PROJECT_ONLY_HOOK");
    await page.getByRole("button", { name: "保存", exact: true }).click();
    await expect(page.getByRole("region", { name: "已安装钩子", exact: true })).toContainText(
      "PROJECT_ONLY_HOOK",
    );
    await scope.click();
    await page.getByRole("option", { name: "本机 · 用户", exact: true }).click();
    await expect(page.getByRole("region", { name: "已安装钩子", exact: true })).toContainText("尚未安装钩子");
    expect(await readdir(project)).not.toContain(".zpi");
  } finally {
    await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
  }
});