Tell the sheets the phone is on its side
The action bar learned two rounds ago that a landscape phone is short of
height and swimming in width: it stops being a bar across the bottom and
becomes a rail down the right. The seat menu was never told.
Measured on the fullest menu the tool can build, twelve-handed with
somebody on the waiting list:
640x360 545px of content in a 306px sheet. Cash out…, Move to…,
Edit seat…, Small blind, Big blind and Cancel below the
fold — Cancel by 224px.
667x375 545 in 319, Cancel 211px under.
844x390 529 in 332, Cancel 183px under.
And on all three the sheet was 420px wide inside a window 640 to 844
wide. Two hundred and forty-seven pixels of width held empty on the one
screen shape whose whole problem is height, while the dealer scrolled a
menu to find the way out of it.
The two tiers this menu has been sorted into since round 7 — what you
press with cards in the air, and housekeeping between hands — stop being
the top and bottom of a column and become the right and left of a row.
Fold and All-in land under the same thumb the rail puts Fold, Call and
Raise under, with Cancel beneath them. The housekeeping grid takes as
many columns as the width holds instead of the two a 360px phone has
room for.
After: 297px in a 298px sheet at 640x360 and at 667x375, 281 in 282 at
844x390. Nothing below the fold on any of them, every button still at
least 44x44 and still hit-testing as itself. The move picker's twelve
chairs and its Back went from four rows and a scroll to three rows and
none. Upright nothing moved at all — 545px before, 545px after, the same
two columns.
auto-fill rather than auto-fit: auto-fit throws away the tracks nothing
sits in, so a group of two stretched to half the sheet each while the
group above it was a third, and the two grids lined up with nothing.
1253 tests pass; the layout sweep reports clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// The seat menu on a phone held sideways.
|
||||
//
|
||||
// The tool learned two rounds ago that a landscape phone is short of height and
|
||||
// swimming in width: the action bar stops being a bar across the bottom and becomes a
|
||||
// rail down the right. The sheets were never told. Measured on the fullest menu the
|
||||
// tool can build — twelve seated, somebody on the waiting list, blinds still the only
|
||||
// money in:
|
||||
//
|
||||
// 640x360 545px of content in a 306px sheet. Cash out…, Move to…, Edit seat…,
|
||||
// Small blind, Big blind and Cancel all below the fold, Cancel by 224px.
|
||||
// 667x375 545 in 319, Cancel 211px under.
|
||||
// 844x390 529 in 332, Cancel 183px under.
|
||||
//
|
||||
// And on all three the sheet was 420px wide inside a window 640 to 844 wide. Two
|
||||
// hundred and forty-seven pixels of width held empty on the one screen shape whose
|
||||
// whole problem is height, while the dealer scrolled a menu to find the way out of it.
|
||||
//
|
||||
// The two tiers this menu has been sorted into since round 7 — what you press with
|
||||
// cards in the air, and housekeeping between hands — stop being the top and bottom of
|
||||
// a column and become the right and left of a row. Fold and All-in land under the same
|
||||
// thumb the rail puts Fold, Call and Raise under, with Cancel beneath them. Measured
|
||||
// after: 297px in a 298px sheet at 640x360 and at 667x375, 281 in 282 at 844x390.
|
||||
// Nothing below the fold on any of them, and the move picker's twelve chairs and its
|
||||
// Back went from four rows and a scroll to three rows and none.
|
||||
//
|
||||
// Upright nothing moved at all: 545px before, 545px after, the same two columns.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Betting, HoldemState, SeatMenu, makeTable } = require("./load");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net");
|
||||
const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
||||
|
||||
const ruleFor = selector => {
|
||||
const at = CSS.indexOf(`\n${selector} {`);
|
||||
assert.ok(at >= 0, `there is no \`${selector}\` rule`);
|
||||
return CSS.slice(at, CSS.indexOf("}", at));
|
||||
};
|
||||
|
||||
// Everything inside one @media block, braces balanced, so a test asks what that block
|
||||
// says rather than whether a string appears anywhere in six thousand lines.
|
||||
const SHORT = "@media (max-height: 560px) and (min-width: 600px) {";
|
||||
function shortViewportBlock() {
|
||||
const at = CSS.indexOf(SHORT);
|
||||
assert.ok(at >= 0, "there is no short-viewport block for the sheets");
|
||||
let depth = 0;
|
||||
for (let i = at + SHORT.length - 1; i < CSS.length; i++) {
|
||||
if (CSS[i] === "{") depth++;
|
||||
else if (CSS[i] === "}" && --depth === 0) return CSS.slice(at, i + 1);
|
||||
}
|
||||
assert.fail("the short-viewport block is never closed");
|
||||
}
|
||||
|
||||
// A document small enough to answer what paint() asks of one. It builds elements,
|
||||
// appends them and reads their labels back; nothing here lays anything out, which is
|
||||
// the stylesheet's half of the job and is checked as text below.
|
||||
function fakeDom() {
|
||||
const element = tag => ({
|
||||
tag,
|
||||
className: "",
|
||||
type: "",
|
||||
textContent: "",
|
||||
children: [],
|
||||
appendChild(child) { this.children.push(child); return child; },
|
||||
addEventListener() {},
|
||||
setAttribute() {},
|
||||
focus() {},
|
||||
get childElementCount() { return this.children.length; },
|
||||
set innerHTML(value) { assert.equal(value, "", "only clearing is modelled"); this.children = []; },
|
||||
get innerHTML() { return ""; },
|
||||
});
|
||||
const byId = {};
|
||||
["seatMenuTitle", "seatMenuSubtitle", "seatMenuActions", "seatMenuCancelBtn",
|
||||
"seatMenuOverlay"].forEach(id => { byId[id] = element("div"); });
|
||||
return {
|
||||
byId,
|
||||
doc: {
|
||||
createElement: element,
|
||||
getElementById: id => byId[id] || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withDocument(doc, body) {
|
||||
const had = Object.prototype.hasOwnProperty.call(globalThis, "document");
|
||||
const before = globalThis.document;
|
||||
globalThis.document = doc;
|
||||
try { return body(); } finally {
|
||||
if (had) globalThis.document = before; else delete globalThis.document;
|
||||
}
|
||||
}
|
||||
|
||||
// The fullest menu the tool can build, aimed at a live seat that isn't the dealer with
|
||||
// the blinds still the only money in and an override already set.
|
||||
function painted(seatIndex = 3, { move = false, seated = [0, 1, 2, 3, 4], waiting = true } = {}) {
|
||||
const state = makeTable({ seatCount: 9, seated, stack: 500, dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
state.blindOverride = { sbIndex: 1, bbIndex: 2 };
|
||||
if (waiting) state.waitingList = [{ id: "w1", name: "Nina" }];
|
||||
const { doc, byId } = fakeDom();
|
||||
return withDocument(doc, () => {
|
||||
SeatMenu.state = state;
|
||||
SeatMenu.hooks = {};
|
||||
SeatMenu.seatIndex = seatIndex;
|
||||
SeatMenu.mode = move ? "move" : null;
|
||||
SeatMenu.paint();
|
||||
return { list: byId.seatMenuActions, cancel: byId.seatMenuCancelBtn, state };
|
||||
});
|
||||
}
|
||||
|
||||
const classes = el => el.className.split(/\s+/).filter(Boolean);
|
||||
const labels = el => el.children.map(child => child.textContent);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("the two tiers are boxes, not a run of siblings", () => {
|
||||
// Five loose children can only ever stack. Two boxes can be placed.
|
||||
test("the sheet's actions are exactly the hand and the housekeeping", () => {
|
||||
const { list } = painted();
|
||||
assert.deepEqual(list.children.map(child => child.className),
|
||||
["seatMenuHand", "seatMenuKeeping"]);
|
||||
});
|
||||
|
||||
test("the hand tier holds the bars a dealer presses with cards in the air", () => {
|
||||
const { list } = painted();
|
||||
const hand = list.children[0];
|
||||
assert.deepEqual(labels(hand), ["Fold", "All-in"]);
|
||||
hand.children.forEach(btn => {
|
||||
assert.ok(classes(btn).includes("seatMenuBtn"));
|
||||
assert.ok(!classes(btn).includes("menuTile"),
|
||||
"a bar is not a tile — the whole point of the tier is that it is a bar");
|
||||
});
|
||||
});
|
||||
|
||||
test("and the housekeeping tier holds its captions and grids in order", () => {
|
||||
const { list } = painted();
|
||||
const keeping = list.children[1];
|
||||
assert.deepEqual(keeping.children.map(child => child.className),
|
||||
["seatMenuCaption", "seatMenuGrid", "seatMenuCaption", "seatMenuGrid"]);
|
||||
assert.deepEqual(keeping.children.map(child => child.tag), ["p", "div", "p", "div"]);
|
||||
});
|
||||
|
||||
// The sideways layout reads the tier's absence to know it has one column rather
|
||||
// than two, so an empty tier has to be absent rather than empty.
|
||||
test("a tier with nothing in it is left out rather than drawn empty", () => {
|
||||
// A chair nobody is sitting in, with nobody waiting to take it: there is
|
||||
// nothing at all to do to it while the cards are in the air.
|
||||
const { list } = painted(7, { seated: [0, 1, 2], waiting: false });
|
||||
assert.deepEqual(list.children.map(child => child.className), ["seatMenuKeeping"],
|
||||
"an empty chair still drew a hand tier with nothing in it");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the move picker", () => {
|
||||
test("its chairs go in the housekeeping tier too, so the sheet has one column", () => {
|
||||
const { list } = painted(3, { move: true });
|
||||
assert.deepEqual(list.children.map(child => child.className), ["seatMenuKeeping"]);
|
||||
});
|
||||
|
||||
// "Seat 3 — Alexandra" sets 116px against "Sit out"'s 50, so this grid asks for a
|
||||
// different column from the one the tiles use.
|
||||
test("and they ask for a column of their own width", () => {
|
||||
const { list } = painted(3, { move: true });
|
||||
const grid = list.children[0].children[0];
|
||||
assert.deepEqual(classes(grid), ["seatMenuGrid", "seatMenuGridNames"]);
|
||||
});
|
||||
|
||||
test("the way back out is still the sheet's own foot button", () => {
|
||||
const { cancel } = painted(3, { move: true });
|
||||
assert.equal(cancel.textContent, "Back");
|
||||
assert.equal(painted(3).cancel.textContent, "Cancel");
|
||||
});
|
||||
});
|
||||
|
||||
describe("upright, nothing moved", () => {
|
||||
// The wrappers gap exactly as the loose children used to, so the sheet a dealer
|
||||
// holding a phone the usual way sees is the one it has been since round 7.
|
||||
test("both tiers are the same column the sheet already was", () => {
|
||||
const rule = ruleFor(".seatMenuHand,\n.seatMenuKeeping");
|
||||
assert.match(rule, /flex-direction:\s*column/);
|
||||
assert.match(rule, /gap:\s*var\(--sp-2\)/);
|
||||
assert.match(rule, /min-width:\s*0/);
|
||||
});
|
||||
|
||||
test("and the grid is still two columns wherever the screen is not short", () => {
|
||||
assert.match(ruleFor(".seatMenuGrid"), /grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("what the short screen does instead", () => {
|
||||
const block = shortViewportBlock();
|
||||
|
||||
// Height and width, not orientation: a short desktop window has the same shortage
|
||||
// and the same answer, and a narrow one has neither the width to spend nor a second
|
||||
// column to spend it on.
|
||||
test("it is scoped on the shortage rather than on which way up the phone is", () => {
|
||||
assert.ok(!/orientation/.test(SHORT), "orientation says nothing about how much height there is");
|
||||
});
|
||||
|
||||
test("the sheet takes the width it has been leaving empty", () => {
|
||||
assert.match(block, /max-width:\s*min\(100% - var\(--sp-5\), 46rem\)/);
|
||||
});
|
||||
|
||||
test("the tiers become the two columns, with the way out under the thumb", () => {
|
||||
assert.match(block, /"header\s+header"/);
|
||||
assert.match(block, /"keeping hand"/);
|
||||
assert.match(block, /"keeping cancel"/);
|
||||
});
|
||||
|
||||
test("which means the box that held them stands aside", () => {
|
||||
const at = block.indexOf("#seatMenuActions {");
|
||||
assert.ok(at >= 0, "#seatMenuActions is not addressed here");
|
||||
assert.match(block.slice(at, block.indexOf("}", at)), /display:\s*contents/);
|
||||
});
|
||||
|
||||
// A column reserved for nothing would narrow the names for no reason.
|
||||
test("a sheet with no hand tier is one column, not two", () => {
|
||||
const at = block.indexOf("#seatMenuSheet:not(:has(.seatMenuHand))");
|
||||
assert.ok(at >= 0, "the move picker still reserves a column for a tier it hasn't got");
|
||||
const rule = block.slice(at, block.indexOf("}", at));
|
||||
assert.match(rule, /grid-template-columns:\s*minmax\(0, 1fr\)/);
|
||||
assert.match(rule, /"keeping"/);
|
||||
});
|
||||
|
||||
test("the tiles spread into as many columns as the width holds", () => {
|
||||
assert.match(block, /repeat\(auto-fill, minmax\(var\(--tileMin\), 1fr\)\)/);
|
||||
});
|
||||
|
||||
// auto-fit throws away the tracks nothing sits in, so a group of two stretched to
|
||||
// half the sheet each while the group above it was a third — two grids in a column
|
||||
// lining up with nothing.
|
||||
test("and they line up with each other, group to group", () => {
|
||||
// The comment above the rule says the word it is here to rule out, so the code
|
||||
// is read with the comments stripped out of it.
|
||||
const code = block.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
assert.ok(!/auto-fit/.test(code),
|
||||
"auto-fit collapses the empty tracks and the grids stop sharing column edges");
|
||||
});
|
||||
|
||||
// The left column is the taller of the two, and stretching left Cancel a 74px
|
||||
// button on one phone and a 48px one on the next.
|
||||
test("nothing grows into the slack the taller column leaves", () => {
|
||||
assert.match(block, /align-items:\s*start/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the numbers the columns are chosen from", () => {
|
||||
// The floor is what the longest of these labels needs, not a round figure that
|
||||
// happened to work on the phone it was measured on.
|
||||
test("a tile's narrowest is a token the grid of names can raise", () => {
|
||||
assert.match(ruleFor(".seatMenuGrid"), /--tileMin:\s*7\.5rem/);
|
||||
const block = shortViewportBlock();
|
||||
const at = block.indexOf(".seatMenuGrid.seatMenuGridNames");
|
||||
assert.ok(at >= 0, "the chairs never ask for more room than a verb");
|
||||
assert.match(block.slice(at, block.indexOf("}", at)), /--tileMin:\s*8\.25rem/);
|
||||
});
|
||||
|
||||
// Two words. Every pixel over that comes out of the left column, where it is a
|
||||
// whole extra column of tiles.
|
||||
test("the hand column is sized for Fold and All-in and no wider", () => {
|
||||
const block = shortViewportBlock();
|
||||
assert.match(block, /grid-template-columns:\s*minmax\(0, 1fr\) minmax\(8rem, 10rem\)/);
|
||||
});
|
||||
|
||||
test("what was measured is written beside the rules", () => {
|
||||
const block = shortViewportBlock();
|
||||
const note = CSS.slice(CSS.indexOf("A phone on its side: the seat menu goes beside itself"),
|
||||
CSS.indexOf(SHORT));
|
||||
assert.match(note, /640x360/);
|
||||
assert.match(note, /545px/);
|
||||
assert.match(note, /224px/);
|
||||
assert.match(block, /10rem it\s+gets 397 and fits three/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user