Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/markdown-links.spec.ts

typescript241 行查看原文
import { once } from "node:events";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import type { ElectronApplication } from "@playwright/test";
import { chunk, deferred, 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";

test("missing file actions in tables show a dismissible window notice without changing cell layout or copied text", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-table-file-notice-"));
  const project = join(dir, "project");
  await mkdir(project);
  const tableText =
    "| 功能 | 文件 | 说明 |\n| --- | --- | --- |\n| 房源评分 | [getHouseScores](./GetHouseScoresTool.java) | 查询房源评分 |";
  const model = await fakeServer((_, response) => {
    send(response, chunk({ content: tableText }));
    done(response);
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: model.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("展示文件表格");
    await page.getByLabel("消息", { exact: true }).press("Enter");
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    const table = page.locator(".markdown-table-block");
    const link = table.getByRole("button", { name: "getHouseScores", exact: true });
    const bounds = await table.locator("table").boundingBox();
    const text = await table.locator("table").innerText();
    for (const action of ["preview", "open", "reveal"] as const) {
      if (action === "reveal") {
        await link.click({ button: "right" });
        await page.getByRole("menuitem", { name: "Finder", exact: true }).click();
      } else if (action === "open") await link.dispatchEvent("click", { metaKey: true });
      else await link.click();
      const notice = page.locator(".shell > .notice-window");
      await expect(notice).toContainText("找不到文件“GetHouseScoresTool.java”");
      await expect(notice).toHaveCSS("position", "absolute");
      await expect(table.getByRole("alert")).toHaveCount(0);
      expect(await table.locator("table").boundingBox()).toEqual(bounds);
      expect(await table.locator("table").innerText()).toBe(text);
      await table.getByRole("button", { name: "复制 Markdown", exact: true }).click();
      expect(await app.evaluate(({ clipboard }) => clipboard.readText())).toBe(
        tableText.replace("[getHouseScores](./GetHouseScoresTool.java)", "getHouseScores"),
      );
      await notice.getByRole("button", { name: "关闭错误", exact: true }).click();
      await expect(notice).toHaveCount(0);
    }
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => model.close(),
      () => app?.close(),
    ]);
  }
});

