test(creation-screen): close the §7 sweep's own emptiness guard, and the M3-a viewport-fit guard

The Luck sweep's non-empty-label assertions could not fail: _bind_bay and
_bind_callings always wrap prose in BBCode italics ("[i]%s[/i]"), and
_origin_prose() always emits the literal connective " Now you carry a %s's
work — " even when every authored fragment/blurb is blank. So
"[i][/i]".strip_edges().is_empty() reads false regardless of what ContentDB
served — the 28-combination sweep was proving the wrapper is non-empty, not
that §7's own guard against a blank ContentDB actually holds. Replaced the
emptiness checks with assertions that the DM panel and detail panel render
the WORLD's own fragment/blurb strings, and that those strings are
themselves non-blank, for every race x calling combination.

The M3-a collapse guard (`assert_gt(s.size.x, 0.0, ...)`) also could not
fail: the .tscn root is authored at exactly 1920x1080, matching the
project's viewport size, so the assertion passed on the authored offsets
alone with _fit_to_viewport() and its size_changed connection deleted
entirely. Replaced it with an actual resize proof: setting
Window.content_scale_size (the property this project's canvas_items stretch
mode actually keys Control.get_viewport_rect() off of — get_viewport().size
does not move it headless) to a non-1920x1080 value and asserting the root
followed, then restoring it.

Also covers three previously-unasserted binder behaviours: a race-granted
skill chip going inert (and a chip going inert once the pool's picks are
spent), the bonus-skill row's visibility gate per race, the ability card's
primary-stat Tag/theme_type_variation, and _on_enter_pressed's early-out on
an illegal draft.

Every new/changed guard was proven red against the bug it names (blanked a
calling's blurb; hardcoded a placeholder origin string; deleted
_fit_to_viewport's wiring; dropped the chip.disabled line; forced the bonus
row visible) and reverted. Suite: 298 -> 302, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 15:53:47 -05:00
parent c5f127e0f1
commit 4494856a44

View File

