import test from "node:test"; import assert from "node:assert/strict"; import { PointerLockController } from "../pointer-lock.js"; function deferred() { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } function fixture(t, request) { const document = new EventTarget(), calls = [], changes = [], errors = []; document.pointerLockElement = null; document.exitPointerLock = () => { calls.push("exit"); document.pointerLockElement = null; document.dispatchEvent(new Event("pointerlockchange")); }; const canvas = { ownerDocument: document, tabIndex: -1, focus(options) { calls.push("focus"); assert.equal(options.preventScroll, true); }, requestPointerLock() { calls.push("request"); return request?.(); }, }; const controller = new PointerLockController(canvas, { onChange: (locked) => changes.push(locked), onError: (error) => errors.push(error), }); const lock = () => { document.pointerLockElement = canvas; document.dispatchEvent(new Event("pointerlockchange")); }; t.after(() => controller.dispose()); return { controller, canvas, document, calls, changes, errors, lock }; } test("focus precedes the synchronous user-gesture request and duplicate pending calls are ignored", (t) => { const pending = deferred(), { controller, canvas, calls } = fixture(t, () => pending.promise); assert.equal(controller.request(), true); assert.deepEqual(calls, ["focus", "request"]); assert.equal(canvas.tabIndex, 0); assert.equal(controller.status, "requesting"); assert.equal(controller.request(), false); assert.deepEqual(calls, ["focus", "request"]); }); test("a rejected request remains retryable and subsequent success clears the error", async (t) => { const first = deferred(), second = deferred(); let count = 0; const { controller, errors, changes, lock } = fixture(t, () => ++count === 1 ? first.promise : second.promise, ); controller.request(); const error = new Error("Document not focused"); first.reject(error); await Promise.resolve(); assert.equal(controller.status, "error"); assert.equal(controller.lastError, error); assert.deepEqual(errors, [error]); assert.equal(controller.request(), true); lock(); second.resolve(); await Promise.resolve(); assert.equal(controller.status, "locked"); assert.equal(controller.locked, true); assert.equal(controller.lastError, null); assert.deepEqual(changes, [true]); }); test("legacy success, ordinary unlock, and recapture follow actual document state", (t) => { const { controller, document, changes, lock, calls } = fixture(t); controller.request(); lock(); assert.equal(controller.request(), false); document.exitPointerLock(); assert.equal(controller.status, "idle"); assert.equal(controller.locked, false); assert.equal(controller.request(), true); lock(); assert.deepEqual(changes, [true, false, true]); assert.equal(calls.filter((call) => call === "request").length, 2); }); test("missing APIs and legacy error events report failure without permanently disabling retries", (t) => { const { controller, canvas, document, errors, lock } = fixture(t); delete canvas.requestPointerLock; assert.equal(controller.request(), false); assert.equal(controller.status, "unsupported"); assert.equal(errors[0].name, "NotSupportedError"); canvas.requestPointerLock = () => undefined; assert.equal(controller.request(), true); document.dispatchEvent(new Event("pointerlockerror")); assert.equal(controller.status, "error"); assert.equal(errors.length, 2); assert.equal(controller.request(), true); lock(); assert.equal(controller.status, "locked"); }); test("a stale promise rejection cannot overwrite a later successful capture", async (t) => { const first = deferred(); const { controller, document, errors, lock } = fixture( t, () => first.promise, ); controller.request(); document.dispatchEvent(new Event("pointerlockerror")); controller.request(); lock(); first.reject(new Error("Late failure from an old request")); await Promise.resolve(); assert.equal(controller.status, "locked"); assert.equal(controller.lastError, null); assert.equal(errors.length, 1); }); test("release cancels pending capture and immediately exits a late acquisition", async (t) => { const pending = deferred(), { controller, changes, errors, lock, calls } = fixture( t, () => pending.promise, ); controller.request(); controller.release(); assert.equal(controller.status, "idle"); lock(); pending.resolve(); await Promise.resolve(); assert.equal(controller.locked, false); assert.equal(controller.status, "idle"); assert.deepEqual(changes, []); assert.deepEqual(errors, []); assert.equal(calls.filter((call) => call === "exit").length, 2); }); test("synchronous failures are retryable and disposal removes event handling", (t) => { const { controller, canvas, document, errors, changes, lock } = fixture( t, () => { throw new Error("Synchronous rejection"); }, ); assert.equal(controller.request(), false); assert.equal(controller.status, "error"); assert.equal(errors.length, 1); canvas.requestPointerLock = () => undefined; controller.request(); lock(); controller.dispose(); assert.deepEqual(changes, [true, false]); assert.equal(controller.request(), false); lock(); document.dispatchEvent(new Event("pointerlockerror")); assert.deepEqual(changes, [true, false]); assert.equal(errors.length, 1); });