extends GdUnitTestSuite ## NEO-131: quest completion reward label HUD tests. const QuestHudController := preload("res://scripts/quest_hud_controller.gd") const QuestDefinitionsClient := preload("res://scripts/quest_definitions_client.gd") const ItemDefsClient := preload("res://scripts/item_definitions_client.gd") const OPERATOR_CHAIN_QUEST_ID := "prototype_quest_operator_chain" class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var body_json: String = "" func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, _request_data: String = "" ) -> Error: request_completed.emit( HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_json.to_utf8_buffer() ) return OK class StubItemDefsClient: extends Node func display_name_for(item_id: String) -> String: if item_id == "survey_drone_kit": return "Survey Drone Kit" return item_id func _make_defs_client(transport: Node) -> Node: var c: Node = QuestDefinitionsClient.new() c.set("injected_http", transport) auto_free(transport) auto_free(c) add_child(c) return c func _gather_defs_json() -> String: return ( '{"schemaVersion":1,"quests":[' + '{"id":"prototype_quest_gather_intro","displayName":"Intro: Salvage Run",' + '"prerequisiteQuestIds":[],"steps":[]}]}' ) func _active_gather_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "quests": [ { "questId": QuestHudController.GATHER_QUEST_ID, "status": "active", "currentStepIndex": 0, "objectiveCounters": {"gather_intro_obj_scrap": 2}, } ], } func _completed_gather_with_summary_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "quests": [ { "questId": QuestHudController.GATHER_QUEST_ID, "status": "completed", "currentStepIndex": 0, "objectiveCounters": {}, "completedAt": "2026-06-07T12:00:00Z", "completionRewardSummary": { "itemGrants": [], "skillXpGrants": [{"skillId": "salvage", "amount": 25}], }, } ], } func _active_operator_chain_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "quests": [ { "questId": OPERATOR_CHAIN_QUEST_ID, "status": "active", "currentStepIndex": 3, "objectiveCounters": {}, } ], } func _completed_operator_chain_with_summary_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "quests": [ { "questId": OPERATOR_CHAIN_QUEST_ID, "status": "completed", "currentStepIndex": 3, "objectiveCounters": {}, "completedAt": "2026-06-07T14:00:00Z", "completionRewardSummary": { "itemGrants": [{"itemId": "survey_drone_kit", "quantity": 1}], "skillXpGrants": [{"skillId": "salvage", "amount": 50}], }, } ], } func _survey_drone_item_defs_json() -> String: return ( '{"schemaVersion":1,"items":[{"id":"survey_drone_kit",' + '"displayName":"Survey Drone Kit","inventorySlotKind":"bag"}]}' ) func test_completion_transition_paints_reward_label_with_skill_xp() -> void: # Arrange var controller: Node = QuestHudController.new() var reward_label := Label.new() auto_free(controller) auto_free(reward_label) add_child(controller) controller.set("_reward_label", reward_label) # Act controller.call("_on_progress_received", _active_gather_snapshot()) controller.call("_on_progress_received", _completed_gather_with_summary_snapshot()) # Assert assert_str(reward_label.text).contains("Quest rewards:") assert_str(reward_label.text).contains("Salvage +25 XP") func test_first_success_paints_reward_when_quest_was_active_in_session() -> void: # Arrange var controller: Node = QuestHudController.new() var reward_label := Label.new() auto_free(controller) auto_free(reward_label) add_child(controller) controller.set("_reward_label", reward_label) controller.call("_mark_quest_active_in_session", QuestHudController.GATHER_QUEST_ID) # Act controller.call("_on_progress_received", _completed_gather_with_summary_snapshot()) # Assert assert_str(reward_label.text).contains("Salvage +25 XP") func test_boot_completed_snapshot_does_not_paint_reward_label() -> void: # Arrange var controller: Node = QuestHudController.new() var reward_label := Label.new() auto_free(controller) auto_free(reward_label) add_child(controller) controller.set("_reward_label", reward_label) # Act controller.call("_on_progress_received", _completed_gather_with_summary_snapshot()) # Assert assert_str(reward_label.text).is_equal("Quest rewards:\n—") func test_completion_transition_paints_item_grant_with_display_name() -> void: # Arrange var controller: Node = QuestHudController.new() var reward_label := Label.new() var item_defs := StubItemDefsClient.new() auto_free(controller) auto_free(reward_label) auto_free(item_defs) add_child(controller) add_child(item_defs) controller.set("_reward_label", reward_label) controller.set("_item_defs_client", item_defs) # Act controller.call("_on_progress_received", _active_operator_chain_snapshot()) controller.call("_on_progress_received", _completed_operator_chain_with_summary_snapshot()) # Assert assert_str(reward_label.text).contains("Survey Drone Kit ×1") assert_str(reward_label.text).contains("Salvage +50 XP") func test_definitions_ready_repaints_reward_label_with_display_name() -> void: # Arrange var defs_transport := MockHttpTransport.new() defs_transport.body_json = _gather_defs_json() var defs_client: Node = _make_defs_client(defs_transport) var controller: Node = QuestHudController.new() var reward_label := Label.new() auto_free(controller) auto_free(reward_label) add_child(controller) controller.set("_defs_client", defs_client) controller.set("_reward_label", reward_label) controller.call("_on_progress_received", _active_gather_snapshot()) controller.call("_on_progress_received", _completed_gather_with_summary_snapshot()) assert_str(reward_label.text).contains(QuestHudController.GATHER_QUEST_ID) # Act defs_client.call("request_sync_from_server") controller.call("_on_definitions_ready", []) # Assert assert_str(reward_label.text).contains("Intro: Salvage Run") assert_str(reward_label.text).contains("Salvage +25 XP") assert_str(reward_label.text).not_contains("prototype_quest_gather_intro") func test_item_definitions_ready_repaints_reward_item_display_name() -> void: # Arrange var item_transport := MockHttpTransport.new() item_transport.body_json = _survey_drone_item_defs_json() var item_defs: Node = ItemDefsClient.new() item_defs.set("injected_http", item_transport) auto_free(item_transport) auto_free(item_defs) add_child(item_defs) await get_tree().process_frame var controller: Node = QuestHudController.new() var reward_label := Label.new() auto_free(controller) auto_free(reward_label) add_child(controller) controller.set("_reward_label", reward_label) controller.set("_item_defs_client", item_defs) controller.call("_on_progress_received", _active_operator_chain_snapshot()) controller.call("_on_progress_received", _completed_operator_chain_with_summary_snapshot()) assert_str(reward_label.text).contains("survey_drone_kit") # Act item_defs.call("request_sync_from_server") controller.call("_on_item_definitions_ready", {}) # Assert assert_str(reward_label.text).contains("Survey Drone Kit ×1") assert_str(reward_label.text).not_contains("survey_drone_kit ×1") func test_sync_failure_shows_progress_error_and_idle_reward_label() -> void: # Arrange var controller: Node = QuestHudController.new() var progress_label := Label.new() var reward_label := Label.new() auto_free(controller) auto_free(progress_label) auto_free(reward_label) add_child(controller) controller.set("_progress_label", progress_label) controller.set("_reward_label", reward_label) controller.set("_last_snapshot", _active_gather_snapshot()) controller.call("_on_progress_received", _completed_gather_with_summary_snapshot()) # Act controller.call("_on_sync_failed", "HTTP 503") # Assert assert_str(progress_label.text).contains("error — HTTP 503") assert_str(reward_label.text).is_equal("Quest rewards:\n—")