Cover the last batch of tickets with tests

The previous ten tickets shipped with almost no coverage - only #109, #113,
#115 and #117 touched a test. That was a gap, not a decision: everything
before it had its behaviour pinned, and #116 in particular added a whole
subsystem nothing was watching.

Four new files and one extended case:

  test_spacial_workshop        #116 - the repair economy end to end: the
                               cost of a repair scales with the damage, the
                               workshop asks logistics for that ore and
                               stops at its buffer, a drone spends when it
                               LEAVES so two cannot double-charge one wall,
                               and a target levelled in flight sends its
                               load home rather than swallowing it
  test_desktop_controls        #118/#121 - both driven through the real
                               routing, because the routing is what was
                               missing; a test calling handle_mouse_button()
                               itself would have passed against both reports
  test_enemy_bomber            #114 - it flies, it faces where it flies, its
                               bombs arrive, and it bombs structures with no
                               fleet in sight
  test_menu_polish             #111/#112/#119/#120 - what each panel shows
  test_wave_system (case G)    #110 - the dev button, and that it does not
                               exist outside dev mode

One assertion worth recording: the craft caption is checked as an EXACT
match against the vessel's display name rather than by hunting for resource
words in it. The first version failed on "Gas Bomber" - a vessel named after
a resource, and always will be. What #111 removed was the price spelled out
underneath, not the name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-24 12:10:31 +02:00
commit ec7a8f1f17
8 changed files with 790 additions and 0 deletions

178
tests/test_menu_polish.gd Normal file
View file

