1import { describe, it, expect } from 'vitest';
2import { AttachmentType } from '$lib/enums';
3import {
4 formatMessageForClipboard,
5 parseClipboardContent,
6 hasClipboardAttachments
7} from '$lib/utils/clipboard';
8
9describe('formatMessageForClipboard', () => {
10 it('returns plain content when no extras', () => {
11 const result = formatMessageForClipboard('Hello world', undefined);
12 expect(result).toBe('Hello world');
13 });
14
15 it('returns plain content when extras is empty array', () => {
16 const result = formatMessageForClipboard('Hello world', []);
17 expect(result).toBe('Hello world');
18 });
19
20 it('handles empty string content', () => {
21 const result = formatMessageForClipboard('', undefined);
22 expect(result).toBe('');
23 });
24
25 it('returns plain content when extras has only non-text attachments', () => {
26 const extras = [
27 {
28 type: AttachmentType.IMAGE as const,
29 name: 'image.png',
30 base64Url: 'data:image/png;base64,...'
31 }
32 ];
33 const result = formatMessageForClipboard('Hello world', extras);
34 expect(result).toBe('Hello world');
35 });
36
37 it('filters non-text attachments and keeps only text ones', () => {
38 const extras = [
39 {
40 type: AttachmentType.IMAGE as const,
41 name: 'image.png',
42 base64Url: 'data:image/png;base64,...'
43 },
44 {
45 type: AttachmentType.TEXT as const,
46 name: 'file.txt',
47 content: 'Text content'
48 },
49 {
50 type: AttachmentType.PDF as const,
51 name: 'doc.pdf',
52 base64Data: 'data:application/pdf;base64,...',
53 content: 'PDF content',
54 processedAsImages: false
55 }
56 ];
57 const result = formatMessageForClipboard('Hello', extras);
58
59 expect(result).toContain('"file.txt"');
60 expect(result).not.toContain('image.png');
61 expect(result).not.toContain('doc.pdf');
62 });
63
64 it('formats message with text attachments', () => {
65 const extras = [
66 {
67 type: AttachmentType.TEXT as const,
68 name: 'file1.txt',
69 content: 'File 1 content'
70 },
71 {
72 type: AttachmentType.TEXT as const,
73 name: 'file2.txt',
74 content: 'File 2 content'
75 }
76 ];
77 const result = formatMessageForClipboard('Hello world', extras);
78
79 expect(result).toContain('"Hello world"');
80 expect(result).toContain('"type": "TEXT"');
81 expect(result).toContain('"name": "file1.txt"');
82 expect(result).toContain('"content": "File 1 content"');
83 expect(result).toContain('"name": "file2.txt"');
84 });
85
86 it('handles content with quotes and special characters', () => {
87 const content = 'Hello "world" with\nnewline';
88 const extras = [
89 {
90 type: AttachmentType.TEXT as const,
91 name: 'test.txt',
92 content: 'Test content'
93 }
94 ];
95 const result = formatMessageForClipboard(content, extras);
96
97 // Should be valid JSON
98 expect(result.startsWith('"')).toBe(true);
99 // The content should be properly escaped
100 const parsed = JSON.parse(result.split('\n')[0]);
101 expect(parsed).toBe(content);
102 });
103
104 it('converts legacy context type to TEXT type', () => {
105 const extras = [
106 {
107 type: AttachmentType.LEGACY_CONTEXT as const,
108 name: 'legacy.txt',
109 content: 'Legacy content'
110 }
111 ];
112 const result = formatMessageForClipboard('Hello', extras);
113
114 expect(result).toContain('"type": "TEXT"');
115 expect(result).not.toContain('"context"');
116 });
117
118 it('handles attachment content with special characters', () => {
119 const extras = [
120 {
121 type: AttachmentType.TEXT as const,
122 name: 'code.js',
123 content: 'const x = "hello\\nworld";\nconst y = `template ${var}`;'
124 }
125 ];
126 const formatted = formatMessageForClipboard('Check this code', extras);
127 const parsed = parseClipboardContent(formatted);
128
129 expect(parsed.textAttachments[0].content).toBe(
130 'const x = "hello\\nworld";\nconst y = `template ${var}`;'
131 );
132 });
133
134 it('handles unicode characters in content and attachments', () => {
135 const extras = [
136 {
137 type: AttachmentType.TEXT as const,
138 name: 'unicode.txt',
139 content: '日本語テスト 🎉 émojis'
140 }
141 ];
142 const formatted = formatMessageForClipboard('Привет мир 👋', extras);
143 const parsed = parseClipboardContent(formatted);
144
145 expect(parsed.message).toBe('Привет мир 👋');
146 expect(parsed.textAttachments[0].content).toBe('日本語テスト 🎉 émojis');
147 });
148
149 it('formats as plain text when asPlainText is true', () => {
150 const extras = [
151 {
152 type: AttachmentType.TEXT as const,
153 name: 'file1.txt',
154 content: 'File 1 content'
155 },
156 {
157 type: AttachmentType.TEXT as const,
158 name: 'file2.txt',
159 content: 'File 2 content'
160 }
161 ];
162 const result = formatMessageForClipboard('Hello world', extras, true);
163
164 expect(result).toBe('Hello world\n\nFile 1 content\n\nFile 2 content');
165 });
166
167 it('returns plain content when asPlainText is true but no attachments', () => {
168 const result = formatMessageForClipboard('Hello world', [], true);
169 expect(result).toBe('Hello world');
170 });
171
172 it('plain text mode does not use JSON format', () => {
173 const extras = [
174 {
175 type: AttachmentType.TEXT as const,
176 name: 'test.txt',
177 content: 'Test content'
178 }
179 ];
180 const result = formatMessageForClipboard('Hello', extras, true);
181
182 expect(result).not.toContain('"type"');
183 expect(result).not.toContain('[');
184 expect(result).toBe('Hello\n\nTest content');
185 });
186});
187
188describe('parseClipboardContent', () => {
189 it('returns plain text as message when not in special format', () => {
190 const result = parseClipboardContent('Hello world');
191
192 expect(result.message).toBe('Hello world');
193 expect(result.textAttachments).toHaveLength(0);
194 });
195
196 it('handles empty string input', () => {
197 const result = parseClipboardContent('');
198
199 expect(result.message).toBe('');
200 expect(result.textAttachments).toHaveLength(0);
201 });
202
203 it('handles whitespace-only input', () => {
204 const result = parseClipboardContent(' \n\t ');
205
206 expect(result.message).toBe(' \n\t ');
207 expect(result.textAttachments).toHaveLength(0);
208 });
209
210 it('returns plain text as message when starts with quote but invalid format', () => {
211 const result = parseClipboardContent('"Unclosed quote');
212
213 expect(result.message).toBe('"Unclosed quote');
214 expect(result.textAttachments).toHaveLength(0);
215 });
216
217 it('returns original text when JSON array is malformed', () => {
218 const input = '"Hello"\n[invalid json';
219
220 const result = parseClipboardContent(input);
221
222 expect(result.message).toBe('"Hello"\n[invalid json');
223 expect(result.textAttachments).toHaveLength(0);
224 });
225
226 it('parses message with text attachments', () => {
227 const input = `"Hello world"
228[
229 {"type":"TEXT","name":"file1.txt","content":"File 1 content"},
230 {"type":"TEXT","name":"file2.txt","content":"File 2 content"}
231]`;
232
233 const result = parseClipboardContent(input);
234
235 expect(result.message).toBe('Hello world');
236 expect(result.textAttachments).toHaveLength(2);
237 expect(result.textAttachments[0].name).toBe('file1.txt');
238 expect(result.textAttachments[0].content).toBe('File 1 content');
239 expect(result.textAttachments[1].name).toBe('file2.txt');
240 expect(result.textAttachments[1].content).toBe('File 2 content');
241 });
242
243 it('handles escaped quotes in message', () => {
244 const input = `"Hello \\"world\\" with quotes"
245[
246 {"type":"TEXT","name":"file.txt","content":"test"}
247]`;
248
249 const result = parseClipboardContent(input);
250
251 expect(result.message).toBe('Hello "world" with quotes');
252 expect(result.textAttachments).toHaveLength(1);
253 });
254
255 it('handles newlines in message', () => {
256 const input = `"Hello\\nworld"
257[
258 {"type":"TEXT","name":"file.txt","content":"test"}
259]`;
260
261 const result = parseClipboardContent(input);
262
263 expect(result.message).toBe('Hello\nworld');
264 expect(result.textAttachments).toHaveLength(1);
265 });
266
267 it('returns message only when no array follows', () => {
268 const input = '"Just a quoted string"';
269
270 const result = parseClipboardContent(input);
271
272 expect(result.message).toBe('Just a quoted string');
273 expect(result.textAttachments).toHaveLength(0);
274 });
275
276 it('filters out invalid attachment objects', () => {
277 const input = `"Hello"
278[
279 {"type":"TEXT","name":"valid.txt","content":"valid"},
280 {"type":"INVALID","name":"invalid.txt","content":"invalid"},
281 {"name":"missing-type.txt","content":"missing"},
282 {"type":"TEXT","content":"missing name"}
283]`;
284
285 const result = parseClipboardContent(input);
286
287 expect(result.message).toBe('Hello');
288 expect(result.textAttachments).toHaveLength(1);
289 expect(result.textAttachments[0].name).toBe('valid.txt');
290 });
291
292 it('handles empty attachments array', () => {
293 const input = '"Hello"\n[]';
294
295 const result = parseClipboardContent(input);
296
297 expect(result.message).toBe('Hello');
298 expect(result.textAttachments).toHaveLength(0);
299 });
300
301 it('roundtrips correctly with formatMessageForClipboard', () => {
302 const originalContent = 'Hello "world" with\nspecial characters';
303 const originalExtras = [
304 {
305 type: AttachmentType.TEXT as const,
306 name: 'file1.txt',
307 content: 'Content with\nnewlines and "quotes"'
308 },
309 {
310 type: AttachmentType.TEXT as const,
311 name: 'file2.txt',
312 content: 'Another file'
313 }
314 ];
315
316 const formatted = formatMessageForClipboard(originalContent, originalExtras);
317 const parsed = parseClipboardContent(formatted);
318
319 expect(parsed.message).toBe(originalContent);
320 expect(parsed.textAttachments).toHaveLength(2);
321 expect(parsed.textAttachments[0].name).toBe('file1.txt');
322 expect(parsed.textAttachments[0].content).toBe('Content with\nnewlines and "quotes"');
323 expect(parsed.textAttachments[1].name).toBe('file2.txt');
324 expect(parsed.textAttachments[1].content).toBe('Another file');
325 });
326});
327
328describe('hasClipboardAttachments', () => {
329 it('returns false for plain text', () => {
330 expect(hasClipboardAttachments('Hello world')).toBe(false);
331 });
332
333 it('returns false for empty string', () => {
334 expect(hasClipboardAttachments('')).toBe(false);
335 });
336
337 it('returns false for quoted string without attachments', () => {
338 expect(hasClipboardAttachments('"Hello world"')).toBe(false);
339 });
340
341 it('returns true for valid format with attachments', () => {
342 const input = `"Hello"
343[{"type":"TEXT","name":"file.txt","content":"test"}]`;
344
345 expect(hasClipboardAttachments(input)).toBe(true);
346 });
347
348 it('returns false for format with empty attachments array', () => {
349 const input = '"Hello"\n[]';
350
351 expect(hasClipboardAttachments(input)).toBe(false);
352 });
353
354 it('returns false for malformed JSON', () => {
355 expect(hasClipboardAttachments('"Hello"\n[broken')).toBe(false);
356 });
357});
358
359describe('roundtrip edge cases', () => {
360 it('preserves empty message with attachments', () => {
361 const extras = [
362 {
363 type: AttachmentType.TEXT as const,
364 name: 'file.txt',
365 content: 'Content only'
366 }
367 ];
368 const formatted = formatMessageForClipboard('', extras);
369 const parsed = parseClipboardContent(formatted);
370
371 expect(parsed.message).toBe('');
372 expect(parsed.textAttachments).toHaveLength(1);
373 expect(parsed.textAttachments[0].content).toBe('Content only');
374 });
375
376 it('preserves attachment with empty content', () => {
377 const extras = [
378 {
379 type: AttachmentType.TEXT as const,
380 name: 'empty.txt',
381 content: ''
382 }
383 ];
384 const formatted = formatMessageForClipboard('Message', extras);
385 const parsed = parseClipboardContent(formatted);
386
387 expect(parsed.message).toBe('Message');
388 expect(parsed.textAttachments).toHaveLength(1);
389 expect(parsed.textAttachments[0].content).toBe('');
390 });
391
392 it('preserves multiple backslashes', () => {
393 const content = 'Path: C:\\\\Users\\\\test\\\\file.txt';
394 const extras = [
395 {
396 type: AttachmentType.TEXT as const,
397 name: 'path.txt',
398 content: 'D:\\\\Data\\\\file'
399 }
400 ];
401 const formatted = formatMessageForClipboard(content, extras);
402 const parsed = parseClipboardContent(formatted);
403
404 expect(parsed.message).toBe(content);
405 expect(parsed.textAttachments[0].content).toBe('D:\\\\Data\\\\file');
406 });
407
408 it('preserves tabs and various whitespace', () => {
409 const content = 'Line1\t\tTabbed\n Spaced\r\nCRLF';
410 const extras = [
411 {
412 type: AttachmentType.TEXT as const,
413 name: 'whitespace.txt',
414 content: '\t\t\n\n '
415 }
416 ];
417 const formatted = formatMessageForClipboard(content, extras);
418 const parsed = parseClipboardContent(formatted);
419
420 expect(parsed.message).toBe(content);
421 expect(parsed.textAttachments[0].content).toBe('\t\t\n\n ');
422 });
423});