Files
GMW/services/frontend/src/lib/constellation/layout-graph.test.ts
T

124 lines
3.6 KiB
TypeScript
Raw Normal View History

import { describe, expect, test } from "bun:test";
import { channelsToGraph, statsToGraph } from "./graph";
import { computeLayout, radiusFor } from "./layout";
const statsFixture = {
total_messages: 1200,
total_users: 40,
total_flagged: 30,
total_clean: 1100,
total_warned: 12,
total_error: 2,
total_voice_recordings: 8,
total_profiles: 40,
today_messages: 55,
today_flagged: 3,
active_users_24h: 18,
top_channels: [
{ channel_id: "c1", channel_name: "general", message_count: 400 },
{ channel_id: "c2", channel_name: "random", message_count: 200 },
],
moderation_overview: { pending: 1, processing: 0, error: 0 },
};
describe("statsToGraph", () => {
test("guild center + channel nodes + edges", () => {
const g = statsToGraph(statsFixture);
expect(g.nodes.length).toBe(3);
expect(g.nodes[0]?.id).toBe("guild");
expect(g.edges.length).toBe(2);
expect(g.edges[0]?.source).toBe("guild");
});
test("bigger channel gets bigger value", () => {
const g = statsToGraph(statsFixture);
const c1 = g.nodes.find((n) => n.label === "general");
const c2 = g.nodes.find((n) => n.label === "random");
expect((c1?.value ?? 0) > (c2?.value ?? 0)).toBe(true);
});
test("empty top_channels still yields guild node", () => {
const g = statsToGraph({ ...statsFixture, top_channels: [] });
expect(g.nodes.length).toBe(1);
expect(g.edges.length).toBe(0);
});
});
describe("channelsToGraph", () => {
test("maps every channel", () => {
const chans = [
{
channel_id: "a",
channel_name: "alpha",
total_messages: 10,
flagged_count: 1,
},
{ channel_id: "b", total_messages: 5, flagged_count: 0 },
];
const g = channelsToGraph(chans);
expect(g.nodes.length).toBe(2);
expect(g.nodes[1]?.label).toBe("b"); // falls back to id when name null
});
});
describe("computeLayout", () => {
const nodes = [
{ id: "guild", kind: "guild", value: 1 },
{ id: "channel:c1", kind: "channel", value: 0.8 },
{ id: "channel:c2", kind: "channel", value: 0.4 },
];
const edges = [
{ source: "guild", target: "channel:c1" },
{ source: "guild", target: "channel:c2" },
];
test("deterministic for same seed", () => {
const a = computeLayout(nodes, edges, {
width: 900,
height: 700,
seed: 42,
});
const b = computeLayout(nodes, edges, {
width: 900,
height: 700,
seed: 42,
});
expect(a.map((n) => [n.id, Math.round(n.x), Math.round(n.y)])).toEqual(
b.map((n) => [n.id, Math.round(n.x), Math.round(n.y)]),
);
});
test("all finite positions within canvas bounds", () => {
const out = computeLayout(nodes, edges, {
width: 900,
height: 700,
seed: 7,
});
for (const n of out) {
expect(Number.isFinite(n.x)).toBe(true);
expect(Number.isFinite(n.y)).toBe(true);
expect(n.x).toBeGreaterThanOrEqual(-50);
expect(n.x).toBeLessThanOrEqual(950);
expect(n.y).toBeGreaterThanOrEqual(-50);
expect(n.y).toBeLessThanOrEqual(750);
}
});
test("reduced motion returns static ring", () => {
const out = computeLayout(nodes, edges, {
width: 900,
height: 700,
reduced: true,
});
expect(out.length).toBe(3);
expect(Number.isFinite(out[0]?.y ?? NaN)).toBe(true);
});
});
describe("radiusFor", () => {
test("guild larger than channel larger than message", () => {
expect(radiusFor("guild", 1)).toBeGreaterThan(radiusFor("channel", 1));
expect(radiusFor("channel", 1)).toBeGreaterThan(radiusFor("message", 1));
});
});