// The size the table is drawn at before anything has been measured.
//
// The felt is not sized by CSS alone. It is sized from `--feltRoomPx`, which actionBar.js
// publishes from the bar's real height — because the bar is fixed to the bottom of the
// screen and grows with the hand, so nothing else can know how much of the window is
// left. That measurement is right. What it was measuring was not.
//
// `ActionBar.mount()` called `watchHeight()` before `render()`, and the bar ships
// `
` in the markup. So the first publish measured a bar that
// was not there yet and published `0px` — not "I don't know", but "the bar takes none of
// the window", which is the one answer that makes the felt draw itself too big.
//
// Measured on a cold load, nine-handed:
//
// 360x640 --feltRoomPx 525px -> felt 336x504, then 336x374 at t=300ms
// 1280x900 --feltRoomPx 755px -> felt 857x617, then 839x604
//
// A hundred and thirty pixels of table, with every seat on it, moving after it was on
// screen and readable. The ResizeObserver does come round on its own once the markup's
// `hidden` comes off — but a frame later at best, and measured with nine seats to lay
// out it was 875ms behind.
//
// Two rules hold it shut, and they are separate on purpose: one stops the wrong number
// being published at all, the other makes sure a right one is published straight away.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
const { ActionBar } = require("./load");
const SRC = path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem");
const BAR = fs.readFileSync(path.join(SRC, "actionBar.js"), "utf8");
const CSS = fs.readFileSync(
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8");
// A document with one action bar in it, and a note of everything published.
const pageWith = ({ hidden, height, layout }) => {
const published = [];
const host = {
hidden,
getBoundingClientRect: () => ({ height, top: 0 }),
};
global.document = {
getElementById: id => (id === "actionBar" ? host : null),
documentElement: {
style: {
setProperty: (name, value) => published.push([name, value]),
getPropertyValue: () => "",
},
},
};
global.getComputedStyle = () => ({ getPropertyValue: () => layout });
global.window = { innerHeight: 844, scrollY: 0, addEventListener: () => {} };
return { host, published, barHeight: () => published.filter(p => p[0] === "--actionBarHeight") };
};
// watchHeight wires listeners and publishes once. The felt half of it is stubbed: it
// reads a wrap that is not in this document, and what is under test here is the number
// the bar decides to hand over.
const publishOnce = page => {
const realFelt = ActionBar.publishFeltRoom;
const roomAsked = [];
ActionBar.publishFeltRoom = value => roomAsked.push(value);
ActionBar.heightWatcher = null;
try {
ActionBar.watchHeight();
} finally {
ActionBar.publishFeltRoom = realFelt;
}
return roomAsked;
};
test.afterEach(() => {
delete global.document;
delete global.getComputedStyle;
delete global.window;
ActionBar.heightWatcher = null;
delete ActionBar.publishHeight;
});
// ---------------------------------------------------------------------------
describe("a bar nobody has drawn yet", () => {
test("says nothing, rather than saying nothing is there", () => {
// The distinction the whole fix rests on. Zero is an answer; "not yet" is not,
// and publishing the first as though it were the second is what made the felt
// believe it had the whole window.
const page = pageWith({ hidden: true, height: 0, layout: "bar" });
publishOnce(page);
assert.deepEqual(page.barHeight(), [],
"a hidden bar published a height, and the felt will be drawn against it");
});
test("and the felt is not asked to resize on the strength of it either", () => {
const page = pageWith({ hidden: true, height: 0, layout: "bar" });
const roomAsked = publishOnce(page);
assert.deepEqual(roomAsked, [], "the felt was handed a number measured off nothing");
});
test("a bar that has been drawn publishes what it measures", () => {
const page = pageWith({ hidden: false, height: 151, layout: "bar" });
publishOnce(page);
assert.deepEqual(page.barHeight(), [["--actionBarHeight", "151px"]]);
});
});
describe("a rail is not an undrawn bar", () => {
// A phone held sideways puts the bar down the right-hand side, where it genuinely
// takes none of the bottom of the window. That zero is a measurement and has to keep
// being published — otherwise the felt goes on reserving room for a bar that has
// moved out of its way.
test("it publishes zero, and it does so even while hidden", () => {
const page = pageWith({ hidden: true, height: 0, layout: "rail" });
publishOnce(page);
assert.deepEqual(page.barHeight(), [["--actionBarHeight", "0px"]],
"a rail stopped saying it takes no height");
});
test("and a drawn rail publishes zero rather than its own height", () => {
const page = pageWith({ hidden: false, height: 390, layout: "rail" });
publishOnce(page);
assert.deepEqual(page.barHeight(), [["--actionBarHeight", "0px"]],
"the rail's height went into the number that means 'how much of the bottom is gone'");
});
});
describe("and something is published as soon as there is a bar to measure", () => {
test("mount publishes after it has rendered, not only before", () => {
// watchHeight still goes first, because render() may reach for publishHeight.
// The publish that matters is the one after.
const mount = BAR.slice(BAR.indexOf("mount(state, hooks) {"), BAR.indexOf("watchHeight() {"));
const watch = mount.indexOf("this.watchHeight();");
const render = mount.indexOf("this.render();");
const publish = mount.indexOf("this.publishHeight();");
assert.ok(watch >= 0 && render >= 0 && publish >= 0,
"mount no longer does all three of these");
assert.ok(watch < render, "watchHeight has to be in place before render can call it");
assert.ok(render < publish,
"mount publishes before the bar has been drawn, which is the whole bug");
});
});
describe("what the felt falls back on before any of that", () => {
// The scripts are at the end of the body and the table is laid out inside main.js's
// DOMContentLoaded, so there is a real frame where --feltRoomPx does not exist yet.
// `100vh` was the worst guess available for it: it says the whole window is free.
test("a phone does not assume the whole window", () => {
// The phone's rule is the one that caps the felt's height; the wide layout caps
// its width instead, from the same number.
const found = CSS.match(/max-height:\s*var\(--feltRoomPx,([^)]*\)?[^;]*);/);
assert.ok(found, "nothing caps the felt's height by the measured room any more");
const fallback = found[1].trim();
assert.notEqual(fallback, "100vh",
"the fallback claims the whole window, so the first frame draws a table that has to shrink");
assert.match(fallback, /var\(--actionBarHeight, 9rem\)/,
"9rem is what the rest of this file assumes a bar costs before it can be asked");
});
test("and 9rem is a number this file already stands behind", () => {
// Two other rules had reached the same conclusion before this one did. If they
// ever disagree, the felt's first frame and the toast's first frame disagree.
const nines = CSS.match(/var\(--actionBarHeight, 9rem\)/g) || [];
assert.ok(nines.length >= 3,
`only ${nines.length} rules assume 9rem; the felt's guess should be the same guess as the rest`);
});
});