Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

112 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
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
21const png = (name = "screenshot.png", size = 1000) => ({ name, type: "image/png", size });
22
23test("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
36test("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
41test("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
49test("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
55test("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
59test("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
67test("an image goes in on a line of its own", () => {
68 const md = "![x](u)";
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
74test("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, "![shot.png](https://u/a)"), "Before\n![shot.png](https://u/a)\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, "![x](u)"), "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
88test("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
94test("deferred images get their addresses once the work item is open", () => {
95 const body = `Steps:\n![a.png](${deferredUrl("k1")})\n![b.png](${deferredUrl("k2")})\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![a.png](https://u/attachments/1)\nEnd");
99 // A ref it was not given is left alone.
100 assert.equal(resolveDeferred(`![c](${deferredUrl("k3")})`, new Map()), "![c](attachment:k3)");
101});
102
103test("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});