test(scenes-component): Add test scenes with GDScript and UI theme files to validate theme application and rendering

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-04 19:52:32 -07:00
parent df2356439a
commit d4d54dc56e
3 changed files with 224 additions and 0 deletions

View file

@ -0,0 +1,213 @@
extends Control
## Proof scene for p2-73 — the UI theme token pipeline.
##
## Two things are proven in one screenshot:
## 1. GLOBAL APPLY. The left column is bare, NON-overriding Control widgets
## (Button in all interaction states, Panel, PanelContainer, Label,
## ItemList). None of them set a `theme` or any `add_theme_*_override`, so
## whatever copper styling they show comes purely from the project-level
## `gui/theme/custom` = ui_theme.tres. If the global apply failed they would
## render as flat grey Godot defaults.
## 2. SEMANTIC ACCESSOR. The right column is a swatch grid built from
## `ThemeAssets.color(<token>)` calls — accent / semantic / text / border /
## background tokens resolved by name out of the generated token table.
##
## Self-contained + headless-safe: launched directly via scripts/ui-proof-capture.sh
## (weston). Owns its own capture + quit and prints `SCREENSHOT_PATH:`.
## Screenshot path is project-local user:// (NOT /tmp — Flatpak sandbox).
const OUTPUT_DIR: String = "user://screenshots"
const SCREENSHOT_NAME: String = "p2-73-ui-theme"
const THEME_ID: String = "age-of-dwarves"
## Token names exercised through ThemeAssets.color(). These are REAL token
## paths from design-tokens.json (dotted), proving dotted-name resolution.
const SWATCH_TOKENS: Array[String] = [
"accent.gold",
"accent.goldResource",
"accent.science",
"accent.sage",
"text.title",
"text.primary",
"text.secondary",
"semantic.positive",
"semantic.negative",
"semantic.warning",
"semantic.diplomacy",
"border.panel",
"border.focus",
"background.panel",
"background.raised",
]
var _captured: bool = false
func _ready() -> void:
get_viewport().size = Vector2i(1280, 720)
if DisplayServer.get_name() != "headless":
DisplayServer.window_set_size(Vector2i(1280, 720))
# color() lazy-loads ui_theme.tres; set_theme primes the palette side too so
# the proof exercises the same path a booted game would.
ThemeAssets.set_theme(THEME_ID)
_build_ui()
# Two process frames so the theme-driven StyleBoxes paint before capture.
await get_tree().process_frame
await get_tree().process_frame
await get_tree().create_timer(0.4).timeout
_capture(SCREENSHOT_NAME)
await get_tree().create_timer(0.3).timeout
get_tree().quit()
func _build_ui() -> void:
# Stable dark backdrop — uses the deepest background token so even the
# backdrop is token-driven, but read raw so a theme failure can't hide it.
var bg: ColorRect = ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0.05, 0.05, 0.07, 1.0)
add_child(bg)
var root: HBoxContainer = HBoxContainer.new()
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 32)
root.offset_left = 40
root.offset_top = 28
root.offset_right = -40
root.offset_bottom = -28
add_child(root)
root.add_child(_build_widgets_column())
root.add_child(_build_swatch_column())
## Left column: bare default widgets with NO local theme / overrides. Proves the
## project-level gui/theme/custom reaches default Controls.
func _build_widgets_column() -> Control:
var col: VBoxContainer = VBoxContainer.new()
col.custom_minimum_size = Vector2(520, 0)
col.add_theme_constant_override("separation", 14)
# Heading uses a token color so the section title itself is themed copper.
var heading: Label = Label.new()
heading.text = "Global theme (no overrides) — gui/theme/custom"
heading.add_theme_font_size_override("font_size", 20)
heading.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
col.add_child(heading)
# Default Label — inherits Label/colors/font_color + font size from theme.
var label: Label = Label.new()
label.text = "Default Label — themed off-white body text from ui_theme.tres"
col.add_child(label)
# Real default Buttons — normal + pressed (toggled on) + disabled. Each picks
# up its StyleBox + font color from the global theme with zero overrides.
col.add_child(_default_button("Default Button — Normal", false, false))
var pressed_btn: Button = _default_button("Default Button — Pressed", false, true)
pressed_btn.toggle_mode = true
pressed_btn.button_pressed = true
col.add_child(pressed_btn)
col.add_child(_default_button("Default Button — Disabled", true, false))
# Default Panel.
var panel: Panel = Panel.new()
panel.custom_minimum_size = Vector2(0, 56)
var panel_label: Label = Label.new()
panel_label.text = "Default Panel — copper border + dark fill"
panel_label.position = Vector2(12, 16)
panel.add_child(panel_label)
col.add_child(panel)
# Default ItemList with a selected row.
var list: ItemList = ItemList.new()
list.custom_minimum_size = Vector2(0, 110)
list.add_item("Ironveil clan")
list.add_item("Stoneguard clan")
list.add_item("Emberfall clan (selected)")
list.add_item("Deephollow clan")
list.select(2)
col.add_child(list)
return col
func _default_button(text: String, disabled: bool, _pressed: bool) -> Button:
var btn: Button = Button.new()
btn.text = text
btn.disabled = disabled
btn.custom_minimum_size = Vector2(0, 38)
return btn
## Right column: swatch grid built entirely from ThemeAssets.color(token).
func _build_swatch_column() -> Control:
var col: VBoxContainer = VBoxContainer.new()
col.custom_minimum_size = Vector2(560, 0)
col.add_theme_constant_override("separation", 10)
var heading: Label = Label.new()
heading.text = "ThemeAssets.color(token) — semantic accessor"
heading.add_theme_font_size_override("font_size", 20)
heading.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
col.add_child(heading)
var grid: GridContainer = GridContainer.new()
grid.columns = 1
grid.add_theme_constant_override("v_separation", 6)
for token_name: String in SWATCH_TOKENS:
grid.add_child(_swatch_row(token_name))
col.add_child(grid)
return col
func _swatch_row(token_name: String) -> Control:
var row: HBoxContainer = HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var resolved: Color = ThemeAssets.color(token_name)
var chip: ColorRect = ColorRect.new()
chip.custom_minimum_size = Vector2(40, 24)
chip.color = resolved
row.add_child(chip)
var name_label: Label = Label.new()
name_label.text = token_name
name_label.custom_minimum_size = Vector2(190, 0)
row.add_child(name_label)
var hex_label: Label = Label.new()
hex_label.text = "#%02x%02x%02x" % [
int(round(resolved.r * 255.0)),
int(round(resolved.g * 255.0)),
int(round(resolved.b * 255.0)),
]
hex_label.add_theme_color_override("font_color", resolved)
row.add_child(hex_label)
return row
func _capture(name: String) -> void:
if _captured:
return
_captured = true
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("ui_theme_proof: viewport image unavailable")
return
var abs_path: String = ProjectSettings.globalize_path("%s/%s.png" % [OUTPUT_DIR, name])
var err: Error = image.save_png(abs_path)
if err == OK:
print("SCREENSHOT_PATH:%s" % abs_path)
print("Screenshot: %dx%d saved to %s" % [image.get_width(), image.get_height(), abs_path])
else:
push_error("ui_theme_proof: save failed: %s" % error_string(err))

View file

@ -0,0 +1 @@
uid://b48nbf7dy3aup

View file

@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://b2u7th3mpr00f"]
[ext_resource type="Script" path="res://engine/scenes/tests/ui_theme_proof.gd" id="1_script"]
[node name="UiThemeProof" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_script")