Files
NetMesh/scripts/mac-dock-icon.test.cjs
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

207 lines
7.2 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const zlib = require("node:zlib");
const { VALID_VARIANTS } = require("../electron/bridges/appIconManager.cjs");
const APP_ICON_VARIANTS = [...VALID_VARIANTS];
function paethPredictor(left, up, upperLeft) {
const estimate = left + up - upperLeft;
const leftDistance = Math.abs(estimate - left);
const upDistance = Math.abs(estimate - up);
const upperLeftDistance = Math.abs(estimate - upperLeft);
if (leftDistance <= upDistance && leftDistance <= upperLeftDistance) return left;
if (upDistance <= upperLeftDistance) return up;
return upperLeft;
}
function readRgbaPng(png, label) {
assert.equal(png.subarray(0, 8).toString("hex"), "89504e470d0a1a0a");
let offset = 8;
let width;
let height;
const imageData = [];
while (offset < png.length) {
const length = png.readUInt32BE(offset);
const type = png.subarray(offset + 4, offset + 8).toString("ascii");
const data = png.subarray(offset + 8, offset + 8 + length);
offset += length + 12;
if (type === "IHDR") {
width = data.readUInt32BE(0);
height = data.readUInt32BE(4);
assert.deepEqual(
[...data.subarray(8, 13)],
[8, 6, 0, 0, 0],
`${label} must be an 8-bit, non-interlaced RGBA PNG`,
);
} else if (type === "IDAT") {
imageData.push(data);
} else if (type === "IEND") {
break;
}
}
const bytesPerPixel = 4;
const stride = width * bytesPerPixel;
const raw = zlib.inflateSync(Buffer.concat(imageData));
assert.equal(raw.length, (stride + 1) * height, `${label} has unexpected PNG data`);
let sourceOffset = 0;
let previous = Buffer.alloc(stride);
let minX = width;
let minY = height;
let maxX = -1;
let maxY = -1;
const rows = [];
for (let y = 0; y < height; y += 1) {
const filter = raw[sourceOffset];
sourceOffset += 1;
const current = Buffer.from(raw.subarray(sourceOffset, sourceOffset + stride));
sourceOffset += stride;
for (let index = 0; index < stride; index += 1) {
const left = index >= bytesPerPixel ? current[index - bytesPerPixel] : 0;
const up = previous[index];
const upperLeft = index >= bytesPerPixel ? previous[index - bytesPerPixel] : 0;
let predictor;
if (filter === 0) predictor = 0;
else if (filter === 1) predictor = left;
else if (filter === 2) predictor = up;
else if (filter === 3) predictor = Math.floor((left + up) / 2);
else if (filter === 4) predictor = paethPredictor(left, up, upperLeft);
else assert.fail(`${label} uses unsupported PNG filter ${filter}`);
current[index] = (current[index] + predictor) & 0xff;
}
for (let x = 0; x < width; x += 1) {
if (current[x * bytesPerPixel + 3] <= 8) continue;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
rows.push(current);
previous = current;
}
return { width, height, minX, minY, maxX, maxY, rows };
}
function readRgbaPngAlphaBounds(file) {
const image = readRgbaPng(fs.readFileSync(file), file);
assert.equal(image.width, 1024, `${file} must keep the 1024px app-icon canvas`);
assert.equal(image.height, 1024, `${file} must keep the 1024px app-icon canvas`);
return {
minX: image.minX,
minY: image.minY,
maxX: image.maxX,
maxY: image.maxY,
};
}
function readIcnsEntry(file, expectedType) {
const icns = fs.readFileSync(file);
assert.equal(icns.subarray(0, 4).toString("ascii"), "icns", `${file} must be ICNS`);
assert.equal(icns.readUInt32BE(4), icns.length, `${file} has an invalid ICNS length`);
for (let offset = 8; offset < icns.length;) {
const type = icns.subarray(offset, offset + 4).toString("ascii");
const length = icns.readUInt32BE(offset + 4);
assert.ok(length >= 8 && offset + length <= icns.length, `${file} has an invalid ${type} entry`);
if (type === expectedType) return icns.subarray(offset + 8, offset + length);
offset += length;
}
assert.fail(`${file} is missing the ${expectedType} representation`);
}
function assertSmallIcnsRepresentation(file, type, expectedPixels) {
const image = readRgbaPng(readIcnsEntry(file, type), `${file}:${type}`);
assert.equal(image.width, expectedPixels);
assert.equal(image.height, expectedPixels);
let unexpectedGreenPixels = 0;
for (const row of image.rows) {
for (let x = 0; x < image.width; x += 1) {
const offset = x * 4;
const [red, green, blue, alpha] = row.subarray(offset, offset + 4);
if (alpha > 8 && green > red + 20 && green > blue + 20) {
unexpectedGreenPixels += 1;
}
}
}
assert.equal(
unexpectedGreenPixels,
0,
`${file}:${type} contains green noise instead of the navy/white app artwork`,
);
}
test("main process leaves macOS Dock icon to the packaged app bundle", () => {
const mainProcess = fs.readFileSync(
path.join(__dirname, "../electron/main.cjs"),
"utf8",
);
assert.equal(
mainProcess.includes("app.dock.setIcon"),
false,
"Do not override the macOS Dock icon at runtime; it can render at a different size than the bundled .icns icon.",
);
});
test("macOS packages a native ICNS and sizes runtime Dock icons separately", () => {
const projectRoot = path.join(__dirname, "..");
const config = require("../electron-builder.config.cjs");
assert.equal(config.mac?.icon ?? config.icon, "build/icon.icns");
const packagedIcon = path.join(projectRoot, "build/icon.icns");
assert.ok(readIcnsEntry(packagedIcon, "ic04").length > 0);
assert.ok(readIcnsEntry(packagedIcon, "ic05").length > 0);
assertSmallIcnsRepresentation(packagedIcon, "ic11", 32);
assertSmallIcnsRepresentation(packagedIcon, "ic12", 64);
const generator = fs.readFileSync(
path.join(projectRoot, "scripts/generate-mac-icon.sh"),
"utf8",
);
assert.match(generator, /SMALL_VECTOR/);
assert.match(generator, /x="104\\\.0" y="104\\\.0"/);
assert.deepEqual(
readRgbaPngAlphaBounds(path.join(projectRoot, "public/icon.png")),
{ minX: 61, minY: 61, maxX: 962, maxY: 962 },
"The packaged icon already looks correct when Netcatty is not running",
);
for (const variant of APP_ICON_VARIANTS) {
const iconFile = path.join(
projectRoot,
"public/icons/variants/macos",
`${variant}.png`,
);
assert.deepEqual(
readRgbaPngAlphaBounds(iconFile),
{ minX: 100, minY: 100, maxX: 923, maxY: 923 },
`${path.relative(projectRoot, iconFile)} must render on the 824px macOS icon grid`,
);
}
});
test("non-macOS runtime icons preserve their existing desktop sizing", () => {
const projectRoot = path.join(__dirname, "..");
assert.deepEqual(
readRgbaPngAlphaBounds(path.join(projectRoot, "public/icon-win.png")),
{ minX: 0, minY: 0, maxX: 1023, maxY: 1023 },
"The packaged Windows icon must remain full bleed",
);
for (const variant of APP_ICON_VARIANTS) {
const iconFile = path.join(projectRoot, "public/icons/variants", `${variant}.png`);
assert.deepEqual(
readRgbaPngAlphaBounds(iconFile),
{ minX: 61, minY: 61, maxX: 962, maxY: 962 },
`${path.relative(projectRoot, iconFile)} must keep the existing desktop runtime size`,
);
}
});