import test from 'node:test'; import assert from 'node:assert/strict'; import { ConstellationLayer } from '../src/constellation.js'; test('rendered links keep correct endpoints after expiry, with labels fixed and fades independent of uploads', () => { // Exercise real Three.js buffers without requiring a browser or GPU. const originalDocument = globalThis.document; globalThis.document = { createElement: () => ({ getContext: () => ({ clearRect() {}, fillText() {} }) }) }; let layer; try { layer = new ConstellationLayer(); const settings = { trail: 3, sensitivity: 1, palette: 'spectrum', colorMode: 'position', glow: 1.35, connections: true, labels: true }; const frame = time => ({ time, rms: .2, entropy: .4, spread: 900, peaks: [{ frequency: 1500 + time * 10, amplitude: .2 }] }); const history = Array.from({ length: 60 }, (_, i) => frame(i / 30)); layer.update(history, 2, settings, 600); const label = layer.labelPool.find(label => label.node); assert.ok(label); const labelNode = label.node; const labelPosition = label.sprite.position.clone(); const uploadVersion = layer.geometry.attributes.position.version; layer.animate(2.01); assert.equal(layer.uniforms.now.value, 2.01); assert.equal(layer.lines.material.uniforms.now.value, 2.01); assert.equal(layer.geometry.attributes.position.version, uploadVersion); layer.update(history, 2.01, settings, 600); assert.equal(layer.geometry.attributes.position.version, uploadVersion); history.push(frame(2.05)); layer.update(history, 2.05, settings, 600); assert.equal(label.node, labelNode); assert.deepEqual(label.sprite.position, labelPosition); // Several old points expire; line indices must still reference the surviving points. history.push(frame(3.1)); layer.update(history, 3.1, settings, 600); assert.ok(layer.model.nodes.every(node => node.time > .1)); assert.equal(layer.lineGeometry.instanceCount, layer.model.links.length); assert.equal(layer.lineGeometry.index.count, 6); assert.ok(layer.model.links.some(([, , temporal]) => temporal)); for (const [i, [a, b]] of layer.model.links.entries()) { assert.deepEqual(Array.from(layer.linePositions.slice(i * 6, i * 6 + 6)), Array.from(new Float32Array([...layer.model.nodes[a].position, ...layer.model.nodes[b].position]))); } settings.connections = false; layer.update(history, 3.1, settings, 600); assert.equal(layer.lines.visible, false); layer.update([], 0, settings, 600); assert.equal(layer.geometry.drawRange.count, 0); assert.equal(layer.lineGeometry.instanceCount, 0); assert.ok(layer.labelPool.every(label => !label.node && !label.sprite.visible)); } finally { layer?.traverse(object => { object.geometry?.dispose(); object.material?.map?.dispose(); object.material?.dispose(); }); if (originalDocument === undefined) delete globalThis.document; else globalThis.document = originalDocument; } }); test('old annotations fade gradually to zero without another audio sample', () => { const originalDocument=globalThis.document; globalThis.document={createElement:()=>({getContext:()=>({clearRect(){},fillText(){}})})}; let layer; try { layer=new ConstellationLayer(); layer.update([{time:0,rms:.2,entropy:.4,spread:900,peaks:[{frequency:1500,amplitude:.2}]}],.2,{trail:6,sensitivity:1,palette:'spectrum',colorMode:'position',glow:1,connections:true,labels:true},600); const label=layer.labelPool.find(label=>label.node); const fresh=label.sprite.material.opacity; layer.animate(3); const halfway=label.sprite.material.opacity; assert.ok(halfway{o.geometry?.dispose();o.material?.map?.dispose();o.material?.dispose();}); if(originalDocument===undefined)delete globalThis.document;else globalThis.document=originalDocument; } });