mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
230 lines
5.9 KiB
JavaScript
230 lines
5.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { copyImageBlobToClipboard, copyTextToClipboard } from "./clipboard.js";
|
|
|
|
function replaceGlobal(name, value) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
writable: true,
|
|
value
|
|
});
|
|
return () => {
|
|
if (descriptor) {
|
|
Object.defineProperty(globalThis, name, descriptor);
|
|
return;
|
|
}
|
|
delete globalThis[name];
|
|
};
|
|
}
|
|
|
|
function createClipboardDocument({ copyResult = true } = {}) {
|
|
const appended = [];
|
|
const commands = [];
|
|
const restoredRanges = [];
|
|
let textarea = null;
|
|
const activeElement = {
|
|
focusCalls: 0,
|
|
focus() {
|
|
this.focusCalls += 1;
|
|
}
|
|
};
|
|
const selectionRange = { id: "existing-selection" };
|
|
|
|
const document = {
|
|
activeElement,
|
|
body: {
|
|
appendChild(element) {
|
|
appended.push(element);
|
|
},
|
|
removeChild(element) {
|
|
const index = appended.indexOf(element);
|
|
if (index >= 0) {
|
|
appended.splice(index, 1);
|
|
}
|
|
}
|
|
},
|
|
createElement(tagName) {
|
|
assert.equal(tagName, "textarea");
|
|
textarea = {
|
|
value: "",
|
|
attributes: new Map(),
|
|
selected: false,
|
|
focused: false,
|
|
style: {},
|
|
setAttribute(name, value) {
|
|
this.attributes.set(name, String(value));
|
|
},
|
|
focus() {
|
|
this.focused = true;
|
|
},
|
|
select() {
|
|
this.selected = true;
|
|
}
|
|
};
|
|
return textarea;
|
|
},
|
|
execCommand(command) {
|
|
commands.push(command);
|
|
return copyResult;
|
|
},
|
|
getSelection() {
|
|
return {
|
|
rangeCount: 1,
|
|
getRangeAt(index) {
|
|
assert.equal(index, 0);
|
|
return selectionRange;
|
|
},
|
|
removeAllRanges() {
|
|
restoredRanges.length = 0;
|
|
},
|
|
addRange(range) {
|
|
restoredRanges.push(range);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
return {
|
|
document,
|
|
get appendedCount() {
|
|
return appended.length;
|
|
},
|
|
get commands() {
|
|
return [...commands];
|
|
},
|
|
get restoredRanges() {
|
|
return [...restoredRanges];
|
|
},
|
|
get textarea() {
|
|
return textarea;
|
|
},
|
|
activeElement
|
|
};
|
|
}
|
|
|
|
test("copyTextToClipboard uses async clipboard when available", async () => {
|
|
const writes = [];
|
|
const restoreNavigator = replaceGlobal("navigator", {
|
|
clipboard: {
|
|
async writeText(text) {
|
|
writes.push(text);
|
|
}
|
|
}
|
|
});
|
|
const fakeDocument = createClipboardDocument();
|
|
const restoreDocument = replaceGlobal("document", fakeDocument.document);
|
|
|
|
try {
|
|
await copyTextToClipboard("hello cad");
|
|
} finally {
|
|
restoreDocument();
|
|
restoreNavigator();
|
|
}
|
|
|
|
assert.deepEqual(writes, ["hello cad"]);
|
|
assert.deepEqual(fakeDocument.commands, []);
|
|
});
|
|
|
|
test("copyTextToClipboard falls back to execCommand when async clipboard rejects", async () => {
|
|
const restoreNavigator = replaceGlobal("navigator", {
|
|
clipboard: {
|
|
async writeText() {
|
|
throw new Error("NotAllowedError");
|
|
}
|
|
}
|
|
});
|
|
const fakeDocument = createClipboardDocument();
|
|
const restoreDocument = replaceGlobal("document", fakeDocument.document);
|
|
|
|
try {
|
|
await copyTextToClipboard("@cad[models/part#f1]");
|
|
} finally {
|
|
restoreDocument();
|
|
restoreNavigator();
|
|
}
|
|
|
|
assert.deepEqual(fakeDocument.commands, ["copy"]);
|
|
assert.equal(fakeDocument.appendedCount, 0);
|
|
assert.equal(fakeDocument.textarea.value, "@cad[models/part#f1]");
|
|
assert.equal(fakeDocument.textarea.selected, true);
|
|
assert.deepEqual(fakeDocument.restoredRanges, [{ id: "existing-selection" }]);
|
|
assert.equal(fakeDocument.activeElement.focusCalls, 1);
|
|
});
|
|
|
|
test("copyTextToClipboard reports unavailable clipboard when every strategy fails", async () => {
|
|
const restoreNavigator = replaceGlobal("navigator", {});
|
|
const fakeDocument = createClipboardDocument({ copyResult: false });
|
|
const restoreDocument = replaceGlobal("document", fakeDocument.document);
|
|
|
|
try {
|
|
await assert.rejects(
|
|
copyTextToClipboard("nope"),
|
|
/Clipboard is unavailable/
|
|
);
|
|
} finally {
|
|
restoreDocument();
|
|
restoreNavigator();
|
|
}
|
|
|
|
assert.deepEqual(fakeDocument.commands, ["copy"]);
|
|
assert.equal(fakeDocument.appendedCount, 0);
|
|
});
|
|
|
|
test("copyImageBlobToClipboard starts clipboard write before screenshot blob resolves", async () => {
|
|
const events = [];
|
|
let resolveBlob;
|
|
class FakeClipboardItem {
|
|
constructor(items) {
|
|
events.push("item");
|
|
this.items = items;
|
|
}
|
|
}
|
|
const restoreClipboardItem = replaceGlobal("ClipboardItem", FakeClipboardItem);
|
|
const restoreNavigator = replaceGlobal("navigator", {
|
|
clipboard: {
|
|
async write(items) {
|
|
events.push("write");
|
|
assert.equal(items.length, 1);
|
|
const item = items[0];
|
|
const pngBlobPromise = item.items["image/png"];
|
|
assert.equal(typeof pngBlobPromise?.then, "function");
|
|
const blob = await pngBlobPromise;
|
|
events.push(`resolved:${blob.type}`);
|
|
}
|
|
}
|
|
});
|
|
const blobPromise = new Promise((resolve) => {
|
|
resolveBlob = resolve;
|
|
});
|
|
|
|
let copied;
|
|
try {
|
|
copied = copyImageBlobToClipboard(blobPromise);
|
|
assert.deepEqual(events, ["item", "write"]);
|
|
resolveBlob(new Blob(["png"], { type: "image/png" }));
|
|
const result = await copied;
|
|
assert.equal(result.type, "image/png");
|
|
} finally {
|
|
restoreNavigator();
|
|
restoreClipboardItem();
|
|
}
|
|
|
|
assert.deepEqual(events, ["item", "write", "resolved:image/png"]);
|
|
});
|
|
|
|
test("copyImageBlobToClipboard reports unsupported image clipboard", async () => {
|
|
const restoreClipboardItem = replaceGlobal("ClipboardItem", class {});
|
|
const restoreNavigator = replaceGlobal("navigator", {});
|
|
|
|
try {
|
|
await assert.rejects(
|
|
copyImageBlobToClipboard(Promise.resolve(new Blob(["png"], { type: "image/png" }))),
|
|
/Clipboard image copy is not supported/
|
|
);
|
|
} finally {
|
|
restoreNavigator();
|
|
restoreClipboardItem();
|
|
}
|
|
});
|