315 lines
14 KiB
GDScript
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)
|