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()), 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( , ); expect(html).toContain(" { 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); expect(html).not.toContain("javascript:"); expect(html).not.toContain("d:alert"); expect(html).not.toContain("chat-markdown-file-link"); }); });