test("Markdown links and streamed file citations share icons, preview locations and browser routing", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-markdown-links-"));
  const project = join(dir, "project");
  await mkdir(project);
  const filename = "订单 %20.json",
    path = join(project, filename);
  await writeFile(path, Array.from({ length: 120 }, (_, index) => `line ${index + 1}`).join("\n"));
  await writeFile(join(project, "README.md"), "# readme");
  await writeFile(join(dir, "outside.txt"), "outside");
  await symlink(join(dir, "outside.txt"), join(project, "escape.txt"));
  const site = createServer((_, response) => response.end("<title>开发网页</title>Local page"));
  site.listen(0, "127.0.0.1");
  await once(site, "listening");
  const localUrl = `http://127.0.0.1:${(site.address() as AddressInfo).port}/page.html`;
  const partial = deferred(),
    finish = deferred();
  const model = await fakeServer(async (_, response) => {
    send(
      response,
      chunk({
        content: `网页:[开发网页](${localUrl}),https://example.com/page.html\n\n本地裸链接:${localUrl}\n\n代码:\`https://example.com/inline\`,\`README.md\`\n\n\`\`\`text\nhttps://example.com/code\n::zcode-file-citation{path="README.md"}\n\`\`\`\n\n普通文字 README.md /tmp/plain.txt\n\n[自定义标签](<./${encodeURIComponent(filename)}:80:3>) [绝对文件](<${path.replaceAll("%", "%25")}:3>) [裸文件名](README.md) [文件 URI](<${pathToFileURL(path).href}#L2-L4>) [不存在](./missing.json) [越界符号链接](./escape.txt) [不支持](javascript:alert) [越界路径](../outside.txt)\n\n[Windows](C:\\Users\\test\\.config\\a.json) [Windows 空格](<C:\\My Files\\.config\\a.json>) [Windows 定义][win]\n\n[win]: <C:\\My Files\\.config\\a&amp;b.json>\n\nWindows 引用:::zcode-file-citation{path="C:\\Users\\test\\.config\\a.json"}\n\n引用:::zcode-file-citation{path="./README`,
      }),
    );
    await partial.promise;
    send(
      response,
      chunk({ content: '.md"}\n\n`::zcode-file-citation{path="README.md"}`\n\n下一引用:::zcode-file' }),
    );
    await finish.promise;
    send(response, chunk({ content: '-citation{path="README.md"}' }));
    done(response);
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: model.url });
    await app.evaluate(({ shell }) => {
      const opened: string[] = [];
      const files: string[] = [];
      Object.assign(globalThis, { markdownExternalUrls: opened, markdownFinderPaths: files });
      shell.openPath = async (path) => {
        files.push(path);
        return "";
      };
      shell.showItemInFolder = (path) => files.push(path);
      shell.openExternal = async (url) => {
        opened.push(url);
      };
    });
    const page = await app.firstWindow();
    const mainUrl = page.url();
    const errors: string[] = [];
    page.on("pageerror", (error) => errors.push(error.message));
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await expect(page.getByRole("button", { name: "选择项目", exact: true })).toHaveText("project");
    const editor = page.getByLabel("消息", { exact: true });
    await editor.fill("链接测试");
    await editor.press("Enter");
    const answer = page.getByTestId("run").last().locator(".answer");
    await expect(answer.getByRole("button", { name: "自定义标签", exact: true })).toBeVisible();
    await expect(answer).not.toContainText('path="./README');
    await expect(answer.getByRole("button", { name: "README.md", exact: true })).toHaveCount(0);
    partial.resolve();
    await expect(answer.getByRole("button", { name: "README.md", exact: true })).toHaveCount(1);
    await expect(answer).not.toContainText("下一引用:::zcode-file");
    finish.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await expect(answer.getByRole("button", { name: "README.md", exact: true })).toHaveCount(2);
    await expect(answer.locator("code")).toContainText([
      "https://example.com/inline",
      "README.md",
      'https://example.com/code\n::zcode-file-citation{path="README.md"}',
      '::zcode-file-citation{path="README.md"}',
    ]);
    await expect(answer.locator("code button.message-web-link, code .message-file-link")).toHaveCount(0);
    await expect(answer.getByRole("button", { name: "不支持", exact: true })).toHaveCount(0);
    await expect(answer.getByRole("button", { name: "越界路径", exact: true })).toHaveCount(0);
    await expect(answer.locator(".message-file-link")).toHaveCount(12);
    const config = answer.getByRole("button", { name: "自定义标签", exact: true });
    await expect(config).toHaveCSS("color", "rgb(0, 102, 221)");
    expect(await config.evaluate((element) => element.style.getPropertyValue("--mention-image"))).toMatch(
      /json\.svg/,
    );
    await expect(answer.getByRole("button", { name: "Windows", exact: true })).toHaveAttribute(
      "title",
      "C:/Users/test/.config/a.json",
    );
    await expect(answer.getByRole("button", { name: "a.json", exact: true })).toHaveAttribute(
      "title",
      "C:/Users/test/.config/a.json",
    );
    await expect(answer.getByRole("button", { name: "Windows 空格", exact: true })).toHaveAttribute(
      "title",
      "C:/My Files/.config/a.json",
    );
    await expect(answer.getByRole("button", { name: "Windows 定义", exact: true })).toHaveAttribute(
      "title",
      "C:/My Files/.config/a&b.json",
    );
    await config.click();
    await expect(page.getByRole("tab", { name: filename, exact: true })).toBeVisible();
    await expect(page.locator(".file-text-preview")).toHaveAttribute("data-line", "80");
    await expect(page.locator(".file-text-preview")).toHaveAttribute("data-column", "3");
    await expect(page.locator('.file-text-preview [data-line="80"]').last()).toBeInViewport();
    await answer.getByRole("button", { name: "文件 URI", exact: true }).click();
    await expect(page.getByRole("tab", { name: filename, exact: true })).toHaveCount(1);
    await expect(page.locator(".file-text-preview")).toHaveAttribute("data-line", "2");
    await answer.getByRole("button", { name: "绝对文件", exact: true }).click();
    await expect(page.locator(".file-text-preview")).toHaveAttribute("data-line", "3");
    await answer.getByRole("button", { name: "裸文件名", exact: true }).click();
    await expect(page.getByRole("tab", { name: "README.md", exact: true })).toBeVisible();
    const external = () =>
      app?.evaluate(
        () => (globalThis as typeof globalThis & { markdownExternalUrls: string[] }).markdownExternalUrls,
      );
    await answer.getByRole("button", { name: "https://example.com/page.html", exact: true }).click();
    await expect.poll(external).toEqual(["https://example.com/page.html"]);
    const local = answer.getByRole("button", { name: "开发网页", exact: true });
    await answer.getByRole("button", { name: localUrl, exact: true }).click();
    await expect(page.getByLabel("浏览器地址").last()).toHaveValue(localUrl);
    await local.click();
    await expect(page.getByLabel("浏览器地址").last()).toHaveValue(localUrl);
    await local.click({ modifiers: ["Meta"] });
    await expect.poll(external).toEqual(["https://example.com/page.html", localUrl]);
    await local.click({ button: "right" });
    await page.getByRole("menuitem", { name: "在浏览器中打开", exact: true }).click();
    await expect.poll(external).toEqual(["https://example.com/page.html", localUrl, localUrl]);
    const publicLink = answer.getByRole("button", { name: "https://example.com/page.html", exact: true });
    await publicLink.click({ button: "right" });
    await page.getByRole("menuitem", { name: "打开", exact: true }).click();
    await expect(page.getByLabel("浏览器地址").last()).toHaveValue("https://example.com/page.html");
    // macOS maps native Ctrl-click to the context menu; exercise its click-event modifier separately.
    await local.dispatchEvent("click", { ctrlKey: true });
    await expect.poll(external).toEqual(["https://example.com/page.html", localUrl, localUrl, localUrl]);
    const taskBounds = await page.locator(".task-body").boundingBox();
    await answer.getByRole("button", { name: "不存在", exact: true }).click();
    await expect(page.locator(".notice-window")).toContainText("找不到文件");
    await expect(page.locator(".notice-window")).not.toContainText(/not_found|ENOENT|Error:/);
    await expect(page.locator(".shell > .notice-window")).toHaveCount(1);
    await expect(page.locator(".notice-window")).toHaveCSS("position", "absolute");
    expect(await page.locator(".task-body").boundingBox()).toEqual(taskBounds);
    await page.getByRole("button", { name: "设置", exact: true }).click();
    await expect(page.locator(".notice-window")).toBeVisible();
    await page.getByLabel("关闭设置").click();
    await answer.getByRole("button", { name: "越界符号链接", exact: true }).click();
    await expect(page.getByText(/相对文件链接超出当前工作目录/).first()).toBeVisible();
    // The overlay is dismissible without changing the conversation layout.
    await page.getByRole("button", { name: "关闭错误", exact: true }).click();
    await expect(page.locator(".notice-window")).toHaveCount(0);
    await answer.getByRole("button", { name: "越界符号链接", exact: true }).click({ button: "right" });
    await page.getByRole("menuitem", { name: "Finder", exact: true }).click();
    await expect(page.locator(".shell > .notice-window")).toContainText("相对文件链接超出当前工作目录");
    await expect(answer.getByRole("alert")).toHaveCount(0);
    expect(
      await app.evaluate(
        () => (globalThis as typeof globalThis & { markdownFinderPaths: string[] }).markdownFinderPaths,
      ),
    ).toEqual([]);
    expect(page.url()).toBe(mainUrl);
    expect(errors).toEqual([]);
    expect(JSON.stringify(model.requests[0].messages)).toContain("::zcode-file-citation");
    expect(JSON.stringify(model.requests[0].messages)).toContain(
      "return local file references as Markdown links",
    );
    expect(JSON.stringify(model.requests[0].messages)).toContain("Return web URLs as Markdown links");
    await page.screenshot({ path: "test-results/desktop-markdown-links.png" });
  } finally {
    partial.resolve();
    finish.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => new Promise<void>((resolve) => site.close(() => resolve())),
      () => site.closeAllConnections(),
      () => model.close(),
      () => app?.close(),
    ]);
  }
});