CB-WP-0024 T01/T02: the ending control says what it does, and the piles are objects
T01. The maintainer asked why the button says "I need to read this" and
why nothing closes. Two defects behind one control: the label described a
reading while the control STOPS THE SERVER (hotseat.rs reads `done` and
breaks its loop), and acknowledging it changed nothing on screen -- the
tab kept a live table and a `play again` pointing at a closed port.
Label is now "end session -- stops the game server". The reply says the
session has ended and the tab can be closed. The script seals the page on
a `closed` reply: removeAttribute('data-drop') on every control, so they
stop being droppable by the same rule that made them droppable. CSS is how
that reads, not the mechanism. removeAttribute rather than
setAttribute(_, null) -- the latter writes the truthy string "null" in a
browser, so the control would stay live while the stub called it sealed.
THE REPLY PATH HAD NEVER BEEN EXECUTABLE IN A TEST. jsrun's fetch stub
returned {then: function(){return this}}, which never invoked its
callbacks, so every line of the script reacting to the server was
unreachable from every test in this project. That is why the defect
survived: a page ignoring the server looked identical to one acting on it.
The stub now delivers a real then-chain and gesture_with_reply reports
which controls survive. The seal is mutation-proven -- deleting the
`closed` branch turns exactly one test red -- and a negative control
asserts an `ok: dealing` reply does NOT seal, since a seal that fired on
every reply would pass the first test and break `play again`.
T02. Draw and discard drawn as offset stacks with their counts. The
shuffle question the task required answering is settled and the answer is
that it already works: games/ground/src/lib.rs:1419-1435 implements the U4
default -- deterministic reshuffle of the discard seeded from seed ^ round,
skip the draw if both are empty -- and ground-game CONFIRMED U4 on
2026-08-03. A ruled rule, not an invented one, nothing to raise. The event
already reads out in the log; what the piles add is the state before it
fires, which is derivable from the view. A claim that a reshuffle HAS
happened would not be, and is not made.
The coverage gate caught its own probe going stale when the "17 remaining"
text was replaced. The count now lives in the pile's <title> -- a stable
probe and what a screen reader announces, where the on-canvas numeral
could be any number on the page.
39 tests pass; cb-play 22 including play_again_deals_a_second_game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f9c2839bb
commit
1edb10d5a3
5 changed files with 443 additions and 15 deletions
|
|
@ -128,7 +128,7 @@ mod coverage {
|
|||
("step", "step Select"),
|
||||
("mode", "scoring BondedCoalitions"),
|
||||
("viewer", "viewing as P1"),
|
||||
("solution_deck_len", "17 remaining"),
|
||||
("solution_deck_len", "draw pile: 17 remaining"),
|
||||
("solution_discard.*.suit", "discard Repair Change"),
|
||||
("players.*.stress", "stress 5"),
|
||||
("players.*.protection", "protect 2"),
|
||||
|
|
@ -630,5 +630,156 @@ mod gamelog {
|
|||
}
|
||||
}
|
||||
|
||||
/// CB-WP-0024 T02 — the draw and discard stacks as objects on the table.
|
||||
///
|
||||
/// The maintainer asked for the piles to be visible and for the discard
|
||||
/// to show a shuffle when the draw runs out. **The reshuffle is real** —
|
||||
/// `games/ground/src/lib.rs::draw_solution` implements the U4 default
|
||||
/// (deterministic reshuffle of the discard; skip the draw if both are
|
||||
/// empty), which ground-game confirmed on 2026-08-03. So this renders the
|
||||
/// state in which the next draw triggers it, rather than inventing a rule.
|
||||
#[cfg(test)]
|
||||
mod piles {
|
||||
use cb_kernel::PlayerId;
|
||||
use games_ground::view::GroundView;
|
||||
|
||||
use crate::doc::document;
|
||||
|
||||
fn view() -> GroundView {
|
||||
crate::testfix::view(Some(PlayerId(0)))
|
||||
}
|
||||
|
||||
fn html(v: &GroundView) -> String {
|
||||
document(v, &[], "/command?t=x", Some(PlayerId(0)), false)
|
||||
}
|
||||
|
||||
/// The counts must come from the projection, never be recomputed.
|
||||
#[test]
|
||||
fn both_counts_are_the_views_own_numbers() {
|
||||
let mut v = view();
|
||||
v.solution_deck_len = 5;
|
||||
let doc = crate::text_of(&html(&v));
|
||||
assert!(
|
||||
doc.contains("draw pile: 5 remaining"),
|
||||
"the drawn deck count is not the view's: {doc}"
|
||||
);
|
||||
let n = v.solution_discard.len();
|
||||
assert!(
|
||||
doc.contains(&format!("discard pile: {n} remaining")),
|
||||
"the drawn discard count is not the view's ({n}): {doc}"
|
||||
);
|
||||
}
|
||||
|
||||
/// An empty discard is an EMPTY pile, not a missing one. A absent slot
|
||||
/// reads as "this game has no discard", which is a different claim.
|
||||
#[test]
|
||||
fn an_empty_pile_is_drawn_rather_than_omitted() {
|
||||
let mut v = view();
|
||||
v.solution_discard.clear();
|
||||
let doc = crate::text_of(&html(&v));
|
||||
assert!(
|
||||
doc.contains("discard pile: 0 remaining"),
|
||||
"an empty discard vanished instead of rendering as empty: {doc}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The U4 state: deck empty, discard holding cards. The next draw
|
||||
/// reshuffles, and the table should say so.
|
||||
#[test]
|
||||
fn an_exhausted_deck_says_the_discard_shuffles_back_in() {
|
||||
let mut v = view();
|
||||
v.solution_deck_len = 0;
|
||||
assert!(!v.solution_discard.is_empty(), "fixture needs a discard");
|
||||
let doc = crate::text_of(&html(&v));
|
||||
assert!(
|
||||
doc.contains("shuffles in on next draw"),
|
||||
"an exhausted deck did not announce the U4 reshuffle: {doc}"
|
||||
);
|
||||
|
||||
// And the negative half: with cards left, no shuffle is promised.
|
||||
let mut full = view();
|
||||
full.solution_deck_len = 12;
|
||||
assert!(
|
||||
!crate::text_of(&html(&full)).contains("shuffles in on next draw"),
|
||||
"a stocked deck claimed a reshuffle was coming"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// CB-WP-0024 T01 — the ending page's one control.
|
||||
///
|
||||
/// The maintainer reported it as *"the button says 'I need to read this'
|
||||
/// — why? the UI is not closing."* Two defects wearing one button: the
|
||||
/// label described a reading while the control stopped a server, and
|
||||
/// acknowledging it changed nothing on screen, leaving a live-looking
|
||||
/// table and a `play again` pointing at a closed port.
|
||||
#[cfg(test)]
|
||||
mod ending_page {
|
||||
use crate::{doc, jsrun};
|
||||
|
||||
fn page() -> String {
|
||||
doc::ending(None, "the game ended", "/command?t=x", &[])
|
||||
}
|
||||
|
||||
/// The label must say what the control DOES. Asserted on the rendered
|
||||
/// text so reverting the wording turns this red — a comment would not.
|
||||
#[test]
|
||||
fn the_control_is_labelled_by_its_effect_not_by_a_reading() {
|
||||
let text = crate::text_of(&page());
|
||||
assert!(
|
||||
text.contains("end session") && text.contains("stops the game server"),
|
||||
"the ending control must name its effect: {text}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("I have read this"),
|
||||
"the label claimed the player had read something; it stops a server"
|
||||
);
|
||||
}
|
||||
|
||||
/// The defect the maintainer actually saw. After the server says the
|
||||
/// session is closed, `play again` must stop being offered — it now
|
||||
/// points at a port nobody is listening on.
|
||||
#[test]
|
||||
fn acknowledging_the_end_stops_the_page_offering_anything() {
|
||||
let html = page();
|
||||
let before = doc::drop_keys(&html);
|
||||
assert!(
|
||||
before.contains("again") && before.contains("done"),
|
||||
"fixture must start with both controls: {before:?}"
|
||||
);
|
||||
|
||||
let (live, status) =
|
||||
jsrun::gesture_with_reply(&html, "done", "done", "closed — the session has ended")
|
||||
.expect("run the page");
|
||||
|
||||
assert!(
|
||||
!live.contains(&"again".to_string()),
|
||||
"`play again` survived the session ending and would post to a closed port: {live:?}"
|
||||
);
|
||||
assert!(
|
||||
live.is_empty(),
|
||||
"every control must be sealed once the server stops, not only `again`: {live:?}"
|
||||
);
|
||||
assert!(
|
||||
status.contains("session has ended"),
|
||||
"the page must say what happened: {status:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The negative control. If `seal` fired on any reply, this test would
|
||||
/// pass for a page that tears itself down whenever it is touched —
|
||||
/// which would break `play again` in the ordinary case.
|
||||
#[test]
|
||||
fn a_dealing_reply_leaves_the_controls_alone() {
|
||||
let html = page();
|
||||
let (live, _) =
|
||||
jsrun::gesture_with_reply(&html, "again", "again", "ok: dealing").expect("run the page");
|
||||
assert!(
|
||||
live.contains(&"again".to_string()) && live.contains(&"done".to_string()),
|
||||
"an 'ok' reply must not seal the page: {live:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod testfix;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue