// How tall the felt is allowed to be. // // The bar is fixed to the bottom of the screen and grows with the hand, so the table // has to be told what is left. Everything below is the arithmetic that decides it — // the part that can be wrong without anyone noticing until a seat is behind the Fold // button. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { ActionBar } = require("./load"); // window height, bar height, and how far down the page the felt starts. const room = (windowHeight, barHeight, top) => ActionBar.feltRoomFor(windowHeight, barHeight, top); describe("the room left for the felt", () => { test("is what the window has after the bar, the header and the bottom seat", () => { const reserved = ActionBar.SEAT_OVERHANG_PX + ActionBar.FELT_GAP_PX; assert.equal(room(900, 127, 146), 900 - 127 - 146 - reserved); }); test("shrinks when the bar grows mid-hand", () => { // Side pots make the bar taller exactly when the dealer most needs to see who // is still in. const quiet = room(900, 117, 146); const sidePots = room(900, 172, 146); assert.equal(quiet - sidePots, 55); }); test("leaves the bottom seat's overhang out of it", () => { // The seat pucks hang below the felt's edge; the felt has to stop short by that // much or the ring ends up in the bar even though the felt doesn't. assert.ok(ActionBar.SEAT_OVERHANG_PX > 30, "half a puck"); assert.ok(room(900, 127, 146) < 900 - 127 - 146); }); }); describe("a window too short to give what was asked", () => { // A phone on its side: 390px tall, 127px of that is the bar and 119px is // everything above the felt. What's left is under 100. const landscape = () => room(390, 127, 119); test("gets the measurement, not a comfortable number", () => { // The floor used to be 220px, which this window hasn't got. Holding out for it // put the bottom row of seats 26px inside the action bar — the exact fault the // measurement exists to prevent. assert.ok(landscape() < 150, `${landscape()}px`); assert.ok(landscape() + 119 + 127 <= 390, "the felt still ends above the bar"); }); test("still draws a table rather than a line", () => { assert.equal(ActionBar.feltRoomFor(200, 160, 100), ActionBar.FELT_FLOOR_PX); assert.equal(ActionBar.feltRoomFor(0, 0, 0), ActionBar.FELT_FLOOR_PX); }); test("never returns something a browser can't lay out", () => { [[390, 300, 200], [100, 90, 90], [-10, 0, 0]].forEach(([h, bar, top]) => { const answer = ActionBar.feltRoomFor(h, bar, top); assert.ok(Number.isFinite(answer) && answer > 0, `${h}/${bar}/${top} gave ${answer}`); }); }); }); describe("a window with room to spare", () => { test("hands over everything that's left, floor or no floor", () => { const reserved = ActionBar.SEAT_OVERHANG_PX + ActionBar.FELT_GAP_PX; assert.equal(room(1000, 127, 146), 1000 - 127 - 146 - reserved); assert.equal(room(1366, 120, 206), 1366 - 120 - 206 - reserved); }); test("gives a whole number of pixels", () => { assert.equal(room(844.4, 127.6, 223.2) % 1, 0); }); }); describe("when the felt changes size, so does everything on it", () => { // The felt is published as a custom property and the seats follow it on their own, // because they are placed in percentages of it. A bet chip anchored inside a seat // is not: it is placed in pixels off that seat's centre, worked out against the felt // as it was, and nothing re-ran that when the felt moved. // // Measured at 640x360 with five players, on a felt that lost 23px of height: the // chips were positioned for the old one and painted on the new, and seat 3's $5 came // to rest on seat 4's puck. The seat pucks had the same problem — they are searched // for a width the felt no longer has — and two pairs of them were overlapping. // // A side pot appearing does this too: it is 55px off the felt, mid-hand. const fakeDom = (windowHeight = 900) => { const root = { style: { values: {}, setProperty(k, v) { this.values[k] = v; }, getPropertyValue(k) { return this.values[k] || ""; } } }; global.document = { documentElement: root, getElementById: id => (id === "pokerTableWrap" ? { getBoundingClientRect: () => ({ top: 146 }) } : null), }; global.window = { innerHeight: windowHeight, scrollY: 0 }; return root; }; const spyRender = () => { const calls = { density: 0, seats: 0 }; global.Render = { state: {}, updateSeatDensity() { calls.density++; }, renderSeats() { calls.seats++; }, }; return calls; }; test.afterEach(() => { delete global.document; delete global.window; delete global.Render; }); test("a felt that changed height lays the seats out again", () => { fakeDom(); const calls = spyRender(); ActionBar.publishFeltRoom(117); assert.equal(calls.seats, 1, "first publish sets a height it did not have"); ActionBar.publishFeltRoom(172); assert.equal(calls.seats, 2, "and a side pot grew the bar by 55px"); }); test("a felt that did not change is left alone", () => { fakeDom(); const calls = spyRender(); ActionBar.publishFeltRoom(117); ActionBar.publishFeltRoom(117); ActionBar.publishFeltRoom(117); assert.equal(calls.seats, 1, "re-laying the table on every resize tick would be work for nothing"); assert.equal(calls.density, 3, "the cheap re-measure still runs each time"); }); test("it publishes the height whether or not anything is listening", () => { const root = fakeDom(); delete global.Render; assert.doesNotThrow(() => ActionBar.publishFeltRoom(117)); assert.match(root.style.getPropertyValue("--feltRoomPx"), /^\d+px$/); }); });