feat(ui): implement responsive player browser experience
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const project = path.resolve(__dirname, "../..");
|
||||
const player = require(path.join(project, "web/player.js"));
|
||||
|
||||
test("keyboard and swipe controls produce equivalent directions", () => {
|
||||
const cases = [
|
||||
["ArrowUp", [50, 80, 50, 20], "north"],
|
||||
["KeyD", [20, 50, 80, 50], "east"],
|
||||
["ArrowDown", [50, 20, 50, 80], "south"],
|
||||
["KeyA", [80, 50, 20, 50], "west"]
|
||||
];
|
||||
for (const [key, swipe, expected] of cases) {
|
||||
assert.equal(player.directionFromKey(key), expected);
|
||||
assert.equal(player.directionFromSwipe(...swipe), expected);
|
||||
}
|
||||
assert.equal(player.directionFromSwipe(0, 0, 12, 4), null);
|
||||
});
|
||||
|
||||
test("command envelopes contain intentions rather than client-owned state", () => {
|
||||
const move = player.createEnvelope("move", 7, "move-7", {direction: "west"});
|
||||
assert.deepEqual(move, {v: 1, type: "move", seq: 7, requestId: "move-7", payload: {direction: "west"}});
|
||||
assert.equal("position" in move.payload, false);
|
||||
assert.deepEqual(player.createEnvelope("drop_key", 8, "drop_key-8", {}), {v: 1, type: "drop_key", seq: 8, requestId: "drop_key-8", payload: {}});
|
||||
});
|
||||
|
||||
test("chunk assembly replaces projection cells without explored-map memory", () => {
|
||||
const assembler = new player.SnapshotAssembler();
|
||||
const second = {v: 1, type: "view", seq: 20, payload: {role: "player1", width: 8, height: 8, chunk: 1, chunks: 2, cells: [[10, 15]], objects: []}};
|
||||
const first = {v: 1, type: "view", seq: 20, payload: {role: "player1", phase: "active", result: "none", width: 8, height: 8, self: 9, inventory: null, visionMoves: 0, hint: "none", chunk: 0, chunks: 2, cells: [[9, 15]], objects: [["key", 0, 0, 10]]}};
|
||||
assert.equal(assembler.accept(second), null);
|
||||
const complete = assembler.accept(first);
|
||||
assert.deepEqual(player.projectedCellIndices(complete), [9, 10]);
|
||||
|
||||
const replacement = assembler.accept({v: 1, type: "view", seq: 21, payload: {role: "player1", phase: "active", result: "none", width: 8, height: 8, self: 18, inventory: 0, visionMoves: 9, hint: "ne", chunk: 0, chunks: 1, cells: [[18, 15]], objects: []}});
|
||||
assert.deepEqual(player.projectedCellIndices(replacement), [18]);
|
||||
assert.equal(replacement.cells.some(cell => cell[0] === 9 || cell[0] === 10), false);
|
||||
assert.equal(assembler.accept({v: 1, type: "view", seq: 22, payload: {chunk: 0, chunks: player.MAX_VIEW_CHUNKS + 1, cells: [], objects: []}}), null);
|
||||
assert.equal(player.snapshotIsValid({width: 128, height: 64, cells: [[8191, 15]], objects: []}), true);
|
||||
assert.equal(player.snapshotIsValid({width: 129, height: 64, cells: [], objects: []}), false);
|
||||
});
|
||||
|
||||
test("refresh contract restores only the token and waits for server projection", () => {
|
||||
const restore = player.createEnvelope("restore", 1, "restore-1", {token: "0123456789abcdef0123456789abcdef"});
|
||||
assert.deepEqual(Object.keys(restore.payload), ["token"]);
|
||||
assert.equal("position" in restore.payload, false);
|
||||
assert.equal("inventory" in restore.payload, false);
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
assert.equal((script.match(/localStorage\.setItem/g) || []).length, 1);
|
||||
assert.match(script, /localStorage\.setItem\(TOKEN_KEY, message\.payload\.token\)/);
|
||||
});
|
||||
|
||||
test("player assets are local, Russian, accessible, and responsive", () => {
|
||||
const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8");
|
||||
const css = fs.readFileSync(path.join(project, "web/player.css"), "utf8");
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
const icons = fs.readFileSync(path.join(project, "web/icons.svg"), "utf8");
|
||||
const all = `${html}\n${css}\n${script}\n${icons}`;
|
||||
assert.doesNotMatch(all, /(?:src|href)=["']https?:|url\(["']?https?:/i);
|
||||
assert.doesNotMatch(all, /@import|googleapis|analytics/i);
|
||||
assert.match(html, /lang="ru"/);
|
||||
assert.match(html, /aria-label="Сбросить ключ"/);
|
||||
assert.match(html, /Кусты уже|Сбор заблудившихся/);
|
||||
assert.match(css, /@media \(max-width: 720px\)/);
|
||||
assert.match(css, /@media \(min-width: 721px\) and \(max-width: 980px\)/);
|
||||
assert.match(css, /height: calc\(100dvh - 78px\)/);
|
||||
assert.match(css, /\.dpad \{ display: none; \}/);
|
||||
assert.match(css, /\(hover: none\) and \(pointer: coarse\) and \(max-height: 720px\)/);
|
||||
assert.match(icons, /id="key"/);
|
||||
assert.match(icons, /id="maze"/);
|
||||
});
|
||||
Reference in New Issue
Block a user