Files
awesome-copilot/extensions/draw/lib/model.test.mjs
T

96 lines
4.4 KiB
JavaScript

// Tests for the element model: cleaning up loose input and picking readable text colors.
// Run `node --test` in the extension folder.
import assert from "node:assert/strict";
import test from "node:test";
import { COLORS, DARK_PALETTE, LIGHT_PALETTE, MAX_PEN_POINTS, diffOps, luminance, mergeOps, normalizeElement, normalizeElements, sameJson, staticPaint } from "./model.mjs";
const contrast = (a, b) => {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
test("text on every solid color is readable in both themes", () => {
for (const [theme, palette] of [["light", LIGHT_PALETTE], ["dark", DARK_PALETTE]]) {
const paint = staticPaint(palette);
for (const color of COLORS) {
const ratio = contrast(paint(color, "onSolid"), paint(color, "solid"));
assert.ok(ratio >= 4.5, `${theme} ${color} is only ${ratio.toFixed(2)}:1`);
}
}
});
test("loose input is cleaned up, and arrows to missing shapes are dropped", () => {
const out = normalizeElements([
{ id: "a", type: "rect" },
{ id: "a", type: "circle", x: 200 },
{ type: "arrow", from: "a", to: "missing" },
{ type: "spaceship" },
null,
"rect",
]);
assert.deepEqual(out.map((e) => e.type), ["rect", "ellipse"]);
assert.equal(out[0].id, "a");
assert.notEqual(out[1].id, "a");
});
test("the ops from diffOps turn one list into the other", () => {
// A small random generator (MINSTD), so a failure can be run again.
let seed = 7;
const rand = (n) => {
seed = (seed * 48271) % 2147483647;
return seed % n;
};
const shape = (id) => normalizeElement({ id, type: "rect", x: rand(5) * 10, text: rand(2) ? "a" : "" });
for (let round = 0; round < 500; round++) {
const before = Array.from({ length: rand(8) }, (_, i) => shape(`e${i}`));
let after = before.filter(() => rand(4)).map((e) => (rand(3) ? e : shape(e.id)));
after.push(...Array.from({ length: rand(3) }, (_, i) => shape(`n${i}`)));
if (rand(2)) after = after.sort(() => rand(3) - 1);
const ops = diffOps(new Map(before.map((e) => [e.id, e])), after);
if (!ops) {
assert.deepEqual(after, before);
continue;
}
assert.deepEqual(mergeOps(before, ops), after);
// Only elements that changed are sent.
for (const el of ops.upserts) assert.ok(!sameJson(el, before.find((e) => e.id === el.id)));
}
});
test("sameJson compares data, not key order", () => {
assert.ok(sameJson({ a: 1, b: [1, { c: 2 }] }, { b: [1, { c: 2 }], a: 1 }));
assert.ok(!sameJson({ a: 1 }, { a: 1, b: 2 }));
assert.ok(!sameJson({ a: 1, c: 2 }, { a: 1, b: 2 }));
assert.ok(!sameJson([1, 2], { 0: 1, 1: 2 }));
assert.ok(!sameJson([[1, 2]], [[1, 3]]));
assert.ok(!sameJson(null, {}));
});
test("a stroke with too many points keeps both its ends", () => {
const points = Array.from({ length: 3 * MAX_PEN_POINTS }, (_, i) => [i, i % 7]);
const pen = normalizeElement({ type: "pen", points });
assert.equal(pen.points.length, MAX_PEN_POINTS);
assert.deepEqual(pen.points[0], points[0]);
assert.deepEqual(pen.points.at(-1), points.at(-1));
// Spread evenly, and in order.
assert.ok(pen.points.every((p, i) => i === 0 || p[0] > pen.points[i - 1][0]));
assert.equal(normalizeElement({ type: "pen", points: points.slice(0, 10) }).points.length, 10);
});
test("any one element, however big, fits in what a page can send as it closes", () => {
// Browsers only deliver keepalive requests after the page is gone, and refuse them past 64 KiB
// in all (see flushBeacon in public/sync.js).
const far = -999999.94;
const pen = normalizeElement({ id: "p".repeat(64), type: "pen", color: "#123456", points: Array.from({ length: 3 * MAX_PEN_POINTS }, () => [far, far]) });
assert.equal(pen.points.length, MAX_PEN_POINTS);
assert.deepEqual(pen.points[0], [-999999.9, -999999.9]);
// A lone surrogate is written as a 6 byte escape, the most any one character of a label takes.
const text = "\ud800".repeat(10000);
const labelled = ["rect", "ellipse", "diamond", "cylinder", "text", "arrow"].map((type) =>
normalizeElement({ id: type[0].repeat(64), type, text, from: "f".repeat(64), to: "g".repeat(64) }));
for (const el of [pen, ...labelled]) {
const body = JSON.stringify({ clientId: "c".repeat(64), drawingId: "d".repeat(64), seq: Number.MAX_SAFE_INTEGER, ops: { upserts: [el], deletes: [] } });
assert.ok(Buffer.byteLength(body) <= 64 * 1024, `${el.type}: ${Buffer.byteLength(body)} bytes`);
}
});