| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | nextPastedTextFileName, |
| 5 | PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES, |
| 6 | isPasteAsTextShortcut, |
| 7 | pastedTextDisposition, |
| 8 | replaceTextSelection, |
| 9 | wouldTextPasteExceedLimit, |
| 10 | } from "./textPaste.ts"; |
| 11 | |
| 12 | describe("pasted text disposition", () => { |
| 13 | it("keeps ordinary text inline and folds at the 32 KiB boundary", () => { |
| 14 | expect( |
| 15 | pastedTextDisposition({ |
| 16 | text: "x".repeat(PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES - 1), |
| 17 | canAttach: true, |
| 18 | }), |
| 19 | ).toBe("inline"); |
| 20 | expect( |
| 21 | pastedTextDisposition({ |
| 22 | text: "x".repeat(PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES), |
| 23 | canAttach: true, |
| 24 | }), |
| 25 | ).toBe("attachment"); |
| 26 | }); |
| 27 | |
| 28 | it("measures UTF-8 bytes instead of UTF-16 characters", () => { |
| 29 | const text = "🙂".repeat(PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES / 4); |
| 30 | expect(text.length).toBeLessThan(PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES); |
| 31 | expect(pastedTextDisposition({ text, canAttach: true })).toBe("attachment"); |
| 32 | }); |
| 33 | |
| 34 | it("keeps text inline for the explicit bypass or without attachment support", () => { |
| 35 | const text = "x".repeat(PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES); |
| 36 | expect(pastedTextDisposition({ text, canAttach: true, bypassAutoAttachment: true })).toBe( |
| 37 | "inline", |
| 38 | ); |
| 39 | expect(pastedTextDisposition({ text, canAttach: false })).toBe("inline"); |
| 40 | }); |
| 41 | |
| 42 | it("folds a smaller paste when the resulting prompt would exceed the input limit", () => { |
| 43 | expect( |
| 44 | pastedTextDisposition({ |
| 45 | text: "small paste", |
| 46 | canAttach: true, |
| 47 | wouldExceedInputLimit: true, |
| 48 | }), |
| 49 | ).toBe("attachment"); |
| 50 | }); |
| 51 | }); |
| 52 | |
| 53 | describe("pasted text attachment names", () => { |
| 54 | it("uses the first available readable sequence", () => { |
| 55 | expect(nextPastedTextFileName([])).toBe("pasted-text.txt"); |
| 56 | expect(nextPastedTextFileName(["PASTED-TEXT.TXT", "pasted-text-2.txt"])).toBe( |
| 57 | "pasted-text-3.txt", |
| 58 | ); |
| 59 | }); |
| 60 | }); |
| 61 | |
| 62 | describe("paste-as-text shortcut", () => { |
| 63 | it.each([ |
| 64 | { metaKey: true, ctrlKey: false, macPlatform: true }, |
| 65 | { metaKey: false, ctrlKey: true, macPlatform: false }, |
| 66 | ])("accepts the platform modifier with Shift", ({ metaKey, ctrlKey, macPlatform }) => { |
| 67 | expect( |
| 68 | isPasteAsTextShortcut( |
| 69 | { |
| 70 | key: "V", |
| 71 | metaKey, |
| 72 | ctrlKey, |
| 73 | shiftKey: true, |
| 74 | altKey: false, |
| 75 | }, |
| 76 | macPlatform, |
| 77 | ), |
| 78 | ).toBe(true); |
| 79 | }); |
| 80 | |
| 81 | it("does not claim ordinary or alternate paste chords", () => { |
| 82 | expect( |
| 83 | isPasteAsTextShortcut( |
| 84 | { |
| 85 | key: "v", |
| 86 | metaKey: true, |
| 87 | ctrlKey: false, |
| 88 | shiftKey: false, |
| 89 | altKey: false, |
| 90 | }, |
| 91 | true, |
| 92 | ), |
| 93 | ).toBe(false); |
| 94 | expect( |
| 95 | isPasteAsTextShortcut( |
| 96 | { |
| 97 | key: "v", |
| 98 | metaKey: true, |
| 99 | ctrlKey: false, |
| 100 | shiftKey: true, |
| 101 | altKey: true, |
| 102 | }, |
| 103 | true, |
| 104 | ), |
| 105 | ).toBe(false); |
| 106 | }); |
| 107 | |
| 108 | it("rejects the other platform's modifier", () => { |
| 109 | const event = { |
| 110 | key: "v", |
| 111 | metaKey: false, |
| 112 | ctrlKey: true, |
| 113 | shiftKey: true, |
| 114 | altKey: false, |
| 115 | }; |
| 116 | expect(isPasteAsTextShortcut(event, true)).toBe(false); |
| 117 | expect(isPasteAsTextShortcut({ ...event, metaKey: true, ctrlKey: false }, false)).toBe(false); |
| 118 | }); |
| 119 | }); |
| 120 | |
| 121 | describe("text paste insertion", () => { |
| 122 | it("replaces the selected range and returns the collapsed cursor", () => { |
| 123 | expect( |
| 124 | replaceTextSelection({ |
| 125 | value: "before old after", |
| 126 | selection: { start: 7, end: 10 }, |
| 127 | text: "new", |
| 128 | }), |
| 129 | ).toEqual({ value: "before new after", cursor: 10 }); |
| 130 | }); |
| 131 | |
| 132 | it("clamps stale native selections", () => { |
| 133 | expect( |
| 134 | replaceTextSelection({ value: "abc", selection: { start: 20, end: 30 }, text: "!" }), |
| 135 | ).toEqual({ value: "abc!", cursor: 4 }); |
| 136 | }); |
| 137 | |
| 138 | it("measures the replacement value after removing the selected range", () => { |
| 139 | expect( |
| 140 | wouldTextPasteExceedLimit({ |
| 141 | valueLength: 100, |
| 142 | selection: { start: 40, end: 80 }, |
| 143 | textLength: 30, |
| 144 | maxLength: 100, |
| 145 | }), |
| 146 | ).toBe(false); |
| 147 | expect( |
| 148 | wouldTextPasteExceedLimit({ |
| 149 | valueLength: 100, |
| 150 | selection: { start: 40, end: 80 }, |
| 151 | textLength: 41, |
| 152 | maxLength: 100, |
| 153 | }), |
| 154 | ).toBe(true); |
| 155 | }); |
| 156 | }); |