import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { createSimulation, neighbourhood } from '../src/lib/graph.js'; import { nodeColor } from '../src/lib/graphColors.js'; const require = createRequire(import.meta.url); const { graph } = require('../electron/links.cjs'); test('graph data: one edge per linked pair, ghosts for missing pages, code ignored', () => { const P = (id, body) => ({ id, title: id.split('/').pop(), parent: id.includes('/') ? id.slice(0, id.lastIndexOf('/')) : null, exists: true, body }); const g = graph([P('A', '[[B]] [[B]] [[Nope]] `[[C]]`'), P('B', '[[A]] [[a]] [[B]]'), P('C', ''), P('X/Y', '[[C]]'), { id: 'X', title: 'X', exists: false, body: '' }]); assert.deepEqual(g.nodes.map((n) => n.id), ['A', 'B', 'C', 'X/Y']); assert.deepEqual(g.edges, [['A', 'B'], ['A', '?nope'], ['B', 'A'], ['X/Y', 'C']]); assert.deepEqual(g.ghosts, [{ id: '?nope', title: 'Nope' }]); }); test('simulation settles: linked notes close, clusters apart, nothing stacked', () => { const ids = Array.from({ length: 40 }, (_, i) => 'n' + i); const edges = []; for (let i = 1; i < 20; i++) edges.push(['n0', 'n' + i]); for (let i = 21; i < 36; i++) edges.push(['n20', 'n' + i]); const sim = createSimulation().setData(ids.map((id) => ({ id })), edges); let n = 0; while (sim.running && n < 2000) { sim.tick(); n++; } assert.ok(n < 600, `settles (${n} ticks)`); const N = sim.nodes, d = (a, b) => Math.hypot(N[a].x - N[b].x, N[a].y - N[b].y); const spoke = d(0, 1); assert.ok(spoke > 40 && spoke < 400, `spoke length ${spoke}`); assert.ok(d(0, 20) > spoke * 1.5, 'the two hubs push apart'); let closest = Infinity; for (let i = 0; i < 40; i++) for (let j = i + 1; j < 40; j++) closest = Math.min(closest, d(i, j)); assert.ok(closest > 5, `no stacked nodes (closest ${closest})`); assert.equal(N[0].degree, 19); }); test('dragged (fixed) nodes stay put; stacked starts separate; neighbourhood depth', () => { const sim = createSimulation().setData([{ id: 'a', x: 0, y: 0 }, { id: 'b', x: 0, y: 0 }, { id: 'c', x: 0, y: 0 }], [['a', 'b']]); sim.nodes[0].fx = 50; sim.nodes[0].fy = 50; sim.tick(200); assert.deepEqual([sim.nodes[0].x, sim.nodes[0].y], [50, 50]); assert.ok(Math.hypot(sim.nodes[1].x - sim.nodes[2].x, sim.nodes[1].y - sim.nodes[2].y) > 5); const edges = [['a', 'b'], ['b', 'c'], ['c', 'd'], ['x', 'y']]; assert.deepEqual([...neighbourhood('b', edges, 1)].sort(), ['a', 'b', 'c']); assert.deepEqual([...neighbourhood('a', edges, 3)].sort(), ['a', 'b', 'c', 'd']); }); test('graph colours: a coloured tag colours its pages; everything else stays plain', () => { const colors = { project: 'purple', asm: 'green' }; assert.equal(nodeColor(['project'], colors, '#plain'), '#9a6de0'); assert.equal(nodeColor(['study', 'asm', 'project'], colors, '#plain'), '#44ad6e', 'the first of its tags that has a colour'); assert.equal(nodeColor(['study'], colors, '#plain'), '#plain', 'a tag without a colour does not colour its pages'); assert.equal(nodeColor(['study'], { study: 'not-a-colour' }, '#plain'), '#plain', 'an unknown colour name is ignored'); assert.equal(nodeColor([], colors, '#plain'), '#plain'); assert.equal(nodeColor(undefined, colors, '#plain'), '#plain'); });