Files
modbison/scripts/smoke_test_avatar_studio_ui.gd
T

315 lines
14 KiB
GDScript

extends SceneTree
## Smoke test for redesigned AvatarCustomizationUI (Tasks 680b6e91d699 & f87183b6d9e4).
##
## Verifies:
## 1. Header Bar: Back button (< Back [ESC], Vector2(120, 38)), Title ("Avatar Studio"),
## Live Buffalo Chips balance label ("Wallet: %d Buffalo Chips"), Status pill ("All changes saved").
## 2. Category Tabs: Tab 0 "Body & Head", Tab 1 "Clothing", Tab 2 "Accessories", Tab 3 "Skin & Colors".
## 3. Human-friendly item names (display_name lookup, no raw IDs like hair_mohawk_01 as primary title).
## 4. Equipped badges ("✓ EQUIPPED") and highlighted borders on equipped items.
## 5. Skin & Colors presets, shading slider, and custom RGB sliders.
## 6. Preview Camera Zoom & Framing Presets:
## - Presets: "Full Body" (3.5, 0.98), "Head" (1.6, 1.62), "Torso" (2.2, 1.12), "Feet" (1.8, 0.35).
## - Zoom clamping: [1.5, 4.5].
## - Mouse-wheel zoom events.
## - Camera controls bar with preset buttons, zoom slider, and +/- buttons.
## 7. Auto-Save on Exit:
## - user://loadout.json written.
## - AvatarSystem sync.
## - Signals: back_requested and closed.
const AvatarCustomizationUI := preload("res://scripts/avatar/avatar_customization_ui.gd")
const AvatarLoadout := preload("res://scripts/avatar/avatar_loadout.gd")
var passed := 0
var failed := 0
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
print("SMOKE: Starting AvatarStudioUI verification...")
var ui: AvatarCustomizationUI = AvatarCustomizationUI.new()
ui.name = "AvatarCustomizationUI"
ui.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_child(ui)
await process_frame
await process_frame
# 1. Header Bar Verification
var back_btn: Button = ui.find_child("BackButton", true, false) as Button
_check("Back button exists", back_btn != null)
if back_btn:
_check("Back button text is '< Back [ESC]'", back_btn.text == "< Back [ESC]")
_check("Back button custom_minimum_size is at least (120, 38)",
back_btn.custom_minimum_size.x >= 120 and back_btn.custom_minimum_size.y >= 38)
var title_lbl: Label = ui.find_child("TitleLabel", true, false) as Label
_check("TitleLabel exists", title_lbl != null)
if title_lbl:
_check("TitleLabel text is 'Avatar Studio'", title_lbl.text == "Avatar Studio")
var fsize: int = title_lbl.get_theme_font_size("font_size")
_check("TitleLabel font_size >= 24", fsize >= 24)
var status_pill: Label = ui.find_child("StatusPill", true, false) as Label
_check("StatusPill exists", status_pill != null)
if status_pill:
_check("StatusPill text is 'All changes saved'", status_pill.text == "All changes saved")
var wallet_lbl: Label = ui.find_child("WalletBalanceLabel", true, false) as Label
_check("WalletBalanceLabel exists", wallet_lbl != null)
if wallet_lbl:
_check("WalletBalanceLabel starts with 'Wallet:' and contains 'Buffalo Chips'",
wallet_lbl.text.begins_with("Wallet:") and wallet_lbl.text.contains("Buffalo Chips"))
ui.update_wallet_balance(750)
_check("WalletBalanceLabel formats 'Wallet: 750 Buffalo Chips'",
wallet_lbl.text == "Wallet: 750 Buffalo Chips")
# 2. Category Tabs Verification
var tab_bar: TabBar = ui.find_child("CategoryTabBar", true, false) as TabBar
_check("CategoryTabBar exists", tab_bar != null)
if tab_bar:
_check("CategoryTabBar has 4 tabs", tab_bar.tab_count == 4)
_check("Tab 0 is 'Body & Head'", tab_bar.get_tab_title(0) == "Body & Head")
_check("Tab 1 is 'Clothing'", tab_bar.get_tab_title(1) == "Clothing")
_check("Tab 2 is 'Accessories'", tab_bar.get_tab_title(2) == "Accessories")
_check("Tab 3 is 'Skin & Colors'", tab_bar.get_tab_title(3) == "Skin & Colors")
# Check Category constants
_check("Category 0 has slots: base_body, head, hair, face",
ui.CATEGORIES[0]["slots"] == ["base_body", "head", "hair", "face"])
_check("Category 1 has slots: chest, pants, torso, left_hand, right_hand, left_foot, right_foot",
ui.CATEGORIES[1]["slots"] == ["chest", "pants", "torso", "left_hand", "right_hand", "left_foot", "right_foot"])
_check("Category 2 has slots: back, accessory",
ui.CATEGORIES[2]["slots"] == ["back", "accessory"])
# 3. Friendly Item Names Verification
var mohawk_name: String = ui.get_friendly_item_name("hair_mohawk_01")
_check("hair_mohawk_01 friendly name is 'Mohawk'", mohawk_name == "Mohawk")
var waterbuffalo_name: String = ui.get_friendly_item_name("hair_waterbuffalo_01")
_check("hair_waterbuffalo_01 friendly name is 'Water Buffalo Lodge Hat'", waterbuffalo_name == "Water Buffalo Lodge Hat")
var right_glove_name: String = ui.get_friendly_item_name("right_hand_glove_01")
_check("right_hand_glove_01 friendly name is 'Right Glove'", right_glove_name == "Right Glove")
var right_boot_name: String = ui.get_friendly_item_name("right_foot_boot_01")
_check("right_foot_boot_01 friendly name is 'Right Boot'", right_boot_name == "Right Boot")
var cyber_name: String = ui.get_friendly_item_name("head_cyber_01")
_check("head_cyber_01 friendly name is 'Cyber Head'", cyber_name == "Cyber Head")
var cargo_name: String = ui.get_friendly_item_name("pants_cargo_01")
_check("pants_cargo_01 friendly name is 'Cargo Pants'", cargo_name == "Cargo Pants")
var base_name: String = ui.get_friendly_item_name("base_human_male_01")
_check("base_human_male_01 friendly name is 'Human Male Base'", base_name == "Human Male Base")
var none_name: String = ui.get_friendly_item_name("")
_check("empty asset_id friendly name is '(None)'", none_name == "(None)")
# 4. Item Selection, Equipped Badges, and Highlighted Borders
ui._select_slot("hair")
await process_frame
var item_list: VBoxContainer = ui.find_child("ItemList", true, false) as VBoxContainer
_check("ItemList exists and has children", item_list != null and item_list.get_child_count() > 0)
var mohawk_btn: Button = ui.find_child("Item_hair_mohawk_01", true, false) as Button
_check("Item_hair_mohawk_01 button exists", mohawk_btn != null)
if mohawk_btn:
_check("Mohawk button primary text does not show raw ID 'hair_mohawk_01'",
not mohawk_btn.text.begins_with("hair_mohawk_01"))
_check("Mohawk button contains friendly name 'Mohawk'",
mohawk_btn.text.contains("Mohawk"))
# Equip mohawk
var tracker := {"loadout_changed": false, "back_requested": false, "closed": false}
ui.loadout_changed.connect(func(_ld): tracker["loadout_changed"] = true)
ui._on_equip_pressed("hair_mohawk_01")
await process_frame
_check("Equipping mohawk emitted loadout_changed signal", tracker["loadout_changed"])
_check("Loadout hair slot is 'hair_mohawk_01'",
ui.loadout.get_equipment("hair") == "hair_mohawk_01")
# Re-check button equipped badge & border
mohawk_btn = ui.find_child("Item_hair_mohawk_01", true, false) as Button
if mohawk_btn:
var badge: Label = mohawk_btn.find_child("EquippedBadge", true, false) as Label
_check("Equipped mohawk has '✓ EQUIPPED' badge",
badge != null or mohawk_btn.text.contains("✓ EQUIPPED"))
var sb: StyleBox = mohawk_btn.get_theme_stylebox("normal")
if sb is StyleBoxFlat:
_check("Equipped mohawk has highlighted border (width >= 2)",
(sb as StyleBoxFlat).border_width_left >= 2)
# Unequip hair
ui._on_equip_pressed("")
await process_frame
_check("Unequipping hair clears hair slot", ui.loadout.get_equipment("hair") == "")
var none_btn: Button = ui.find_child("Item_none", true, false) as Button
if none_btn:
var none_badge: Label = none_btn.find_child("EquippedBadge", true, false) as Label
_check("(None) button has '✓ EQUIPPED' badge when slot is unequipped",
none_badge != null or none_btn.text.contains("✓ EQUIPPED"))
# 5. Category Tab Switching
ui._on_category_tab_changed(1) # Clothing
await process_frame
var slot_list: HBoxContainer = ui.find_child("SlotList", true, false) as HBoxContainer
_check("Switching to Clothing tab populates slot buttons", slot_list != null and slot_list.get_child_count() >= 5)
var chest_slot_btn: Button = ui.find_child("Slot_chest", true, false) as Button
_check("Slot_chest button exists in Clothing tab", chest_slot_btn != null)
var right_hand_slot_btn: Button = ui.find_child("Slot_right_hand", true, false) as Button
_check("Slot_right_hand button exists in Clothing tab", right_hand_slot_btn != null)
var right_foot_slot_btn: Button = ui.find_child("Slot_right_foot", true, false) as Button
_check("Slot_right_foot button exists in Clothing tab", right_foot_slot_btn != null)
# Verify selecting right_hand slot populates items
ui._select_slot("right_hand")
await process_frame
var right_glove_btn: Button = ui.find_child("Item_right_hand_glove_01", true, false) as Button
_check("Item_right_hand_glove_01 button exists under right_hand slot", right_glove_btn != null)
# 6. Skin & Colors Tab Verification
ui._on_category_tab_changed(3) # Skin & Colors
await process_frame
var items_section: Control = ui.find_child("ItemsSection", true, false) as Control
var skin_section: Control = ui.find_child("SkinSection", true, false) as Control
_check("Skin & Colors tab hides ItemsSection and shows SkinSection",
items_section != null and not items_section.visible and skin_section != null and skin_section.visible)
var shade_slider: HSlider = ui.find_child("ShadeSlider", true, false) as HSlider
_check("ShadeSlider exists", shade_slider != null)
var slider_r: HSlider = ui.find_child("SliderR", true, false) as HSlider
var slider_g: HSlider = ui.find_child("SliderG", true, false) as HSlider
var slider_b: HSlider = ui.find_child("SliderB", true, false) as HSlider
_check("RGB sliders (R, G, B) exist", slider_r != null and slider_g != null and slider_b != null)
var skin_preview: ColorRect = ui.find_child("SkinColorPreview", true, false) as ColorRect
_check("SkinColorPreview exists", skin_preview != null)
# Test swatch press
var test_color := Color(0.33, 0.21, 0.15)
ui._on_skin_swatch_pressed(test_color)
await process_frame
_check("Pressing swatch updates loadout skin_tone",
ui.loadout.skin_tone.is_equal_approx(test_color))
if skin_preview:
_check("SkinColorPreview matches swatch color",
skin_preview.color.is_equal_approx(test_color))
# Test RGB slider change
if slider_r:
slider_r.value = 0.75
ui._on_rgb_slider_changed(0.75)
await process_frame
_check("Adjusting RGB slider updates loadout skin_tone.r",
is_equal_approx(ui.loadout.skin_tone.r, 0.75))
_check("Status pill remains 'All changes saved'",
status_pill != null and status_pill.text == "All changes saved")
# 7. Camera Zoom & Framing Presets Verification (Task f87183b6d9e4)
# Check framing presets
ui.frame_camera_preset("Head")
_check("Head preset frames camera to dist 1.6, height 1.62",
is_equal_approx(ui.camera_distance, 1.6) and is_equal_approx(ui.camera_target_height, 1.62))
ui.frame_camera_preset("Torso")
_check("Torso preset frames camera to dist 2.2, height 1.12",
is_equal_approx(ui.camera_distance, 2.2) and is_equal_approx(ui.camera_target_height, 1.12))
ui.frame_camera_preset("Feet")
_check("Feet preset frames camera to dist 1.8, height 0.35",
is_equal_approx(ui.camera_distance, 1.8) and is_equal_approx(ui.camera_target_height, 0.35))
ui.frame_camera_preset("Full Body")
_check("Full Body preset frames camera to dist 3.5, height 0.98",
is_equal_approx(ui.camera_distance, 3.5) and is_equal_approx(ui.camera_target_height, 0.98))
# Check zoom clamping [1.5, 4.5]
ui.set_camera_zoom(1.0) # should clamp to 1.5
_check("set_camera_zoom(1.0) clamps to 1.5", is_equal_approx(ui.camera_distance, 1.5))
ui.set_camera_zoom(5.0) # should clamp to 4.5
_check("set_camera_zoom(5.0) clamps to 4.5", is_equal_approx(ui.camera_distance, 4.5))
# Check mouse wheel zoom on preview container
ui.set_camera_zoom(3.0)
var wheel_up := InputEventMouseButton.new()
wheel_up.button_index = MOUSE_BUTTON_WHEEL_UP
wheel_up.pressed = true
ui._on_preview_gui_input(wheel_up)
_check("Wheel up zooms in by 0.2m (3.0 -> 2.8)", is_equal_approx(ui.camera_distance, 2.8))
var wheel_down := InputEventMouseButton.new()
wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
wheel_down.pressed = true
ui._on_preview_gui_input(wheel_down)
_check("Wheel down zooms out by 0.2m (2.8 -> 3.0)", is_equal_approx(ui.camera_distance, 3.0))
# Check Camera Controls Bar UI elements
var p_head_btn: Button = ui.find_child("Preset_Head", true, false) as Button
var p_full_btn: Button = ui.find_child("Preset_FullBody", true, false) as Button
var p_torso_btn: Button = ui.find_child("Preset_Torso", true, false) as Button
var p_feet_btn: Button = ui.find_child("Preset_Feet", true, false) as Button
_check("Preset buttons (Full Body, Head, Torso, Feet) exist",
p_head_btn != null and p_full_btn != null and p_torso_btn != null and p_feet_btn != null)
var zoom_slider: HSlider = ui.find_child("CameraZoomSlider", true, false) as HSlider
var zoom_in: Button = ui.find_child("ZoomInBtn", true, false) as Button
var zoom_out: Button = ui.find_child("ZoomOutBtn", true, false) as Button
_check("Zoom slider and +/- buttons exist",
zoom_slider != null and zoom_in != null and zoom_out != null)
# 8. Auto-Save on Exit Verification (Task f87183b6d9e4)
ui.back_requested.connect(func(): tracker["back_requested"] = true)
ui.closed.connect(func(): tracker["closed"] = true)
# Equip a distinct item before exit
ui._select_slot("head")
ui._on_equip_pressed("head_cyber_01")
await process_frame
# Trigger save_and_exit
await ui.save_and_exit()
await process_frame
_check("save_and_exit emits back_requested signal", tracker["back_requested"])
_check("save_and_exit emits closed signal", tracker["closed"])
# Check that user://loadout.json exists and contains head_cyber_01
_check("user://loadout.json was saved to disk", FileAccess.file_exists("user://loadout.json"))
if FileAccess.file_exists("user://loadout.json"):
var f := FileAccess.open("user://loadout.json", FileAccess.READ)
var json_str := f.get_as_text()
f.close()
_check("user://loadout.json contains 'head_cyber_01'", json_str.contains("head_cyber_01"))
# Check AvatarSystem sync
var avatars := root.get_node_or_null("AvatarSystem")
if avatars != null and avatars.current_loadout != null:
_check("AvatarSystem current_loadout synced 'head_cyber_01'",
avatars.current_loadout.get_equipment("head") == "head_cyber_01")
ui.queue_free()
_finish()
func _check(description: String, condition: bool) -> bool:
if condition:
print(" PASS: ", description)
passed += 1
else:
printerr(" FAIL: ", description)
failed += 1
return condition
func _finish() -> void:
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
quit(0 if failed == 0 else 1)