Hold'em dealer: 55 polish rounds, one merge #101

Merged
jheaps merged 71 commits from release/casino-holdem-polish into master 2026-09-02 10:00:15 -06:00
Showing only changes of commit b64da7a787 - Show all commits
+82 -4
View File
@@ -93,10 +93,44 @@ const LayoutAudit = {
return false;
},
// The modal that is currently on top, if any.
//
// Every check below asks "is this thing reachable", and while a sheet or dialog is
// open the answer for everything behind it is legitimately no — that is what a
// modal is for. Without knowing that, the audit reported the pot wizard covering
// the seats as ten faults and the rulings sheet as seven, which is the noise that
// made it useless in exactly the states nobody had audited. Now it scopes to what
// is actually on top and says so.
MODAL_SELECTOR: "#potWizardOverlay, #rulingsOverlay, #seatMenuOverlay, #cardPickerOverlay, " +
"#confirmOverlay, #addChipsOverlay, #seatEditorOverlay",
openModal() {
const open = [...document.querySelectorAll(this.MODAL_SELECTOR)]
.filter(el => !el.hidden && getComputedStyle(el).display !== "none");
// Last in the DOM wins, which is how they stack.
return open.length > 0 ? open[open.length - 1] : null;
},
// A seat sheet on a phone isn't an overlay element, it's a body class — but it
// covers the felt just the same.
modalScope() {
const overlay = this.openModal();
if (overlay) return overlay;
if (document.body.classList.contains("seatSheetOpen")) {
return document.getElementById("seatEditorPanel") || null;
}
return null;
},
occlusion(selector = "#actionBar button, #holdemHeader button, .seat") {
const offenders = [];
const warnings = [];
[...document.querySelectorAll(selector)].forEach(el => {
// While something is open on top, only what is inside it can be judged. The
// rest of the page is meant to be unreachable.
const scope = this.modalScope();
const candidates = [...document.querySelectorAll(selector)]
.filter(el => !scope || scope.contains(el));
candidates.forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
@@ -504,10 +538,25 @@ const LayoutAudit = {
if (!enforceOnPointerDevices && matchMedia("(hover: hover)").matches) {
return { name: `touch targets at least ${minimum}px`, offenders, skipped: "pointer device" };
}
const unmeasured = [];
this.visibleControls()
.filter(el => el.type !== "color")
.filter(el => !(exempt && el.matches(exempt)))
.forEach(el => {
// A control inside a scrolling sheet can sit below its container's
// visible area while still having a viewport rect. elementFromPoint at
// its centre then returns the sheet, effectiveTarget gives up, and the
// check used to pass it in silence — which is how "Confirm Winner(s)",
// the button that pays the pot out, went unmeasured for 36 rounds.
// Bring it into view before probing.
const scroller = el.closest("[style*='overflow'], .modalBody, #potWizardBody, #rulingsSheet, #seatEditorPanel");
if (scroller && scroller.scrollHeight > scroller.clientHeight) {
const box = scroller.getBoundingClientRect();
const own = el.getBoundingClientRect();
if (own.bottom > box.bottom || own.top < box.top) {
el.scrollIntoView({ block: "center" });
}
}
const rect = el.getBoundingClientRect();
// Drawn big enough is big enough — no need to probe, and probing wide
// elements is where the cap above used to produce nonsense. Seats sit
@@ -515,15 +564,35 @@ const LayoutAudit = {
// measuring their reach would report the crowding as a fault.
if (rect.width >= minimum - 1 && rect.height >= minimum - 1) return;
// A checkbox inside a label is tapped by tapping the label, so the
// label is the target. Measured rather than probed, because probing
// fails on anything scrolled off the viewport — which is how a 24px
// checkbox with a 335x44 label around it came up as unprobeable
// instead of as the perfectly good target it is.
const label = el.closest("label");
if (label) {
const lr = label.getBoundingClientRect();
if (lr.width >= minimum - 1 && lr.height >= minimum - 1) return;
}
const size = this.effectiveTarget(el);
if (!size) return;
// Still can't measure it. That is not a pass — it is the check failing
// to run, and quietly returning here is exactly the kind of exemption
// that hides real faults. Say so instead.
if (!size) {
unmeasured.push(
`${this.describe(el)} :: could not be probed` +
` (drawn ${Math.round(rect.width)}x${Math.round(rect.height)})`
);
return;
}
if (size.width >= minimum - 1 && size.height >= minimum - 1) return;
offenders.push(
`${this.describe(el)} :: hittable ${size.width}x${size.height}` +
` (drawn ${Math.round(rect.width)}x${Math.round(rect.height)})`
);
});
return { name: `touch targets at least ${minimum}px`, offenders };
return { name: `touch targets at least ${minimum}px`, offenders, unmeasured };
},
// Tapping the middle of a seat must give you that seat.
@@ -537,6 +606,13 @@ const LayoutAudit = {
// Reported separately from occlusion because the cause is different: not an
// overlay on top of the table, but two pieces of the table fighting over a tap.
seatOwnership() {
// Seats behind an open sheet aren't fighting each other for taps — nothing is
// meant to reach them at all. Asking the question there produced six offenders
// per modal, every one of them the modal doing its job.
if (this.modalScope()) {
return { name: "a seat's centre belongs to that seat", offenders: [],
skipped: "a sheet is open over the table" };
}
const seats = [...document.querySelectorAll(".seat")];
const offenders = [];
@@ -617,7 +693,9 @@ const LayoutAudit = {
viewport: `${innerWidth}x${innerHeight}`,
failed: checks.filter(c => c.offenders.length > 0),
// Things worth knowing that aren't faults — see occlusion() on toasts.
warnings: checks.flatMap(c => c.warnings || []),
// Controls a check couldn't probe go here too: not a fault, but not a pass
// either, and a silent skip is how faults hide.
warnings: checks.flatMap(c => [...(c.warnings || []), ...(c.unmeasured || [])]),
passed: checks.filter(c => c.offenders.length === 0).map(c => c.name),
clean: checks.every(c => c.offenders.length === 0),
};