MeshChatX/tests/frontend/networkVisualiserNodeLook.test.js

157 lines
6.1 KiB
JavaScript

// SPDX-License-Identifier: 0BSD
/**
* Regression tests for WebGL visualiser node look:
* crisp discs, glyph-on-color badges, readable sizes, no soft fuzzy blobs.
*/
import { describe, expect, it } from "vitest";
import {
ATLAS_CELL,
isGlyphStyleVisualiserIcon,
NODE_BORDER_INNER,
NODE_BORDER_OUTER,
NODE_EDGE_INNER,
prepareVisualiserIconPixels,
resolveVisualiserAssetUrl,
} from "@/js/networkVisualiserWebGL.js";
import {
graphToSceneRequest,
KIND_IFACE_ON,
KIND_ME,
KIND_PEER,
webglNodeSizeFor,
} from "@/js/networkVisualiserWebGLEngine.js";
describe("visualiser WebGL node look regressions", () => {
it("keeps disc soft-edge tight (no fuzzy blob AA)", () => {
expect(NODE_EDGE_INNER).toBeGreaterThanOrEqual(0.94);
expect(NODE_BORDER_INNER).toBeLessThan(NODE_BORDER_OUTER);
expect(NODE_BORDER_OUTER).toBeLessThanOrEqual(NODE_EDGE_INNER + 0.001);
});
it("treats network-visualiser badge PNGs as glyph-style icons", () => {
expect(isGlyphStyleVisualiserIcon("/assets/images/network-visualiser/user.png")).toBe(true);
expect(isGlyphStyleVisualiserIcon("/assets/images/network-visualiser/interface_connected.png")).toBe(true);
expect(isGlyphStyleVisualiserIcon("/assets/images/reticulum_logo_512.png")).toBe(false);
expect(isGlyphStyleVisualiserIcon("")).toBe(false);
});
it("uses a high-res atlas cell so icons stay sharp on HiDPI", () => {
expect(ATLAS_CELL).toBeGreaterThanOrEqual(128);
});
it("extracts white glyph and clears solid fill from badge pixels", () => {
// 2x2: blue fill, white glyph, blue fill, empty
const data = new Uint8ClampedArray([59, 130, 246, 255, 255, 255, 255, 255, 59, 130, 246, 255, 0, 0, 0, 0]);
const { painted, glyphPixels } = prepareVisualiserIconPixels(data, "glyph");
expect(painted).toBe(3);
expect(glyphPixels).toBe(1);
expect(data[0]).toBe(0);
expect(data[3]).toBe(0);
expect(data[4]).toBe(255);
expect(data[7]).toBe(255);
expect(data[8]).toBe(0);
expect(data[11]).toBe(0);
});
it("preserves soft alpha on mid-luma glyph AA fringes", () => {
// Grey fringe between white glyph and blue fill (binary threshold would crunch this).
const data = new Uint8ClampedArray([180, 180, 180, 255]);
const { glyphPixels } = prepareVisualiserIconPixels(data, "glyph");
expect(glyphPixels).toBe(1);
expect(data[0]).toBe(255);
expect(data[1]).toBe(255);
expect(data[2]).toBe(255);
expect(data[3]).toBeGreaterThan(40);
expect(data[3]).toBeLessThan(220);
});
it("opaque mode preserves soft alpha and only promotes a=0 RGB pixels", () => {
const data = new Uint8ClampedArray([
10,
20,
30,
0, // RGB quirk: promote alpha
40,
50,
60,
90, // soft fringe: keep
0,
0,
0,
0, // empty
]);
const { painted } = prepareVisualiserIconPixels(data, "opaque");
expect(painted).toBe(2);
expect(data[3]).toBe(255);
expect(data[7]).toBe(90);
});
it("stepwise atlas blit is exported for large logo downscales", async () => {
const { drawImageToAtlasCell } = await import("@/js/networkVisualiserWebGL.js");
expect(typeof drawImageToAtlasCell).toBe("function");
});
it("falls back to opaque when glyph extraction would wipe the icon", () => {
// All mid-saturation blue: no bright glyph pixels.
const data = new Uint8ClampedArray(16);
for (let i = 0; i < 16; i += 4) {
data[i] = 59;
data[i + 1] = 130;
data[i + 2] = 246;
data[i + 3] = 255;
}
const backup = new Uint8ClampedArray(data);
const first = prepareVisualiserIconPixels(data, "glyph");
expect(first.glyphPixels).toBe(0);
expect(first.painted).toBe(4);
// paintSlot restores the pre-glyph buffer before opaque fallback.
data.set(backup);
prepareVisualiserIconPixels(data, "opaque");
expect(data[3]).toBe(255);
expect(data[0]).toBe(59);
});
it("WebGL node sizes stay large enough for glyphs", () => {
expect(webglNodeSizeFor({ size: 25 }, KIND_PEER)).toBeGreaterThanOrEqual(18);
expect(webglNodeSizeFor({ size: 50 }, KIND_ME)).toBeGreaterThanOrEqual(30);
expect(webglNodeSizeFor({ size: 35 }, KIND_IFACE_ON)).toBeGreaterThanOrEqual(24);
// Must not shrink to the old fuzzy 0.55 scale (~13px for size 25).
expect(webglNodeSizeFor({ size: 25 }, KIND_PEER)).toBeGreaterThan(16);
});
it("graph scene colors use vivid border for disc fill", () => {
const req = graphToSceneRequest(
[
{
id: "peer",
group: "announce",
size: 25,
color: { border: "#3b82f6", background: "#eff6ff" },
},
{
id: "direct",
group: "announce",
size: 25,
color: { border: "#10b981", background: "#ecfdf5" },
},
],
[],
{ width: 100, height: 100, zoom: 1 }
);
expect(req.nodes[0].r).toBeCloseTo(0x3b / 255, 2);
expect(req.nodes[0].g).toBeCloseTo(0x82 / 255, 2);
expect(req.nodes[0].b).toBeCloseTo(0xf6 / 255, 2);
expect(req.nodes[1].r).toBeCloseTo(0x10 / 255, 2);
expect(req.nodes[1].g).toBeCloseTo(0xb9 / 255, 2);
// Pale backgrounds must not become the disc color (that looked washed out).
expect(req.nodes[0].r).toBeGreaterThan(0.15);
expect(req.nodes[0].size).toBeGreaterThanOrEqual(18);
});
it("resolveVisualiserAssetUrl keeps absolute asset paths resolvable", () => {
const url = resolveVisualiserAssetUrl("/assets/images/network-visualiser/user.png");
expect(url).toContain("/assets/images/network-visualiser/user.png");
});
});