neon-sprawl/client/test/contract_hud_controller_tes...

250 lines
7.5 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
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_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_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 —")