import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BoxGeometry, Group, Mesh, MeshBasicMaterial, PerspectiveCamera } from 'three'; import { createCharacterOverlay } from '../src/ui/characterOverlay.ts'; test('character menu follows its subject, stays inside the viewport and hides behind the camera', () => { const player = new Group(), head = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial()); head.name = 'Head'; head.position.y = 1; player.add(head); const camera = new PerspectiveCamera(60, 1, .1, 100); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); camera.updateMatrixWorld(true); const canvas = { getBoundingClientRect: () => ({ left: 0, top: 0, right: 320, bottom: 320, width: 320, height: 320 }) } as unknown as HTMLCanvasElement; const panel = { hidden: true, style: { left: '', top: '' }, offsetWidth: 220, offsetHeight: 80 } as unknown as HTMLElement; const overlay = createCharacterOverlay(player, canvas, panel, 12); overlay.update(camera, true); assert.equal(panel.hidden, false); assert.equal(parseFloat(panel.style.left), 160); const firstTop = parseFloat(panel.style.top); player.position.x = 1; overlay.update(camera, true); assert.ok(parseFloat(panel.style.left) > 160); assert.ok(parseFloat(panel.style.left) + panel.offsetWidth / 2 <= 308); player.position.y = 1; overlay.update(camera, true); assert.ok(parseFloat(panel.style.top) < firstTop); assert.ok(parseFloat(panel.style.top) - panel.offsetHeight >= 12); player.position.z = 10; overlay.update(camera, true); assert.equal(panel.hidden, true); player.position.set(0, 0, 0); overlay.update(camera, false); assert.equal(panel.hidden, true); overlay.update(camera, true); assert.equal(panel.hidden, false); head.geometry.dispose(); (head.material as MeshBasicMaterial).dispose(); });