The diagonal stripe graphic started left of the paper and long titles or unbreakable words (repository and file names) overflowed the sides and bottom. Posters are now clipped to the paper, and text is fitted: the largest size at which it wraps within the width, line limit and remaining height; words too wide even at the smallest size are shortened individually with an ellipsis so the rest of the text stays.
129 lines
7.7 KiB
JavaScript
129 lines
7.7 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { ShelfPacker } from '../web/labels.js';
|
|
import { flatQuad, wallSigns, facadeQuad } from '../web/city.js';
|
|
|
|
test('shelf packer fills rows then reports full', () => {
|
|
const packer = new ShelfPacker(100, 20, 10);
|
|
assert.deepEqual(packer.add(60), { x: 0, y: 0, w: 60, h: 10 });
|
|
assert.deepEqual(packer.add(60), { x: 0, y: 10, w: 60, h: 10 });
|
|
assert.deepEqual(packer.add(30), { x: 62, y: 10, w: 30, h: 10 });
|
|
assert.equal(packer.add(30), null);
|
|
packer.reset();
|
|
assert.deepEqual(packer.add(500), { x: 0, y: 0, w: 100, h: 10 });
|
|
});
|
|
|
|
const building = { x: 0, y: 0, w: 20, h: 10, height: 30 };
|
|
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
|
|
test('wall signs only appear on walls facing the viewer and read left to right', () => {
|
|
const south = wallSigns(building, [10, 50, 1.7], 4);
|
|
assert.equal(south.length, 1);
|
|
assert.ok(south[0].c[1] > 10 && south[0].u[0] > 0, 'south wall reads along +x for a viewer looking north');
|
|
const corner = wallSigns(building, [40, 40, 1.7], 4);
|
|
assert.equal(corner.length, 2);
|
|
for (const quad of corner) {
|
|
// u x v points out of the wall toward the viewer when text reads correctly (left-handed world).
|
|
const normal = cross(quad.u, quad.v), toEye = [40 - quad.c[0], 40 - quad.c[1], 0];
|
|
assert.ok(normal[0] * toEye[0] + normal[1] * toEye[1] < 0);
|
|
}
|
|
assert.ok(wallSigns(building, [10, 50, 1.7], 100)[0].u[0] * 2 <= 20 * .85 + 1e-9, 'long names shrink to the wall');
|
|
const long = wallSigns({ x: 0, y: 0, w: 100, h: 10, height: 30 }, [50, 50, 1.7], 4);
|
|
assert.equal(long.length, 4, 'a 100 m wall repeats its sign');
|
|
assert.ok(long.every(quad => quad.c[1] > 10) && new Set(long.map(quad => Math.round(quad.c[0]))).size === 4);
|
|
});
|
|
|
|
test('facade covers the most squarely facing wall within the building', () => {
|
|
const quad = facadeQuad(building, [60, 5, 1.7], .5);
|
|
assert.ok(quad.c[0] > 20, 'east wall');
|
|
const height = quad.v[2] * 2, width = Math.hypot(quad.u[0], quad.u[1]) * 2;
|
|
assert.ok(Math.abs(width / height - .5) < 1e-9 && quad.c[2] + height / 2 <= building.height);
|
|
assert.deepEqual(flatQuad(1, 2, 3, 4, 2), { c: [1, 2, 3], u: [2, 0, 0], v: [0, -1, 0] });
|
|
});
|
|
|
|
import { folderBlades, folderLabel, readableFrom } from '../web/city.js';
|
|
|
|
// A 2x2 grid of buildings inside a block whose kerb is 2.5 m from the boundary.
|
|
const block = { x: 0, y: 0, w: 50, h: 50 };
|
|
const grid = [
|
|
{ x: 2.5, y: 2.5, w: 20, h: 20, height: 30 }, { x: 27.5, y: 2.5, w: 20, h: 20, height: 30 },
|
|
{ x: 2.5, y: 27.5, w: 20, h: 20, height: 30 }, { x: 27.5, y: 27.5, w: 20, h: 20, height: 30 },
|
|
];
|
|
|
|
test('folder blades sit at every block corner, on street-facing walls, pointing outward', () => {
|
|
const blades = folderBlades(block, grid, null, 4);
|
|
assert.equal(blades.length, 16, 'two walls per corner, two faces per blade');
|
|
for (let i = 0; i < blades.length; i += 2) {
|
|
const [a, b] = [blades[i], blades[i + 1]];
|
|
const outward = [Math.sign(a.u[0]), Math.sign(a.u[1])];
|
|
const corner = [a.c[0] < 25 ? 0 : 50, a.c[1] < 25 ? 0 : 50];
|
|
assert.ok(Math.hypot(a.c[0] - corner[0], a.c[1] - corner[1]) < 6, `near its corner: ${a.c}`);
|
|
assert.ok(outward[0] ? (outward[0] > 0) === (a.c[0] > 25) : (outward[1] > 0) === (a.c[1] > 25), 'projects into the street');
|
|
for (const eye of [[a.c[0] + a.v[2] * 0 + (a.u[1] ? 10 : 0), a.c[1] + (a.u[0] ? 10 : 0), 2], [a.c[0] - (a.u[1] ? 10 : 0), a.c[1] - (a.u[0] ? 10 : 0), 2]]) {
|
|
assert.equal(readableFrom(a, eye) + readableFrom(b, eye), 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('courtyard walls, hidden walls and cramped gaps get no blades', () => {
|
|
const inner = [{ x: 10, y: 10, w: 30, h: 30, height: 30 }];
|
|
assert.equal(folderBlades(block, inner, null, 4).length, 0, 'set back from the street');
|
|
const northOnly = folderBlades(block, grid, [25, -40, 1.7], 4);
|
|
assert.ok(northOnly.length === 4 && northOnly.every(q => q.c[1] < 2.5), 'only walls facing the viewer');
|
|
assert.equal(folderBlades(block, grid, null, 4, () => 1).length, 0);
|
|
assert.ok(readableFrom(flatQuad(0, 0, 0, 4, 1), [0, 0, 50]), 'ground labels read from above');
|
|
});
|
|
|
|
test('folder labels keep the tail of long paths', () => {
|
|
assert.equal(folderLabel('', 'prometheus'), 'prometheus');
|
|
assert.equal(folderLabel('tsdb/chunks', 'x'), 'tsdb/chunks');
|
|
const long = folderLabel('web/ui/react-app/src/pages/graph/components', 'x');
|
|
assert.ok(long.startsWith('…/') && long.endsWith('graph/components') && long.length <= 34 + 2, long);
|
|
});
|
|
|
|
import { alertsByPath, burns, tapeQuads } from '../web/city.js';
|
|
|
|
test('alerts group by file with the worst severity', () => {
|
|
const byPath = alertsByPath({ available: true, codeScanning: { alerts: [{ path: 'a.go', severity: 'medium' }, { path: 'a.go', severity: 'critical' }] }, dependabot: { alerts: [{ path: 'go.mod', severity: 'low' }, { path: '', severity: 'high' }] } });
|
|
assert.equal(byPath.size, 2);
|
|
assert.equal(byPath.get('a.go').worst, 'critical');
|
|
assert.equal(byPath.get('a.go').codeScanning.length, 2);
|
|
assert.equal(byPath.get('go.mod').dependabot.length, 1);
|
|
assert.equal(alertsByPath({ available: false, reason: 'signin' }).size, 0);
|
|
assert.ok(burns('high') && burns('critical') && !burns('medium') && !burns(undefined));
|
|
});
|
|
|
|
test('hazard tape crosses each wall corner to corner, readable from outside', () => {
|
|
const b = { x: 0, y: 0, w: 20, h: 10, height: 40 };
|
|
for (const direction of [1, -1]) {
|
|
const quads = tapeQuads(b, 6, direction);
|
|
const south = quads.filter(q => q.c[1] > 10);
|
|
const length = south.reduce((sum, q) => sum + Math.hypot(...q.u) * 2, 0);
|
|
assert.ok(Math.abs(length - Math.hypot(20, 40)) < 1e-6, `covers the diagonal: ${length}`);
|
|
const [first, last] = [south[0], south.at(-1)];
|
|
assert.ok(direction > 0 ? first.c[2] < last.c[2] : first.c[2] > last.c[2], 'runs the right way');
|
|
assert.ok(south.every(q => q.c[2] >= 0 && q.c[2] <= 40 && q.c[0] >= 0 && q.c[0] <= 20));
|
|
assert.ok(south.every(q => readableFrom(q, [10, 60, 20])) && south.every(q => !readableFrom(q, [10, 5, 20])));
|
|
assert.ok(south.at(-1).fraction <= 1 && south.slice(0, -1).every(q => q.fraction === 1));
|
|
assert.equal(new Set(quads.map(q => `${Math.sign(q.c[0] - 10)},${Math.sign(q.c[1] - 5)}`)).size >= 4, true, 'all four walls');
|
|
}
|
|
assert.equal(tapeQuads({ x: 0, y: 0, w: 2, h: 2, height: 30 }, 6).length, 0, 'too narrow for tape');
|
|
});
|
|
|
|
import { fitText } from '../web/labels.js';
|
|
|
|
test('poster text shrinks and wraps until it fits inside the box', () => {
|
|
const measure = (text, size) => text.length * size * .6; // monospace-ish stand-in for canvas
|
|
const short = fitText(measure, 'SHIP SMALL', 200, 120, 34, 12, 3);
|
|
assert.equal(short.size, 34);
|
|
const long = fitText(measure, 'WELCOME TO AN EXTREMELY LONG REPOSITORY NAME THAT GOES ON', 200, 120, 34, 12, 3);
|
|
assert.ok(long.lines.length <= 3 && long.lines.length * long.size * 1.08 <= 120);
|
|
for (const fit of [short, long]) for (const line of fit.lines) assert.ok(measure(line, fit.size) <= 200, line);
|
|
const word = fitText(measure, 'supercalifragilistic_expialidocious_really_long_filename.go', 200, 120, 34, 12, 3);
|
|
assert.ok(word.lines.every(line => measure(line, word.size) <= 200) && word.lines[0].endsWith('…'), 'unbreakable words are truncated');
|
|
const mixed = fitText(measure, 'WELCOME TO PROMETHEUS-OPERATOR-HELM-CHARTS-MONOREPO', 200, 120, 34, 12, 3);
|
|
assert.ok(mixed.lines[0].startsWith('WELCOME TO') && mixed.lines.every(line => measure(line, mixed.size) <= 200), `keeps the short words: ${mixed.lines}`);
|
|
const crowded = fitText(measure, 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen', 60, 30, 20, 12, 3);
|
|
assert.ok(crowded.lines.length * 12 * 1.08 <= 30 && crowded.lines.every(line => measure(line, 12) <= 60 + 1e-9), `crowded: ${crowded.lines}`);
|
|
});
|