clipboard.test.ts 12 KB

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