[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
307
electron/bridges/clipboardFiles.test.cjs
Normal file
307
electron/bridges/clipboardFiles.test.cjs
Normal file
@@ -0,0 +1,307 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {
|
||||
createClipboardImageFileName,
|
||||
decodeWindowsHDrop,
|
||||
decodeWindowsFileNameW,
|
||||
hasClipboardImage,
|
||||
parseClipboardTextFilePaths,
|
||||
readClipboardFiles,
|
||||
readClipboardImage,
|
||||
} = require("./clipboardFiles.cjs");
|
||||
|
||||
const createFs = (entries) => ({
|
||||
existsSync: (filePath) => filePath in entries,
|
||||
statSync: (filePath) => ({
|
||||
isDirectory: () => entries[filePath] === "directory",
|
||||
isFile: () => entries[filePath] === "file",
|
||||
size: entries[filePath] === "file" ? 42 : 0,
|
||||
}),
|
||||
});
|
||||
|
||||
test("decodes Windows FileNameW clipboard buffers", () => {
|
||||
const buffer = Buffer.from("C:\\Users\\me\\a.txt\0\0", "utf16le");
|
||||
|
||||
assert.deepEqual(decodeWindowsFileNameW(buffer), [
|
||||
"C:\\Users\\me\\a.txt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("decodes Windows CF_HDROP clipboard buffers", () => {
|
||||
const paths = "C:\\Users\\me\\a.txt\0D:\\b.txt\0\0";
|
||||
const filesOffset = 20;
|
||||
const header = Buffer.alloc(filesOffset);
|
||||
header.writeUInt32LE(filesOffset, 0);
|
||||
header.writeUInt32LE(1, 16);
|
||||
const buffer = Buffer.concat([header, Buffer.from(paths, "utf16le")]);
|
||||
|
||||
assert.deepEqual(decodeWindowsHDrop(buffer), [
|
||||
"C:\\Users\\me\\a.txt",
|
||||
"D:\\b.txt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses text and uri-list clipboard file urls", () => {
|
||||
const fsImpl = createFs({
|
||||
"/Users/me/a.txt": "file",
|
||||
"/Users/me/folder": "directory",
|
||||
});
|
||||
|
||||
const files = parseClipboardTextFilePaths(
|
||||
"file:///Users/me/a.txt\nfile:///Users/me/folder\n/Users/me/missing.txt",
|
||||
{ fsImpl, pathImpl: require("node:path") },
|
||||
);
|
||||
|
||||
assert.deepEqual(files, [
|
||||
{ path: "/Users/me/a.txt", name: "a.txt", isDirectory: false, size: 42 },
|
||||
{ path: "/Users/me/folder", name: "folder", isDirectory: true, size: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses Windows file urls with drive letters and UNC paths", () => {
|
||||
const fsImpl = createFs({
|
||||
"C:\\Users\\me\\a file.txt": "file",
|
||||
"\\\\server\\share\\b.txt": "file",
|
||||
});
|
||||
|
||||
const files = parseClipboardTextFilePaths(
|
||||
"file:///C:/Users/me/a%20file.txt\nfile://server/share/b.txt",
|
||||
{ fsImpl, pathImpl: require("node:path"), windows: true },
|
||||
);
|
||||
|
||||
assert.deepEqual(files, [
|
||||
{ path: "C:\\Users\\me\\a file.txt", name: "a file.txt", isDirectory: false, size: 42 },
|
||||
{ path: "\\\\server\\share\\b.txt", name: "b.txt", isDirectory: false, size: 42 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("ignores plain text paths without file uri formats", () => {
|
||||
const fsImpl = createFs({ "/Users/me/a.txt": "file" });
|
||||
|
||||
assert.deepEqual(parseClipboardTextFilePaths("/Users/me/a.txt", { fsImpl, pathImpl: require("node:path") }), []);
|
||||
});
|
||||
|
||||
test("reads macOS public.file-url clipboard paths", () => {
|
||||
const fsImpl = createFs({
|
||||
"/Users/me/folder": "directory",
|
||||
});
|
||||
const clipboard = {
|
||||
availableFormats: () => ["public.file-url"],
|
||||
read: (format) => {
|
||||
if (format === "public.file-url") {
|
||||
return "file:///Users/me/folder";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(readClipboardFiles({ clipboard, fsImpl, pathImpl: require("node:path") }), [
|
||||
{ path: "/Users/me/folder", name: "folder", isDirectory: true, size: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("reads macOS NSFilenamesPboardType clipboard paths", () => {
|
||||
const fsImpl = createFs({
|
||||
"/Users/me/folder": "directory",
|
||||
"/Users/me/a.txt": "file",
|
||||
});
|
||||
const clipboard = {
|
||||
availableFormats: () => ["NSFilenamesPboardType"],
|
||||
read: (format) => {
|
||||
if (format === "NSFilenamesPboardType") {
|
||||
return "/Users/me/folder\n/Users/me/a.txt";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(readClipboardFiles({ clipboard, fsImpl, pathImpl: require("node:path") }), [
|
||||
{ path: "/Users/me/folder", name: "folder", isDirectory: true, size: 0 },
|
||||
{ path: "/Users/me/a.txt", name: "a.txt", isDirectory: false, size: 42 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("reads CF_HDROP before falling back to FileNameW", () => {
|
||||
const filesOffset = 20;
|
||||
const header = Buffer.alloc(filesOffset);
|
||||
header.writeUInt32LE(filesOffset, 0);
|
||||
header.writeUInt32LE(1, 16);
|
||||
const hdropBuffer = Buffer.concat([
|
||||
header,
|
||||
Buffer.from("C:\\Users\\me\\a.txt\0D:\\b.txt\0\0", "utf16le"),
|
||||
]);
|
||||
const buffer = Buffer.from("C:\\Users\\me\\a.txt\0\0", "utf16le");
|
||||
const fsImpl = createFs({ "C:\\Users\\me\\a.txt": "file", "D:\\b.txt": "file" });
|
||||
const clipboard = {
|
||||
availableFormats: () => ["CF_HDROP", "FileNameW", "text/plain"],
|
||||
readBuffer: (format) => {
|
||||
if (format === "CF_HDROP") return hdropBuffer;
|
||||
if (format === "FileNameW") return buffer;
|
||||
return Buffer.alloc(0);
|
||||
},
|
||||
readText: () => "file:///fallback.txt",
|
||||
};
|
||||
|
||||
assert.deepEqual(readClipboardFiles({ clipboard, fsImpl, pathImpl: require("node:path") }), [
|
||||
{ path: "C:\\Users\\me\\a.txt", name: "a.txt", isDirectory: false, size: 42 },
|
||||
{ path: "D:\\b.txt", name: "b.txt", isDirectory: false, size: 42 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("creates stable clipboard image file names", () => {
|
||||
assert.equal(
|
||||
createClipboardImageFileName(new Date(2026, 5, 11, 3, 4, 5, 6)),
|
||||
"netcatty-paste-20260611-030405-006.png",
|
||||
);
|
||||
});
|
||||
|
||||
test("writes clipboard image to Netcatty temp directory", async () => {
|
||||
const writes = [];
|
||||
const content = Buffer.from([1, 2, 3]);
|
||||
const result = await readClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
toPNG: () => content,
|
||||
}),
|
||||
},
|
||||
fsImpl: {
|
||||
promises: {
|
||||
writeFile: async (filePath, data) => writes.push({ filePath, data }),
|
||||
},
|
||||
},
|
||||
tempDirBridge: {
|
||||
getTempFilePath: (name) => `/netcatty-temp/${name}`,
|
||||
},
|
||||
now: () => new Date(2026, 5, 11, 3, 4, 5, 6),
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
path: "/netcatty-temp/netcatty-paste-20260611-030405-006.png",
|
||||
name: "netcatty-paste-20260611-030405-006.png",
|
||||
mediaType: "image/png",
|
||||
size: 3,
|
||||
});
|
||||
assert.deepEqual(writes, [
|
||||
{ filePath: "/netcatty-temp/netcatty-paste-20260611-030405-006.png", data: content },
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns null when clipboard image is empty", async () => {
|
||||
const result = await readClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => true,
|
||||
toPNG: () => Buffer.from([1]),
|
||||
}),
|
||||
},
|
||||
tempDirBridge: {
|
||||
getTempFilePath: () => "/netcatty-temp/unused.png",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("hasClipboardImage is true when native clipboard holds an image", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
getSize: () => ({ width: 10, height: 8 }),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage is true when availableFormats lists an image type", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
availableFormats: () => ["text/plain", "image/png"],
|
||||
readImage: () => assert.fail("formats probe should short-circuit"),
|
||||
},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage falls back to readImage when availableFormats throws", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
availableFormats: () => {
|
||||
throw new Error("formats unavailable");
|
||||
},
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
getSize: () => ({ width: 1, height: 1 }),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage is false when native clipboard image is empty", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => true,
|
||||
getSize: () => ({ width: 10, height: 8 }),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage is false when clipboard image has no measurable size", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
getSize: () => ({ width: 0, height: 0 }),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage falls back to toPNG when getSize is unavailable", () => {
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
toPNG: () => Buffer.from("png"),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hasClipboardImage({
|
||||
clipboard: {
|
||||
readImage: () => ({
|
||||
isEmpty: () => false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("hasClipboardImage is false when clipboard API is unavailable", () => {
|
||||
assert.equal(hasClipboardImage({ clipboard: null }), false);
|
||||
assert.equal(hasClipboardImage({ clipboard: {} }), false);
|
||||
});
|
||||
Reference in New Issue
Block a user