| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 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 | |
| 16 | describe("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 | |
| 62 | describe("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 | |
| 87 | describe("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 | }); |