spaceship/tests/test_desktop_controls.gd

226 lines
8.8 KiB
GDScript3
Raw Normal View History

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>
2026-08-24 12:10:31 +02:00
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_desktop_controls.gd
# Forgejo #118 and #121: two raw-touch controls that a mouse could not
# reach.
#
# Both read InputEventScreenTouch/ScreenDrag, and the project no longer turns
# clicks into them - the same routing gap #93 found in the research tree.
# The boost was unreachable on a desktop and the formation could not be
# rearranged at all.
#
# Driven through the REAL routing in each case - the viewport itself for the
# fader, BuildController for the editor - rather than by calling the
# controls directly, because the routing is what was missing: a test that
# called handle_mouse_button() itself would have passed against both
# reports. It did, in fact, and hid a second one: the fader was dispatched
# from Main, which is the LAST node to see an unhandled press, so
# MouseCombatController fired the guns with it and the knob never moved.
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>
2026-08-24 12:10:31 +02:00
#
# Covers:
# A. #118 - a click on the fader takes hold of it, dragging moves the
# boost, and releasing lets go
# B. the press stops at the fader: not the camera behind it, and not the
# guns, which claim every other left press on a desktop
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>
2026-08-24 12:10:31 +02:00
# C. #121 - a click grabs a formation slot, moving drags it, and the
# formation really changes
# D. releasing drops the slot, and moving afterwards does not keep dragging
# E. the wheel zooms the formation panel, which is what replaces the pinch
var game_state: Node
var scene: Node
var controller: Node
var fleet: Node
var fader: Control
var editor: Control
var formation_menu: Control
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.vessel_levels["unlock_distorder"] = 1
# #219: fleet capacity is built now - one berth free, one per Fleet
# Office. This test flies more than one vessel, so it needs the offices
# standing before the fleet spawns into them.
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
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>
2026-08-24 12:10:31 +02:00
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
fleet = scene.get_node("Fleet")
controller = scene.get_node("BuildController")
fader = scene.get_node("UI/SpeedFader")
editor = scene.get_node("UI/FormationMenu/Editor")
formation_menu = scene.get_node("UI/FormationMenu")
func _fail(message: String) -> bool:
print("TEST_DESKTOP_CONTROLS: FAIL (%s)" % message)
quit(1)
return true
# Pushed through the viewport, not handed to a node's own _unhandled_input:
# WHICH node sees a press first is the thing under test. #118 shipped a fader
# that only worked when a test called it directly - MouseCombatController
# claims every left press to fire the guns, and it is dispatched well before
# Main's own handler, so on a real desktop the click never arrived.
# `true` keeps the coordinates in viewport space, which is the space the
# controls are laid out in.
func _push(event: InputEvent) -> void:
root.push_input(event, true)
func _push_click(at: Vector2, pressed: bool) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = at
_push(event)
func _push_move(to: Vector2) -> void:
var event: InputEventMouseMotion = InputEventMouseMotion.new()
event.position = to
_push(event)
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>
2026-08-24 12:10:31 +02:00
func _click(at: Vector2, pressed: bool, target: Node) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = at
target._unhandled_input(event)
func _move(to: Vector2, target: Node) -> void:
var event: InputEventMouseMotion = InputEventMouseMotion.new()
event.position = to
target._unhandled_input(event)
func _wheel(button: int, at: Vector2) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = button
event.pressed = true
event.position = at
controller._unhandled_input(event)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# The fader only exists where the fleet can actually boost.
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER]))
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
if not fader.visible:
return _fail("the fleet has an Energy Propulser and the fader is still hidden")
if game_state.max_fleet_speed_multiplier <= 1.0:
return _fail("the boost has no range to move through")
# --- A. click, drag, release -----------------------------
# With mouse aiming live, which is what a desktop always has:
# the guns are the other thing a left press means, and they used
# to take every one of them.
var mouse_combat: Node = scene.get_node("MouseCombatController")
mouse_combat.pointer_mode = mouse_combat.PointerMode.ON
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>
2026-08-24 12:10:31 +02:00
var top: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.track_margin + 2.0)
var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.size.y - fader.track_margin - 2.0)
_push_click(bottom, true)
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>
2026-08-24 12:10:31 +02:00
if not fader.is_active():
return _fail("a click on the fader did not take hold of it")
if mouse_combat.firing:
return _fail("the click that grabbed the fader also fired the fleet's guns")
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>
2026-08-24 12:10:31 +02:00
var at_bottom: float = game_state.fleet_speed_multiplier
_push_move(top)
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>
2026-08-24 12:10:31 +02:00
var at_top: float = game_state.fleet_speed_multiplier
if at_top <= at_bottom:
return _fail("dragging the fader from bottom to top left the boost at %.2f" % at_top)
_push_click(top, false)
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>
2026-08-24 12:10:31 +02:00
if fader.is_active():
return _fail("the fader is still held after the button came up")
_push_move(bottom)
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>
2026-08-24 12:10:31 +02:00
if not is_equal_approx(game_state.fleet_speed_multiplier, at_top):
return _fail("the fader kept following the pointer after release")
print("fader: %.2fx at the bottom, %.2fx at the top, released cleanly" \
% [at_bottom, at_top])
# --- B. it claims the press ------------------------------
var zoom_before: float = scene.get_node("Camera2D").zoom.x
_push_click(bottom, true)
_push_click(bottom, false)
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>
2026-08-24 12:10:31 +02:00
if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before):
return _fail("pressing the fader also moved the camera behind it")
if mouse_combat.firing:
return _fail("the fader's press reached the guns behind it")
mouse_combat.pointer_mode = mouse_combat.PointerMode.AUTO
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>
2026-08-24 12:10:31 +02:00
print("the press stops at the fader; the world behind it is untouched")
# --- C/D/E. the formation editor -------------------------
formation_menu.visible = true
editor.queue_redraw()
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
var slot_before: Vector2 = game_state.formation[0]
var grab: Vector2 = editor.global_position + editor._slot_screen_pos(0)
_click(grab, true, controller)
if editor.dragging_index != 0:
return _fail("a click on the first formation slot grabbed %d" % editor.dragging_index)
_move(grab + Vector2(70.0, 40.0), controller)
if game_state.formation[0] == slot_before:
return _fail("dragging the slot did not move it")
var dragged: Vector2 = game_state.formation[0]
print("slot 0 moved from %s to %s under a mouse" % [str(slot_before), str(dragged)])
# --- D. release lets go ----------------------------------
_click(grab + Vector2(70.0, 40.0), false, controller)
if editor.dragging_index != -1:
return _fail("the slot is still held after the button came up")
_move(grab + Vector2(200.0, 200.0), controller)
if game_state.formation[0] != dragged:
return _fail("the slot kept following the pointer after release")
print("released, and the slot stopped following")
# --- E. the wheel zooms ----------------------------------
var scale_before: float = editor.scale_factor
_wheel(MOUSE_BUTTON_WHEEL_UP, editor.global_position + editor.size * 0.5)
if editor.scale_factor <= scale_before:
return _fail("the wheel did not zoom the formation panel in")
_wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5)
_wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5)
if editor.scale_factor >= scale_before:
return _fail("the wheel did not zoom back out")
print("the wheel zooms the panel both ways (%.2f -> %.2f)" \
% [scale_before, editor.scale_factor])
print("TEST_DESKTOP_CONTROLS: PASS")
quit(0)
return true
return false