1import { afterEach, describe, expect, it, vi } from "vite-plus/test";
2import { EnvironmentId } from "@t3tools/contracts";
3
4import type { ComposerFileAttachment } from "../../composerDraftStore";
5import {
6 wrapExpandedImageIndex,
7 attachVideoThumbnail,
8 buildAttachmentVideoPreview,
9 buildExpandedImagePreview,
10 resolveMarkdownMediaPreview,
11} from "./ExpandedImagePreview";
12
13describe("resolveMarkdownMediaPreview", () => {
14 afterEach(() => {
15 vi.unstubAllGlobals();
16 });
17
18 it.each([
19 ["t3code:", "https:"],
20 ["t3code-dev:", "https:"],
21 ["http:", "http:"],
22 ["https:", "https:"],
23 ])(
24 "resolves protocol-relative media on %s without changing its source",
25 async (pageProtocol, mediaProtocol) => {
26 vi.stubGlobal("window", { location: { protocol: pageProtocol } });
27 const source = "//cdn.example.com/recording.mp4?token=a%2FB&v=1#t=2";
28 const preview = await resolveMarkdownMediaPreview({
29 source,
30 createAssetUrl: async () => {
31 throw new Error("Remote media must not request a local asset URL.");
32 },
33 });
34
35 expect(preview?.images[0]).toMatchObject({
36 src: `${mediaProtocol}${source}`,
37 originalUrl: source,
38 actionsSource: {
39 src: `${mediaProtocol}${source}`,
40 reference: { kind: "url", url: source },
41 },
42 });
43 },
44 );
45});
46
47describe("buildExpandedImagePreview", () => {
48 it("builds a signed-asset preview for a persisted video attachment", () => {
49 const preview = buildAttachmentVideoPreview(EnvironmentId.make("environment-1"), {
50 type: "file",
51 id: "attachment-video-1",
52 name: "demo.mp4",
53 mimeType: "video/mp4",
54 sizeBytes: 42,
55 });
56
57 expect(preview).toEqual({
58 images: [
59 {
60 src: null,
61 name: "demo.mp4",
62 type: "video",
63 actionsSource: {
64 kind: "video",
65 name: "demo.mp4",
66 src: null,
67 asset: {
68 environmentId: "environment-1",
69 resource: {
70 _tag: "attachment",
71 attachmentId: "attachment-video-1",
72 fileName: "demo.mp4",
73 mimeType: "video/mp4",
74 },
75 },
76 },
77 },
78 ],
79 index: 0,
80 });
81 });
82
83 it("builds a video preview for a local video attachment", () => {
84 const file = new File([new Uint8Array([1, 2, 3])], "demo.mp4", { type: "video/mp4" });
85 const attachment: ComposerFileAttachment = {
86 type: "file",
87 id: "video-1",
88 name: file.name,
89 mimeType: file.type,
90 sizeBytes: file.size,
91 file,
92 };
93
94 const preview = buildExpandedImagePreview([attachment], attachment.id);
95
96 expect(preview).toMatchObject({
97 images: [{ name: "demo.mp4", type: "video" }],
98 index: 0,
99 });
100 expect(preview?.images[0]?.src).toMatch(/^blob:/);
101 URL.revokeObjectURL(preview?.images[0]?.src ?? "");
102 });
103
104 it("releases a video thumbnail URL when detached", async () => {
105 const video = { src: "" } as HTMLVideoElement;
106 const file = new File([new Uint8Array([1, 2, 3])], "demo.mp4", { type: "video/mp4" });
107
108 const detach = attachVideoThumbnail(video, file);
109 const url = video.src;
110
111 expect((await fetch(url)).ok).toBe(true);
112 detach();
113 await expect(fetch(url)).rejects.toThrow();
114 });
115});
116
117it("keeps backward media navigation visible beyond a complete cycle", () => {
118 const images = ["first", "second"];
119 expect(
120 Array.from({ length: 7 }, (_, step) => images[wrapExpandedImageIndex(-step, images.length)]),
121 ).toEqual(["first", "second", "first", "second", "first", "second", "first"]);
122 let index = 0;
123 for (let step = 1; step <= 7; step++) {
124 index = wrapExpandedImageIndex(index - 1, images.length);
125 expect(images[index]).toBe(step % 2 === 1 ? "second" : "first");
126 }
127});