Files
stickman/tests/test_prop_editor_theme.gd
ryan ddb298f631 Add visual harmonization tests for editor UI components and ground finish chooser
- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency.
- Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values.
- Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage.
- Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
2026-10-23 15:20:12 -04:00

170 lines
8.8 KiB
GDScript

# Phase 9 Prop Editor - headless regression checks for PropTheme
# (fallback hierarchy, invalid-input fallback, physics-not-themeable).
# Run with:
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\tmp\stickman" --script "res://tests/test_prop_editor_theme.gd"
extends SceneTree
const PROP_THEME := preload("res://scripts/editors/prop/prop_theme.gd")
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
var _checks: int = 0
var _failures: int = 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("\n=== PHASE 9 PROP THEME TEST ===")
await _test_default_file()
await _test_missing_file_fallback()
await _test_custom_override_and_fallback()
await _test_invalid_color_fallback()
await _test_invalid_number_fallback()
await _test_physics_not_themeable()
await _test_primitive_defaults()
await _test_preset_clamp()
await _test_ui_tokens()
print("----------------------------------------")
if _failures == 0:
print("RESULT: ALL PASSED (%d assertions)" % _checks)
quit(0)
else:
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
quit(1)
func check(label: String, condition: bool) -> void:
_checks += 1
if condition:
print("PASS: %s" % label)
else:
_failures += 1
print("FAIL: %s" % label)
func _test_default_file() -> void:
var theme: PropTheme = PROP_THEME.new()
var loaded: bool = theme.load_from(PROP_THEME.DEFAULT_PATH)
check("committed theme loads", loaded)
check("grid_size default 15", theme.grid_size() == 15)
check("outline width default 2.0", is_equal_approx(theme.outline_width_default(), 2.0))
check("outline width min 0.5", is_equal_approx(theme.outline_width_min(), 0.5))
check("outline width max 32.0", is_equal_approx(theme.outline_width_max(), 32.0))
check("wood fill from theme", theme.material_tint(PROP_BLOCK.MaterialPreset.WOOD, "fill").is_equal_approx(Color.from_string("#8C6133", Color.BLACK)))
check("wood outline from theme", theme.material_tint(PROP_BLOCK.MaterialPreset.WOOD, "outline").is_equal_approx(Color.from_string("#40290F", Color.BLACK)))
check("none fill from theme", theme.material_tint(PROP_BLOCK.MaterialPreset.NONE, "fill").is_equal_approx(Color.from_string("#8C6133", Color.BLACK)))
check("canvas bg color", theme.canvas_background_color().is_equal_approx(Color.from_string("#1F1F1F", Color.BLACK)))
func _test_missing_file_fallback() -> void:
var theme: PropTheme = PROP_THEME.new()
var loaded: bool = theme.load_from("res://definitely_missing_theme.json")
check("missing file reports false", not loaded)
check("missing file falls back to defaults", theme.grid_size() == 15 and is_equal_approx(theme.outline_width_default(), 2.0))
check("missing file keeps material tints", theme.material_tint(PROP_BLOCK.MaterialPreset.RUBBER, "fill").is_equal_approx(Color.from_string("#E63333", Color.BLACK)))
func _write_user_theme(text: String, path: String) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(text)
f.close()
func _test_custom_override_and_fallback() -> void:
var path := "user://prop_theme_test_custom.json"
_write_user_theme('{"version":"1.0","canvas":{"grid_size":33},"outline_widths":{"default":5.0}}', path)
var theme: PropTheme = PROP_THEME.new()
check("custom theme loads", theme.load_from(path))
check("custom grid override", theme.grid_size() == 33)
check("custom width override", is_equal_approx(theme.outline_width_default(), 5.0))
check("unoverridden key falls back", theme.material_tint(PROP_BLOCK.MaterialPreset.METAL, "fill").is_equal_approx(Color.from_string("#808C99", Color.BLACK)))
check("unoverridden primitive falls back", is_equal_approx(theme.primitive_default("box", "size_x"), 48.0))
DirAccess.remove_absolute(path)
func _test_invalid_color_fallback() -> void:
var path := "user://prop_theme_test_badcolor.json"
_write_user_theme('{"version":"1.0","material_tints":{"wood":{"fill":"not-a-color","outline":"also bad"}}}', path)
var theme: PropTheme = PROP_THEME.new()
theme.load_from(path)
check("invalid fill falls back to PropBlock tint", theme.material_tint(PROP_BLOCK.MaterialPreset.WOOD, "fill").is_equal_approx(PROP_BLOCK.tint_for(PROP_BLOCK.MaterialPreset.WOOD)))
check("invalid outline falls back to darkened tint", theme.material_tint(PROP_BLOCK.MaterialPreset.WOOD, "outline").is_equal_approx(PROP_BLOCK.tint_for(PROP_BLOCK.MaterialPreset.WOOD).darkened(0.55)))
DirAccess.remove_absolute(path)
func _test_invalid_number_fallback() -> void:
var path := "user://prop_theme_test_badnum.json"
_write_user_theme('{"version":"1.0","outline_widths":{"default":"wide"}}', path)
var theme: PropTheme = PROP_THEME.new()
theme.load_from(path)
check("invalid number falls back", is_equal_approx(theme.outline_width_default(), 2.0))
check("number clamp low", is_equal_approx(theme.number("outline_widths.default", 2.0, 1.0, 10.0), 2.0))
var path2 := "user://prop_theme_test_clamp.json"
_write_user_theme('{"version":"1.0","outline_widths":{"default":500}}', path2)
var theme2: PropTheme = PROP_THEME.new()
theme2.load_from(path2)
check("number clamp high", is_equal_approx(theme2.number("outline_widths.default", 2.0, -INF, 32.0), 32.0))
DirAccess.remove_absolute(path)
DirAccess.remove_absolute(path2)
func _test_physics_not_themeable() -> void:
check("mass lives in PropBlock", is_equal_approx(PROP_BLOCK.mass_for(PROP_BLOCK.MaterialPreset.WOOD), 3.0))
check("bounce lives in PropBlock", is_equal_approx(PROP_BLOCK.physics_material(PROP_BLOCK.MaterialPreset.RUBBER).bounce, 0.85))
var theme: PropTheme = PROP_THEME.new()
check("theme default config has no physics section", not theme.value("physics", null) is Dictionary)
var path := "user://prop_theme_test_physics.json"
_write_user_theme('{"version":"1.0","physics":{"wood":{"mass":99.0}}}', path)
theme.load_from(path)
check("attempted physics override has no effect on accessors", is_equal_approx(theme.outline_width_default(), 2.0) and theme.grid_size() == 15)
DirAccess.remove_absolute(path)
func _test_primitive_defaults() -> void:
var theme: PropTheme = PROP_THEME.new()
theme.load_from(PROP_THEME.DEFAULT_PATH)
check("box size_x default", is_equal_approx(theme.primitive_default("box", "size_x"), 48.0))
check("ball radius default", is_equal_approx(theme.primitive_default("ball", "radius"), 24.0))
check("plank length default", is_equal_approx(theme.primitive_default("plank", "length"), 160.0))
check("triangle height default", is_equal_approx(theme.primitive_default("triangle", "height"), 48.0))
check("circle segments default", is_equal_approx(theme.primitive_default("circle", "segments"), 12.0))
check("unknown key uses caller fallback", is_equal_approx(theme.primitive_default("box", "radius", 7.5), 7.5))
check("unknown primitive uses caller fallback", is_equal_approx(theme.primitive_default("blob", "size_x", 3.0), 3.0))
func _test_preset_clamp() -> void:
var theme: PropTheme = PROP_THEME.new()
theme.load_from(PROP_THEME.DEFAULT_PATH)
check("out-of-range preset clamps to NONE", theme.material_tint(999, "fill").is_equal_approx(theme.material_tint(PROP_BLOCK.MaterialPreset.NONE, "fill")))
check("negative preset clamps to NONE", theme.material_tint(-3, "outline").is_equal_approx(theme.material_tint(PROP_BLOCK.MaterialPreset.NONE, "outline")))
func _test_ui_tokens() -> void:
var theme: PropTheme = PROP_THEME.new()
theme.load_from(PROP_THEME.DEFAULT_PATH)
check("font_body 18", theme.font_body() == 18)
check("font_header 22", theme.font_header() == 22)
check("font_menu 20", theme.font_menu() == 20)
check("font_menu_bar 20", theme.font_menu_bar() == 20)
check("font_mode 24", theme.font_mode() == 24)
check("font_status 16", theme.font_status() == 16)
check("font_prop_name 20", theme.font_prop_name() == 20)
check("font_context_menu 20", theme.font_context_menu() == 20)
check("button_min_height 44", theme.button_min_height() == 44)
check("row_min_height 44", theme.row_min_height() == 44)
check("mode_button_min_height 56", theme.mode_button_min_height() == 56)
check("text_color token", theme.text_color().is_equal_approx(Color.from_string("#F5F7FA", Color.WHITE)))
check("text_muted token", theme.text_muted_color().is_equal_approx(Color.from_string("#AAB2C0", Color.WHITE)))
check("border token", theme.border_color().is_equal_approx(Color.from_string("#FFFFFF33", Color.WHITE)))
check("accent token", theme.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
var path := "user://prop_theme_test_ui.json"
_write_user_theme('{"version":"1.0","ui":{"fonts":{"body":24},"colors":{"accent":"#FF8800"}}}', path)
var custom: PropTheme = PROP_THEME.new()
custom.load_from(path)
check("legacy PropTheme ui font does not override shared tokens", custom.font_body() == 18)
check("legacy PropTheme ui accent does not override shared tokens", custom.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
DirAccess.remove_absolute(path)