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:
parent
df2356439a
commit
d4d54dc56e
3 changed files with 224 additions and 0 deletions
213
src/game/engine/scenes/tests/ui_theme_proof.gd
Normal file
213
src/game/engine/scenes/tests/ui_theme_proof.gd
Normal 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))
|
||||
1
src/game/engine/scenes/tests/ui_theme_proof.gd.uid
Normal file
1
src/game/engine/scenes/tests/ui_theme_proof.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b48nbf7dy3aup
|
||||
10
src/game/engine/scenes/tests/ui_theme_proof.tscn
Normal file
10
src/game/engine/scenes/tests/ui_theme_proof.tscn
Normal 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")
|
||||
Loading…
Add table
Reference in a new issue