291 lines · 9.9 KB · TypeScript
RawDownload
import { EnvironmentId } from "@t3tools/contracts";import { renderToStaticMarkup } from "react-dom/server";import { describe, expect, it, vi } from "vite-plus/test"; vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));vi.mock("../state/use-atom-query-runner", () => ({ useAtomQueryRunner: () => vi.fn() }));vi.mock("../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));vi.mock("../state/session", async (importOriginal) => ({  ...(await importOriginal<typeof import("../state/session")>()),  usePreparedConnection: () => ({ _tag: "Loading" }),}));vi.mock("../state/entities", () => ({  readThreadShell: () => null,  useProjects: () => [],}));vi.mock("../remoteOpen", () => ({  useRemoteOpenResolution: () => ({ state: { mode: "local-exec" }, isResolved: true }),}));vi.mock("../editorPreferences", () => ({  useOpenInPreferredEditor: () => vi.fn(),  usePreferredEditor: () => [null, vi.fn()],}));vi.mock("~/lib/openPullRequestLink", () => ({  findProjectForChangeRequest: () => undefined,  matchesLinkedPullRequestUrl: () => false,  parseChangeRequestUrl: () => null,  useOpenChangeRequestLink: () => vi.fn(),})); import ChatMarkdown, {  canUseMarkdownFileShellActions,  hasMarkdownFilePrimaryAction,  orderedListGutterStyle,  shouldUseMarkdownFileBrowserPrimaryAction,} from "./ChatMarkdown"; describe("canUseMarkdownFileShellActions", () => {  const environmentId = EnvironmentId.make("environment-1");   it("allows editor and file manager actions for local environments", () => {    expect(canUseMarkdownFileShellActions(environmentId, "local-exec", true)).toBe(true);  });   it("hides shell actions until the environment mode is resolved", () => {    expect(canUseMarkdownFileShellActions(environmentId, "local-exec", false)).toBe(false);  });   it("hides editor and file manager actions for remote environments", () => {    expect(canUseMarkdownFileShellActions(environmentId, "remote-links", true)).toBe(false);    expect(canUseMarkdownFileShellActions(environmentId, "remote-unavailable", true)).toBe(false);  });   it("hides shell actions when no environment owns the markdown", () => {    expect(canUseMarkdownFileShellActions(null, "local-exec", true)).toBe(false);  });}); describe("hasMarkdownFilePrimaryAction", () => {  it("keeps the chip interactive when an editor, browser, or panel can open it", () => {    expect(      hasMarkdownFilePrimaryAction({        canOpenInEditor: true,        canOpenInBrowser: false,        canOpenInPanel: false,      }),    ).toBe(true);    expect(      hasMarkdownFilePrimaryAction({        canOpenInEditor: false,        canOpenInBrowser: true,        canOpenInPanel: false,      }),    ).toBe(true);    expect(      hasMarkdownFilePrimaryAction({        canOpenInEditor: false,        canOpenInBrowser: false,        canOpenInPanel: true,      }),    ).toBe(true);  });   it("removes the link affordance when no primary action can open the file", () => {    expect(      hasMarkdownFilePrimaryAction({        canOpenInEditor: false,        canOpenInBrowser: false,        canOpenInPanel: false,      }),    ).toBe(false);  });}); describe("ChatMarkdown file option chips", () => {  it("keeps the fallback button text selectable", () => {    const html = renderToStaticMarkup(      <ChatMarkdown cwd="/tmp/project" text="[Source](/tmp/project/src/main.ts)" />,    );     expect(html).toContain("<button");    expect(html).toContain('aria-haspopup="menu"');    expect(html).toContain("select-text");  });}); describe("shouldUseMarkdownFileBrowserPrimaryAction", () => {  it("uses the browser when it is the only available primary action", () => {    expect(      shouldUseMarkdownFileBrowserPrimaryAction({        iconPath: "/tmp/report.html",        canOpenInEditor: false,        canOpenInBrowser: true,        canOpenInPanel: false,      }),    ).toBe(true);  });   it("preserves the normal editor and panel defaults for HTML files", () => {    expect(      shouldUseMarkdownFileBrowserPrimaryAction({        iconPath: "/tmp/report.html",        canOpenInEditor: true,        canOpenInBrowser: true,        canOpenInPanel: false,      }),    ).toBe(false);    expect(      shouldUseMarkdownFileBrowserPrimaryAction({        iconPath: "/tmp/report.html",        canOpenInEditor: false,        canOpenInBrowser: true,        canOpenInPanel: true,      }),    ).toBe(false);  });   it("continues to open PDF files in the browser by default", () => {    expect(      shouldUseMarkdownFileBrowserPrimaryAction({        iconPath: "/tmp/report.pdf",        canOpenInEditor: true,        canOpenInBrowser: true,        canOpenInPanel: true,      }),    ).toBe(true);  });}); describe("orderedListGutterStyle", () => {  it("leaves the default gutter alone for single-digit lists", () => {    expect(orderedListGutterStyle(9, undefined)).toBeUndefined();  });   it("leaves the default gutter alone for two-digit lists", () => {    expect(orderedListGutterStyle(99, undefined)).toBeUndefined();  });   it("leaves the default gutter alone for a two-digit list that starts above 1", () => {    // start=50 + 49 items => last marker is "98", still two digits.    expect(orderedListGutterStyle(49, 50)).toBeUndefined();  });   it("widens the gutter once the last marker reaches three digits", () => {    // item 100 is the bug from #6512: a 100-item list starting at 1.    expect(orderedListGutterStyle(100, undefined)).toEqual({ "--list-gutter": "4ch" });  });   it("accounts for a non-default start attribute", () => {    // start=95 + 9 items => last marker is "103", three digits.    expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" });    expect(orderedListGutterStyle(5, "999995")).toEqual({ "--list-gutter": "7ch" });  });   it("scales further for four-digit markers", () => {    expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" });  });   it("uses the widest marker and includes a negative start's minus sign", () => {    expect(orderedListGutterStyle(1001, -1000)).toEqual({ "--list-gutter": "6ch" });    expect(orderedListGutterStyle(3, -15)).toEqual({ "--list-gutter": "4ch" });    expect(orderedListGutterStyle(3, -5)).toBeUndefined();  });   it("treats a missing/zero item count as a single item", () => {    expect(orderedListGutterStyle(0, undefined)).toBeUndefined();    expect(orderedListGutterStyle(0, 100)).toEqual({ "--list-gutter": "4ch" });  });}); describe("ChatMarkdown Windows file links", () => {  const environmentId = EnvironmentId.make("env-windows");   it.each([true, false])("preserves drive paths with parseRawHtml=%s", (parseRawHtml) => {    const html = renderToStaticMarkup(      <ChatMarkdown        cwd="C:/Users/shawn/project"        environmentId={environmentId}        text="[Open](C:/Users/shawn/project/src/main.ts)"        lineBreaks={!parseRawHtml}        parseRawHtml={parseRawHtml}      />,    );     expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"');    expect(html).toContain("chat-markdown-file-link");  });   it.each([true, false])("normalizes backslashes with parseRawHtml=%s", (parseRawHtml) => {    const html = renderToStaticMarkup(      <ChatMarkdown        cwd="C:/Users/shawn/project"        environmentId={environmentId}        text={String.raw`[Open](C:\Users\shawn\project\src\main.ts)`}        lineBreaks={!parseRawHtml}        parseRawHtml={parseRawHtml}      />,    );     expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"');    expect(html).toContain("chat-markdown-file-link");  });   it.each([true, false])(    "distinguishes same-named backslash paths with parseRawHtml=%s",    (parseRawHtml) => {      const html = renderToStaticMarkup(        <ChatMarkdown          cwd="C:/Users/shawn/project"          environmentId={environmentId}          text={String.raw`[Source](C:\Users\shawn\project\src\index.ts) and [Test](C:\Users\shawn\project\test\index.ts)`}          lineBreaks={!parseRawHtml}          parseRawHtml={parseRawHtml}        />,      );       expect(html).toContain("index.ts · project/src");      expect(html).toContain("index.ts · project/test");    },  );   it.each([true, false])(    "does not disambiguate the same file in links and inline code with parseRawHtml=%s",    (parseRawHtml) => {      const path = String.raw`C:\Users\shawn\project\src\main.ts`;      const html = renderToStaticMarkup(        <ChatMarkdown          cwd="C:/Users/shawn/project"          environmentId={environmentId}          text={`[Source](${path}) and \`${path}\``}          lineBreaks={!parseRawHtml}          parseRawHtml={parseRawHtml}        />,      );       expect(html.match(/chat-markdown-file-link/g)).toHaveLength(2);      expect(html).not.toContain("main.ts ·");    },  );   it.each([true, false])("preserves reference links with parseRawHtml=%s", (parseRawHtml) => {    const html = renderToStaticMarkup(      <ChatMarkdown        cwd="C:/Users/shawn/project"        environmentId={environmentId}        text={"[Open][source]\n\n[source]: C:/Users/shawn/project/src/main.ts"}        lineBreaks={!parseRawHtml}        parseRawHtml={parseRawHtml}      />,    );     expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"');    expect(html).toContain("chat-markdown-file-link");  });   it.each([true, false])("still rejects unsafe schemes with parseRawHtml=%s", (parseRawHtml) => {    const html = renderToStaticMarkup(      <ChatMarkdown        cwd="C:/Users/shawn/project"        environmentId={environmentId}        text="[unsafe](javascript:alert(1)) and [unknown](d:alert(1))"        lineBreaks={!parseRawHtml}        parseRawHtml={parseRawHtml}      />,    );     expect(html).not.toContain("javascript:");    expect(html).not.toContain("d:alert");    expect(html).not.toContain("chat-markdown-file-link");  });});