208 lines
6.4 KiB
JavaScript
208 lines
6.4 KiB
JavaScript
import { expect } from "@esm-bundle/chai";
|
|
import sinon from "sinon";
|
|
import * as THREE from "three";
|
|
import { InputManager } from "../../src/core/InputManager.js";
|
|
|
|
describe("Core: InputManager", () => {
|
|
let inputManager;
|
|
let camera;
|
|
let scene;
|
|
let domElement;
|
|
let originalGetGamepads;
|
|
|
|
beforeEach(() => {
|
|
camera = new THREE.PerspectiveCamera();
|
|
scene = new THREE.Scene();
|
|
|
|
// Mock DOM Element
|
|
domElement = {
|
|
getBoundingClientRect: () => ({
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100,
|
|
}),
|
|
addEventListener: sinon.spy(),
|
|
removeEventListener: sinon.spy(),
|
|
};
|
|
|
|
inputManager = new InputManager(camera, scene, domElement);
|
|
|
|
// Save original navigator.getGamepads to restore later
|
|
originalGetGamepads = navigator.getGamepads;
|
|
});
|
|
|
|
afterEach(() => {
|
|
inputManager.detach();
|
|
// Restore original navigator function
|
|
navigator.getGamepads = originalGetGamepads;
|
|
});
|
|
|
|
it("CoA 1: Should attach event listeners on creation", () => {
|
|
expect(domElement.addEventListener.calledWith("mousemove")).to.be.true;
|
|
expect(domElement.addEventListener.calledWith("click")).to.be.true;
|
|
});
|
|
|
|
it("CoA 2: Should calculate normalized mouse coordinates", () => {
|
|
// Simulate mouse event at center (50, 50)
|
|
const event = { clientX: 50, clientY: 50 };
|
|
inputManager.updateMouseCoords(event);
|
|
|
|
// Center should be (0, 0) in NDC
|
|
expect(inputManager.mouse.x).to.equal(0);
|
|
expect(inputManager.mouse.y).to.equal(0);
|
|
});
|
|
|
|
it("CoA 3: Raycast should identify voxel coordinates based on intersection", () => {
|
|
// Mock Raycaster intersection result
|
|
// Use (0, 0.5, 0) which is the exact center of the top face of a block at (0,0,0)
|
|
// 1x1x1 box at 0,0,0 extends from -0.5 to 0.5
|
|
const mockIntersection = {
|
|
object: { isInstancedMesh: true },
|
|
point: new THREE.Vector3(0, 0.5, 0),
|
|
face: { normal: new THREE.Vector3(0, 1, 0) }, // Up normal
|
|
};
|
|
|
|
// Stub the internal raycaster call
|
|
sinon
|
|
.stub(inputManager.raycaster, "intersectObjects")
|
|
.returns([mockIntersection]);
|
|
|
|
const result = inputManager.raycast();
|
|
|
|
expect(result).to.exist;
|
|
|
|
// Hit Logic: 0.5 + (-0.5 * 1) = 0.
|
|
// Block hit is at 0,0,0
|
|
expect(result.hitPosition.x).to.equal(0);
|
|
expect(result.hitPosition.y).to.equal(0);
|
|
expect(result.hitPosition.z).to.equal(0);
|
|
|
|
// Target Logic: 0 + 1 = 1
|
|
// Voxel Position (Target) should be 0,1,0 (Above the floor)
|
|
expect(result.voxelPosition.x).to.equal(0);
|
|
expect(result.voxelPosition.y).to.equal(1);
|
|
expect(result.voxelPosition.z).to.equal(0);
|
|
});
|
|
|
|
it("CoA 4: Mouse move should update cursor and emit hover", () => {
|
|
const hoverSpy = sinon.spy();
|
|
inputManager.addEventListener("hover", hoverSpy);
|
|
|
|
// Stub raycast to return a hit
|
|
sinon.stub(inputManager, "raycast").returns({
|
|
voxelPosition: new THREE.Vector3(1, 1, 1),
|
|
});
|
|
|
|
// Simulate Move
|
|
inputManager.onMouseMove({ clientX: 0, clientY: 0 });
|
|
|
|
expect(inputManager.cursor.visible).to.be.true;
|
|
expect(inputManager.cursor.position.x).to.equal(1);
|
|
expect(hoverSpy.called).to.be.true;
|
|
});
|
|
|
|
it("CoA 5: Mouse click should emit click event with coords", () => {
|
|
const clickSpy = sinon.spy();
|
|
inputManager.addEventListener("click", clickSpy);
|
|
|
|
// Use the API method to ensure internal state (cursorPos) is synced with Mesh
|
|
// Directly setting mesh.position might desync the internal tracker used for the event payload
|
|
inputManager.setCursor(2, 2, 2);
|
|
|
|
// Simulate Click
|
|
inputManager.onMouseClick({});
|
|
|
|
expect(clickSpy.calledOnce).to.be.true;
|
|
const payload = clickSpy.firstCall.args[0].detail; // Access detail
|
|
expect(payload.x).to.equal(2);
|
|
expect(payload.y).to.equal(2);
|
|
expect(payload.z).to.equal(2);
|
|
});
|
|
|
|
it("CoA 6: Keyboard events should be tracked and emitted", () => {
|
|
const keySpy = sinon.spy();
|
|
inputManager.addEventListener("keydown", keySpy);
|
|
|
|
// Dispatch window event
|
|
const event = new KeyboardEvent("keydown", { code: "KeyW" });
|
|
window.dispatchEvent(event);
|
|
|
|
expect(keySpy.called).to.be.true;
|
|
expect(keySpy.firstCall.args[0].detail).to.equal("KeyW");
|
|
expect(inputManager.isKeyPressed("KeyW")).to.be.true;
|
|
|
|
// Key Up
|
|
const upEvent = new KeyboardEvent("keyup", { code: "KeyW" });
|
|
window.dispatchEvent(upEvent);
|
|
|
|
expect(inputManager.isKeyPressed("KeyW")).to.be.false;
|
|
});
|
|
|
|
it("CoA 7: Gamepad button press should emit events", () => {
|
|
const buttonSpy = sinon.spy();
|
|
inputManager.addEventListener("gamepadbuttondown", buttonSpy);
|
|
|
|
// Mock Gamepad State
|
|
const mockGamepad = {
|
|
index: 0,
|
|
buttons: [{ pressed: true }], // Button 0 pressed
|
|
axes: [0, 0],
|
|
};
|
|
|
|
// Mock Navigator directly on the window object (browser environment)
|
|
// Note: Some browsers make navigator read-only, but usually this works in test runners.
|
|
// If strict, we might need Object.defineProperty
|
|
Object.defineProperty(navigator, "getGamepads", {
|
|
value: () => [mockGamepad],
|
|
writable: true,
|
|
});
|
|
|
|
// Trigger Poll
|
|
inputManager.update();
|
|
|
|
expect(buttonSpy.calledOnce).to.be.true;
|
|
expect(buttonSpy.firstCall.args[0].detail).to.deep.equal({
|
|
gamepadIndex: 0,
|
|
buttonIndex: 0,
|
|
});
|
|
});
|
|
|
|
it("CoA 8: Gamepad axis movement should emit events with deadzone", () => {
|
|
const axisSpy = sinon.spy();
|
|
inputManager.addEventListener("gamepadaxis", axisSpy);
|
|
|
|
// Mock Gamepad State
|
|
const mockGamepad = {
|
|
index: 0,
|
|
buttons: [],
|
|
axes: [0.5, 0.05], // Axis 0 moved, Axis 1 inside deadzone (0.15)
|
|
};
|
|
|
|
Object.defineProperty(navigator, "getGamepads", {
|
|
value: () => [mockGamepad],
|
|
writable: true,
|
|
});
|
|
|
|
inputManager.update();
|
|
|
|
expect(axisSpy.calledOnce).to.be.true; // Only Axis 0 should trigger
|
|
expect(axisSpy.firstCall.args[0].detail.value).to.equal(0.5);
|
|
});
|
|
|
|
it("CoA 9: setCursor should prevent movement to invalid locations if validator provided", () => {
|
|
// Setup a validator that only allows positive X values
|
|
inputManager.setValidator((x, y, z) => {
|
|
return x >= 0;
|
|
});
|
|
|
|
// Attempt valid move
|
|
inputManager.setCursor(5, 1, 5);
|
|
expect(inputManager.cursor.position.x).to.equal(5);
|
|
|
|
// Attempt invalid move (blocked by validator)
|
|
inputManager.setCursor(-1, 1, 5);
|
|
// Should stay at previous valid position
|
|
expect(inputManager.cursor.position.x).to.equal(5);
|
|
});
|
|
});
|