201 lines
6.2 KiB
GDScript
201 lines
6.2 KiB
GDScript
extends GdUnitTestSuite
|
|
|
|
# Note: declaration-order-only edits in main scene scripts can still trigger hook-required
|
|
# client test restaging; this suite is included in the same commit without behavior changes.
|
|
const HotbarClient := preload("res://scripts/hotbar_loadout_client.gd")
|
|
const HotbarState := preload("res://scripts/hotbar_state.gd")
|
|
|
|
|
|
class MockHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
var last_url: String = ""
|
|
var last_body: String = ""
|
|
var last_method: HTTPClient.Method = HTTPClient.METHOD_GET
|
|
var _queue: Array[Dictionary] = []
|
|
|
|
func enqueue(result: int, code: int, body: String) -> void:
|
|
_queue.append({"result": result, "code": code, "body": body})
|
|
|
|
func request(
|
|
url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
|
request_data: String = ""
|
|
) -> Error:
|
|
last_url = url
|
|
last_method = method
|
|
last_body = request_data
|
|
if _queue.is_empty():
|
|
return ERR_UNAVAILABLE
|
|
var r: Dictionary = _queue.pop_front()
|
|
request_completed.emit(
|
|
r["result"], r["code"], PackedStringArray(), str(r["body"]).to_utf8_buffer()
|
|
)
|
|
return OK
|
|
|
|
|
|
class HoldFirstThenAutoTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
var request_count: int = 0
|
|
var last_body: String = ""
|
|
var _first: bool = true
|
|
|
|
func request(
|
|
_url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
|
request_data: String = ""
|
|
) -> Error:
|
|
request_count += 1
|
|
last_body = request_data
|
|
var body_json := (
|
|
'{"schemaVersion":1,"updated":true,"loadout":'
|
|
+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
|
|
)
|
|
var body_bytes: PackedByteArray = body_json.to_utf8_buffer()
|
|
if _first:
|
|
_first = false
|
|
call_deferred("_emit", body_bytes)
|
|
else:
|
|
request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)
|
|
return OK
|
|
|
|
func _emit(body_bytes: PackedByteArray) -> void:
|
|
request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)
|
|
|
|
|
|
func _make_client(transport: Node) -> Node:
|
|
var c: Node = HotbarClient.new()
|
|
c.set("injected_http", transport)
|
|
auto_free(transport)
|
|
auto_free(c)
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func _sample_slots_json() -> String:
|
|
return (
|
|
'{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":['
|
|
+ '{"slotIndex":0,"abilityId":"prototype_pulse"},'
|
|
+ '{"slotIndex":1,"abilityId":null},'
|
|
+ '{"slotIndex":2,"abilityId":"prototype_guard"},'
|
|
+ '{"slotIndex":3,"abilityId":null},'
|
|
+ '{"slotIndex":4,"abilityId":null},'
|
|
+ '{"slotIndex":5,"abilityId":null},'
|
|
+ '{"slotIndex":6,"abilityId":null},'
|
|
+ '{"slotIndex":7,"abilityId":null}'
|
|
+ "]}"
|
|
)
|
|
|
|
|
|
func test_sync_get_applies_slots_to_hotbar_state() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _sample_slots_json())
|
|
var c := _make_client(transport)
|
|
var state := HotbarState.new()
|
|
auto_free(state)
|
|
add_child(state)
|
|
c.call("set_hotbar_state", state)
|
|
# Act
|
|
c.call("request_sync_from_server")
|
|
# Assert
|
|
var slots: Array = state.call("slots_snapshot")
|
|
assert_that(slots.size()).is_equal(8)
|
|
assert_that(slots[0]).is_equal("prototype_pulse")
|
|
assert_that(slots[2]).is_equal("prototype_guard")
|
|
assert_that(slots[1]).is_null()
|
|
|
|
|
|
func test_bind_slot_posts_expected_payload() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
(
|
|
'{"schemaVersion":1,"updated":true,"loadout":'
|
|
+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
|
|
)
|
|
)
|
|
var c := _make_client(transport)
|
|
# Act
|
|
c.call("request_bind_slot", 3, "prototype_burst")
|
|
# Assert
|
|
assert_that(transport.last_url).contains("/hotbar-loadout")
|
|
assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
|
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
|
assert_that(parsed is Dictionary).is_true()
|
|
var body: Dictionary = parsed
|
|
assert_that(int(body.get("schemaVersion", 0))).is_equal(1)
|
|
var slots: Array = body.get("slots", [])
|
|
assert_that(slots.size()).is_equal(1)
|
|
assert_that(int((slots[0] as Dictionary).get("slotIndex", -1))).is_equal(3)
|
|
assert_that((slots[0] as Dictionary).get("abilityId")).is_equal("prototype_burst")
|
|
|
|
|
|
func test_denied_update_emits_reason_signal() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
(
|
|
'{"schemaVersion":1,"updated":false,"reasonCode":"unknown_ability","loadout":'
|
|
+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
|
|
)
|
|
)
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.call("request_bind_slot", 1, "not_real")
|
|
# Assert
|
|
await assert_signal(c).is_emitted("loadout_update_denied", "unknown_ability", any())
|
|
|
|
|
|
func test_bind_slot_while_busy_is_ignored() -> void:
|
|
# Arrange
|
|
var transport := HoldFirstThenAutoTransport.new()
|
|
var c := _make_client(transport)
|
|
# Act
|
|
c.call("request_bind_slot", 1, "prototype_pulse")
|
|
c.call("request_bind_slot", 2, "prototype_guard")
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
# Assert
|
|
assert_that(transport.request_count).is_equal(1)
|
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
|
assert_that(parsed is Dictionary).is_true()
|
|
var slots: Array = (parsed as Dictionary).get("slots", [])
|
|
assert_that(int((slots[0] as Dictionary).get("slotIndex", -1))).is_equal(1)
|
|
|
|
|
|
func test_http_failure_does_not_mutate_cached_loadout() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_CANT_CONNECT, 0, "")
|
|
var c := _make_client(transport)
|
|
# Act
|
|
c.call("request_sync_from_server")
|
|
# Assert
|
|
var cached: Dictionary = c.call("cached_loadout")
|
|
assert_that(cached.is_empty()).is_true()
|
|
|
|
|
|
func test_non_json_error_response_keeps_cached_loadout_empty() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 500, "oops")
|
|
var c := _make_client(transport)
|
|
# Act
|
|
c.call("request_sync_from_server")
|
|
# Assert
|
|
var cached: Dictionary = c.call("cached_loadout")
|
|
assert_that(cached.is_empty()).is_true()
|