1import { EnvironmentId, ThreadId } from "@t3tools/contracts";
2import { AsyncResult } from "effect/reactivity";
3import { act, type ComponentProps, type ReactNode } from "react";
4import { create, type ReactTestRenderer } from "react-test-renderer";
5import { expect, it, vi } from "vite-plus/test";
6
7const mint = vi.hoisted(() => vi.fn());
8vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));
9vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));
10vi.mock("../hooks/useFileMetadata", () => ({ useFileMetadata: () => undefined }));
11vi.mock("../hooks/useSettings", async (importOriginal) => {
12 const actual = await importOriginal<typeof import("../hooks/useSettings")>();
13 const settings = actual.getClientSettings();
14 return {
15 ...actual,
16 useClientSettings: (select: (value: typeof settings) => unknown) => select(settings),
17 };
18});
19vi.mock("./ui/tooltip", async () => {
20 const { cloneElement, isValidElement } = await import("react");
21 return {
22 Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
23 TooltipTrigger({
24 render,
25 children,
26 }: ComponentProps<typeof import("./ui/tooltip").TooltipTrigger>) {
27 if (!isValidElement(render)) return <>{children}</>;
28 return children === undefined ? render : cloneElement(render, undefined, children);
29 },
30 TooltipPopup: () => null,
31 };
32});
33vi.mock("../state/use-atom-query-runner", () => ({ useAtomQueryRunner: () => mint }));
34vi.mock("../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));
35vi.mock("../state/session", async (importOriginal) => ({
36 ...(await importOriginal<typeof import("../state/session")>()),
37 readEnvironmentScope: () => false,
38 useEnvironmentScope: () => false,
39 usePreparedConnection: () => ({ _tag: "Some", value: { httpBaseUrl: "https://host.test" } }),
40}));
41vi.mock("../state/entities", () => ({
42 readEnvironmentSupportsServerBrowser: () => false,
43 readThreadShell: () => null,
44 useProjects: () => [],
45 useServerConfigs: () => new Map(),
46}));
47vi.mock("../remoteOpen", () => ({
48 useRemoteOpenResolution: () => ({ state: { mode: "local-exec" }, isResolved: true }),
49}));
50vi.mock("../editorPreferences", () => ({
51 useOpenInPreferredEditor: () => vi.fn(),
52 usePreferredEditor: () => [null, vi.fn()],
53}));
54vi.mock("~/lib/openPullRequestLink", () => ({
55 findProjectForChangeRequest: () => undefined,
56 matchesLinkedPullRequestUrl: () => false,
57 parseChangeRequestUrl: () => null,
58 useOpenChangeRequestLink: () => vi.fn(),
59}));
60
61import ChatMarkdown from "./ChatMarkdown";
62
63it("opens host media through server authorization before the client grant loads", async () => {
64 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
65 mint.mockResolvedValue(
66 AsyncResult.success({ relativeUrl: "/api/assets/image.png", expiresAt: 1 }),
67 );
68 const onImageExpand = vi.fn();
69 const threadRef = {
70 environmentId: EnvironmentId.make("media-environment"),
71 threadId: ThreadId.make("media-thread"),
72 };
73 let renderer: ReactTestRenderer | undefined;
74 try {
75 await act(async () => {
76 renderer = create(
77 <ChatMarkdown
78 cwd="/repo"
79 threadRef={threadRef}
80 text="[Open image](/tmp/image.png)"
81 onImageExpand={onImageExpand}
82 />,
83 );
84 });
85 await act(async () => {
86 const link = renderer!.root
87 .findAllByType("a")
88 .find((node) => node.props.href === "/tmp/image.png");
89 expect(link).toBeDefined();
90 link!.props.onClick({ preventDefault: vi.fn(), stopPropagation: vi.fn() });
91 });
92 expect(onImageExpand).toHaveBeenCalledWith(
93 expect.objectContaining({
94 images: [expect.objectContaining({ src: "https://host.test/api/assets/image.png" })],
95 }),
96 );
97 } finally {
98 await act(async () => renderer?.unmount());
99 vi.unstubAllGlobals();
100 }
101});