Files
skribbl-gartic-color/test/unit.test.js
T

151 lines
5.2 KiB
JavaScript

// Unit tests for server-side game logic. Uses node:test (no extra deps).
// Run with: node test/unit.test.js (NOT `node --test`) — see comment at bottom.
const test = require("node:test");
const assert = require("node:assert");
const {
normalize,
maskWord,
pickRevealIndex,
isCloseGuess,
} = require("../server/word-utils.js");
const roomStore = require("../server/room-store.js");
const { genCode, getRoom, setRoom, deleteRoom, allRooms, makeId } = roomStore;
const { makeRoom, makePlayer } = require("../server/game-state.js");
// Unref the room-store cleanup interval so node exits cleanly after tests.
function unrefAllHandles() {
const handles = process._getActiveHandles ? process._getActiveHandles() : [];
for (const h of handles) {
if (h && typeof h.unref === "function") {
try { h.unref(); } catch (_) {}
}
}
}
unrefAllHandles();
// --- word-utils tests ---
test("normalize: lowercases and trims", () => {
assert.strictEqual(normalize(" HELLO "), "hello");
assert.strictEqual(normalize("Foo Bar"), "foo bar");
assert.strictEqual(normalize("MULTI SPACE"), "multi space");
assert.strictEqual(normalize(""), "");
assert.strictEqual(normalize(undefined), "");
assert.strictEqual(normalize(null), "");
});
test("maskWord: blanks letters but preserves spaces", () => {
const out = maskWord("ice cream");
// letters become "_", space stays
assert.strictEqual(out, "___ _____");
});
test("maskWord: with all indices revealed produces lowercased word", () => {
const word = "Apple";
const revealed = new Set([0, 1, 2, 3, 4]);
const out = maskWord(word, revealed);
assert.strictEqual(out, "apple");
});
test("maskWord: partial reveal mixes letters and underscores", () => {
const out = maskWord("apple", new Set([0, 2]));
// a _ p _ _
assert.strictEqual(out, "a_p__");
});
test("pickRevealIndex: picks unrevealed non-space index", () => {
const word = "ab cd";
const revealed = new Set([0]);
const idx = pickRevealIndex(word, revealed);
// valid candidate indices: 1, 3, 4
assert.ok([1, 3, 4].includes(idx), `unexpected index ${idx}`);
assert.notStrictEqual(idx, 2); // never the space
});
test("pickRevealIndex: returns null when all letters revealed", () => {
const word = "ab";
const revealed = new Set([0, 1]);
assert.strictEqual(pickRevealIndex(word, revealed), null);
});
test("revealHint flow: applying picked index reveals exactly N more letters", () => {
// simulate revealing: count underscores decreases by exactly 1 each step
let revealed = new Set();
const word = "rocket";
let prevMask = maskWord(word, revealed);
let prevUnderscores = (prevMask.match(/_/g) || []).length;
for (let i = 0; i < 3; i++) {
const idx = pickRevealIndex(word, revealed);
assert.notStrictEqual(idx, null);
revealed.add(idx);
const m = maskWord(word, revealed);
const newUnderscores = (m.match(/_/g) || []).length;
assert.strictEqual(newUnderscores, prevUnderscores - 1, "exactly 1 letter revealed");
prevUnderscores = newUnderscores;
prevMask = m;
}
});
test("revealHint idempotence: re-applying same revealed set yields same mask", () => {
const word = "tree";
const revealed = new Set([0, 2]);
const m1 = maskWord(word, revealed);
const m2 = maskWord(word, revealed);
assert.strictEqual(m1, m2);
});
test("isCloseGuess: edit distance 1-2 returns true", () => {
assert.strictEqual(isCloseGuess("aple", "apple"), true); // 1 edit
assert.strictEqual(isCloseGuess("aplee", "apple"), true); // 1 edit
assert.strictEqual(isCloseGuess("apple", "apple"), false); // exact match -> not "close"
assert.strictEqual(isCloseGuess("xyz", "apple"), false); // too far
});
// --- room-store tests ---
test("genCode: returns a unique 6-char alphanumeric code", () => {
const seen = new Set();
for (let i = 0; i < 10; i++) {
const c = genCode();
assert.strictEqual(typeof c, "string");
assert.strictEqual(c.length, 6);
assert.ok(/^[A-Z0-9]+$/.test(c), `code ${c} should be uppercase alnum`);
seen.add(c);
}
assert.strictEqual(seen.size, 10, "codes should be unique");
});
test("setRoom + getRoom: returns same instance, case-insensitive lookup", () => {
const code = genCode();
const player = makePlayer({ nickname: "U", avatar: "x" });
const room = makeRoom({ code, hostId: player.id, mode: "skribbl", settings: {} });
room.players.push(player);
setRoom(code, room);
const got = getRoom(code);
assert.strictEqual(got, room, "same instance");
// lower-case lookup also works
const gotLower = getRoom(code.toLowerCase());
assert.strictEqual(gotLower, room);
deleteRoom(code);
assert.strictEqual(getRoom(code), undefined);
});
test("makeId: returns a non-empty string id", () => {
const id1 = makeId();
const id2 = makeId();
assert.strictEqual(typeof id1, "string");
assert.ok(id1.length > 0);
assert.notStrictEqual(id1, id2);
});
test("allRooms: exposes the underlying Map", () => {
const map = allRooms();
assert.ok(map instanceof Map);
});
// Force exit after a short tick — room-store uses an unrefable setInterval
// that we already unrefed above, but be defensive.
process.on("beforeExit", () => process.exit(0));
setTimeout(() => process.exit(0), 1500).unref();