spaceship/tests/test_crafter_feedback.gd
Loïc Wisniewski 7a8f521941 Test the store button through a real tap, not the handler
The bug reported three times was in the input path, and every test I had
called the handlers directly - so they all passed while the button was
inert. test_crafter_feedback now taps the control the way a touch does,
including after the build menu has been opened and closed, which is the
sequence that used to block every button in the game.

Verified against the old guard: the direct-call assertions still pass,
and the real tap fails with "store button dead after the build menu was
opened and closed".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:44:29 +02:00

163 lines
6 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_crafter_feedback.gd
# Every press in the ship crafter must say what happened. Reported twice from
# play-testing as "the buttons do nothing".
#
# There is no silent-failure path left to press: storing with an empty hold,
# ordering something the crafter cannot pay for yet, and storing successfully
# all report. The queue in particular is invisible by nature - an order that
# cannot be afforded is accepted and waits (#16), which looks exactly like a
# dead button unless it says so.
#
# Verifies:
# 1. storing with an empty hold explains where ore comes from
# 2. ordering against an empty stock says it is queued AND how much more
# ore the building needs
# 3. a successful store reports the amount
# 4. the message survives the per-frame status refresh - it used to be
# overwritten on the very next frame, which is what made a working
# button look dead
# 5. the message is the FIRST line, not buried under the standing state
var game_state: Node
var scene: Node
var controller: Node
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.reset_progression()
game_state.unlocked = ["shipyards", "coordination", "wing"]
game_state.rocks = 100
game_state.fleet_cargo = 0
game_state.vessels_owed.clear()
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
func _first_line() -> String:
return controller.craft_status.text.split("\n")[0]
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
scene.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(500.0, 0.0))
crafter_id = int(game_state.buildings[-1].id)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
controller.craft_building_id = crafter_id
controller.craft_menu.visible = true
controller._refresh_craft_status()
# --- 1. store with an empty hold ----------------------------
controller._on_store_ore()
var empty_msg: String = _first_line()
if "carrying nothing" not in empty_msg:
print("got: %s" % empty_msg)
print("TEST_CRAFTER_FEEDBACK: FAIL (storing with an empty hold said nothing useful)")
quit(1)
return true
print("empty hold -> %s" % empty_msg)
# --- 2. order against an empty stock ------------------------
controller._on_craft_interceptor()
var queued_msg: String = _first_line()
if "queued" not in queued_msg.to_lower() or "more ore" not in queued_msg:
print("got: %s" % queued_msg)
print("TEST_CRAFTER_FEEDBACK: FAIL (a queued order did not say it was queued and what it still needs)")
quit(1)
return true
if game_state.pending_orders(crafter_id) != 1:
print("TEST_CRAFTER_FEEDBACK: FAIL (the order was not actually queued)")
quit(1)
return true
print("unaffordable order -> %s" % queued_msg)
# --- 3. a successful store ----------------------------------
game_state.withdraw_to_fleet(30)
controller._on_store_ore()
var stored_msg: String = _first_line()
if "Stored" not in stored_msg:
print("got: %s" % stored_msg)
print("TEST_CRAFTER_FEEDBACK: FAIL (a successful store did not report)")
quit(1)
return true
print("successful store -> %s" % stored_msg)
elapsed = 0.0
stage = 2
return false
2:
# --- 4 + 5. it survives the refresh, and stays first --------
if elapsed < 0.5:
return false
controller._refresh_craft_status()
var still: String = _first_line()
if "Stored" not in still:
print("first line half a second later: %s" % still)
print("TEST_CRAFTER_FEEDBACK: FAIL (the message was overwritten by the per-frame refresh)")
quit(1)
return true
print("half a second later, still first: %s" % still)
# --- 6. a REAL tap, through the input path ------------------
# Everything above calls the handlers directly, which is how a
# whole class of bug kept getting missed: the crafter logic was
# fine and the BUTTON was inert, blocked by a modal guard. This
# taps the control the way a touch does.
game_state.withdraw_to_fleet(20)
var stock_before: int = game_state.building_stock(crafter_id)
var store_button: Control = scene.get_node("UI/CraftMenu/StoreOreButton")
for pressed in [true, false]:
var ev: InputEventScreenTouch = InputEventScreenTouch.new()
ev.pressed = pressed
ev.position = store_button.size / 2.0
store_button._gui_input(ev)
if game_state.building_stock(crafter_id) <= stock_before:
print("stock %d -> %d after tapping Store ore" % [
stock_before, game_state.building_stock(crafter_id)
])
print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the store button did nothing)")
quit(1)
return true
print("real tap on Store ore moved %d ore into the crafter" % [
game_state.building_stock(crafter_id) - stock_before
])
# And the same tap must still work after the build menu has been
# opened and closed, which used to block every button forever.
controller._on_build_pressed()
controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER)
scene.get_node("UI/BuildMenu").visible = false
game_state.withdraw_to_fleet(10)
var stock_mid: int = game_state.building_stock(crafter_id)
for pressed2 in [true, false]:
var ev2: InputEventScreenTouch = InputEventScreenTouch.new()
ev2.pressed = pressed2
ev2.position = store_button.size / 2.0
store_button._gui_input(ev2)
if game_state.building_stock(crafter_id) <= stock_mid:
print("TEST_CRAFTER_FEEDBACK: FAIL (store button dead after the build menu was opened and closed)")
quit(1)
return true
print("still works after opening and closing the build menu")
print("TEST_CRAFTER_FEEDBACK: PASS")
quit(0)
return true
return false