1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 filePreviewKind,
5 decodeFilePreviewText,
6 FILE_TEXT_PREVIEW_MAX_BYTES,
7 hostPreviewMimeTypeFromExtension,
8 isWorkspaceAudioPreviewPath,
9 isWorkspaceBrowserPreviewPath,
10 isWorkspaceImagePreviewPath,
11 isWorkspacePreviewEntryPath,
12 isWorkspaceVideoPreviewPath,
13 mediaKindFromPath,
14} from "./filePreview.ts";
15
16describe("workspace file previews", () => {
17 it.each(["report.html", "report.HTM", "document#draft.pdf", "reports?old/document.pdf"])(
18 "recognizes browser preview path %s",
19 (path) => {
20 expect(isWorkspaceBrowserPreviewPath(path)).toBe(true);
21 expect(isWorkspacePreviewEntryPath(path)).toBe(true);
22 },
23 );
24
25 it.each([
26 "icon.png",
27 "photo.JPEG",
28 "animation.gif",
29 "vector#mark.svg",
30 "photo?edited.JPEG",
31 "images#archive/icon.png",
32 "texture.webp",
33 "image.avif",
34 ])("recognizes image preview path %s", (path) => {
35 expect(isWorkspaceImagePreviewPath(path)).toBe(true);
36 expect(isWorkspacePreviewEntryPath(path)).toBe(true);
37 });
38
39 it.each([
40 "README.md",
41 "src/index.ts",
42 "image.png.ts",
43 "png",
44 "image.png#notes.txt",
45 "image.svg?notes.txt",
46 "document.pdf?download=1",
47 "report.html#notes.txt",
48 "image%2Epng",
49 ])("rejects non-preview path %s", (path) => {
50 expect(isWorkspacePreviewEntryPath(path)).toBe(false);
51 });
52
53 it("serves audio in place from the host like video and browser documents", () => {
54 expect(isWorkspaceAudioPreviewPath("notes/recording.WAV")).toBe(true);
55 expect(isWorkspaceAudioPreviewPath("recording.wav.ts")).toBe(false);
56 expect(hostPreviewMimeTypeFromExtension(".m4a")).toBe("audio/mp4");
57 expect(hostPreviewMimeTypeFromExtension(".mp4")).toBe("video/mp4");
58 expect(hostPreviewMimeTypeFromExtension(".txt")).toBeNull();
59 });
60});
61
62describe("media path parsing", () => {
63 it.each([
64 ["https://cdn.example/clip.webm?download=1#t=2", "video"],
65 ["https://example.com/download?name=recording.mp4", null],
66 ["https://example.png", null],
67 ["images%2Fresult%2Epng", "image"],
68 ["images/result%23v2.png", "image"],
69 ["images/result.png%23secret.txt", null],
70 ["images/result.png%3Fsecret.txt", null],
71 ["/tmp/100%.png", "image"],
72 ])("classifies the decoded pathname of %s", (source, kind) => {
73 expect(mediaKindFromPath(source)).toBe(kind);
74 });
75
76 it.each([
77 ["recording.mp4#t=2", "video", false],
78 ["recording%2Emp4", "video", false],
79 ["recording#take2.mp4", null, true],
80 ["recording?take2.mp4", null, true],
81 ])("distinguishes authored URLs from literal filenames in %s", (source, kind, literalVideo) => {
82 expect(mediaKindFromPath(source)).toBe(kind);
83 expect(isWorkspaceVideoPreviewPath(source)).toBe(literalVideo);
84 });
85});
86
87describe("attachment preview classification", () => {
88 it.each([
89 ["example.json", "application/octet-stream", "text"],
90 ["README.md", "text/plain", "markdown"],
91 ["component.tsx", "", "text"],
92 ["report.pdf", "application/pdf", "pdf"],
93 ["page.HTML", "", "html"],
94 ["recording.mp3", "", "audio"],
95 ["payload", "application/problem+json", "text"],
96 ["archive.zip", "application/zip", "unsupported"],
97 ["misleading.json", "application/pdf", "pdf"],
98 ["misleading.pdf", "application/zip", "unsupported"],
99 ])("classifies %s (%s) as %s", (name, mimeType, expected) => {
100 expect(filePreviewKind({ name, mimeType })).toBe(expected);
101 });
102 it("rejects binary and invalid UTF-8 data", () => {
103 expect(() => decodeFilePreviewText(new Uint8Array([65, 0, 66]))).toThrow("binary");
104 expect(() => decodeFilePreviewText(new Uint8Array([255]))).toThrow("UTF-8");
105 });
106 it("does not corrupt a multi-byte character at the preview boundary", () => {
107 const bytes = new Uint8Array(FILE_TEXT_PREVIEW_MAX_BYTES + 1).fill(97);
108 bytes[FILE_TEXT_PREVIEW_MAX_BYTES - 1] = 0xe2;
109 bytes[FILE_TEXT_PREVIEW_MAX_BYTES] = 0x82;
110 const preview = decodeFilePreviewText(bytes);
111 expect(preview.truncated).toBe(true);
112 expect(preview.text.endsWith("�")).toBe(false);
113 expect(preview.text.length).toBe(FILE_TEXT_PREVIEW_MAX_BYTES - 1);
114 });
115});