extends GutTest ## The control theme. A missing texture gives a null and a silently unstyled ## control, which looks exactly like the theme never having been applied -- so ## every path is checked rather than assumed. func test_every_declared_texture_loads() -> void: for key in UiTheme.TEXTURES: var tex := UiTheme.texture(key) assert_not_null(tex, "%s (%s) did not load" % [key, UiTheme.TEXTURES[key]]) assert_gt(tex.get_width(), 0) assert_gt(tex.get_height(), 0) func test_the_theme_styles_the_controls_the_game_actually_builds() -> void: var t := UiTheme.get_theme() for state in ["normal", "hover", "pressed", "disabled", "focus"]: assert_true(t.has_stylebox(state, "Button"), "Button has no %s style" % state) assert_true(t.has_stylebox("panel", "Panel")) assert_true(t.has_stylebox("panel", "PanelContainer")) assert_true(t.has_stylebox("normal", "LineEdit")) ## The pack's surfaces are cream with a black outline, so text on them has to be ## dark. Everything the game draws over the world stays light; only controls ## sitting on this art flip. func test_text_on_the_packs_surfaces_is_dark() -> void: var t := UiTheme.get_theme() for key in ["font_color", "font_hover_color", "font_pressed_color"]: var c: Color = t.get_color(key, "Button") assert_lt(c.get_luminance(), 0.35, "%s is too light for cream art" % key) assert_lt((t.get_color("font_color", "LineEdit") as Color).get_luminance(), 0.35) ## Nine-patch margins are what stop a 1px border smearing across a stretched ## button. Zero margins would render as a plain stretched texture. func test_button_styles_are_nine_patched() -> void: var t := UiTheme.get_theme() for state in ["normal", "hover", "pressed", "disabled"]: var sb := t.get_stylebox(state, "Button") as StyleBoxTexture assert_not_null(sb) for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]: assert_gt(sb.get_texture_margin(side), 0.0, "%s has no stretch margin on side %d" % [state, side]) ## The corner size cannot exceed half the sprite, or the two caps overlap and ## the middle has negative width. func test_stretch_margins_fit_inside_their_sprites() -> void: var t := UiTheme.get_theme() for entry in [["normal", "Button"], ["panel", "Panel"], ["normal", "LineEdit"]]: var sb := t.get_stylebox(entry[0], entry[1]) as StyleBoxTexture var size := Vector2(sb.texture.get_size()) assert_lte(sb.get_texture_margin(SIDE_LEFT) + sb.get_texture_margin(SIDE_RIGHT), size.x, "%s margins exceed its width" % entry[1]) assert_lte(sb.get_texture_margin(SIDE_TOP) + sb.get_texture_margin(SIDE_BOTTOM), size.y, "%s margins exceed its height" % entry[1]) ## Built once and shared. Themes are read-only data, so a second instance would ## be waste rather than isolation. func test_the_theme_is_built_once() -> void: assert_eq(UiTheme.get_theme(), UiTheme.get_theme()) ## The four button states have to be four different pictures, or the interface ## gives no feedback and the pack's state art is going unused. func test_the_button_states_are_visually_distinct() -> void: var seen := {} for key in ["button", "button_hover", "button_pressed", "button_disabled"]: seen[UiTheme.TEXTURES[key]] = true assert_eq(seen.size(), 4, "two button states share a texture") ## The inventory draws slots at exactly twice the source size. A non-integer ## scale on pixel art with nearest filtering gives uneven pixel widths, which on ## a 1px border reads as a wobble along every edge. func test_the_inventory_slot_is_drawn_at_a_whole_multiple() -> void: var slot := UiTheme.texture("slot") var scale := HUD_SLOT / float(slot.get_width()) assert_eq(scale, floorf(scale), "slot art is scaled by %f" % scale) assert_eq(slot.get_width(), slot.get_height(), "the slot should be square") const HUD_SLOT := 64.0 ## A Control resolves its theme by walking up the parent chain, which is why ## one assignment near the root covers screens that share no parent. Checked on ## the mechanism rather than on a Window, so it runs headless. func test_the_theme_reaches_controls_through_their_ancestors() -> void: var root: Control = autofree(Control.new()) root.theme = UiTheme.get_theme() var middle := Control.new() root.add_child(middle) var button := Button.new() middle.add_child(button) assert_eq(button.get_theme_stylebox("normal", "Button"), UiTheme.get_theme().get_stylebox("normal", "Button"), "a button two levels down should still find the theme") assert_eq(button.get_theme_color("font_color", "Button"), UiTheme.INK) func test_applying_to_nothing_is_harmless() -> void: UiTheme.apply_to(null) assert_true(true, "a null root must not be a crash") func test_a_themed_root_fills_its_parent_and_carries_the_theme() -> void: var root: Control = autofree(UiTheme.themed_root()) assert_eq(root.theme, UiTheme.get_theme()) assert_eq(root.anchor_right, 1.0) assert_eq(root.anchor_bottom, 1.0) ## Every screen in the game is built in code and hangs off either a plain Node ## or a CanvasLayer, and theme inheritance travels Control-to-Control ONLY -- ## the chain breaks at the first non-Control parent. Setting the theme once on ## the Window therefore looked like it worked and styled nothing, which is a ## mistake that is invisible until somebody looks at the screen. So: every ## screen is instantiated here and asked what its buttons actually resolve. func test_every_screen_actually_gets_the_theme() -> void: var screens := { "HUD": "res://src/ui/hud.gd", "GameMenu": "res://src/ui/game_menu.gd", "CharacterSelect": "res://src/ui/character_select.gd", "UpgradeScreen": "res://src/ui/upgrade_screen.gd", "SettingsScreen": "res://src/ui/settings_screen.gd", "CreditsScreen": "res://src/ui/credits_screen.gd", "MainMenu": "res://src/ui/main_menu.gd", } var wanted := UiTheme.get_theme().get_stylebox("normal", "Button") for name in screens: var node: Node = (load(screens[name]) as Script).new() add_child_autofree(node) var buttons := node.find_children("*", "Button", true, false) assert_gt(buttons.size(), 0, "%s has no button to check" % name) for b: Button in buttons: assert_eq(b.get_theme_stylebox("normal", "Button"), wanted, "%s: '%s' is not themed" % [name, b.text])