feat(frontend): constellation lib scaffold — graph/layout/camera pure modules (tested)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user