@ -0,0 +1,178 @@
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_menu_polish.gd
# Forgejo #111, #112, #119 and #120 - four panels that were hard to read,
# each for its own reason. Grouped because they are all "what does this
# panel actually show", and because each alone is a handful of assertions.
#
# Covers:
# A. #111 - the craft menu names no resources: prices are glyphs, and
# every vessel gets a caption that does not overlap its neighbour
# B. #111 - and a price strip really carries the vessel's cost
# C. #120 - the hangar's captions have room for two lines, and there is a
# rule between what is parked and what is flying
# D. #112 - every skill node has a mark, and a node that unlocks a
# building uses that building's own icon
# E. #119 - the command centre draws the vessels actually in the fleet,
# not one placeholder repeated
var game_state: Node
var scene: Node
var controller: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_waves()
game_state.reset_formation()
game_state.intro_active = false
game_state.rocks = 5000
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
controller = scene.get_node("BuildController")
func _fail(message: String) -> bool:
print("TEST_MENU_POLISH: FAIL (%s)" % message)
quit(1)
return true
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.4:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# --- A. the craft menu spells no resource out -------------
controller.craft_building_id = 1
controller._refresh_craft_status()
# The caption is the vessel's NAME and nothing else. Checked as an
# exact match rather than by hunting for resource words in it: a
# Gas Bomber is named after a resource and always will be, and
# what #111 removed was the PRICE spelled out underneath.
var fleet_defs: Array = VesselDefinitions.fleet()
for i in range(mini(controller.craft_vessel_labels.size(), fleet_defs.size())):
var caption: String = String(controller.craft_vessel_labels[i].text)
var expected: String = String(fleet_defs[i].display_name)
if caption != expected:
return _fail("a craft caption reads \"%s\", expected just \"%s\"" \
% [caption, expected])
# Captions must not sit on top of one another, which is what
# made the old single row unreadable.
for i in range(controller.craft_vessel_labels.size()):
for j in range(i + 1, controller.craft_vessel_labels.size()):
var a: Control = controller.craft_vessel_labels[i]
var b: Control = controller.craft_vessel_labels[j]
if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)):
return _fail("craft captions %d and %d overlap" % [i, j])
print("craft captions: %d, one line each, none overlapping" \
% controller.craft_vessel_labels.size())
# --- B. and the price is really there ---------------------
if controller.craft_price_strips.size() != controller.craft_vessel_buttons.size():
return _fail("%d price strips for %d vessels" \
% [controller.craft_price_strips.size(),
controller.craft_vessel_buttons.size()])
var priced: int = 0
for i in range(controller.craft_price_strips.size()):
var definition: Resource = VesselDefinitions.fleet()[i]
var costs: Dictionary = game_state.product_costs(definition.definition_id)
var strip: Control = controller.craft_price_strips[i]
if costs.is_empty():
continue
if strip.is_free():
return _fail("%s costs %s and its strip is empty" \
% [definition.definition_id, str(costs)])
priced += 1
if priced == 0:
return _fail("no vessel showed a price at all")
print("%d vessels show their price as glyphs" % priced)
# --- C. the hangar ---------------------------------------
controller._layout_hangar()
if controller.hangar_divider_y <= 0.0:
return _fail("the hangar laid out with no divider between its halves")
var available_bottom: float = 0.0
var fleet_top: float = 1.0e9
for label in controller.hangar_available_labels:
available_bottom = maxf(available_bottom, label.position.y + label.size.y)
for slot in controller.hangar_fleet_slots:
fleet_top = minf(fleet_top, slot.position.y)
if not (controller.hangar_divider_y > available_bottom - 40.0
and controller.hangar_divider_y < fleet_top):
return _fail("the divider at %.0f is not between the two grids (%.0f / %.0f)" \
% [controller.hangar_divider_y, available_bottom, fleet_top])
# Two lines of caption. The font is 24-ish, so anything under
# about 44 cannot show a second line at all.
var caption_height: float = controller.hangar_available_labels[0].size.y
if caption_height < 44.0:
return _fail("hangar captions are %.0fpx tall - a two-word name is clipped" \
% caption_height)
print("hangar: %.0fpx captions, divider at %.0f between %.0f and %.0f" \
% [caption_height, controller.hangar_divider_y, available_bottom, fleet_top])
# --- D. every skill node has a mark -----------------------
var tree_view: Control = scene.get_node("UI/SkillMenu/Tree")
var marked: int = 0
for node in GameState.SKILL_TREE:
var icon: int = game_state.skill_node_icon(node)
if icon < 0:
return _fail("skill node \"%s\" has no icon" % node.id)
marked += 1
var unlocks: String = String(node.get("unlocks", ""))
if unlocks == "":
continue
var definition: Resource = game_state.building_definition(unlocks)
if definition != null and icon != int(definition.menu_icon):
return _fail("\"%s\" unlocks the %s but draws a different icon" \
% [node.id, unlocks])
if tree_view._glyphs.size() != marked:
return _fail("%d nodes have icons but the tree built %d glyphs" \
% [marked, tree_view._glyphs.size()])
print("%d skill nodes, every one marked, unlocks matching their building" % marked)
# --- E. the formation shows the real fleet ----------------
scene.get_node("Fleet").spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD]))
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.2:
return false
var editor: Control = scene.get_node("UI/FormationMenu/Editor")
var crewed: PackedStringArray = game_state.fleet_vessel_types
if crewed.size() != 2:
return _fail("the fleet mirror holds %d types, expected 2" % crewed.size())
var interceptor: Resource = game_state.vessel_definition(GameState.VESSEL_INTERCEPTOR)
var shield: Resource = game_state.vessel_definition(GameState.VESSEL_SHIELD)
if interceptor.hull_points == shield.hull_points:
return _fail("the two vessels share a hull, so this case proves nothing")
if not editor.has_method("_draw_vessel_slot"):
return _fail("the editor has no per-vessel drawing at all")
# The panel draws from the mirror, so the mirror having the right
# types in the right order IS what makes the markers differ.
if String(crewed[0]) != GameState.VESSEL_INTERCEPTOR \
or String(crewed[1]) != GameState.VESSEL_SHIELD:
return _fail("the fleet mirror is %s, not the order they were crewed in" \
% str(crewed))
print("formation draws a %s and a %s, in the slots they fly in" \
% [interceptor.display_name, shield.display_name])
print("TEST_MENU_POLISH: PASS")
quit(0)
return true
return false