Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s
MVP checks / mvp (push) Canceled after 0s
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user