1import { EnvironmentId } from "@t3tools/contracts";
2import { act, type ReactNode } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
5
6import { AttachmentFilePreview } from "./AttachmentFilePreview";
7
8const { refresh } = vi.hoisted(() => ({ refresh: vi.fn<() => Promise<string | null>>() }));
9
10vi.mock("~/assets/assetUrls", () => ({ useAssetUrlRefresh: () => refresh }));
11vi.mock("~/hooks/useCopyToClipboard", () => ({
12 useCopyToClipboard: () => ({ copyToClipboard: vi.fn(), isCopied: false }),
13}));
14vi.mock("~/components/ui/toast", () => ({ toastManager: { add: vi.fn() } }));
15vi.mock("~/components/ChatMarkdown", () => ({ default: () => null }));
16vi.mock("~/components/ui/scroll-area", () => ({
17 ScrollArea: ({ children }: { children: ReactNode }) => children,
18}));
19vi.mock("./ReadOnlySourcePreview", () => ({
20 default: ({ text }: { text: string }) => <pre>{text}</pre>,
21}));
22vi.mock("./fileSurfaceChrome", () => ({
23 FILE_SURFACE_SUBHEADER_CLASS: "",
24 FileSurfaceAction: ({ label, onPress }: { label: string; onPress: () => void }) => (
25 <button aria-label={label} onClick={onPress} />
26 ),
27 FileSurfaceFailure: ({ message }: { message: string }) => <div role="alert">{message}</div>,
28 FileSurfaceLoading: () => <div role="status">Loading</div>,
29 FileSurfaceNotice: ({ children }: { children: ReactNode }) => <div>{children}</div>,
30}));
31
32describe("attachment HTML preview recovery", () => {
33 const originalUrl = "https://environment.test/original.html";
34 const renewedUrl = "https://environment.test/renewed.html";
35 let now = 0;
36 let renderer: ReactTestRenderer;
37
38 beforeEach(() => {
39 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
40 now = 0;
41 vi.spyOn(Date, "now").mockImplementation(() => now);
42 refresh.mockReset().mockResolvedValueOnce(originalUrl).mockResolvedValue(renewedUrl);
43 vi.stubGlobal(
44 "fetch",
45 vi.fn(async () => new Response("<p>Captured HTML</p>")),
46 );
47 });
48
49 afterEach(async () => {
50 if (renderer) await act(() => renderer.unmount());
51 vi.restoreAllMocks();
52 vi.unstubAllGlobals();
53 });
54
55 const openRemote = async () => {
56 await act(async () => {
57 renderer = create(
58 <AttachmentFilePreview
59 name="document.html"
60 mimeType="text/html"
61 sizeBytes={100}
62 asset={{ environmentId: EnvironmentId.make("test-environment"), attachmentId: "html" }}
63 />,
64 );
65 });
66 };
67
68 const toggleMode = async (label: string) => {
69 await act(async () => {
70 renderer.root.findByProps({ "aria-label": label }).props.onClick();
71 });
72 };
73
74 it("keeps a renewed URL for subsequent rendered and source views", async () => {
75 await openRemote();
76 now = 61 * 60_000;
77 await toggleMode("Show HTML source");
78 expect(fetch).toHaveBeenCalledExactlyOnceWith(renewedUrl, expect.any(Object));
79
80 await toggleMode("Show rendered page");
81 expect(renderer.root.findByType("iframe").props.src).toBe(renewedUrl);
82 await toggleMode("Show HTML source");
83 expect(refresh).toHaveBeenCalledTimes(2);
84 expect(fetch).toHaveBeenCalledTimes(2);
85 expect(vi.mocked(fetch).mock.calls.map(([url]) => url)).toEqual([renewedUrl, renewedUrl]);
86 });
87
88 it("does not fetch or mark an expired URL fresh when reauthorization is unavailable", async () => {
89 await openRemote();
90 now = 61 * 60_000;
91 refresh.mockResolvedValue(null);
92 await toggleMode("Show HTML source");
93 expect(fetch).not.toHaveBeenCalled();
94 expect(renderer.root.findByProps({ role: "alert" }).children).toEqual([
95 "Reconnect to the environment and try again.",
96 ]);
97
98 await toggleMode("Show rendered page");
99 await toggleMode("Show HTML source");
100 expect(refresh).toHaveBeenCalledTimes(3);
101 expect(fetch).not.toHaveBeenCalled();
102 });
103
104 it("can return to rendered HTML after local source decoding fails", async () => {
105 const bytes = new Uint8Array([0x3c, 0x70, 0x3e, 0xe9]);
106 const file = new Blob([bytes], { type: "text/html" });
107 vi.spyOn(file, "stream").mockImplementation(
108 () =>
109 new ReadableStream({
110 start: (controller) => {
111 controller.enqueue(bytes);
112 controller.close();
113 },
114 }),
115 );
116 await act(async () => {
117 renderer = create(
118 <AttachmentFilePreview
119 name="document.html"
120 mimeType="text/html"
121 sizeBytes={file.size}
122 file={file}
123 />,
124 );
125 });
126 await toggleMode("Show HTML source");
127 expect(renderer.root.findByProps({ role: "alert" }).children.join("")).toContain("not UTF-8");
128
129 await toggleMode("Show rendered page");
130 expect(renderer.root.findAllByProps({ role: "alert" })).toHaveLength(0);
131 expect(renderer.root.findByType("iframe").props.title).toBe("document.html");
132 });
133});