| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | MAX_ATTACHMENT_BYTES, |
| 6 | caretAfter, |
| 7 | deferredRefs, |
| 8 | deferredUrl, |
| 9 | dragsFiles, |
| 10 | imageMarkdown, |
| 11 | imageName, |
| 12 | insertion, |
| 13 | pastedImages, |
| 14 | placeholderFor, |
| 15 | refusal, |
| 16 | replacePlaceholder, |
| 17 | resolveDeferred, |
| 18 | uploadFailure, |
| 19 | } from "./attachments.ts"; |
| 20 | |
| 21 | const png = (name = "screenshot.png", size = 1000) => ({ name, type: "image/png", size }); |
| 22 | |
| 23 | test("a paste uploads image files only when it has no text", () => { |
| 24 | // A screenshot, or an image copied from a page (its HTML, no text). |
| 25 | assert.deepEqual(pastedImages({ files: [png()], text: "" }), [png()]); |
| 26 | // A spreadsheet's cells or a document's paragraphs, with a picture of themselves. |
| 27 | assert.deepEqual(pastedImages({ files: [png("image.png")], text: "a\tb\n1\t2" }), []); |
| 28 | // Plain text. |
| 29 | assert.deepEqual(pastedImages({ files: [], text: "hello" }), []); |
| 30 | // Files that are not images are left to the browser. |
| 31 | assert.deepEqual(pastedImages({ files: [{ name: "a.pdf", type: "application/pdf", size: 3 }], text: "" }), []); |
| 32 | // Several images, each its own. |
| 33 | assert.equal(pastedImages({ files: [png("a.png"), png("b.png")], text: " " }).length, 2); |
| 34 | }); |
| 35 | |
| 36 | test("only a drag that carries files is taken", () => { |
| 37 | assert.equal(dragsFiles(["Files"]), true); |
| 38 | assert.equal(dragsFiles(["text/plain", "text/html"]), false); |
| 39 | }); |
| 40 | |
| 41 | test("what is refused before it is sent, in a plain sentence", () => { |
| 42 | assert.equal(refusal(png()), null); |
| 43 | assert.equal(refusal({ name: "big.png", type: "image/png", size: MAX_ATTACHMENT_BYTES + 1 }), "big.png is over 10 MB, so it wasn't attached."); |
| 44 | assert.equal(refusal({ name: "logo.svg", type: "image/svg+xml", size: 10 }), "logo.svg isn't a PNG, JPEG, GIF or WebP image, so it wasn't attached."); |
| 45 | assert.equal(refusal({ name: "x.png", type: "image/png", size: 0 }), "x.png is empty, so it wasn't attached."); |
| 46 | assert.equal(refusal({ name: "a.jpg", type: "image/JPEG", size: 10 }), null); |
| 47 | }); |
| 48 | |
| 49 | test("a pasted image with no name is image.png", () => { |
| 50 | assert.equal(imageName({ name: "", type: "image/png", size: 1 }), "image.png"); |
| 51 | assert.equal(imageName({ name: "", type: "image/jpeg", size: 1 }), "image.jpg"); |
| 52 | assert.equal(imageName({ name: "C:\\shots\\Preview 1.png", type: "image/png", size: 1 }), "Preview 1.png"); |
| 53 | }); |
| 54 | |
| 55 | test("Markdown escapes the name's brackets", () => { |
| 56 | assert.equal(imageMarkdown("a]b[c\\.png", "https://u/attachments/ab"), "![a\\]b\\[c\\\\.png](https://u/attachments/ab)"); |
| 57 | }); |
| 58 | |
| 59 | test("each upload has a placeholder of its own", () => { |
| 60 | const first = placeholderFor("image.png", ""); |
| 61 | assert.equal(first, "![Uploading image.png…]()"); |
| 62 | const second = placeholderFor("image.png", `Look:\n${first}\n`); |
| 63 | assert.equal(second, "![Uploading image.png (2)…]()"); |
| 64 | assert.notEqual(placeholderFor("image.png", `${first}${second}`), second); |
| 65 | }); |
| 66 | |
| 67 | test("an image goes in on a line of its own", () => { |
| 68 | const md = ""; |
| 69 | assert.deepEqual(insertion("", 0, 0, md), { text: md, start: 0, end: 0, caret: md.length }); |
| 70 | assert.deepEqual(insertion("Before after", 6, 7, md), { text: `\n${md}\n`, start: 6, end: 7, caret: 7 + md.length }); |
| 71 | assert.deepEqual(insertion("Line\n", 5, 5, md), { text: md, start: 5, end: 5, caret: 5 + md.length }); |
| 72 | }); |
| 73 | |
| 74 | test("the placeholder is swapped where it is now, keeping what was written around it", () => { |
| 75 | const placeholder = placeholderFor("shot.png", ""); |
| 76 | const typedSince = `Before\n${placeholder}\nAfter, written while it uploaded`; |
| 77 | assert.equal(replacePlaceholder(typedSince, placeholder, ""), "Before\n\nAfter, written while it uploaded"); |
| 78 | // A failed upload takes the placeholder and its line out. |
| 79 | assert.equal(replacePlaceholder(typedSince, placeholder, ""), "Before\nAfter, written while it uploaded"); |
| 80 | assert.equal(replacePlaceholder(`Text\n${placeholder}`, placeholder, ""), "Text"); |
| 81 | // Deleted by the person: nothing changes. |
| 82 | assert.equal(replacePlaceholder("They removed it", placeholder, ""), "They removed it"); |
| 83 | // Two of the same name: only its own. |
| 84 | const second = placeholderFor("shot.png", placeholder); |
| 85 | assert.equal(replacePlaceholder(`${placeholder}\n${second}`, second, "B"), `${placeholder}\nB`); |
| 86 | }); |
| 87 | |
| 88 | test("a refusal from the service reads as a sentence", () => { |
| 89 | assert.equal(uploadFailure("a.png", { code: "too_large", message: "An attachment can be at most 10 MB." }), "a.png is over 10 MB, so it wasn't attached."); |
| 90 | assert.equal(uploadFailure("a.png", { code: "forbidden" }), "You can't attach images here, so a.png wasn't attached."); |
| 91 | assert.equal(uploadFailure("a.png", null), "a.png didn't upload. Try again in a moment."); |
| 92 | }); |
| 93 | |
| 94 | test("deferred images get their addresses once the work item is open", () => { |
| 95 | const body = `Steps:\n})\n})\nEnd`; |
| 96 | assert.deepEqual([...deferredRefs(body)], ["k1", "k2"]); |
| 97 | const resolved = resolveDeferred(body, new Map([["k1", "https://u/attachments/1"], ["k2", null]])); |
| 98 | assert.equal(resolved, "Steps:\n\nEnd"); |
| 99 | // A ref it was not given is left alone. |
| 100 | assert.equal(resolveDeferred(`})`, new Map()), ""); |
| 101 | }); |
| 102 | |
| 103 | test("the caret stays in the person's writing when a placeholder is swapped", () => { |
| 104 | // "ab[PLACEHOLDER]cd": the placeholder is 2..10, and becomes 20 characters. |
| 105 | assert.equal(caretAfter(1, 2, 10, 20), 1); |
| 106 | assert.equal(caretAfter(2, 2, 10, 20), 2); |
| 107 | // Right at its end, where a paste leaves it: after the image, nothing selected. |
| 108 | assert.equal(caretAfter(10, 2, 10, 20), 22); |
| 109 | assert.equal(caretAfter(12, 2, 10, 20), 24); |
| 110 | // Taken out (a failed upload): after it moves back. |
| 111 | assert.equal(caretAfter(12, 2, 10, 0), 4); |
| 112 | }); |