test_committed_tres_matches_builder has now been "fixed" three times (traps.md #12). v1 hardcoded 3 checks; v2 iterated ThemeKeys.ALL but missed the six font roles; v3 added a meta-guard specifically to catch "the builder styles a thing guarded by nothing" — but that meta-guard contained `if fresh.get_type_variation_base(variation) == &"": continue`, which whitelisted exactly the case it was built to catch. The builder styles RichTextLabel directly (build_game_theme.gd's _rich_text()), with no set_type_variation, so it reports base "" and was skipped unconditionally — and RichTextLabel is what renders every line of DM prose the creation screen shows (_origin_text, _detail_blurb). Nothing compared its fonts, font size, or colour, nor the theme-level default_font/default_font_size, against the committed .tres. Adds ThemeKeys.BASE_TYPES for base Control types the builder styles directly, folds it into the drift guard's coverage, replaces the meta-guard's continue with an assertion that names the offending type, and adds the missing theme-level default font/size comparison. Proved with three reverted breaks: a RichTextLabel font-size edit, a theme-level default_font_size edit, and an unregistered "GhostRole" variation — all three now go red and name the drifted property. Also: strengthens test_creation_copy's error-copy sweep to assert a mapped error actually reaches its authored line rather than silently falling through to the generic UNSPOKEN fallback (CreationCopy.UNSPOKEN satisfied all four prior checks, so a renamed validator string could regress silently); and removes two assertions that were true by construction / already true before their test's action ran. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
195 lines
11 KiB
GDScript
195 lines
11 KiB
GDScript
# client/tests/unit/test_theme_resource.gd
|
|
extends "res://addons/gut/test.gd"
|
|
|
|
const Builder = preload("res://scripts/theme/build_game_theme.gd")
|
|
const THEME_PATH := "res://assets/theme/game_theme.tres"
|
|
|
|
|
|
func test_theme_loads():
|
|
assert_true(FileAccess.file_exists(THEME_PATH), "theme not built — run build_game_theme.gd")
|
|
var t = load(THEME_PATH)
|
|
assert_true(t is Theme)
|
|
|
|
|
|
func test_every_variation_resolves():
|
|
var t: Theme = load(THEME_PATH)
|
|
for key in ThemeKeys.ALL:
|
|
var base: String = ThemeKeys.ALL[key]
|
|
assert_eq(t.get_type_variation_base(key), StringName(base),
|
|
"variation %s missing or wrong base" % key)
|
|
|
|
|
|
func test_cta_has_normal_stylebox():
|
|
var t: Theme = load(THEME_PATH)
|
|
assert_true(t.has_stylebox(&"normal", ThemeKeys.PRIMARY_CTA))
|
|
|
|
|
|
func test_default_font_is_set():
|
|
var t: Theme = load(THEME_PATH)
|
|
assert_true(t.default_font is FontFile)
|
|
|
|
|
|
func _guarded_variations() -> Array:
|
|
# EVERY variation OR BASE TYPE the builder touches — the stylebox variations
|
|
# (ThemeKeys.ALL), the six font roles (ThemeKeys.FONT_ROLES), AND the base
|
|
# types the builder styles directly with no variation on top
|
|
# (ThemeKeys.BASE_TYPES — today just RichTextLabel). ALL and FONT_ROLES
|
|
# deliberately exclude each other's half by their own docstrings; missing
|
|
# BASE_TYPES here left RichTextLabel's two fonts, its font size and its
|
|
# font colour — the ONLY things rendering DM prose on the creation screen —
|
|
# unguarded against a stale .tres. traps.md #12, the third miss: iterating
|
|
# ALL alone, then ALL+FONT_ROLES, both still left a gap "covers everything"
|
|
# did not actually cover.
|
|
var out: Array = ThemeKeys.ALL.keys()
|
|
out.append_array(ThemeKeys.FONT_ROLES.keys())
|
|
out.append_array(ThemeKeys.BASE_TYPES.keys())
|
|
return out
|
|
|
|
|
|
func test_the_drift_guard_covers_every_variation_the_builder_touches():
|
|
# The guard below is only as good as the set it walks. Pin that set to the
|
|
# builder's own output: every variation the fresh Theme carries a stylebox, a
|
|
# font, a font size or a font colour for MUST be in _guarded_variations(), or
|
|
# it can drift unwatched. A new variation added to the builder and forgotten in
|
|
# ThemeKeys goes red here rather than sitting unguarded forever.
|
|
var fresh: Theme = Builder.build_theme()
|
|
var guarded := _guarded_variations()
|
|
for variation in fresh.get_type_list():
|
|
var touched := (not fresh.get_stylebox_list(variation).is_empty()
|
|
or not fresh.get_font_list(variation).is_empty()
|
|
or not fresh.get_font_size_list(variation).is_empty()
|
|
or not fresh.get_color_list(variation).is_empty())
|
|
if not touched:
|
|
continue
|
|
if fresh.get_type_variation_base(variation) == &"":
|
|
# "" means one of two things, and they must be told apart, not both
|
|
# waved through: (a) a KNOWN base type the builder styles directly
|
|
# with no set_type_variation (RichTextLabel today — registered in
|
|
# ThemeKeys.BASE_TYPES), or (b) a type nobody ever registered at
|
|
# all — styled with set_stylebox/set_font/set_color but never
|
|
# given a set_type_variation, which reports the same "" and used
|
|
# to slip through this `continue` forever. The old guard could not
|
|
# tell these apart because it treated "" as "definitely (a)" and
|
|
# skipped it unconditionally — the exact whitelist that let
|
|
# RichTextLabel through in the first place (traps.md #12, third
|
|
# miss). Assert (a); anything else IS (b) and must be named.
|
|
assert_true(variation in ThemeKeys.BASE_TYPES,
|
|
("the builder styles %s directly (no set_type_variation) and it is not in " +
|
|
"ThemeKeys.BASE_TYPES — add it there, or if it should be a real variation, " +
|
|
"give it a set_type_variation(...) base so ALL/FONT_ROLES can guard it") % variation)
|
|
continue
|
|
assert_true(variation in guarded,
|
|
"the builder styles the variation %s but no drift guard walks it — add it to ThemeKeys.ALL or ThemeKeys.FONT_ROLES" % variation)
|
|
|
|
|
|
func test_committed_tres_matches_builder():
|
|
# Catches "palette/builder changed but nobody regenerated game_theme.tres."
|
|
# Preloading the builder script and calling its static build_theme() does
|
|
# NOT run _init() (that only fires on .new()), so this is side-effect-free.
|
|
#
|
|
# Drives its checks off ThemeKeys.ALL *and* ThemeKeys.FONT_ROLES so every
|
|
# variation is guarded, not just whichever three someone remembered to hardcode
|
|
# here — and not just the stylebox half. For each variation: every stylebox
|
|
# state, every font, every font size and every font colour the builder actually
|
|
# set is compared. Nothing the builder never touches is asserted on.
|
|
var fresh: Theme = Builder.build_theme()
|
|
var committed: Theme = load(THEME_PATH)
|
|
|
|
# Theme-LEVEL defaults (build_game_theme.gd:69-70) live on the Theme object
|
|
# itself, not under any type or variation, so no per-variation loop below
|
|
# ever reaches them — they need their own comparison or a changed
|
|
# default_font_size (18 -> anything) ships silently. Font by resource_path,
|
|
# not the Resource: load() caches, so two references to the same .ttf are
|
|
# `==` even when the builder swapped fonts, unless the .tres happens to
|
|
# still point at the same file.
|
|
assert_eq(committed.default_font.resource_path, fresh.default_font.resource_path,
|
|
"stale game_theme.tres — theme.default_font drifted from the builder; re-run build_game_theme.gd")
|
|
assert_eq(committed.default_font_size, fresh.default_font_size,
|
|
"stale game_theme.tres — theme.default_font_size drifted from the builder; re-run build_game_theme.gd")
|
|
|
|
for variation in _guarded_variations():
|
|
for state in fresh.get_stylebox_list(variation):
|
|
assert_true(committed.has_stylebox(state, variation),
|
|
"stale game_theme.tres — %s/%s stylebox is missing entirely; re-run build_game_theme.gd" % [variation, state])
|
|
var fresh_box: StyleBox = fresh.get_stylebox(state, variation)
|
|
var committed_box: StyleBox = committed.get_stylebox(state, variation)
|
|
assert_eq(committed_box.get_class(), fresh_box.get_class(),
|
|
"stale game_theme.tres — %s/%s stylebox type drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
|
|
if fresh_box is StyleBoxFlat:
|
|
var f: StyleBoxFlat = fresh_box
|
|
var c: StyleBoxFlat = committed_box
|
|
assert_eq(c.bg_color, f.bg_color,
|
|
"stale game_theme.tres — %s/%s bg_color drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.border_color, f.border_color,
|
|
"stale game_theme.tres — %s/%s border_color drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.border_width_left, f.border_width_left,
|
|
"stale game_theme.tres — %s/%s border_width_left drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.border_width_top, f.border_width_top,
|
|
"stale game_theme.tres — %s/%s border_width_top drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.border_width_right, f.border_width_right,
|
|
"stale game_theme.tres — %s/%s border_width_right drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.border_width_bottom, f.border_width_bottom,
|
|
"stale game_theme.tres — %s/%s border_width_bottom drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.corner_radius_top_left, f.corner_radius_top_left,
|
|
"stale game_theme.tres — %s/%s corner_radius_top_left drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.corner_radius_top_right, f.corner_radius_top_right,
|
|
"stale game_theme.tres — %s/%s corner_radius_top_right drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.corner_radius_bottom_right, f.corner_radius_bottom_right,
|
|
"stale game_theme.tres — %s/%s corner_radius_bottom_right drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
assert_eq(c.corner_radius_bottom_left, f.corner_radius_bottom_left,
|
|
"stale game_theme.tres — %s/%s corner_radius_bottom_left drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
|
|
|
# The FONT half — the whole reason this guard could not fail against a font
|
|
# role. Compare the font's resource_path, not the Resource: `load()` caches,
|
|
# so two references to the same .ttf are the same instance and `==` would be
|
|
# true even when the builder swapped SERIF for MONO... only if the .tres
|
|
# happened to point at the same file anyway. The path is the thing that drifts.
|
|
for font_name in fresh.get_font_list(variation):
|
|
assert_true(committed.has_font(font_name, variation),
|
|
"stale game_theme.tres — %s/%s font is missing entirely; re-run build_game_theme.gd" % [variation, font_name])
|
|
var fresh_font: Font = fresh.get_font(font_name, variation)
|
|
var committed_font: Font = committed.get_font(font_name, variation)
|
|
assert_eq(committed_font.resource_path, fresh_font.resource_path,
|
|
"stale game_theme.tres — %s/%s font drifted from the builder; re-run build_game_theme.gd" % [variation, font_name])
|
|
|
|
for size_name in fresh.get_font_size_list(variation):
|
|
assert_true(committed.has_font_size(size_name, variation),
|
|
"stale game_theme.tres — %s/%s font size is missing entirely; re-run build_game_theme.gd" % [variation, size_name])
|
|
assert_eq(committed.get_font_size(size_name, variation), fresh.get_font_size(size_name, variation),
|
|
"stale game_theme.tres — %s/%s font size drifted from the builder; re-run build_game_theme.gd" % [variation, size_name])
|
|
|
|
for color_name in fresh.get_color_list(variation):
|
|
assert_true(committed.has_color(color_name, variation),
|
|
"stale game_theme.tres — %s/%s colour is missing entirely; re-run build_game_theme.gd" % [variation, color_name])
|
|
var fresh_color: Color = fresh.get_color(color_name, variation)
|
|
var committed_color: Color = committed.get_color(color_name, variation)
|
|
assert_eq(committed_color, fresh_color,
|
|
"stale game_theme.tres — %s/%s drifted from the builder; re-run build_game_theme.gd" % [variation, color_name])
|
|
|
|
|
|
func test_creation_variations_exist_with_the_states_the_screen_needs():
|
|
var t: Theme = load(THEME_PATH)
|
|
|
|
# A race/calling card is a Button that must show a CHOSEN state.
|
|
assert_true(t.has_stylebox(&"normal", ThemeKeys.SELECT_CARD))
|
|
assert_true(t.has_stylebox(&"hover", ThemeKeys.SELECT_CARD))
|
|
assert_true(t.has_stylebox(&"pressed", ThemeKeys.SELECT_CARD), "pressed IS the chosen ring")
|
|
assert_true(t.has_stylebox(&"disabled", ThemeKeys.SELECT_CARD), "an origin may disallow a calling")
|
|
|
|
# A skill chip is a toggle with three states: unpicked, picked, and granted-so-inert.
|
|
assert_true(t.has_stylebox(&"normal", ThemeKeys.SKILL_CHIP))
|
|
assert_true(t.has_stylebox(&"pressed", ThemeKeys.SKILL_CHIP))
|
|
assert_true(t.has_stylebox(&"disabled", ThemeKeys.SKILL_CHIP))
|
|
|
|
# The tags and the primary ring.
|
|
assert_true(t.has_stylebox(&"normal", ThemeKeys.CHOSEN_TAG))
|
|
assert_true(t.has_stylebox(&"normal", ThemeKeys.PRIMARY_TAG))
|
|
assert_true(t.has_stylebox(&"panel", ThemeKeys.PRIMARY_CARD))
|
|
|
|
|
|
func test_section_label_is_a_font_role_on_parchment():
|
|
var t: Theme = load(THEME_PATH)
|
|
assert_true(t.has_font(&"font", ThemeKeys.SECTION_LABEL))
|
|
assert_eq(t.get_color(&"font_color", ThemeKeys.SECTION_LABEL), Palette.INK_LABEL_MUTED)
|