Add unique identifier for test_phase3c_walk_recovery.gd
This commit is contained in:
@@ -0,0 +1,620 @@
|
||||
# test_phase3c_bugfix.gd
|
||||
# Headless regression suite for the Phase 3c bugfix pass (five bugs):
|
||||
#
|
||||
# Bug 1 (queue rows never parented): QueuePanel._make_row now calls
|
||||
# _list.add_child(panel), so rows are actually in the rendered tree.
|
||||
# Regression: assert the rendered tree — _list.get_child_count() matches
|
||||
# queue size + the always-present empty label, each _rows entry is a direct
|
||||
# child of _list, and each row has non-zero geometry after the panel pops up
|
||||
# (layout frame) instead of asserting only _rows.size().
|
||||
#
|
||||
# Bug 2 (rule rows never parented): same fix in RulePanel._make_row, same
|
||||
# rendered-tree assertions for the filtered rule list.
|
||||
#
|
||||
# Bug 3 ("Edit Queue…" gating): in BOTH the Director action popup
|
||||
# (ACT_EDIT_QUEUE) and the stickman right-click context popup
|
||||
# (RIG_CTX_EDIT_QUEUE) the item is disabled when rig.get_queue().is_empty()
|
||||
# and enabled otherwise, refreshed on about_to_popup by
|
||||
# _refresh_action_popup_items() / _refresh_rig_context_items().
|
||||
#
|
||||
# Bug 4 ("Edit Rules…" gating): the same two popups disable
|
||||
# ACT_EDIT_RULES / RIG_CTX_EDIT_RULES when no rule has
|
||||
# trigger.source == rig.get_instance_id(), and enable them otherwise.
|
||||
#
|
||||
# Bug 5 (font/size config): sandbox_theme.json's extended fonts block is
|
||||
# parsed — ui_font_bold/ui_font_italic, per-widget sizes
|
||||
# (queue/rule/action_editor/rule_editor/panel_row/panel_title), the
|
||||
# panel_title_bold / rule_label_bold / badge_bold flags, and the
|
||||
# "action_popup" object form — with defaults preserved when the keys are
|
||||
# absent. action_popup_emoji_size is consumed (previously dead) and applied
|
||||
# by _apply_popup_theme; the four Phase 3c widgets expose apply_font(...)
|
||||
# and tolerate null fonts; stage_director_visuals.set_style() honours the
|
||||
# bold flags.
|
||||
#
|
||||
# Run with:
|
||||
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase3c_bugfix.gd --path .
|
||||
#
|
||||
# Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL.
|
||||
|
||||
extends SceneTree
|
||||
|
||||
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var watchdog := create_timer(120.0)
|
||||
watchdog.timeout.connect(func() -> void:
|
||||
print("FAIL: watchdog timeout - test run aborted before quit()")
|
||||
quit(2))
|
||||
|
||||
print("")
|
||||
print("========================================================")
|
||||
print(" PHASE 3c BUGFIX REGRESSION TEST (headless)")
|
||||
print("========================================================")
|
||||
|
||||
await _test_queue_rows_in_rendered_tree()
|
||||
await _test_rule_rows_in_rendered_tree()
|
||||
await _test_edit_queue_gating()
|
||||
await _test_edit_rules_gating()
|
||||
await _test_theme_keys_parsed_and_defaults()
|
||||
await _test_apply_font_null_fonts_and_bold_title()
|
||||
await _test_popup_emoji_size_applied()
|
||||
await _test_visuals_style_bold_flags()
|
||||
|
||||
print("--------------------------------------------------------")
|
||||
if _failures == 0:
|
||||
print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 1: QueuePanel rows are parented into _list and render.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_queue_rows_in_rendered_tree() -> void:
|
||||
print("")
|
||||
print("--- QueuePanel rows live in _list with real geometry ---")
|
||||
var stage := _new_stage()
|
||||
var rig: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
_check(rig != null, "stickman rig spawns for queue tree test")
|
||||
if rig == null:
|
||||
await _free_stage(stage)
|
||||
return
|
||||
|
||||
rig.queue_action({ "type": "walk_to", "target": Vector2(100, -100) })
|
||||
rig.queue_action({ "type": "speak", "text": "Hello", "duration": 2.0 })
|
||||
rig.queue_action({ "type": "wait", "duration": 3.0 })
|
||||
rig.queue_action({ "type": "ragdoll" })
|
||||
|
||||
stage._open_queue_panel(rig)
|
||||
var qp: QueuePanel = stage._queue_panel
|
||||
_check(qp._rows.size() == 4, "queue panel tracks 4 rows (got %d)" % qp._rows.size())
|
||||
# The empty label is an always-present child of _list, so the rendered tree
|
||||
# must hold label + one row per queue entry. This is the assertion the
|
||||
# pre-fix suite missed (it only counted _rows.size()).
|
||||
_check(qp._list.get_child_count() == 5,
|
||||
"_list holds the empty label + 4 row children (got %d)" % qp._list.get_child_count())
|
||||
var all_parented := true
|
||||
var all_children := true
|
||||
for row: PanelContainer in qp._rows:
|
||||
if row.get_parent() != qp._list:
|
||||
all_parented = false
|
||||
if not qp._list.get_children().has(row):
|
||||
all_children = false
|
||||
_check(all_parented, "every queue row's parent is _list")
|
||||
_check(all_children, "every queue row is a child of _list")
|
||||
|
||||
# After the popup lays out, every row must have non-zero geometry.
|
||||
await process_frame
|
||||
await process_frame
|
||||
var geometry_ok := true
|
||||
for i: int in qp._rows.size():
|
||||
var row: PanelContainer = qp._rows[i]
|
||||
if row.size.x <= 0.0 or row.size.y <= 0.0:
|
||||
geometry_ok = false
|
||||
print(" row %d size = %s" % [i, str(row.size)])
|
||||
_check(geometry_ok, "all 4 queue rows have non-zero geometry after layout")
|
||||
|
||||
# Clear the queue: refresh must empty the rendered tree down to the label.
|
||||
rig.clear_queue()
|
||||
qp.refresh()
|
||||
await process_frame
|
||||
_check(qp._rows.is_empty(), "queue refresh clears _rows after clear")
|
||||
_check(qp._list.get_child_count() == 1,
|
||||
"queue _list drops to just the empty label after clear (got %d)" % qp._list.get_child_count())
|
||||
_check(qp._empty_label.visible, "queue empty label visible after clear")
|
||||
|
||||
# Re-add one action and refresh: rendered tree follows.
|
||||
rig.queue_action({ "type": "wait", "duration": 1.0 })
|
||||
qp.refresh()
|
||||
await process_frame
|
||||
_check(qp._list.get_child_count() == 2,
|
||||
"queue _list regrows one row after re-add (got %d)" % qp._list.get_child_count())
|
||||
_check(qp._rows[0].get_parent() == qp._list, "regrown queue row is parented into _list")
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 2: RulePanel rows are parented into _list and render.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_rule_rows_in_rendered_tree() -> void:
|
||||
print("")
|
||||
print("--- RulePanel rows live in _list with real geometry ---")
|
||||
var stage := _new_stage()
|
||||
var rigA: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
var rigB: StickmanRig = stage._spawner.spawn("stickman", Vector2(400, 0))
|
||||
_check(rigA != null and rigB != null, "two stickmen spawn for rule tree test")
|
||||
if rigA == null or rigB == null:
|
||||
await _free_stage(stage)
|
||||
return
|
||||
|
||||
var rule0 := {
|
||||
"id": 0,
|
||||
"trigger": { "type": "arrived_at_waypoint", "source": rigA.get_instance_id(), "target": -1, "params": { "waypoint_pos": Vector2(100, -100) } },
|
||||
"actions": [{ "type": "speak", "target": rigB.get_instance_id(), "params": { "text": "Hi", "duration": 1.0 } }],
|
||||
}
|
||||
var rule1 := {
|
||||
"id": 1,
|
||||
"trigger": { "type": "action_finished", "source": rigA.get_instance_id(), "target": -1, "params": {} },
|
||||
"actions": [{ "type": "wait", "target": rigB.get_instance_id(), "params": { "duration": 2.0 } }],
|
||||
}
|
||||
var rules: Array[Dictionary] = [rule0, rule1]
|
||||
stage._event_rules = rules
|
||||
|
||||
stage._open_rules_panel_for_rig(rigA)
|
||||
var rp: RulePanel = stage._rule_panel
|
||||
_check(rp._rows.size() == 2, "rule panel tracks 2 filtered rows (got %d)" % rp._rows.size())
|
||||
_check(rp._list.get_child_count() == 3,
|
||||
"rule _list holds the empty label + 2 row children (got %d)" % rp._list.get_child_count())
|
||||
var all_parented := true
|
||||
for row: PanelContainer in rp._rows:
|
||||
if row.get_parent() != rp._list:
|
||||
all_parented = false
|
||||
_check(all_parented, "every rule row's parent is _list")
|
||||
|
||||
await process_frame
|
||||
await process_frame
|
||||
var geometry_ok := true
|
||||
for i: int in rp._rows.size():
|
||||
var row: PanelContainer = rp._rows[i]
|
||||
if row.size.x <= 0.0 or row.size.y <= 0.0:
|
||||
geometry_ok = false
|
||||
print(" rule row %d size = %s" % [i, str(row.size)])
|
||||
_check(geometry_ok, "all rule rows have non-zero geometry after layout")
|
||||
|
||||
# A rig with no rules shows an empty rendered tree (label only).
|
||||
stage._open_rules_panel_for_rig(rigB)
|
||||
await process_frame
|
||||
_check(stage._rule_panel._rows.is_empty(), "no-rule filter clears _rows")
|
||||
_check(stage._rule_panel._list.get_child_count() == 1,
|
||||
"no-rule filter leaves just the empty label (got %d)" % stage._rule_panel._list.get_child_count())
|
||||
_check(stage._rule_panel._empty_label.visible, "rule empty label visible with no rules")
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 3: "Edit Queue…" gating (queue empty -> disabled, else enabled) in both
|
||||
# popups, refreshed on about_to_popup.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_edit_queue_gating() -> void:
|
||||
print("")
|
||||
print("--- Edit Queue… gating in action popup + rig context popup ---")
|
||||
var stage := _new_stage()
|
||||
var rig: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
_check(rig != null, "stickman rig spawns for queue gating test")
|
||||
if rig == null:
|
||||
await _free_stage(stage)
|
||||
return
|
||||
|
||||
# Empty queue -> ACT_EDIT_QUEUE disabled in the Director action popup.
|
||||
stage._context_rig = rig
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
var act_idx: int = stage._action_popup.get_item_index(stage.ACT_EDIT_QUEUE)
|
||||
_check(act_idx >= 0, "action popup contains ACT_EDIT_QUEUE")
|
||||
_check(stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Queue… disabled with an empty queue")
|
||||
|
||||
# Empty queue -> RIG_CTX_EDIT_QUEUE disabled in the rig right-click menu.
|
||||
stage._open_rig_context(Vector2(0, 0), rig)
|
||||
var ctx_idx: int = stage._rig_context_popup.get_item_index(stage.RIG_CTX_EDIT_QUEUE)
|
||||
_check(ctx_idx >= 0, "rig context popup contains RIG_CTX_EDIT_QUEUE")
|
||||
_check(stage._rig_context_popup.is_item_disabled(ctx_idx),
|
||||
"rig context Edit Queue… disabled with an empty queue")
|
||||
stage._rig_context_popup.hide()
|
||||
|
||||
# Non-empty queue -> both items enabled on the next popup.
|
||||
rig.queue_action({ "type": "walk_to", "target": Vector2(300, -300) })
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
act_idx = stage._action_popup.get_item_index(stage.ACT_EDIT_QUEUE)
|
||||
_check(not stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Queue… enabled with a queued action")
|
||||
stage._action_popup.hide()
|
||||
|
||||
stage._open_rig_context(Vector2(0, 0), rig)
|
||||
ctx_idx = stage._rig_context_popup.get_item_index(stage.RIG_CTX_EDIT_QUEUE)
|
||||
_check(not stage._rig_context_popup.is_item_disabled(ctx_idx),
|
||||
"rig context Edit Queue… enabled with a queued action")
|
||||
stage._rig_context_popup.hide()
|
||||
|
||||
# Back to empty: gating follows the live queue (disabled again).
|
||||
rig.clear_queue()
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
act_idx = stage._action_popup.get_item_index(stage.ACT_EDIT_QUEUE)
|
||||
_check(stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Queue… re-disabled after the queue clears")
|
||||
stage._action_popup.hide()
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 4: "Edit Rules…" gating (no rule with source == rig -> disabled, else
|
||||
# enabled) in both popups.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_edit_rules_gating() -> void:
|
||||
print("")
|
||||
print("--- Edit Rules… gating in action popup + rig context popup ---")
|
||||
var stage := _new_stage()
|
||||
var rigA: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
var rigB: StickmanRig = stage._spawner.spawn("stickman", Vector2(400, 0))
|
||||
_check(rigA != null and rigB != null, "two stickmen spawn for rules gating test")
|
||||
if rigA == null or rigB == null:
|
||||
await _free_stage(stage)
|
||||
return
|
||||
|
||||
# No rules at all -> both popups disable Edit Rules… for rigA.
|
||||
stage._context_rig = rigA
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
var act_idx: int = stage._action_popup.get_item_index(stage.ACT_EDIT_RULES)
|
||||
_check(stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Rules… disabled when rigA has no rules")
|
||||
stage._action_popup.hide()
|
||||
stage._open_rig_context(Vector2(0, 0), rigA)
|
||||
var ctx_idx: int = stage._rig_context_popup.get_item_index(stage.RIG_CTX_EDIT_RULES)
|
||||
_check(stage._rig_context_popup.is_item_disabled(ctx_idx),
|
||||
"rig context Edit Rules… disabled when rigA has no rules")
|
||||
stage._rig_context_popup.hide()
|
||||
|
||||
# A rule owned by ANOTHER rig (rigB) must not enable rigA's entry.
|
||||
var other_rule := {
|
||||
"id": 0,
|
||||
"trigger": { "type": "arrived_at_waypoint", "source": rigB.get_instance_id(), "target": -1, "params": { "waypoint_pos": Vector2(100, -100) } },
|
||||
"actions": [],
|
||||
}
|
||||
var other_rules: Array[Dictionary] = [other_rule]
|
||||
stage._event_rules = other_rules
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
act_idx = stage._action_popup.get_item_index(stage.ACT_EDIT_RULES)
|
||||
_check(stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Rules… stays disabled for rigA when only rigB owns rules")
|
||||
stage._action_popup.hide()
|
||||
stage._open_rig_context(Vector2(0, 0), rigA)
|
||||
ctx_idx = stage._rig_context_popup.get_item_index(stage.RIG_CTX_EDIT_RULES)
|
||||
_check(stage._rig_context_popup.is_item_disabled(ctx_idx),
|
||||
"rig context Edit Rules… stays disabled for rigA when only rigB owns rules")
|
||||
stage._rig_context_popup.hide()
|
||||
|
||||
# A rule sourced by rigA enables both entries.
|
||||
var my_rule := {
|
||||
"id": 1,
|
||||
"trigger": { "type": "action_finished", "source": rigA.get_instance_id(), "target": -1, "params": {} },
|
||||
"actions": [{ "type": "wait", "target": rigB.get_instance_id(), "params": { "duration": 1.0 } }],
|
||||
}
|
||||
var my_rules: Array[Dictionary] = [my_rule]
|
||||
stage._event_rules = my_rules
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
act_idx = stage._action_popup.get_item_index(stage.ACT_EDIT_RULES)
|
||||
_check(not stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Rules… enabled when rigA owns a rule")
|
||||
stage._action_popup.hide()
|
||||
stage._open_rig_context(Vector2(0, 0), rigA)
|
||||
ctx_idx = stage._rig_context_popup.get_item_index(stage.RIG_CTX_EDIT_RULES)
|
||||
_check(not stage._rig_context_popup.is_item_disabled(ctx_idx),
|
||||
"rig context Edit Rules… enabled when rigA owns a rule")
|
||||
stage._rig_context_popup.hide()
|
||||
|
||||
# Removing the rigA rule re-disables the entries (live _event_rules read).
|
||||
var empty_rules: Array[Dictionary] = []
|
||||
stage._event_rules = empty_rules
|
||||
stage._action_popup.popup(Rect2i(10, 10, 0, 0))
|
||||
act_idx = stage._action_popup.get_item_index(stage.ACT_EDIT_RULES)
|
||||
_check(stage._action_popup.is_item_disabled(act_idx),
|
||||
"action popup Edit Rules… re-disabled after the rigA rule is removed")
|
||||
stage._action_popup.hide()
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 5a: sandbox_theme.json extended fonts block parses; defaults preserved.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_theme_keys_parsed_and_defaults() -> void:
|
||||
print("")
|
||||
print("--- Theme parse: new keys honoured, absent keys keep defaults ---")
|
||||
var stage := _new_stage()
|
||||
|
||||
var full_path := "user://bugfix_theme_full.json"
|
||||
_write_theme(full_path, {
|
||||
"fonts": {
|
||||
"ui_font": "",
|
||||
"emoji_font": "",
|
||||
"ui_font_bold": "",
|
||||
"ui_font_italic": "",
|
||||
"action_popup_font_size": 30,
|
||||
"action_popup_emoji_size": 19,
|
||||
"queue_panel_font_size": 21,
|
||||
"rule_panel_font_size": 22,
|
||||
"action_editor_font_size": 23,
|
||||
"rule_editor_font_size": 24,
|
||||
"panel_row_font_size": 14,
|
||||
"panel_title_font_size": 17,
|
||||
"panel_title_bold": false,
|
||||
"rule_label_bold": true,
|
||||
"badge_bold": false,
|
||||
"action_popup": { "size": 33, "bold": true, "italic": false },
|
||||
},
|
||||
"grid": { "snap_size": 35.0 },
|
||||
})
|
||||
stage._load_theme(full_path)
|
||||
|
||||
_check(stage._queue_panel_font_size == 21, "queue_panel_font_size parsed (got %d)" % stage._queue_panel_font_size)
|
||||
_check(stage._rule_panel_font_size == 22, "rule_panel_font_size parsed (got %d)" % stage._rule_panel_font_size)
|
||||
_check(stage._action_editor_font_size == 23, "action_editor_font_size parsed (got %d)" % stage._action_editor_font_size)
|
||||
_check(stage._rule_editor_font_size == 24, "rule_editor_font_size parsed (got %d)" % stage._rule_editor_font_size)
|
||||
_check(stage._panel_row_font_size == 14, "panel_row_font_size parsed (got %d)" % stage._panel_row_font_size)
|
||||
_check(stage._panel_title_font_size == 17, "panel_title_font_size parsed (got %d)" % stage._panel_title_font_size)
|
||||
_check(stage._panel_title_bold == false, "panel_title_bold parsed (got %s)" % str(stage._panel_title_bold))
|
||||
_check(stage._rule_label_bold == true, "rule_label_bold parsed (got %s)" % str(stage._rule_label_bold))
|
||||
_check(stage._badge_bold == false, "badge_bold parsed (got %s)" % str(stage._badge_bold))
|
||||
# action_popup object form overrides the flat action_popup_font_size.
|
||||
_check(stage._action_popup_font_size == 33,
|
||||
"action_popup {size} overrides the flat key (got %d)" % stage._action_popup_font_size)
|
||||
_check(stage._action_popup_bold == true, "action_popup {bold} parsed (got %s)" % str(stage._action_popup_bold))
|
||||
_check(stage._action_popup_italic == false, "action_popup {italic} parsed (got %s)" % str(stage._action_popup_italic))
|
||||
_check(stage._action_popup_emoji_size == 19,
|
||||
"action_popup_emoji_size consumed from the theme (got %d)" % stage._action_popup_emoji_size)
|
||||
_check(int(stage._font_sizes.get("queue_panel", -1)) == 21, "_font_sizes[queue_panel] matches")
|
||||
_check(int(stage._font_sizes.get("action_editor", -1)) == 23, "_font_sizes[action_editor] matches")
|
||||
_check(int(stage._font_sizes.get("panel_row", -1)) == 14, "_font_sizes[panel_row] matches")
|
||||
_check(int(stage._font_sizes.get("panel_title", -1)) == 17, "_font_sizes[panel_title] matches")
|
||||
_check(bool(stage._font_sizes.get("panel_title_bold", true)) == false, "_font_sizes[panel_title_bold] matches")
|
||||
_check(stage._font_sizes.get("bold_font", null) == null, "_font_sizes[bold_font] null with no ui_font configured")
|
||||
|
||||
# Theme missing the new keys -> shipped defaults remain.
|
||||
var bare_path := "user://bugfix_theme_bare.json"
|
||||
_write_theme(bare_path, { "grid": { "snap_size": 10.0 } })
|
||||
stage._load_theme(bare_path)
|
||||
_check(stage._action_popup_font_size == 24, "absent action_popup_font_size keeps default 24 (got %d)" % stage._action_popup_font_size)
|
||||
_check(stage._action_popup_emoji_size == 22, "absent action_popup_emoji_size keeps default 22 (got %d)" % stage._action_popup_emoji_size)
|
||||
_check(stage._queue_panel_font_size == 18, "absent queue_panel_font_size keeps default 18 (got %d)" % stage._queue_panel_font_size)
|
||||
_check(stage._rule_panel_font_size == 18, "absent rule_panel_font_size keeps default 18 (got %d)" % stage._rule_panel_font_size)
|
||||
_check(stage._action_editor_font_size == 18, "absent action_editor_font_size keeps default 18 (got %d)" % stage._action_editor_font_size)
|
||||
_check(stage._rule_editor_font_size == 18, "absent rule_editor_font_size keeps default 18 (got %d)" % stage._rule_editor_font_size)
|
||||
_check(stage._panel_row_font_size == 16, "absent panel_row_font_size keeps default 16 (got %d)" % stage._panel_row_font_size)
|
||||
_check(stage._panel_title_font_size == 18, "absent panel_title_font_size keeps default 18 (got %d)" % stage._panel_title_font_size)
|
||||
_check(stage._panel_title_bold == true, "absent panel_title_bold keeps default true (got %s)" % str(stage._panel_title_bold))
|
||||
_check(stage._rule_label_bold == false, "absent rule_label_bold keeps default false (got %s)" % str(stage._rule_label_bold))
|
||||
_check(stage._badge_bold == true, "absent badge_bold keeps default true (got %s)" % str(stage._badge_bold))
|
||||
_check(stage._action_popup_bold == false, "absent action_popup object form keeps default bold false (got %s)" % str(stage._action_popup_bold))
|
||||
_check(stage._action_popup_italic == false, "absent action_popup object form keeps default italic false (got %s)" % str(stage._action_popup_italic))
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 5b: apply_font(...) tolerates null fonts and applies bold titles.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_apply_font_null_fonts_and_bold_title() -> void:
|
||||
print("")
|
||||
print("--- apply_font: null fonts no-crash + bold title override ---")
|
||||
var stage := _new_stage()
|
||||
|
||||
# The four Phase 3c widgets are already added by _build_ui; re-applying with
|
||||
# null fonts + an empty sizes dict must be a safe no-op-ish walk.
|
||||
stage._queue_panel.apply_font(null, null, {})
|
||||
stage._rule_panel.apply_font(null, null, {})
|
||||
stage._action_editor.apply_font(null, null, {})
|
||||
stage._rule_editor.apply_font(null, null, {})
|
||||
_check(true, "apply_font(null, null, {}) runs on all four widgets without error")
|
||||
|
||||
# With a ui font + bold font configured, the queue panel title label gets the
|
||||
# bold font and the panel_title size; the body walks the base font/size.
|
||||
var base_font := SystemFont.new()
|
||||
var bold_font := SystemFont.new()
|
||||
stage._queue_panel.apply_font(base_font, null, {
|
||||
"queue_panel": 20,
|
||||
"panel_row": 15,
|
||||
"panel_title": 22,
|
||||
"panel_title_bold": true,
|
||||
"bold_font": bold_font,
|
||||
})
|
||||
_check(stage._queue_panel._title_label.get_theme_font("font") == bold_font,
|
||||
"queue panel title uses the bold font when panel_title_bold is true")
|
||||
_check(stage._queue_panel._title_label.get_theme_font_size("font_size") == 22,
|
||||
"queue panel title uses the panel_title size (got %d)" % stage._queue_panel._title_label.get_theme_font_size("font_size"))
|
||||
_check(stage._queue_panel._empty_label.get_theme_font_size("font_size") == 15,
|
||||
"queue panel empty label uses the row size (got %d)" % stage._queue_panel._empty_label.get_theme_font_size("font_size"))
|
||||
|
||||
# With panel_title_bold false the title falls back to the ui font.
|
||||
stage._queue_panel.apply_font(base_font, null, {
|
||||
"queue_panel": 20,
|
||||
"panel_row": 15,
|
||||
"panel_title": 22,
|
||||
"panel_title_bold": false,
|
||||
"bold_font": bold_font,
|
||||
})
|
||||
_check(stage._queue_panel._title_label.get_theme_font("font") == base_font,
|
||||
"queue panel title uses the ui font when panel_title_bold is false")
|
||||
|
||||
# apply_font must not break row creation: a panel refreshed afterwards still
|
||||
# parents rows and applies the row size.
|
||||
var rig: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
_check(rig != null, "rig spawns for apply_font row test")
|
||||
if rig != null:
|
||||
rig.queue_action({ "type": "wait", "duration": 1.0 })
|
||||
stage._open_queue_panel(rig)
|
||||
await process_frame
|
||||
_check(stage._queue_panel._list.get_child_count() == 2,
|
||||
"queue rows still build after apply_font (got %d)" % stage._queue_panel._list.get_child_count())
|
||||
var row: PanelContainer = stage._queue_panel._rows[0]
|
||||
# Row structure: PanelContainer -> HBoxContainer -> [number, summary, edit, remove, drag].
|
||||
var row_hbox := row.get_child(0) as HBoxContainer
|
||||
var summary_label := row_hbox.get_child(1) as Label
|
||||
_check(summary_label.get_theme_font_size("font_size") == 15,
|
||||
"queue row summary uses the row font size after apply_font (got %d)" % summary_label.get_theme_font_size("font_size"))
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 5c: action_popup_emoji_size is applied by _apply_popup_theme.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_popup_emoji_size_applied() -> void:
|
||||
print("")
|
||||
print("--- action_popup_emoji_size applied by _apply_popup_theme ---")
|
||||
var stage := _new_stage()
|
||||
|
||||
# Baseline: no fonts configured -> popup keeps the action_popup_font_size.
|
||||
var no_font_path := "user://bugfix_theme_no_font.json"
|
||||
_write_theme(no_font_path, { "fonts": { "action_popup_font_size": 24, "action_popup_emoji_size": 22 } })
|
||||
stage._load_theme(no_font_path)
|
||||
stage._apply_popup_theme(stage._action_popup)
|
||||
_check(stage._action_popup.get_theme_font_size("font_size") == 24,
|
||||
"popup keeps action_popup_font_size when no font is configured (got %d)" % stage._action_popup.get_theme_font_size("font_size"))
|
||||
_check(not stage._action_popup.has_theme_font_override("font"),
|
||||
"popup has no font override when no font is configured")
|
||||
|
||||
# With an emoji font configured the emoji size becomes the menu font size.
|
||||
var emoji_font := SystemFont.new()
|
||||
stage._emoji_font = emoji_font
|
||||
stage._apply_popup_theme(stage._action_popup)
|
||||
_check(stage._action_popup.has_theme_font_override("font"),
|
||||
"popup uses the configured emoji font")
|
||||
_check(stage._action_popup.get_theme_font_size("font_size") == 22,
|
||||
"action_popup_emoji_size applied as the menu font size (got %d)" % stage._action_popup.get_theme_font_size("font_size"))
|
||||
|
||||
# A non-default emoji size from the theme also flows through.
|
||||
var sized_path := "user://bugfix_theme_sized.json"
|
||||
_write_theme(sized_path, { "fonts": { "action_popup_emoji_size": 19 } })
|
||||
stage._load_theme(sized_path)
|
||||
_check(stage._action_popup_emoji_size == 19,
|
||||
"theme action_popup_emoji_size 19 parsed (got %d)" % stage._action_popup_emoji_size)
|
||||
stage._emoji_font = emoji_font
|
||||
stage._apply_popup_theme(stage._action_popup)
|
||||
_check(stage._action_popup.get_theme_font_size("font_size") == 19,
|
||||
"applied emoji size follows the parsed key (got %d)" % stage._action_popup.get_theme_font_size("font_size"))
|
||||
|
||||
# Style-variant bold flag: bold font wins over the emoji font.
|
||||
var bold_font := SystemFont.new()
|
||||
stage._action_popup_bold = true
|
||||
stage._ui_font_bold = bold_font
|
||||
stage._apply_popup_theme(stage._action_popup)
|
||||
_check(stage._action_popup.get_theme_font("font") == bold_font,
|
||||
"action_popup bold flag selects the bold font")
|
||||
_check(stage._action_popup.get_theme_font_size("font_size") == 19,
|
||||
"emoji size still applied when the bold font is active (got %d)" % stage._action_popup.get_theme_font_size("font_size"))
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug 5d: stage_director_visuals.set_style() honours rule_label_bold / badge_bold.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _test_visuals_style_bold_flags() -> void:
|
||||
print("")
|
||||
print("--- StageDirectorVisuals set_style bold flags ---")
|
||||
var stage := _new_stage()
|
||||
var visuals = stage._director_visuals
|
||||
_check(visuals != null, "stage owns a director visuals node")
|
||||
|
||||
var ui_font := SystemFont.new()
|
||||
var bold_font := SystemFont.new()
|
||||
var emoji_font := SystemFont.new()
|
||||
visuals.ui_font = ui_font
|
||||
visuals.bold_font = bold_font
|
||||
visuals.emoji_font = emoji_font
|
||||
|
||||
# Defaults from an empty theme block: badge bold on, rule label not bold.
|
||||
visuals.set_style({})
|
||||
_check(visuals.rule_label_bold == false, "set_style default rule_label_bold false")
|
||||
_check(visuals.badge_bold == true, "set_style default badge_bold true")
|
||||
_check(visuals._rule_label_font() == ui_font,
|
||||
"rule label uses ui_font when rule_label_bold is off")
|
||||
_check(visuals._badge_font() == bold_font,
|
||||
"badge uses the bold font when badge_bold is on")
|
||||
|
||||
# Bold rule labels + non-bold badges.
|
||||
visuals.set_style({ "fonts": { "rule_label_bold": true, "badge_bold": false } })
|
||||
_check(visuals.rule_label_bold == true, "set_style honours rule_label_bold true")
|
||||
_check(visuals.badge_bold == false, "set_style honours badge_bold false")
|
||||
_check(visuals._rule_label_font() == bold_font,
|
||||
"rule label uses the bold font when rule_label_bold is on")
|
||||
_check(visuals._badge_font() == emoji_font,
|
||||
"badge falls back to the emoji font when badge_bold is off")
|
||||
|
||||
# Back off both: rule label returns to ui_font.
|
||||
visuals.set_style({ "fonts": { "rule_label_bold": false, "badge_bold": false } })
|
||||
_check(visuals._rule_label_font() == ui_font,
|
||||
"rule label returns to ui_font when rule_label_bold toggles off")
|
||||
_check(visuals._badge_font() == emoji_font, "badge stays on the emoji font with badge_bold off")
|
||||
|
||||
# Bold fallback when no bold font is configured: uses ui/emoji fonts.
|
||||
visuals.bold_font = null
|
||||
visuals.set_style({ "fonts": { "rule_label_bold": true, "badge_bold": true } })
|
||||
_check(visuals._rule_label_font() == ui_font,
|
||||
"rule label falls back to ui_font when bold_font is null")
|
||||
_check(visuals._badge_font() == emoji_font,
|
||||
"badge falls back to the emoji font when bold_font is null")
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _new_stage() -> Node2D:
|
||||
var stage: Node2D = STAGE_SCENE.instantiate()
|
||||
root.add_child(stage)
|
||||
stage._snap_enabled = false
|
||||
return stage
|
||||
|
||||
|
||||
func _free_stage(stage: Node2D) -> void:
|
||||
stage.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _write_theme(path: String, data: Dictionary) -> void:
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify(data, " ", false))
|
||||
file.close()
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: " + message)
|
||||
else:
|
||||
_failures += 1
|
||||
print("FAIL: " + message)
|
||||
Reference in New Issue
Block a user