Skip to content
Source
Sign inSign up
Open History to see the last commit

composerAttachmentFiles.test.ts

194 lines6.9 KB

1import {
2 EnvironmentId,
3 getProviderAttachmentLimitError,
4 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
5} from "@t3tools/contracts";
6import { describe, expect, it } from "vite-plus/test";
7​
8import { isFileAttachment, isImageAttachment } from "../../types";
9import type { ComposerFileAttachment, ComposerImageAttachment } from "../../composerDraftStore";
10import {
11 attachmentsToReleaseOnUploadCapabilityLoss,
12 classifyComposerAttachmentFile,
13 composerFileAttachmentMimeType,
14 fileAttachmentCapabilityBlockReason,
15 fileAttachmentStagingLimit,
16 normalizeComposerImageFileMimeType,
17 shouldHandleComposerAttachmentPaste,
18} from "./composerAttachmentFiles";
19​
20describe("composer attachment files", () => {
21 it("keeps previewable images providers cannot accept on the file path", () => {
22 expect(classifyComposerAttachmentFile({ name: "diagram.svg", type: "image/svg+xml" })).toBe(
23 "file",
24 );
25 expect(classifyComposerAttachmentFile({ name: "diagram.SVG", type: "" })).toBe("file");
26 expect(
27 classifyComposerAttachmentFile({ name: "diagram.svg", type: "application/octet-stream" }),
28 ).toBe("file");
29 expect(classifyComposerAttachmentFile({ name: "photo.tiff", type: "image/tiff" })).toBe("file");
30 expect(classifyComposerAttachmentFile({ name: "report.pdf", type: "application/pdf" })).toBe(
31 "file",
32 );
33 expect(classifyComposerAttachmentFile({ name: "picture.bmp", type: "image/bmp" })).toBe("file");
34 expect(
35 classifyComposerAttachmentFile({ name: "layers.psd", type: "image/vnd.adobe.photoshop" }),
36 ).toBe("unsupported-image");
37 });
38​
39 it("keeps DNG originals on the file path without changing their bytes", async () => {
40 const bytes = new Uint8Array([0x49, 0x49, 0x2a, 0x00, 0x08, 0, 0, 0, 0xfe, 0xff, 0x00, 0x80]);
41 const files = [
42 new File([bytes], "photo.DNG", { type: "image/tiff" }),
43 new File([bytes], "photo.dng", { type: "image/png" }),
44 new File([bytes], "photo.dng", { type: "image/jpeg" }),
45 new File([bytes], "clipboard", { type: "image/dng" }),
46 new File([bytes], "clipboard", { type: "image/x-dng" }),
47 new File([bytes], "clipboard", { type: "image/x-adobe-dng" }),
48 ];
49​
50 const attachments = await Promise.all(
51 files.map(async (file) => ({
52 kind: classifyComposerAttachmentFile(file),
53 bytes: new Uint8Array(await normalizeComposerImageFileMimeType(file).arrayBuffer()),
54 })),
55 );
56​
57 expect(attachments).toEqual(files.map(() => ({ kind: "file", bytes })));
58 });
59​
60 it("keeps misleading DNG MIME labels out of downstream image detection and budgets", () => {
61 for (const type of ["image/png", "image/jpeg", "image/tiff", "image/dng", ""]) {
62 const attachment = {
63 type: "file" as const,
64 id: "raw",
65 name: "photo.DNG",
66 mimeType: composerFileAttachmentMimeType({ name: "photo.DNG", type }),
67 sizeBytes: 81 * 1024 * 1024,
68 };
69​
70 expect(isImageAttachment(attachment)).toBe(false);
71 expect(isFileAttachment(attachment)).toBe(true);
72 expect(getProviderAttachmentLimitError([attachment])).toBeUndefined();
73 }
74 });
75​
76 it("claims a captioned DNG paste even when its MIME is generic", () => {
77 const file = new File(["raw"], "photo.dng", { type: "application/octet-stream" });
78​
79 expect(
80 shouldHandleComposerAttachmentPaste({ files: [file], plainText: "Inspect this original" }),
81 ).toBe(true);
82 });
83​
84 it("keeps ordinary images on their existing paths", () => {
85 const files = [
86 { name: "photo.jpg", type: "image/jpeg" },
87 { name: "export.dng.png", type: "image/png" },
88 { name: "photo.webp", type: "image/webp" },
89 { name: "photo.gif", type: "image/gif" },
90 { name: "photo.heic", type: "" },
91 { name: "scan.tif", type: "image/tiff" },
92 { name: "photo.cr2", type: "image/x-canon-cr2" },
93 ];
94​
95 expect(files.map(classifyComposerAttachmentFile)).toEqual([
96 "image",
97 "image",
98 "image",
99 "image",
100 "image",
101 "file",
102 "unsupported-image",
103 ]);
104 });
105​
106 it("preserves text paste when an application adds a synthetic generic file", () => {
107 const file = new File(["clipboard"], "clipboard.rtf", { type: "application/rtf" });
108​
109 expect(
110 shouldHandleComposerAttachmentPaste({
111 files: [file],
112 plainText: "Copied text",
113 }),
114 ).toBe(false);
115 });
116​
117 it("claims SVG pastes even when text is present", () => {
118 const image = new File(["svg"], "diagram.svg", { type: "image/svg+xml" });
119 expect(
120 shouldHandleComposerAttachmentPaste({ files: [image], plainText: "Image caption" }),
121 ).toBe(true);
122 });
123​
124 it("preserves text when the clipboard also includes generic image files", () => {
125 const image = new File(["tiff"], "photo.tiff", { type: "image/tiff" });
126 expect(
127 shouldHandleComposerAttachmentPaste({ files: [image], plainText: "Image caption" }),
128 ).toBe(false);
129 });
130​
131 it("uses the hard local limit while server config is unknown", () => {
132 expect(
133 fileAttachmentStagingLimit({
134 attachmentUploadsCapabilityKnown: false,
135 supportsAttachmentUploads: false,
136 maxFileAttachmentBytes: null,
137 }),
138 ).toBe(PROVIDER_SEND_TURN_MAX_FILE_BYTES);
139 expect(
140 fileAttachmentCapabilityBlockReason({
141 files: [{ name: "pending.zip", sizeBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES }],
142 attachmentUploadsCapabilityKnown: false,
143 supportsAttachmentUploads: false,
144 maxFileAttachmentBytes: null,
145 }),
146 ).toBe("Waiting for the server before file attachments can send");
147 });
148​
149 it("keeps draft-persisted file uploads when the upload capability flips off", () => {
150 const environmentId = EnvironmentId.make("environment-1");
151 const image: ComposerImageAttachment = {
152 type: "image",
153 id: "image-1",
154 name: "photo.png",
155 mimeType: "image/png",
156 sizeBytes: 3,
157 previewUrl: "blob:photo",
158 file: new File([new Uint8Array([1, 2, 3])], "photo.png", { type: "image/png" }),
159 };
160 const uploadingFile: ComposerFileAttachment = {
161 type: "file",
162 id: "file-uploading",
163 name: "fresh.pdf",
164 mimeType: "application/pdf",
165 sizeBytes: 3,
166 file: new File([new Uint8Array([1, 2, 3])], "fresh.pdf", { type: "application/pdf" }),
167 };
168 const hydratedFile: ComposerFileAttachment = {
169 type: "file",
170 id: "file-hydrated",
171 name: "report.pdf",
172 mimeType: "application/pdf",
173 sizeBytes: 3,
174 file: null,
175 uploadedAttachmentId: "pending-report-pdf",
176 uploadEnvironmentId: environmentId,
177 };
178 const uploadedLocalFile: ComposerFileAttachment = {
179 ...uploadingFile,
180 id: "file-uploaded-local",
181 uploadedAttachmentId: "pending-fresh-pdf",
182 uploadEnvironmentId: environmentId,
183 };
184​
185 const released = attachmentsToReleaseOnUploadCapabilityLoss([
186 image,
187 uploadingFile,
188 hydratedFile,
189 uploadedLocalFile,
190 ]);
191​
192 expect(released.map((attachment) => attachment.id)).toEqual(["image-1", "file-uploading"]);
193 });
194});