@@ -60,7 +60,25 @@ func test_screen_applies_theme_and_fits_viewport():
var s := _screen()
assert_true(s is Control)
assert_true(s.theme is Theme, "the screen pulls the shared theme")
assert_gt(s.size.x, 0.0, "viewport-fit sized the root (the M3-a collapse guard)")
# The .tscn root is authored at exactly 1920x1080 (project.godot's viewport
# size too) — assert_gt(s.size.x, 0.0) passes on the AUTHORED offsets alone,
# even with _fit_to_viewport() and its size_changed connection deleted
# outright. Prove the fit is actually live: change the window's reported
# resolution to something that is NOT 1920x1080 and assert the root followed.
#
# get_viewport().size / get_window().size do not move get_viewport_rect() —
# this project runs with window/stretch/mode="canvas_items", so a Control's
# visible rect tracks content_scale_size, not the raw window pixel size.
# content_scale_size is the property that actually reaches Viewport.size_changed
# here, headless included.
var win := s.get_window()
var original_scale_size: Vector2i = win.content_scale_size
win.content_scale_size = Vector2i(1600, 900)
assert_eq(s.size, Vector2(1600, 900),
"the root must follow a resize through _fit_to_viewport()'s size_changed hook (the M3-a collapse guard)")
win.content_scale_size = original_scale_size
assert_eq(s.size, Vector2(1920, 1080), "restoring the viewport must restore the root, or later tests see a stale size")
func test_a_draft_exists_with_a_seed_on_load():
@@ -127,10 +145,32 @@ func test_no_node_on_the_screen_says_luck():
# `checked_a_rich_text_label` alone only proves the sweep REACHED a
# RichTextLabel, not that it had anything in it — an empty label would
# pass the Luck check for a reason that has nothing to do with §7.
assert_false(s._origin_text.text.strip_edges().is_empty(),
"the origin card must actually render prose at %s" % combo)
assert_false(s._detail_blurb.text.strip_edges().is_empty(),
"the calling detail must actually render prose at %s" % combo)
#
# But _bind_bay() and _bind_callings() both wrap their prose in BBCode
# italics ("[i]%s[/i]"), and _origin_prose() always emits the literal
# connective " Now you carry a %s's work — " even when every fragment is
# blank. So `"[i][/i]".strip_edges().is_empty()` is FALSE — the wrapper
# alone would pass this guard even if ContentDB served nothing at all.
# Assert the labels render the WORLD's own strings, not just non-blankness.
var race_id: String = Races.IDS[race_i]
var calling_id: String = allowed[calling_i]
var race_frag: String = str(s.world.race(race_id).get("fragment", s.world.race(race_id).get("blurb", "")))
var calling_frag: String = str(s.world.calling(calling_id).get("fragment", s.world.calling(calling_id).get("blurb", "")))
var calling_blurb: String = str(s.world.calling(calling_id).get("blurb", ""))
assert_false(race_frag.strip_edges().is_empty(),
"race %s has no fragment (or blurb) to render — the content itself must not be blank" % race_id)
assert_false(calling_frag.strip_edges().is_empty(),
"calling %s has no fragment (or blurb) to render — the content itself must not be blank" % calling_id)
assert_false(calling_blurb.strip_edges().is_empty(),
"calling %s has no blurb to render — the content itself must not be blank" % calling_id)
assert_true(s._origin_text.text.contains(race_frag),
"the DM panel must render %s's own fragment, not just non-empty BBCode, at %s" % [race_id, combo])
assert_true(s._origin_text.text.contains(calling_frag),
"the DM panel must render %s's own fragment, not just non-empty BBCode, at %s" % [calling_id, combo])
assert_true(s._detail_blurb.text.contains(calling_blurb),
"the detail panel must render %s's own blurb, not just non-empty BBCode, at %s" % [calling_id, combo])
assert_true(checked_a_rich_text_label,
"the sweep must actually reach the RichTextLabels or it is checking nothing")
@@ -384,3 +424,87 @@ func test_minus_is_disabled_at_the_rolled_floor_plus_at_the_spent_pool():
assert_true(plus.disabled, "the pool is spent — this card's Plus must go inert")
var other_plus: Button = s._ability_cards[1].get_node("Box/Buttons/Plus")
assert_true(other_plus.disabled, "points_left() is pool-wide — every card's Plus disables together")
func test_a_race_granted_skill_chip_is_inert_and_so_is_a_chip_once_picks_are_spent():
# Two real binder behaviours in one chip's line
# (chip.disabled = draft.is_granted(skill) or (not is_picked(skill) and not can_pick(skill)))
# that nothing in the suite observed. Elf grants "perception"; Cutpurse's pool
# has 6 skills (one of them "perception") and needs 4 picks — the only
# race/calling pairing where both halves of that OR are exercised at once.
var s := _screen()
s._race_cards[Races.IDS.find("elf")].pressed.emit()
var cutpurse_i: int = s._allowed_callings().find("cutpurse")
s._calling_cards[cutpurse_i].pressed.emit()
var pool: Array = s.draft.skill_pool()
var perception_i: int = pool.find("perception")
assert_ne(perception_i, -1, "cutpurse's pool must include perception for this test to mean anything")
assert_true(s._pool_chips[perception_i].disabled,
"a race-granted skill must be inert — the player already has it for free")
# Spend every pick on the OTHER skills in the pool, leaving exactly one behind.
var picked := 0
var left_behind_i := -1
for i in range(pool.size()):
if i == perception_i:
continue
if picked < s.draft.picks_needed():
s._pool_chips[i].pressed.emit()
picked += 1
else:
left_behind_i = i
assert_eq(picked, s.draft.picks_needed(), "the pool must actually be spent for this test to mean anything")
assert_ne(left_behind_i, -1, "cutpurse's pool must be wider than its pick count for this test to mean anything")
assert_true(s._pool_chips[left_behind_i].disabled,
"once the picks are spent, the remaining UNPICKED chip must go inert too")
assert_false(s._pool_chips[perception_i].button_pressed,
"the granted chip was never toggled — it was inert from the start")
func test_bonus_row_shows_only_for_the_race_that_wants_one():
# _bonus_row.visible = draft.wants_bonus_skill() — nothing in the suite checks
# this. Human wants a bonus skill; the other three races do not.
var s := _screen() # default race is human
assert_true(s._bonus_row.visible, "human wants a bonus skill choice")
for race_id in ["elf", "dwarf", "beastfolk"]:
s._race_cards[Races.IDS.find(race_id)].pressed.emit()
assert_false(s._bonus_row.visible, "%s does not want a bonus skill choice" % race_id)
s._race_cards[Races.IDS.find("human")].pressed.emit()
assert_true(s._bonus_row.visible, "switching back to a bonus-wanting race must show the row again")
func test_ability_card_surfaces_the_calling_s_primary_stat():
# The RACE card's Tag is checked (test_pressing_a_race_card_sets_the_draft_s_race);
# the ability card's equivalent — Tag.visible and theme_type_variation marking
# which of the five stats is the calling's primary — is not.
var s := _screen()
var idx: int = s._allowed_callings().find("cutpurse")
s._calling_cards[idx].pressed.emit()
var primary: String = Callings.primary("cutpurse")
assert_eq(primary, "dex", "cutpurse's primary must be dex for this test to mean anything")
for i in range(Attributes.IDS.size()):
var stat: String = Attributes.IDS[i]
var card: PanelContainer = s._ability_cards[i]
var is_primary := (stat == primary)
assert_eq(card.get_node("Tag").visible, is_primary,
"%s's Tag must show only when it is the calling's primary stat" % stat)
var expected_variation: StringName = ThemeKeys.PRIMARY_CARD if is_primary else ThemeKeys.PARCHMENT_CARD
assert_eq(card.theme_type_variation, expected_variation,
"%s's card style must reflect whether it is the primary stat" % stat)
func test_pressing_enter_on_an_illegal_draft_emits_nothing():
# _on_enter_pressed's early-out (`if not draft.is_complete(): return`) is the
# one branch in the file nothing else in the suite enters — every other Enter
# test first drives the draft to a legal state. Button.pressed still fires even
# on a disabled Button, so the early-out must be proven explicitly.
var s := _screen()
assert_true(s._enter.disabled, "an empty draft must start illegal, or this test proves nothing")
watch_signals(s)
s._enter.pressed.emit()
assert_signal_not_emitted(s, "creation_confirmed",
"an illegal draft must never reach creation_confirmed, even when Enter fires anyway")