Two findings in the seat editor, and the second is why the first was
hard to notice.
A seat is sitting out, in the hand, or folded, and "folded" means
nothing while the seat is out. The form asked that as two checkboxes,
which offers a fourth corner that has no meaning, and the code that
stopped a dealer reaching it disabled the Fold box whenever In hand was
clear. Measured: that left the label at opacity 1 and colour
rgb(170,170,170) — identical to a live one — with a title of "Folded"
on it. Pressing Fold on a seat that was sitting out gave no movement and
no reason, which is the exact failure round 16 went through the tool to
remove; it hid from the sweep rule that finds those because the title
sat on the <label> rather than on the control the rule inspects.
Three named states can't contradict each other and none of them is ever
unavailable, so there is nothing left to dim. Sitting out · In the hand ·
Folded, as a radiogroup — the same shape the card picker's suits use —
with the chosen Folded segment in the red the seat menu's own Fold
already wears.
And the form itself. The phone block's comment has always said why the
editor becomes a bottom sheet — "the seat editor lives far enough down
the page that tapping a seat looks like it did nothing" — but the sheet
was scoped to max-width 700px while #holdemLayout goes to one column at
900. The two tiers in between had exactly the fault the sheet was
written to fix. Measured, tapping a seat:
844x390 the editor drew in the flow at y=369 in a 390px window —
21px of 401 on screen, 5% of it, at scrollY 0 with nothing
scrolling it into view. The seat lit up green and the form
it opened was under the bottom edge.
768x1024 31% visible, the rest below the fold, again no scroll.
An iPhone held sideways and an iPad held upright are not obscure. The
query is the layout's own breakpoint now, and the seat-sheet rules moved
out of the phone block into their own so it could be asked separately.
Afterwards every viewport from 360x640 to 900x500 shows 97-99% of the
sheet, and above 900 the editor is a static block at the top of a
sidebar beside the table — 100% visible there too.
1307 tests pass; the sweep is clean at 428 audits across 13 viewports.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
264 lines
12 KiB
JavaScript
264 lines
12 KiB
JavaScript
// What a seat is doing, asked once.
|
|
//
|
|
// The seat editor had two checkboxes, "In hand" and "Fold". A seat is really in one of
|
|
// three states — sitting out, in the hand, folded — and "folded" means nothing while
|
|
// the seat is out, so two boxes offered a fourth corner that has no meaning. The code
|
|
// that stopped a dealer reaching it `disabled` the Fold box whenever In hand was clear.
|
|
//
|
|
// Measured: that left the label at opacity 1 and colour rgb(170,170,170) — identical to
|
|
// a live one — with a `title` of "Folded" on it. So pressing Fold on a seat that was
|
|
// sitting out gave no movement and no reason, which is the exact failure round 16 went
|
|
// through the whole tool to remove. It hid from the sweep rule that finds those because
|
|
// the title sat on the <label> rather than on the control the rule inspects.
|
|
//
|
|
// Three named states cannot contradict each other and none of them is ever unavailable,
|
|
// so there is nothing left to dim.
|
|
//
|
|
// The second half of this round is where that form appears at all. The phone block's
|
|
// own comment has always said why the editor becomes a bottom sheet — "the seat editor
|
|
// lives far enough down the page that tapping a seat looks like it did nothing" — but
|
|
// the sheet was scoped to 700px while #holdemLayout goes to one column at 900. The two
|
|
// tiers in between had precisely the fault the sheet was written to fix. Measured,
|
|
// tapping a seat:
|
|
//
|
|
// 844x390 the editor drew in the flow at y=369 in a 390px window: 21px of 401 on
|
|
// screen, 5% of it, at scrollY 0 with nothing scrolling it into view.
|
|
// 768x1024 31% visible, the rest below the fold, and again no scroll.
|
|
//
|
|
// Afterwards every viewport from 360x640 to 900x500 shows 97-99% of the sheet, and
|
|
// above 900 the editor is a static block at the top of a sidebar that is beside the
|
|
// table rather than below it — 100% visible there too.
|
|
const test = require("node:test");
|
|
const { describe } = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { Render, makeTable } = require("./load");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net");
|
|
const PAGE = fs.readFileSync(path.join(ROOT, "Pages", "Casino", "TexasHoldem.cshtml"), "utf8");
|
|
const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
|
|
|
const seatForm = () => {
|
|
const at = PAGE.indexOf('<template id="seatEditorTemplate">');
|
|
assert.ok(at >= 0, "there is no seat editor template");
|
|
return PAGE.slice(at, PAGE.indexOf("</template>", at));
|
|
};
|
|
|
|
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 every block with this query, braces balanced and joined. Every one
|
|
// of them: this stylesheet opens `@media (hover: none)` twice, and a helper that stops
|
|
// at the first has twice caught a test asserting about the wrong half of the file.
|
|
function blockAt(query) {
|
|
const found = [];
|
|
for (let at = CSS.indexOf(query); at >= 0; at = CSS.indexOf(query, at + 1)) {
|
|
let depth = 0;
|
|
for (let i = at + query.length - 1; i < CSS.length; i++) {
|
|
if (CSS[i] === "{") depth++;
|
|
else if (CSS[i] === "}" && --depth === 0) { found.push(CSS.slice(at, i + 1)); break; }
|
|
}
|
|
}
|
|
assert.ok(found.length, `there is no \`${query}\` block`);
|
|
return found.join("\n");
|
|
}
|
|
|
|
// The three buttons, and enough of a form to hand them back.
|
|
function fakeForm() {
|
|
const options = Render.SEAT_STATES.map(state => ({
|
|
dataset: { seatState: state.key },
|
|
attrs: {},
|
|
tabIndex: 0,
|
|
handlers: {},
|
|
setAttribute(name, value) { this.attrs[name] = value; },
|
|
getAttribute(name) { return this.attrs[name]; },
|
|
addEventListener(type, fn) { this.handlers[type] = fn; },
|
|
focus() { options.focused = this.dataset.seatState; },
|
|
}));
|
|
return {
|
|
options,
|
|
querySelectorAll: selector => {
|
|
assert.equal(selector, ".seatStateOption");
|
|
return options;
|
|
},
|
|
};
|
|
}
|
|
|
|
const checked = form => form.options.filter(o => o.getAttribute("aria-checked") === "true")
|
|
.map(o => o.dataset.seatState);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("three states, not two booleans", () => {
|
|
test("the markup asks one question with three answers", () => {
|
|
const form = seatForm();
|
|
const at = form.indexOf('class="seatState"');
|
|
assert.ok(at >= 0, "the seat form has no state control");
|
|
const group = form.slice(form.lastIndexOf("<div", at), form.indexOf("</div>", at));
|
|
assert.match(group, /role="radiogroup"/);
|
|
assert.match(group, /aria-label="This seat"/);
|
|
["out", "in", "folded"].forEach(key => {
|
|
assert.match(group, new RegExp(`data-seat-state="${key}"`), `there is no "${key}" answer`);
|
|
});
|
|
assert.equal((group.match(/role="radio"/g) || []).length, 3);
|
|
});
|
|
|
|
test("and the two checkboxes are gone from the page and the stylesheet", () => {
|
|
["inHandCheckbox", "foldCheckbox", "inHandToggle", "foldToggle", "seatCardToggles"]
|
|
.forEach(name => {
|
|
assert.ok(!PAGE.includes(name), `${name} is still in the markup`);
|
|
assert.ok(!CSS.includes(name), `${name} is still in the stylesheet`);
|
|
});
|
|
});
|
|
|
|
// The pair (out, folded) is the one the old control could be asked for.
|
|
test("every state names both halves, and none of them is the meaningless pair", () => {
|
|
assert.deepEqual(Render.SEAT_STATES.map(s => s.key), ["out", "in", "folded"]);
|
|
Render.SEAT_STATES.forEach(state => {
|
|
assert.equal(typeof state.inHand, "boolean");
|
|
assert.equal(typeof state.folded, "boolean");
|
|
assert.ok(!(state.folded && !state.inHand),
|
|
`"${state.key}" is folded and not in the hand, which means nothing`);
|
|
});
|
|
});
|
|
|
|
test("and a seat reads back as the state it was put in", () => {
|
|
Render.SEAT_STATES.forEach(state => {
|
|
assert.equal(Render.seatStateOf({ inHand: state.inHand, folded: state.folded }), state.key);
|
|
});
|
|
// Including a seat saved before any of this, where folded was left set on a
|
|
// chair that had sat out.
|
|
assert.equal(Render.seatStateOf({ inHand: false, folded: true }), "out");
|
|
});
|
|
});
|
|
|
|
describe("what the control draws", () => {
|
|
test("exactly one answer is marked, and it is the seat's", () => {
|
|
const form = fakeForm();
|
|
Render.paintSeatState(form, { inHand: true, folded: true });
|
|
assert.deepEqual(checked(form), ["folded"]);
|
|
Render.paintSeatState(form, { inHand: false, folded: false });
|
|
assert.deepEqual(checked(form), ["out"]);
|
|
Render.paintSeatState(form, { inHand: true, folded: false });
|
|
assert.deepEqual(checked(form), ["in"]);
|
|
});
|
|
|
|
// One stop for the group, arrows inside it — the APG's radio group, and the same
|
|
// shape the card picker's suits use.
|
|
test("and exactly one of the three is a Tab stop", () => {
|
|
const form = fakeForm();
|
|
Render.paintSeatState(form, { inHand: true, folded: false });
|
|
assert.deepEqual(form.options.map(o => o.tabIndex), [-1, 0, -1]);
|
|
});
|
|
});
|
|
|
|
describe("what pressing one does", () => {
|
|
const wired = player => {
|
|
const form = fakeForm();
|
|
const commits = [];
|
|
const realCommit = Render.commitSeatChange;
|
|
Render.commitSeatChange = () => commits.push(true);
|
|
try {
|
|
Render.wireSeatState(form, player, 0);
|
|
} finally {
|
|
Render.commitSeatChange = realCommit;
|
|
}
|
|
return { form, commits, press: key => {
|
|
const btn = form.options.find(o => o.dataset.seatState === key);
|
|
const again = Render.commitSeatChange;
|
|
Render.commitSeatChange = () => commits.push(true);
|
|
try { btn.handlers.click(); } finally { Render.commitSeatChange = again; }
|
|
} };
|
|
};
|
|
|
|
test("it writes both halves of the seat, not one", () => {
|
|
const player = { inHand: true, folded: false };
|
|
const { press } = wired(player);
|
|
press("folded");
|
|
assert.deepEqual({ inHand: player.inHand, folded: player.folded }, { inHand: true, folded: true });
|
|
press("out");
|
|
assert.deepEqual({ inHand: player.inHand, folded: player.folded }, { inHand: false, folded: false });
|
|
press("in");
|
|
assert.deepEqual({ inHand: player.inHand, folded: player.folded }, { inHand: true, folded: false });
|
|
});
|
|
|
|
test("and says so, so the table redraws", () => {
|
|
const player = { inHand: true, folded: false };
|
|
const { commits, press } = wired(player);
|
|
const before = commits.length;
|
|
press("out");
|
|
assert.ok(commits.length > before, "nothing was committed, so the felt still shows the old state");
|
|
});
|
|
|
|
// Typing a name into an empty chair seats somebody. The control has to say so —
|
|
// it is the only thing on the form that shows it.
|
|
test("naming an empty chair repaints the control rather than leaving it stale", () => {
|
|
const src = fs.readFileSync(
|
|
path.join(ROOT, "wwwroot", "js", "casino", "holdem", "render.js"), "utf8");
|
|
const at = src.indexOf("const wasEmpty = !player.name.trim()");
|
|
assert.ok(at >= 0);
|
|
assert.match(src.slice(at, at + 400), /this\.paintSeatState\(form, player\);/);
|
|
});
|
|
});
|
|
|
|
describe("how it is drawn", () => {
|
|
// The answer moves between the three as the hand goes on, and a segment that
|
|
// resizes under the thumb reads as the control flinching.
|
|
test("three equal columns, whatever the words in them", () => {
|
|
assert.match(ruleFor(".seatState"), /grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/);
|
|
});
|
|
|
|
test("the chosen one is the one that looks pressed", () => {
|
|
assert.match(ruleFor('.seatStateOption[aria-checked="true"]'), /background:\s*var\(--surface-3\)/);
|
|
assert.match(ruleFor('.seatStateOption[aria-checked="true"]'), /color:\s*var\(--text-bright\)/);
|
|
});
|
|
|
|
// The same red the seat menu's own Fold wears, and only when it is the answer: a red
|
|
// segment sitting unpressed beside two grey ones reads as a warning about the seat.
|
|
test("folded is red, and only once it is chosen", () => {
|
|
const rule = ruleFor('.seatStateOption[data-seat-state="folded"][aria-checked="true"]');
|
|
assert.match(rule, /#3d1f1f/);
|
|
assert.ok(!/\n\.seatStateOption\[data-seat-state="folded"\] \{/.test(CSS),
|
|
"an unchosen Folded segment is painted red");
|
|
});
|
|
|
|
test("and a thumb gets its 44px wherever there is no pointer", () => {
|
|
assert.match(blockAt("@media (hover: none) {"), /\.seatStateOption \{\s*min-height: 44px;/);
|
|
});
|
|
});
|
|
|
|
describe("where the form appears at all", () => {
|
|
// The sheet exists because a stacked sidebar puts the editor far below the felt.
|
|
// That is true at every width where the layout is one column.
|
|
test("the sheet is scoped to the breakpoint that stacks the layout", () => {
|
|
const oneColumn = blockAt("@media (max-width: 900px) {");
|
|
assert.match(oneColumn, /#holdemLayout \{\s*grid-template-columns: 1fr;/,
|
|
"900px is no longer where the sidebar stops being a column beside the table");
|
|
const sheet = CSS.slice(CSS.indexOf("---- Seat editor as a bottom sheet ----"));
|
|
const query = sheet.slice(sheet.indexOf("@media"), sheet.indexOf("{", sheet.indexOf("@media")) + 1);
|
|
assert.equal(query.trim(), "@media (max-width: 900px) {");
|
|
});
|
|
|
|
test("and it is a sheet, fixed to the bottom, not a block in the flow", () => {
|
|
const sheet = CSS.slice(CSS.indexOf("---- Seat editor as a bottom sheet ----"));
|
|
const at = sheet.indexOf(" #seatEditorPanel {");
|
|
assert.ok(at >= 0, "the sheet block no longer positions the panel");
|
|
const rule = sheet.slice(at, sheet.indexOf("}", at));
|
|
assert.match(rule, /position:\s*fixed/);
|
|
assert.match(rule, /bottom:\s*0/);
|
|
assert.match(rule, /max-height:\s*74vh/);
|
|
});
|
|
|
|
test("what was measured is written beside the query", () => {
|
|
const note = CSS.slice(CSS.indexOf("---- Seat editor as a bottom sheet ----"),
|
|
CSS.indexOf("@media (max-width: 900px) {", CSS.indexOf("---- Seat editor as a bottom sheet ----")));
|
|
assert.match(note, /844x390/);
|
|
assert.match(note, /768x1024/);
|
|
assert.match(note, /5% of it/);
|
|
});
|
|
});
|