291 lines
8.7 KiB
GDScript
291 lines
8.7 KiB
GDScript
extends GdUnitTestSuite
|
|
|
|
## NEO-153: contract HUD controller issue key, active label, reward transition.
|
|
|
|
const ContractHudController := preload("res://scripts/contract_hud_controller.gd")
|
|
const ContractClient := preload("res://scripts/contract_client.gd")
|
|
|
|
const INSTANCE_ID := "ci_test_instance"
|
|
const ENCOUNTER_ID := "prototype_combat_pocket"
|
|
|
|
|
|
class MockHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
var response_code: int = 200
|
|
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,
|
|
response_code,
|
|
PackedStringArray(),
|
|
body_json.to_utf8_buffer()
|
|
)
|
|
return OK
|
|
|
|
|
|
class NoopHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
|
|
func request(
|
|
_url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
|
_request_data: String = ""
|
|
) -> Error:
|
|
return OK
|
|
|
|
|
|
class PendingIssueHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
|
|
func request(
|
|
_url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
_method: HTTPClient.Method = HTTPClient.METHOD_POST,
|
|
_request_data: String = ""
|
|
) -> Error:
|
|
return OK
|
|
|
|
|
|
class TrackingFactionClient:
|
|
extends Node
|
|
var sync_count: int = 0
|
|
|
|
func request_sync_from_server() -> void:
|
|
sync_count += 1
|
|
|
|
|
|
static func _active_snapshot() -> Dictionary:
|
|
return {
|
|
"schemaVersion": 1,
|
|
"playerId": "dev-local-1",
|
|
"contracts":
|
|
[
|
|
{
|
|
"contractInstanceId": INSTANCE_ID,
|
|
"templateId": ContractHudController.PROTOTYPE_CONTRACT_TEMPLATE_ID,
|
|
"templateDisplayName": "Clear Combat Pocket (Repeat)",
|
|
"status": "active",
|
|
"encounterTemplateId": ENCOUNTER_ID,
|
|
"seedBucket": ContractHudController.PROTOTYPE_CONTRACT_SEED_BUCKET,
|
|
"issuedAt": "2026-06-28T12:00:00Z",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
static func _completed_snapshot() -> Dictionary:
|
|
return {
|
|
"schemaVersion": 1,
|
|
"playerId": "dev-local-1",
|
|
"contracts":
|
|
[
|
|
{
|
|
"contractInstanceId": INSTANCE_ID,
|
|
"templateId": ContractHudController.PROTOTYPE_CONTRACT_TEMPLATE_ID,
|
|
"templateDisplayName": "Clear Combat Pocket (Repeat)",
|
|
"status": "completed",
|
|
"encounterTemplateId": ENCOUNTER_ID,
|
|
"seedBucket": ContractHudController.PROTOTYPE_CONTRACT_SEED_BUCKET,
|
|
"issuedAt": "2026-06-28T12:00:00Z",
|
|
"completedAt": "2026-06-28T13:00:00Z",
|
|
"completionRewardSummary":
|
|
{
|
|
"itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}],
|
|
"skillXpGrants": [{"skillId": "salvage", "amount": 15}],
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
func _make_contract_client(sync_transport: Node, issue_transport: Node = null) -> Node:
|
|
var c: Node = ContractClient.new()
|
|
c.set("injected_sync_http", sync_transport)
|
|
var issue_http: Node = issue_transport if issue_transport != null else sync_transport
|
|
c.set("injected_issue_http", issue_http)
|
|
auto_free(sync_transport)
|
|
if issue_transport != null and issue_transport != sync_transport:
|
|
auto_free(issue_transport)
|
|
auto_free(c)
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func _build_controller(sync_transport: Node, issue_transport: Node) -> Dictionary:
|
|
var controller: Node = ContractHudController.new()
|
|
var contract: Node = _make_contract_client(sync_transport, issue_transport)
|
|
var active_label := Label.new()
|
|
var issue_label := Label.new()
|
|
var reward_label := Label.new()
|
|
auto_free(controller)
|
|
auto_free(active_label)
|
|
auto_free(issue_label)
|
|
auto_free(reward_label)
|
|
add_child(controller)
|
|
await get_tree().process_frame
|
|
controller.call(
|
|
"setup",
|
|
contract,
|
|
active_label,
|
|
issue_label,
|
|
reward_label,
|
|
Callable(self, "_noop_http_config")
|
|
)
|
|
await get_tree().process_frame
|
|
return {
|
|
"controller": controller,
|
|
"contract": contract,
|
|
"active_label": active_label,
|
|
"issue_label": issue_label,
|
|
"reward_label": reward_label,
|
|
}
|
|
|
|
|
|
func _noop_http_config(_client: Node) -> void:
|
|
pass
|
|
|
|
|
|
func test_try_issue_key_input_handles_shift_c() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), PendingIssueHttpTransport.new()
|
|
)
|
|
var event := InputEventKey.new()
|
|
event.pressed = true
|
|
event.shift_pressed = true
|
|
event.keycode = KEY_C
|
|
# Act
|
|
var handled: bool = bool(built["controller"].call("try_issue_key_input", event))
|
|
# Assert
|
|
assert_bool(handled).is_true()
|
|
assert_str(built["issue_label"].text).contains("sending")
|
|
|
|
|
|
func test_try_issue_key_input_shows_busy_while_issue_in_flight() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), PendingIssueHttpTransport.new()
|
|
)
|
|
var event := InputEventKey.new()
|
|
event.pressed = true
|
|
event.shift_pressed = true
|
|
event.keycode = KEY_C
|
|
built["controller"].call("try_issue_key_input", event)
|
|
# Act
|
|
var handled: bool = bool(built["controller"].call("try_issue_key_input", event))
|
|
# Assert
|
|
assert_bool(handled).is_true()
|
|
assert_str(built["issue_label"].text).contains("busy")
|
|
|
|
|
|
func test_active_label_shows_encounter_objective_id() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
# Act
|
|
built["controller"].call("_on_contracts_received", _active_snapshot())
|
|
# Assert
|
|
assert_str(built["active_label"].text).contains(ENCOUNTER_ID)
|
|
assert_str(built["active_label"].text).contains("active")
|
|
|
|
|
|
func test_issue_deny_shows_economy_cap_copy() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
# Act
|
|
built["controller"].call(
|
|
"_on_issue_result_received", {"issued": false, "reasonCode": "economy_cap_exceeded"}
|
|
)
|
|
# Assert
|
|
assert_str(built["issue_label"].text).contains("economy cap exceeded")
|
|
|
|
|
|
func test_issue_deny_shows_active_contract_exists_copy() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
# Act
|
|
built["controller"].call(
|
|
"_on_issue_result_received", {"issued": false, "reasonCode": "active_contract_exists"}
|
|
)
|
|
# Assert
|
|
assert_str(built["issue_label"].text).contains("active contract already issued")
|
|
|
|
|
|
func test_reward_label_populates_on_active_to_completed_transition() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
built["controller"].call("_on_contracts_received", _active_snapshot())
|
|
# Act
|
|
built["controller"].call("_on_contracts_received", _completed_snapshot())
|
|
# Assert
|
|
assert_str(built["reward_label"].text).contains("Contract rewards:")
|
|
assert_str(built["reward_label"].text).contains("Salvage +15 XP")
|
|
|
|
|
|
func test_completion_transition_refreshes_faction_standing() -> void:
|
|
# Arrange
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
var faction_client := TrackingFactionClient.new()
|
|
auto_free(faction_client)
|
|
add_child(faction_client)
|
|
built["controller"].set("_faction_standing_client", faction_client)
|
|
built["controller"].call("_on_contracts_received", _active_snapshot())
|
|
# Act
|
|
built["controller"].call("_on_contracts_received", _completed_snapshot())
|
|
# Assert
|
|
assert_int(faction_client.sync_count).is_equal(1)
|
|
|
|
|
|
func test_reward_label_populates_when_first_get_is_already_completed() -> void:
|
|
# Arrange — issue marks session active; encounter completes before GET returns active
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
var issue_row: Dictionary = (_active_snapshot()["contracts"] as Array)[0] as Dictionary
|
|
built["controller"].call("_on_issue_result_received", {"issued": true, "contract": issue_row})
|
|
# Act
|
|
built["controller"].call("_on_contracts_received", _completed_snapshot())
|
|
# Assert
|
|
assert_str(built["reward_label"].text).contains("Contract rewards:")
|
|
assert_str(built["reward_label"].text).contains("Salvage +15 XP")
|
|
|
|
|
|
func test_completed_get_clears_stale_issue_patch_on_active_label() -> void:
|
|
# Arrange — simulate successful issue patch still held when encounter-complete GET arrives
|
|
var built: Dictionary = await _build_controller(
|
|
NoopHttpTransport.new(), NoopHttpTransport.new()
|
|
)
|
|
var issue_row: Dictionary = (_active_snapshot()["contracts"] as Array)[0] as Dictionary
|
|
built["controller"].set("_issue_contract_patch", issue_row.duplicate(true))
|
|
built["controller"].set("_tracked_instance_id", INSTANCE_ID)
|
|
# Act
|
|
built["controller"].call("_on_contracts_received", _completed_snapshot())
|
|
# Assert
|
|
assert_str(built["active_label"].text).contains("completed")
|
|
assert_str(built["active_label"].text).not_contains(": active —")
|