forked from phenom/healer-man
464 lines
20 KiB
TypeScript
464 lines
20 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
CONTROLLER_SYSTEM_BACK_TOKEN,
|
|
emitControllerToken,
|
|
orderedControllerTokenTransitions,
|
|
resetControllerState,
|
|
setExternalControllerMovement,
|
|
interceptControllerInput,
|
|
} from "../input/controller";
|
|
import {
|
|
MAX_MOUSE_DELTA_PER_EVENT,
|
|
MAX_MOUSE_DELTA_PER_FRAME,
|
|
POINTER_LOCK_MOUSE_GRACE_MS,
|
|
beginMouseLookDrag,
|
|
clearBrowserSelection,
|
|
clearJumpRequest,
|
|
consumeJumpRequest,
|
|
consumeMouseLook,
|
|
controllerCommandForToken,
|
|
createMouseLookBuffer,
|
|
endMouseLookDrag,
|
|
installInput,
|
|
isMouseLookDragActive,
|
|
isDesktopCameraLookPointer,
|
|
keyboardAxesForKeys,
|
|
keyboardCommandForCode,
|
|
queueJumpRequest,
|
|
sanitizeMouseDelta,
|
|
readInputSnapshot,
|
|
type InputActions,
|
|
} from "./inputManager";
|
|
import { normalizeInputPreferences, replaceInputBinding } from "../input/keybinds";
|
|
import { setCapturingInput } from "../input/keybindRuntime";
|
|
|
|
function stubInputEventTargets(selection: Pick<Selection, "removeAllRanges"> | null = null): void {
|
|
const windowTarget = new EventTarget();
|
|
const documentTarget = new EventTarget();
|
|
Object.defineProperty(windowTarget, "getSelection", { configurable: true, value: () => selection });
|
|
Object.defineProperty(documentTarget, "pointerLockElement", { configurable: true, value: null });
|
|
vi.stubGlobal("window", windowTarget);
|
|
vi.stubGlobal("document", documentTarget);
|
|
}
|
|
|
|
function inputActionSpies(blocked = false): InputActions {
|
|
return {
|
|
togglePause: vi.fn(),
|
|
toggleMap: vi.fn(),
|
|
toggleInventory: vi.fn(),
|
|
toggleSpellbook: vi.fn(),
|
|
toggleTalents: vi.fn(),
|
|
lootActiveBoss: vi.fn(() => false),
|
|
castActionBinding: vi.fn(),
|
|
cycleTarget: vi.fn(),
|
|
cyclePartyTarget: vi.fn(),
|
|
setPartyCommand: vi.fn(),
|
|
recallParty: vi.fn(),
|
|
clearTarget: vi.fn(),
|
|
groundTargetActive: vi.fn(() => false),
|
|
confirmGroundTarget: vi.fn(),
|
|
cancelGroundTarget: vi.fn(),
|
|
gameplayActionsBlocked: vi.fn(() => blocked),
|
|
setCameraLookActive: vi.fn(),
|
|
setInputMode: vi.fn(),
|
|
setActionLayer: vi.fn(),
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
setCapturingInput(false);
|
|
endMouseLookDrag();
|
|
clearJumpRequest();
|
|
resetControllerState();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function keyboardEvent(type: "keydown" | "keyup", code: string, modifiers: Partial<KeyboardEvent> = {}) {
|
|
const event = new Event(type, { cancelable: true });
|
|
Object.defineProperties(event, Object.fromEntries(Object.entries({ code, target: null, ...modifiers }).map(([key, value]) => [key, { value }])));
|
|
window.dispatchEvent(event);
|
|
return event;
|
|
}
|
|
|
|
describe("remappable gameplay controls", () => {
|
|
it("keeps moving forward when a legacy WoW skill also owns W", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const preferences = normalizeInputPreferences(null);
|
|
preferences.pc["bar:0:0"] = "KeyW";
|
|
next.getBindings = (device) => preferences[device];
|
|
next.desktopActionBarsActive = () => true;
|
|
next.castDesktopSlot = vi.fn();
|
|
const dispose = installInput(next);
|
|
keyboardEvent("keydown", "KeyW");
|
|
expect(readInputSnapshot().moveForward).toBe(1);
|
|
expect(next.castDesktopSlot).not.toHaveBeenCalled();
|
|
keyboardEvent("keyup", "KeyW");
|
|
expect(readInputSnapshot().moveForward).toBe(0);
|
|
dispose();
|
|
});
|
|
it("honors remapped movement and clears held input when a menu opens", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const preferences = normalizeInputPreferences(null);
|
|
preferences.pc = replaceInputBinding(preferences.pc, "pc", "move-forward", "ArrowUp");
|
|
next.getBindings = (device) => preferences[device];
|
|
const dispose = installInput(next);
|
|
keyboardEvent("keydown", "KeyW"); expect(readInputSnapshot().moveForward).toBe(0);
|
|
keyboardEvent("keydown", "ArrowUp");
|
|
expect(readInputSnapshot()).toMatchObject({ moveForward: 1, lookY: 0 });
|
|
(next.gameplayActionsBlocked as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
expect(readInputSnapshot().moveForward).toBe(0);
|
|
(next.gameplayActionsBlocked as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
expect(readInputSnapshot().moveForward).toBe(0);
|
|
dispose();
|
|
});
|
|
it("casts remapped bar chords once and gives ground confirmation its context", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const preferences = normalizeInputPreferences(null);
|
|
preferences.pc["bar:4:11"] = "Ctrl+Shift+KeyQ";
|
|
preferences.pc["ground-confirm"] = "Ctrl+Shift+KeyQ";
|
|
next.getBindings = (device) => preferences[device]; next.desktopActionBarsActive = () => true; next.castDesktopSlot = vi.fn();
|
|
const dispose = installInput(next);
|
|
keyboardEvent("keydown", "KeyQ", { ctrlKey: true, shiftKey: true });
|
|
keyboardEvent("keydown", "KeyQ", { ctrlKey: true, shiftKey: true, repeat: true });
|
|
expect(next.castDesktopSlot).toHaveBeenCalledExactlyOnceWith(4, 11);
|
|
(next.groundTargetActive as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
keyboardEvent("keydown", "KeyQ", { ctrlKey: true, shiftKey: true });
|
|
expect(next.confirmGroundTarget).toHaveBeenCalledTimes(1);
|
|
expect(next.castDesktopSlot).toHaveBeenCalledTimes(1);
|
|
dispose();
|
|
});
|
|
it("remaps controller axes and buttons with analog movement preserved", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const preferences = normalizeInputPreferences(null);
|
|
preferences.controller = replaceInputBinding(preferences.controller, "controller", "move-forward", "Axis3-");
|
|
preferences.controller = replaceInputBinding(preferences.controller, "controller", "move-right", "Button3");
|
|
next.getBindings = (device) => preferences[device];
|
|
const dispose = installInput(next);
|
|
setExternalControllerMovement({ moveX: 0, moveY: -1, lookX: 0, lookY: 0 });
|
|
expect(readInputSnapshot().moveForward).toBe(0);
|
|
setExternalControllerMovement({ moveX: 0, moveY: 0, lookX: 0, lookY: -.7 });
|
|
expect(readInputSnapshot().moveForward).toBeGreaterThan(.3);
|
|
expect(readInputSnapshot().moveForward).toBeLessThan(1);
|
|
expect(readInputSnapshot().lookY).toBe(0);
|
|
setExternalControllerMovement({ moveX: 0, moveY: 0, lookX: 0, lookY: 0 });
|
|
emitControllerToken({ token: "Button3", repeat: false, pressed: true });
|
|
expect(readInputSnapshot().moveX).toBe(1);
|
|
expect(next.castActionBinding).not.toHaveBeenCalled();
|
|
emitControllerToken({ token: "Button3", repeat: false, pressed: false });
|
|
expect(readInputSnapshot().moveX).toBe(0);
|
|
dispose();
|
|
});
|
|
it("uses customized controller combinations before their base button", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const preferences = normalizeInputPreferences(null);
|
|
preferences.controller = replaceInputBinding(preferences.controller, "controller", "jump", "Button8&Button0");
|
|
next.getBindings = (device) => preferences[device];
|
|
const dispose = installInput(next);
|
|
emitControllerToken({ token: "Button8", repeat: false, pressed: true });
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: true });
|
|
expect(consumeJumpRequest()).not.toBeNull();
|
|
expect(next.castActionBinding).not.toHaveBeenCalled();
|
|
expect(next.lootActiveBoss).not.toHaveBeenCalled();
|
|
dispose();
|
|
});
|
|
it("blocks keyboard actions, controller actions and movement throughout capture", () => {
|
|
stubInputEventTargets();
|
|
const next = inputActionSpies(); const dispose = installInput(next);
|
|
setCapturingInput(true);
|
|
const captured = vi.fn(() => true); const unsubscribe = interceptControllerInput(captured);
|
|
keyboardEvent("keydown", "KeyW"); keyboardEvent("keydown", "KeyI"); keyboardEvent("keydown", "Digit1");
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: true });
|
|
emitControllerToken({ token: "Button9", repeat: false, pressed: true });
|
|
setExternalControllerMovement({ moveX: 1, moveY: -1, lookX: 0, lookY: 0 });
|
|
expect(readInputSnapshot()).toMatchObject({ moveX: 0, moveForward: 0 });
|
|
expect(next.castActionBinding).not.toHaveBeenCalled(); expect(next.toggleInventory).not.toHaveBeenCalled(); expect(next.togglePause).not.toHaveBeenCalled();
|
|
expect(captured).toHaveBeenCalledTimes(2);
|
|
unsubscribe(); dispose();
|
|
});
|
|
});
|
|
|
|
describe("gameplay input bindings", () => {
|
|
it("consumes custom action keys before movement, menus and legacy action bindings", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
nextActions.handleCustomKey = vi.fn(() => true);
|
|
const dispose = installInput(nextActions);
|
|
for (const code of ["KeyW", "KeyI", "Digit1"]) {
|
|
const event = new Event("keydown", { cancelable: true });
|
|
Object.defineProperties(event, { code: { value: code }, target: { value: null } });
|
|
window.dispatchEvent(event);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
}
|
|
expect(nextActions.toggleInventory).not.toHaveBeenCalled();
|
|
expect(nextActions.castActionBinding).not.toHaveBeenCalled();
|
|
expect(nextActions.handleCustomKey).toHaveBeenCalledTimes(3);
|
|
dispose();
|
|
});
|
|
it("does not fire the old numbered bindings when desktop bars are enabled", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
nextActions.desktopActionBarsActive = () => true;
|
|
const dispose = installInput(nextActions);
|
|
const event = new Event("keydown", { cancelable: true });
|
|
Object.defineProperties(event, { code: { value: "Digit1" }, target: { value: null } });
|
|
window.dispatchEvent(event);
|
|
expect(nextActions.castActionBinding).not.toHaveBeenCalled();
|
|
dispose();
|
|
});
|
|
it("reserves I, P, N, and E for equipment and loot interactions", () => {
|
|
expect(keyboardCommandForCode("KeyI")).toBe("inventory");
|
|
expect(keyboardCommandForCode("KeyE")).toBe("loot");
|
|
expect(keyboardCommandForCode("KeyP")).toBe("spellbook");
|
|
expect(keyboardCommandForCode("KeyN")).toBe("talents");
|
|
expect(keyboardCommandForCode("F1")).toBe("party-attack");
|
|
expect(keyboardCommandForCode("F2")).toBe("party-defend");
|
|
expect(keyboardCommandForCode("F3")).toBe("party-stop");
|
|
expect(keyboardCommandForCode("F4")).toBe("party-recall");
|
|
expect(keyboardCommandForCode("Escape")).toBe("pause");
|
|
expect(keyboardCommandForCode("Tab", true)).toBe("target-previous");
|
|
});
|
|
|
|
it("uses the primary controller button to loot before casting", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
const loot = nextActions.lootActiveBoss as ReturnType<typeof vi.fn>;
|
|
loot.mockReturnValue(true);
|
|
const dispose = installInput(nextActions);
|
|
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: true });
|
|
expect(loot).toHaveBeenCalledTimes(1);
|
|
expect(nextActions.castActionBinding).not.toHaveBeenCalled();
|
|
|
|
dispose();
|
|
});
|
|
|
|
it("confirms and cancels an active ground reticle before face-button casts", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
(nextActions.groundTargetActive as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
const dispose = installInput(nextActions);
|
|
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: true });
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: false });
|
|
emitControllerToken({ token: "Button1", repeat: false, pressed: true });
|
|
|
|
expect(nextActions.confirmGroundTarget).toHaveBeenCalledTimes(1);
|
|
expect(nextActions.cancelGroundTarget).toHaveBeenCalledTimes(1);
|
|
expect(nextActions.lootActiveBoss).not.toHaveBeenCalled();
|
|
expect(nextActions.castActionBinding).not.toHaveBeenCalled();
|
|
|
|
dispose();
|
|
});
|
|
|
|
it("maps Thor controls without stealing Select from display routing", () => {
|
|
expect(controllerCommandForToken("Button9")).toBe("pause");
|
|
expect(controllerCommandForToken("Button11")).toBe("target-next");
|
|
expect(controllerCommandForToken("Button10")).toBe("jump");
|
|
expect(controllerCommandForToken("Button12")).toBe("party-previous");
|
|
expect(controllerCommandForToken("Button13")).toBe("party-next");
|
|
expect(controllerCommandForToken("Button4")).toBeNull();
|
|
expect(controllerCommandForToken("Button6")).toBeNull();
|
|
expect(controllerCommandForToken("Button0")).toBeNull();
|
|
expect(controllerCommandForToken("Button15")).toBeNull();
|
|
expect(controllerCommandForToken("Button8")).toBeNull();
|
|
expect(controllerCommandForToken(CONTROLLER_SYSTEM_BACK_TOKEN)).toBe("pause");
|
|
});
|
|
|
|
it("resolves held modifiers end to end and returns to Base on release", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
const dispose = installInput(nextActions);
|
|
|
|
emitControllerToken({ token: "Button6", repeat: false, pressed: true });
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: true });
|
|
expect(nextActions.castActionBinding).toHaveBeenLastCalledWith("tertiary", "face-bottom");
|
|
|
|
emitControllerToken({ token: "Button0", repeat: false, pressed: false });
|
|
for (const transition of orderedControllerTokenTransitions(
|
|
new Set(["Button6"]),
|
|
new Set(["Button0"]),
|
|
)) emitControllerToken(transition);
|
|
expect(nextActions.castActionBinding).toHaveBeenLastCalledWith("primary", "face-bottom");
|
|
expect(nextActions.castActionBinding).toHaveBeenCalledTimes(2);
|
|
|
|
dispose();
|
|
});
|
|
|
|
it("uses semantic Android Back for gameplay pause but leaves blocked menus in control", () => {
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
const blocked = nextActions.gameplayActionsBlocked as ReturnType<typeof vi.fn>;
|
|
const dispose = installInput(nextActions);
|
|
|
|
emitControllerToken({ token: CONTROLLER_SYSTEM_BACK_TOKEN, repeat: false, pressed: true });
|
|
emitControllerToken({ token: CONTROLLER_SYSTEM_BACK_TOKEN, repeat: false, pressed: false });
|
|
expect(nextActions.togglePause).toHaveBeenCalledTimes(1);
|
|
|
|
blocked.mockReturnValue(true);
|
|
emitControllerToken({ token: CONTROLLER_SYSTEM_BACK_TOKEN, repeat: false, pressed: true });
|
|
emitControllerToken({ token: CONTROLLER_SYSTEM_BACK_TOKEN, repeat: false, pressed: false });
|
|
emitControllerToken({ token: "Button1", repeat: false, pressed: true });
|
|
expect(nextActions.togglePause).toHaveBeenCalledTimes(1);
|
|
expect(nextActions.castActionBinding).not.toHaveBeenCalled();
|
|
|
|
dispose();
|
|
});
|
|
|
|
it("leaves digit keys to the layered action-binding resolver", () => {
|
|
expect(keyboardCommandForCode("Digit1")).toBeNull();
|
|
expect(keyboardCommandForCode("Digit8")).toBeNull();
|
|
});
|
|
|
|
it("queues Space and L3 jump presses once and blocks them behind menus", () => {
|
|
expect(keyboardCommandForCode("Space")).toBe("jump");
|
|
queueJumpRequest(25);
|
|
expect(consumeJumpRequest()).toBe(25);
|
|
expect(consumeJumpRequest()).toBeNull();
|
|
|
|
stubInputEventTargets();
|
|
const nextActions = inputActionSpies();
|
|
const blocked = nextActions.gameplayActionsBlocked as ReturnType<typeof vi.fn>;
|
|
const dispose = installInput(nextActions);
|
|
|
|
emitControllerToken({ token: "Button10", repeat: false, pressed: true });
|
|
expect(consumeJumpRequest()).not.toBeNull();
|
|
|
|
emitControllerToken({ token: "Button10", repeat: true, pressed: true });
|
|
expect(consumeJumpRequest()).toBeNull();
|
|
|
|
blocked.mockReturnValue(true);
|
|
emitControllerToken({ token: "Button10", repeat: false, pressed: false });
|
|
emitControllerToken({ token: "Button10", repeat: false, pressed: true });
|
|
expect(consumeJumpRequest()).toBeNull();
|
|
|
|
dispose();
|
|
});
|
|
|
|
it("uses WASD for movement and arrow keys for camera look", () => {
|
|
expect(keyboardAxesForKeys(new Set(["KeyW", "KeyD"]))).toEqual({
|
|
moveX: 1,
|
|
moveForward: 1,
|
|
lookX: 0,
|
|
lookY: 0,
|
|
});
|
|
expect(keyboardAxesForKeys(new Set(["ArrowRight", "ArrowUp"]))).toEqual({
|
|
moveX: 0,
|
|
moveForward: 0,
|
|
lookX: 1,
|
|
lookY: -1,
|
|
});
|
|
expect(keyboardAxesForKeys(new Set(["ArrowLeft", "ArrowDown"]))).toEqual({
|
|
moveX: 0,
|
|
moveForward: 0,
|
|
lookX: -1,
|
|
lookY: 1,
|
|
});
|
|
});
|
|
|
|
it("starts desktop camera look only from the mouse's right button", () => {
|
|
expect(isDesktopCameraLookPointer("mouse", 2)).toBe(true);
|
|
expect(isDesktopCameraLookPointer("mouse", 0)).toBe(false);
|
|
expect(isDesktopCameraLookPointer("mouse", 1)).toBe(false);
|
|
expect(isDesktopCameraLookPointer("touch", 2)).toBe(false);
|
|
expect(isDesktopCameraLookPointer("pen", 2)).toBe(false);
|
|
});
|
|
|
|
it("turns the camera from a right-mouse drag without pointer lock", () => {
|
|
const selection = { removeAllRanges: vi.fn() };
|
|
stubInputEventTargets(selection);
|
|
const nextActions = inputActionSpies();
|
|
const dispose = installInput(nextActions);
|
|
beginMouseLookDrag(100, 80);
|
|
|
|
expect(selection.removeAllRanges).toHaveBeenCalledOnce();
|
|
|
|
const move = new Event("pointermove") as PointerEvent;
|
|
Object.defineProperties(move, {
|
|
clientX: { value: 114 },
|
|
clientY: { value: 73 },
|
|
pointerType: { value: "mouse" },
|
|
movementX: { value: 9_999 },
|
|
movementY: { value: 9_999 },
|
|
});
|
|
document.dispatchEvent(move);
|
|
|
|
expect(document.pointerLockElement).toBeNull();
|
|
expect(isMouseLookDragActive()).toBe(true);
|
|
expect(consumeMouseLook()).toEqual([14, -7]);
|
|
expect(nextActions.setCameraLookActive).toHaveBeenLastCalledWith(true);
|
|
|
|
endMouseLookDrag();
|
|
expect(isMouseLookDragActive()).toBe(false);
|
|
expect(nextActions.setCameraLookActive).toHaveBeenLastCalledWith(false);
|
|
dispose();
|
|
});
|
|
|
|
it("prevents page selection only while right-mouse camera drag is active", () => {
|
|
stubInputEventTargets();
|
|
const dispose = installInput(inputActionSpies());
|
|
|
|
const idleSelection = new Event("selectstart", { cancelable: true });
|
|
document.dispatchEvent(idleSelection);
|
|
expect(idleSelection.defaultPrevented).toBe(false);
|
|
|
|
beginMouseLookDrag(20, 30);
|
|
const cameraSelection = new Event("selectstart", { cancelable: true });
|
|
document.dispatchEvent(cameraSelection);
|
|
expect(cameraSelection.defaultPrevented).toBe(true);
|
|
|
|
endMouseLookDrag();
|
|
const releasedSelection = new Event("selectstart", { cancelable: true });
|
|
document.dispatchEvent(releasedSelection);
|
|
expect(releasedSelection.defaultPrevented).toBe(false);
|
|
dispose();
|
|
});
|
|
|
|
it("can clear an existing browser selection without a camera drag", () => {
|
|
const selection = { removeAllRanges: vi.fn() };
|
|
stubInputEventTargets(selection);
|
|
clearBrowserSelection();
|
|
expect(selection.removeAllRanges).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("contains malformed and stale pointer-lock movement", () => {
|
|
expect(sanitizeMouseDelta(Number.NaN)).toBe(0);
|
|
expect(sanitizeMouseDelta(Number.POSITIVE_INFINITY)).toBe(0);
|
|
expect(sanitizeMouseDelta(undefined)).toBe(0);
|
|
expect(sanitizeMouseDelta(42)).toBe(42);
|
|
expect(sanitizeMouseDelta(9_999)).toBe(MAX_MOUSE_DELTA_PER_EVENT);
|
|
expect(sanitizeMouseDelta(-9_999)).toBe(-MAX_MOUSE_DELTA_PER_EVENT);
|
|
});
|
|
|
|
it("drops the complete pointer-lock transition burst, then accepts real movement", () => {
|
|
let now = 1_000;
|
|
const buffer = createMouseLookBuffer(() => now);
|
|
buffer.reset(POINTER_LOCK_MOUSE_GRACE_MS);
|
|
expect(buffer.push(9_999, -9_999)).toBe(false);
|
|
now += POINTER_LOCK_MOUSE_GRACE_MS - 1;
|
|
expect(buffer.push(-9_999, 9_999)).toBe(false);
|
|
expect(buffer.consume()).toEqual([0, 0]);
|
|
|
|
now += 1;
|
|
expect(buffer.push(Number.NaN, undefined)).toBe(false);
|
|
expect(buffer.push(9_999, 9_999)).toBe(true);
|
|
expect(buffer.push(9_999, 9_999)).toBe(true);
|
|
expect(buffer.push(9_999, 9_999)).toBe(true);
|
|
expect(buffer.consume()).toEqual([MAX_MOUSE_DELTA_PER_FRAME, MAX_MOUSE_DELTA_PER_FRAME]);
|
|
expect(buffer.consume()).toEqual([0, 0]);
|
|
});
|
|
|
|
it("does not suppress movement when a reset has no pointer-lock grace period", () => {
|
|
let now = 200;
|
|
const buffer = createMouseLookBuffer(() => now);
|
|
buffer.reset();
|
|
expect(buffer.push(7, -4)).toBe(true);
|
|
expect(buffer.consume()).toEqual([7, -4]);
|
|
|
|
now += 1;
|
|
buffer.reset(POINTER_LOCK_MOUSE_GRACE_MS);
|
|
now += POINTER_LOCK_MOUSE_GRACE_MS;
|
|
expect(buffer.push(3, 5)).toBe(true);
|
|
expect(buffer.consume()).toEqual([3, 5]);
|
|
});
|
|
});
|