158 lines
9.2 KiB
JavaScript
158 lines
9.2 KiB
JavaScript
/**
|
|
* @file Prüft Anhangsvorschauen, Upload-Zustände, Wiederholungen und das Verwerfen von Entwürfen.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { appendAttachments, createAttachmentComposer } from "../public/js/attachments.js";
|
|
import {createDraftStore} from "../public/js/draftStore.js";
|
|
|
|
class Element {
|
|
constructor() { this.children = []; this.listeners = {}; this.classList = { add() { }, remove() { } }; }
|
|
append(...children) { this.children.push(...children); }
|
|
replaceChildren(...children) { this.children = children; }
|
|
setAttribute() { }
|
|
addEventListener(name, listener) { this.listeners[name] = listener; }
|
|
click() { this.clicked = true; }
|
|
}
|
|
|
|
test("uploaded draft references recover after reload and unavailable images cannot be sent", async t => {
|
|
const elements = Object.fromEntries(["attachImage", "imagePicker", "uploadPreviews", "composer"].map(id => [id, new Element()]));
|
|
const originalDocument = globalThis.document;
|
|
t.after(() => { globalThis.document = originalDocument; });
|
|
globalThis.document = {getElementById: id => elements[id], querySelector: () => elements.composer, createElement: () => new Element()};
|
|
t.mock.method(globalThis, "fetch", async () => Response.json({maxFiles: 5, maxSizeMb: 10}));
|
|
const values = new Map();
|
|
const storage = {getItem: key => values.get(key), setItem: (key, value) => values.set(key, value)};
|
|
const saved = createDraftStore("alice", {storage});
|
|
const metadata = {id: "12345678-1234-1234-1234-123456789abc", originalName: "draft.png"};
|
|
saved.setAttachments(7, [metadata]);
|
|
const composer = createAttachmentComposer({onChange() {}, onError: message => assert.fail(message)});
|
|
composer.setDraftStore(createDraftStore("alice", {storage}));
|
|
composer.restoreDraft(7);
|
|
assert.equal(elements.uploadPreviews.children[0].children[0].src, `/attachments/${metadata.id}`);
|
|
assert.equal(composer.pending, true);
|
|
elements.uploadPreviews.children[0].children[0].onload();
|
|
assert.equal(composer.pending, false);
|
|
assert.deepEqual(composer.attachments, [metadata]);
|
|
elements.uploadPreviews.children[0].children[0].onerror();
|
|
assert.equal(composer.pending, true);
|
|
assert.deepEqual(composer.attachments, []);
|
|
assert.deepEqual(createDraftStore("alice", {storage}).attachments(7), [metadata]);
|
|
elements.uploadPreviews.children[0].children.at(-1).onclick();
|
|
assert.equal(composer.pending, false);
|
|
assert.deepEqual(createDraftStore("alice", {storage}).attachments(7), []);
|
|
});
|
|
test("composer preview, pending state, upload failure/retry, removal, paste, and history rendering", async t => {
|
|
const originalDocument = globalThis.document, originalFetch = globalThis.fetch;
|
|
t.after(() => { globalThis.document = originalDocument; globalThis.fetch = originalFetch; });
|
|
const elements = Object.fromEntries(["attachImage", "imagePicker", "uploadPreviews", "composer"].map(id => [id, new Element()]));
|
|
globalThis.document = { getElementById: id => elements[id], querySelector: () => elements.composer, createElement: () => new Element() };
|
|
let completeUpload;
|
|
let changes = 0;
|
|
globalThis.fetch = async (url) => {
|
|
if (url === "/uploads/config") return Response.json({ maxFiles: 5, maxSizeMb: 10 });
|
|
if (url === "/uploads") return new Promise(resolve => { completeUpload = resolve; });
|
|
return Response.json({ success: true });
|
|
};
|
|
const composer = createAttachmentComposer({ onChange: () => changes++, onError: message => assert.fail(message) });
|
|
elements.attachImage.onclick(); assert.equal(elements.imagePicker.clicked, true);
|
|
elements.imagePicker.files = [new File(["image fixture"], "screenshot.png", { type: "image/png" })];
|
|
elements.imagePicker.onchange();
|
|
assert.equal(composer.pending, true); assert.equal(composer.attachments.length, 0);
|
|
assert.equal(elements.uploadPreviews.children.length, 1);
|
|
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
completeUpload(Response.json({ error: "Malformed image" }, { status: 400 })); await tick();
|
|
assert.match(elements.uploadPreviews.children[0].children[1].textContent, /Malformed image/);
|
|
elements.uploadPreviews.children[0].children[2].onclick();
|
|
const metadata = { id: "12345678-1234-1234-1234-123456789abc", originalName: "screenshot.png" };
|
|
completeUpload(Response.json(metadata, { status: 201 })); await tick();
|
|
assert.equal(composer.pending, false); assert.deepEqual(composer.attachments, [metadata]);
|
|
composer.setBusy(true); assert.equal(elements.attachImage.disabled, true);
|
|
composer.setBusy(false);
|
|
elements.uploadPreviews.children[0].children.at(-1).onclick();
|
|
assert.equal(composer.attachments.length, 0); assert.equal(elements.uploadPreviews.hidden, true);
|
|
let prevented = false;
|
|
elements.composer.listeners.paste({ clipboardData: { files: [], getData: () => "text" }, preventDefault: () => { prevented = true; } });
|
|
assert.equal(prevented, false);
|
|
const history = new Element(); appendAttachments(history); assert.equal(history.children.length, 0);
|
|
appendAttachments(history, [metadata]);
|
|
assert.equal(history.children[0].children[0].href, `/attachments/${metadata.id}`);
|
|
assert.ok(changes > 4);
|
|
});
|
|
|
|
test("discarding a chat draft aborts upload, deletes temporary files, and serializes Retry", async t => {
|
|
const originalDocument = globalThis.document, originalFetch = globalThis.fetch;
|
|
t.after(() => { globalThis.document = originalDocument; globalThis.fetch = originalFetch; });
|
|
const elements = Object.fromEntries(["attachImage", "imagePicker", "uploadPreviews", "composer"].map(id => [id, new Element()]));
|
|
globalThis.document = {getElementById: id => elements[id], querySelector: () => elements.composer, createElement: () => new Element()};
|
|
let uploads = 0;
|
|
let aborted = false;
|
|
const deletes = [];
|
|
globalThis.fetch = (url, options = {}) => {
|
|
if (url === "/uploads/config") return Promise.resolve(Response.json({maxFiles: 5, maxSizeMb: 10}));
|
|
if (url.startsWith("/attachments/")) { deletes.push(url); return Promise.resolve(Response.json({success: true})); }
|
|
uploads++;
|
|
if (uploads === 1) return Promise.resolve(Response.json({error: "Try again"}, {status: 503}));
|
|
if (uploads === 2) return new Promise((_resolve, reject) => options.signal.addEventListener("abort", () => {
|
|
aborted = true; reject(new DOMException("Aborted", "AbortError"));
|
|
}, {once: true}));
|
|
return Promise.resolve(Response.json({id: `00000000-0000-0000-0000-${String(uploads).padStart(12, "0")}`}));
|
|
};
|
|
const composer = createAttachmentComposer({onChange() {}, onError: message => assert.fail(message)});
|
|
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
const addFile = name => {
|
|
elements.imagePicker.files = [new File(["fixture"], name, {type: "image/png"})];
|
|
elements.imagePicker.onchange();
|
|
};
|
|
addFile("pending.png");
|
|
await tick();
|
|
const retry = elements.uploadPreviews.children[0].children[2];
|
|
retry.onclick(); retry.onclick();
|
|
assert.equal(uploads, 2, "Repeated Retry clicks start only one upload");
|
|
composer.discard();
|
|
await tick();
|
|
assert.equal(aborted, true);
|
|
assert.equal(composer.pending, false);
|
|
assert.equal(elements.uploadPreviews.hidden, true);
|
|
|
|
addFile("temporary.png");
|
|
await tick();
|
|
const temporaryId = composer.attachments[0].id;
|
|
composer.discard();
|
|
assert.deepEqual(deletes, [`/attachments/${temporaryId}`]);
|
|
|
|
addFile("accepted.png");
|
|
await tick();
|
|
composer.clear();
|
|
assert.deepEqual(deletes, [`/attachments/${temporaryId}`], "Sent attachments are retained");
|
|
});
|
|
|
|
test("attachments stay with their chat while uploads finish after switching", async t => {
|
|
const originalDocument = globalThis.document, originalFetch = globalThis.fetch;
|
|
t.after(() => { globalThis.document = originalDocument; globalThis.fetch = originalFetch; });
|
|
const elements = Object.fromEntries(["attachImage", "imagePicker", "uploadPreviews", "composer"].map(id => [id, new Element()]));
|
|
globalThis.document = {getElementById: id => elements[id], querySelector: () => elements.composer, createElement: () => new Element()};
|
|
let completeUpload;
|
|
const deletes = [];
|
|
globalThis.fetch = (url) => {
|
|
if (url === "/uploads/config") return Promise.resolve(Response.json({maxFiles: 5, maxSizeMb: 10}));
|
|
if (url === "/uploads") return new Promise(resolve => { completeUpload = resolve; });
|
|
deletes.push(url);
|
|
return Promise.resolve(Response.json({success: true}));
|
|
};
|
|
const composer = createAttachmentComposer({onChange() {}, onError: message => assert.fail(message)});
|
|
elements.imagePicker.files = [new File(["fixture"], "draft.png", {type: "image/png"})];
|
|
elements.imagePicker.onchange();
|
|
composer.saveDraft(1);
|
|
composer.restoreDraft(2);
|
|
assert.equal(composer.attachments.length, 0);
|
|
const metadata = {id: "12345678-1234-1234-1234-123456789abc"};
|
|
completeUpload(Response.json(metadata));
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
assert.deepEqual(deletes, []);
|
|
composer.saveDraft(2);
|
|
composer.restoreDraft(1);
|
|
assert.deepEqual(composer.attachments, [metadata]);
|
|
composer.discard();
|
|
});
|