413 lines
13 KiB
GDScript
413 lines
13 KiB
GDScript
extends GdUnitTestSuite
|
||
|
||
## NEO-131: quest completion reward label HUD tests.
|
||
## NEO-132: quest_completion_reward_transition signal for economy refresh.
|
||
|
||
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 InventoryClient := preload("res://scripts/inventory_client.gd")
|
||
const SkillProgressionClient := preload("res://scripts/skill_progression_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
|
||
|
||
|
||
class MockEconomySyncClient:
|
||
extends Node
|
||
|
||
var sync_count: int = 0
|
||
|
||
func request_sync_from_server() -> void:
|
||
sync_count += 1
|
||
|
||
|
||
class PendingSyncHttpTransport:
|
||
extends Node
|
||
signal request_completed(
|
||
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||
)
|
||
var request_count: int = 0
|
||
var body_json: String = ""
|
||
|
||
func request(
|
||
_url: String,
|
||
_custom_headers: PackedStringArray = PackedStringArray(),
|
||
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
||
_request_data: String = ""
|
||
) -> Error:
|
||
request_count += 1
|
||
return OK
|
||
|
||
func complete_pending() -> void:
|
||
request_completed.emit(
|
||
HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_json.to_utf8_buffer()
|
||
)
|
||
|
||
|
||
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}],
|
||
"reputationGrants":
|
||
[
|
||
{
|
||
"factionId": "prototype_faction_grid_operators",
|
||
"amount": 15,
|
||
}
|
||
],
|
||
},
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
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")
|
||
assert_str(reward_label.text).contains("Grid Operators +15 rep")
|
||
|
||
|
||
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—")
|
||
|
||
|
||
func test_completion_transition_emits_economy_refresh_signal() -> void:
|
||
# Arrange
|
||
var controller: Node = QuestHudController.new()
|
||
auto_free(controller)
|
||
add_child(controller)
|
||
monitor_signals(controller)
|
||
# Act
|
||
controller.call("_on_progress_received", _active_gather_snapshot())
|
||
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||
# Assert
|
||
await assert_signal(controller).is_emitted("quest_completion_reward_transition", any(), any())
|
||
|
||
|
||
func test_completion_transition_invokes_economy_refresh_callback() -> void:
|
||
# Arrange
|
||
var controller: Node = QuestHudController.new()
|
||
var inventory_client := MockEconomySyncClient.new()
|
||
var skill_client := MockEconomySyncClient.new()
|
||
auto_free(controller)
|
||
auto_free(inventory_client)
|
||
auto_free(skill_client)
|
||
add_child(controller)
|
||
add_child(inventory_client)
|
||
add_child(skill_client)
|
||
controller.set("_inventory_client", inventory_client)
|
||
controller.set("_skill_progression_client", skill_client)
|
||
# Act
|
||
controller.call("_on_progress_received", _active_gather_snapshot())
|
||
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||
# Assert
|
||
assert_int(inventory_client.sync_count).is_equal(1)
|
||
assert_int(skill_client.sync_count).is_equal(1)
|
||
|
||
|
||
func test_boot_completed_does_not_emit_economy_refresh_signal() -> void:
|
||
# Arrange
|
||
var controller: Node = QuestHudController.new()
|
||
auto_free(controller)
|
||
add_child(controller)
|
||
monitor_signals(controller)
|
||
# Act
|
||
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||
# Assert
|
||
await (assert_signal(controller).wait_until(200).is_not_emitted(
|
||
"quest_completion_reward_transition"
|
||
))
|
||
|
||
|
||
func test_completion_transition_queues_economy_refresh_while_clients_busy() -> void:
|
||
# Arrange
|
||
var inv_transport := PendingSyncHttpTransport.new()
|
||
var bag_parts: PackedStringArray = PackedStringArray()
|
||
for i in range(24):
|
||
bag_parts.append('{"slotIndex":%d,"quantity":0}' % i)
|
||
inv_transport.body_json = (
|
||
(
|
||
'{"schemaVersion":1,"playerId":"dev-local-1","bagSlots":[%s],'
|
||
+ '"equipmentSlots":[{"slotIndex":0,"quantity":0}]}'
|
||
)
|
||
% ", ".join(bag_parts)
|
||
)
|
||
var skill_transport := PendingSyncHttpTransport.new()
|
||
skill_transport.body_json = (
|
||
'{"schemaVersion":1,"playerId":"dev-local-1","skills":'
|
||
+ '[{"id":"salvage","xp":0,"level":1}]}'
|
||
)
|
||
var inventory_client: Node = InventoryClient.new()
|
||
var skill_client: Node = SkillProgressionClient.new()
|
||
inventory_client.set("injected_http", inv_transport)
|
||
skill_client.set("injected_http", skill_transport)
|
||
var controller: Node = QuestHudController.new()
|
||
auto_free(inv_transport)
|
||
auto_free(skill_transport)
|
||
auto_free(inventory_client)
|
||
auto_free(skill_client)
|
||
auto_free(controller)
|
||
add_child(inventory_client)
|
||
add_child(skill_client)
|
||
add_child(controller)
|
||
controller.set("_inventory_client", inventory_client)
|
||
controller.set("_skill_progression_client", skill_client)
|
||
inventory_client.call("request_sync_from_server")
|
||
skill_client.call("request_sync_from_server")
|
||
# Act
|
||
controller.call("_on_progress_received", _active_gather_snapshot())
|
||
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||
# Assert
|
||
assert_int(inv_transport.request_count).is_equal(1)
|
||
assert_int(skill_transport.request_count).is_equal(1)
|
||
inv_transport.complete_pending()
|
||
skill_transport.complete_pending()
|
||
assert_int(inv_transport.request_count).is_equal(2)
|
||
assert_int(skill_transport.request_count).is_equal(2)
|