spaxel/dashboard/js/fresnel.test.js
jedarden 3150dadf32 fix(dashboard): clean up Fresnel toggle sync and fix semi-minor axis formula
Simplify Fresnel debug overlay toggle to use single toggleFresnelDebugOverlay
call instead of duplicating Viz3D sync logic. Fix semi-minor axis calculation
in viz3d.js (was missing factor of 2 in distance term). Add Math.max(0,...)
guard against negative sqrt. Add missing dispose mocks in fresnel tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:25:24 -04:00

388 lines
14 KiB
JavaScript

/**
* Spaxel Dashboard - Fresnel Zone Tests
*
* Tests for Fresnel ellipsoid geometry computation.
* These tests verify the correctness of the Fresnel zone calculations
* used for both the debug overlay and explainability overlay.
*/
describe('Fresnel Module', function() {
// Mock THREE.js before all tests
beforeAll(function() {
if (typeof THREE === 'undefined') {
global.THREE = {
Vector3: function(x, y, z) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
},
Quaternion: function() {
this._x = 0;
this._y = 0;
this._z = 0;
this._w = 1;
},
SphereGeometry: function() {},
EdgesGeometry: function() {},
LineBasicMaterial: function() {},
LineSegments: function() {
this.scale = { copy: function() {} };
this.position = { copy: function() {} };
this.quaternion = { copy: function() {} };
this.userData = {};
this.geometry = { dispose: function() {} };
this.material = { dispose: function() {} };
},
MeshBasicMaterial: function() {},
Mesh: function() {
this.scale = { copy: function() {} };
this.position = { copy: function() {} };
this.quaternion = { copy: function() {} };
this.userData = {};
this.geometry = { dispose: function() {} };
this.material = { dispose: function() {} };
},
DoubleSide: 2
};
// Add prototype methods to Vector3
THREE.Vector3.prototype.distanceTo = function(v) {
return Math.sqrt(
Math.pow(this.x - v.x, 2) +
Math.pow(this.y - v.y, 2) +
Math.pow(this.z - v.z, 2)
);
};
THREE.Vector3.prototype.addVectors = function(v1, v2) {
this.x = v1.x + v2.x;
this.y = v1.y + v2.y;
this.z = v1.z + v2.z;
return this;
};
THREE.Vector3.prototype.subVectors = function(v1, v2) {
this.x = v1.x - v2.x;
this.y = v1.y - v2.y;
this.z = v1.z - v2.z;
return this;
};
THREE.Vector3.prototype.normalize = function() {
var len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
if (len > 0) { this.x /= len; this.y /= len; this.z /= len; }
return this;
};
THREE.Vector3.prototype.multiplyScalar = function(s) {
this.x *= s;
this.y *= s;
this.z *= s;
return this;
};
THREE.Vector3.prototype.clone = function() {
return new THREE.Vector3(this.x, this.y, this.z);
};
THREE.Vector3.prototype.copy = function(v) {
this.x = v.x;
this.y = v.y;
this.z = v.z;
return this;
};
THREE.Quaternion.prototype.setFromUnitVectors = function(from, to) {
this._w = 1;
return this;
};
THREE.Quaternion.prototype.clone = function() {
return new THREE.Quaternion();
};
THREE.Quaternion.prototype.copy = function(q) {
this._w = q._w;
return this;
};
}
// Load the Fresnel module if not already loaded
if (typeof window.Fresnel === 'undefined') {
require('./fresnel.js');
}
});
describe('calculateFresnelEllipsoid', function() {
var calculateFresnelEllipsoid;
beforeAll(function() {
calculateFresnelEllipsoid = window.Fresnel.calculateFresnelEllipsoid;
});
it('should calculate correct axes for 4m link at 5 GHz (spec test case)', function() {
// TX at (0,0,0), RX at (4,0,0), lambda=0.06m (channel 36 = 5 GHz)
// d = 4, a = (4 + 0.03) / 2 = 2.015
// b = sqrt(2.015^2 - 2^2) = sqrt(0.060225) = 0.2454
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var channel = 36;
var result = calculateFresnelEllipsoid(tx, rx, channel);
expect(result.lambda).toBeCloseTo(0.06, 3);
expect(result.d).toBeCloseTo(4, 3);
expect(result.a).toBeCloseTo(2.015, 3);
expect(result.b).toBeCloseTo(0.245, 3);
});
it('should calculate correct semi-minor axis for very short link (edge case)', function() {
// d=0.1m -> b should be very small but positive
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(0.1, 0, 0);
var channel = 6;
var result = calculateFresnelEllipsoid(tx, rx, channel);
expect(result.b).toBeGreaterThan(0);
expect(result.b).toBeLessThan(0.1);
});
it('should calculate correct axes for diagonal link (3-4-5 triangle)', function() {
// TX at (0,0,0), RX at (3,4,0), distance = 5m, 2.4 GHz
// d = 5, lambda = 0.125
// a = (5 + 0.0625) / 2 = 2.53125
// b = sqrt(2.53125^2 - 2.5^2) = sqrt(0.1572...) = 0.3965
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(3, 4, 0);
var channel = 6;
var result = calculateFresnelEllipsoid(tx, rx, channel);
expect(result.d).toBeCloseTo(5, 3);
expect(result.a).toBeCloseTo(2.531, 3);
expect(result.b).toBeCloseTo(0.397, 3);
});
it('should use correct wavelength for 5 GHz channel', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result = calculateFresnelEllipsoid(tx, rx, 36);
expect(result.lambda).toBeCloseTo(0.06, 3);
});
it('should use correct wavelength for 2.4 GHz channel', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result = calculateFresnelEllipsoid(tx, rx, 6);
expect(result.lambda).toBeCloseTo(0.125, 3);
});
it('should default to 2.4 GHz for invalid channel', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result = calculateFresnelEllipsoid(tx, rx, 0);
expect(result.lambda).toBeCloseTo(0.125, 3);
});
it('should calculate ellipsoid center at midpoint', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result = calculateFresnelEllipsoid(tx, rx, 6);
expect(result.center.x).toBeCloseTo(2, 3);
expect(result.center.y).toBeCloseTo(0, 3);
expect(result.center.z).toBeCloseTo(0, 3);
});
it('should calculate ellipsoid center for diagonal link', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(6, 8, 0);
var result = calculateFresnelEllipsoid(tx, rx, 6);
expect(result.center.x).toBeCloseTo(3, 3);
expect(result.center.y).toBeCloseTo(4, 3);
expect(result.center.z).toBeCloseTo(0, 3);
});
it('should use X-axis as major axis in semiAxes', function() {
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result = calculateFresnelEllipsoid(tx, rx, 6);
// X should be semi-major (a), Y and Z should be semi-minor (b)
expect(result.semiAxes.x).toBeCloseTo(result.a, 6);
expect(result.semiAxes.y).toBeCloseTo(result.b, 6);
expect(result.semiAxes.z).toBeCloseTo(result.b, 6);
});
});
describe('toggle layer on/off', function() {
it('should add correct number of mesh objects when toggled on', function() {
// Create a mock scene that tracks added/removed children
var addedObjects = [];
var mockScene = {
add: function(obj) { addedObjects.push(obj); },
remove: function() {}
};
window.Fresnel.init(mockScene);
// Simulate 3 links
var ellipsoids = [];
var links = [
{ tx: new THREE.Vector3(0, 0, 0), rx: new THREE.Vector3(4, 0, 0) },
{ tx: new THREE.Vector3(0, 0, 0), rx: new THREE.Vector3(0, 3, 0) },
{ tx: new THREE.Vector3(1, 1, 0), rx: new THREE.Vector3(5, 4, 0) }
];
links.forEach(function(link) {
var e = window.Fresnel.addFresnelEllipsoid(link.tx, link.rx, 6, 0x4FC3F7);
if (e) ellipsoids.push(e);
});
// Each ellipsoid adds 2 objects (wireframe + fill)
expect(addedObjects.length).toBe(6);
// Clean up
ellipsoids.forEach(function(e) { window.Fresnel.removeFresnelEllipsoid(e); });
});
it('should remove all mesh objects when toggled off', function() {
var removedObjects = [];
var mockScene = {
add: function() {},
remove: function(obj) { removedObjects.push(obj); }
};
window.Fresnel.init(mockScene);
var e1 = window.Fresnel.addFresnelEllipsoid(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(4, 0, 0),
6, 0x4FC3F7
);
// Remove should dispose of wireframe + fill
window.Fresnel.removeFresnelEllipsoid(e1);
expect(removedObjects.length).toBe(2);
});
it('should not leave orphan objects when clearing all ellipsoids', function() {
var sceneChildren = [];
var mockScene = {
add: function(obj) { sceneChildren.push(obj); },
remove: function(obj) {
var idx = sceneChildren.indexOf(obj);
if (idx !== -1) sceneChildren.splice(idx, 1);
}
};
window.Fresnel.init(mockScene);
// Add 3 ellipsoids (6 objects)
var ellipsoids = [];
for (var i = 0; i < 3; i++) {
var e = window.Fresnel.addFresnelEllipsoid(
new THREE.Vector3(i, 0, 0),
new THREE.Vector3(i + 4, 0, 0),
6, 0x4FC3F7
);
if (e) ellipsoids.push(e);
}
expect(sceneChildren.length).toBe(6);
// Remove all
ellipsoids.forEach(function(e) { window.Fresnel.removeFresnelEllipsoid(e); });
expect(sceneChildren.length).toBe(0);
});
});
describe('hover tooltip data', function() {
it('should include correct link metadata in ellipsoid data', function() {
var mockScene = { add: function() {}, remove: function() {} };
window.Fresnel.init(mockScene);
var tx = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var channel = 6;
var ellipsoid = window.Fresnel.addFresnelEllipsoid(tx, rx, channel, 0x66bb6a);
expect(ellipsoid).not.toBeNull();
var data = ellipsoid.data;
expect(data.d).toBeCloseTo(4, 3);
expect(data.b).toBeGreaterThan(0);
expect(data.lambda).toBeCloseTo(0.125, 3);
expect(data.channel).toBe(6);
// Clean up
window.Fresnel.removeFresnelEllipsoid(ellipsoid);
});
it('should store tx and rx positions in ellipsoid data', function() {
var mockScene = { add: function() {}, remove: function() {} };
window.Fresnel.init(mockScene);
var tx = new THREE.Vector3(1, 2, 3);
var rx = new THREE.Vector3(4, 5, 6);
var ellipsoid = window.Fresnel.addFresnelEllipsoid(tx, rx, 6, 0x66bb6a);
expect(ellipsoid).not.toBeNull();
var data = ellipsoid.data;
expect(data.tx.x).toBeCloseTo(1, 3);
expect(data.tx.y).toBeCloseTo(2, 3);
expect(data.rx.x).toBeCloseTo(4, 3);
expect(data.rx.y).toBeCloseTo(5, 3);
// Clean up
window.Fresnel.removeFresnelEllipsoid(ellipsoid);
});
});
describe('node position update', function() {
it('should compute different ellipsoids when node position changes', function() {
var calc = window.Fresnel.calculateFresnelEllipsoid;
// Original position
var tx1 = new THREE.Vector3(0, 0, 0);
var rx = new THREE.Vector3(4, 0, 0);
var result1 = calc(tx1, rx, 6);
// Move TX node to (1, 0, 0)
var tx2 = new THREE.Vector3(1, 0, 0);
var result2 = calc(tx2, rx, 6);
// Distance changed from 4 to 3, so ellipsoid should differ
expect(result1.d).toBeCloseTo(4, 3);
expect(result2.d).toBeCloseTo(3, 3);
expect(result1.a).not.toBeCloseTo(result2.a, 3);
expect(result1.b).not.toBeCloseTo(result2.b, 3);
});
it('should produce larger semi-minor axis for shorter links', function() {
var calc = window.Fresnel.calculateFresnelEllipsoid;
var tx = new THREE.Vector3(0, 0, 0);
var rx1 = new THREE.Vector3(1, 0, 0); // short link
var rx2 = new THREE.Vector3(10, 0, 0); // long link
var result1 = calc(tx, rx1, 6);
var result2 = calc(tx, rx2, 6);
// Shorter link has relatively larger Fresnel zone
var ratio1 = result1.b / result1.d;
var ratio2 = result2.b / result2.d;
expect(ratio1).toBeGreaterThan(ratio2);
});
});
});