spaceship/tests/test_icon_button_press_fires.gd

101 lines
4.1 KiB
GDScript3
Raw Normal View History

Fire buttons on press where the browser never delivers a release Every button in the web build was dead - reported as "Play does nothing". Diagnosed on the deployed build with a temporary on-screen input probe, because none of it reproduces here: the suite passes, and pressing Play under the real gl_compatibility renderer (the same path WebGL2 takes) reaches Main.tscn fine. The probe reported, from a phone browser: #6 touch down @ 362,781 play rect 296,725 128x128 hit=YES Three taps, six events, every one a DOWN and not a single UP - and the button left drawn in its bright held state, pressed_down stuck true because nothing ever came to clear it. So touches arrive, their coordinates are correct, and they land inside the button; the browser simply never delivers the release, in _gui_input or in raw _input. IconButton emits `pressed` only on release, deliberately, so that dragging off a button cancels it. With no release that means it never emits at all - on every button in the game, not just Play. Which also explains why the earlier suspects went nowhere: the scene change was never reached, so it was never the scene change, and hit=YES ruled out coordinate mapping. fire_on_press defaults to OS.has_feature("web"). There, a press completes the tap itself and does NOT latch, since nothing is coming to unlatch it. Every other platform is untouched and keeps drag-off-to-cancel. The cost, stated plainly: a web tap can no longer be cancelled by sliding off before lifting, and that applies to destructive buttons too. The alternative - keeping release-firing and treating a new press as completing the previous one - preserves cancellation but leaves the very first tap dead, which is worse. tests/test_icon_button_press_fires.gd drives the flag directly, since a headless Linux run cannot BE a web build: one tap emits exactly once, a late release does not emit again, the button does not latch, and with the flag off the release still drives the press and dragging off still cancels. MainMenu keeps the load/start bisection introduced while chasing this - a failed change_scene_to_packed otherwise leaves the menu up, which looks exactly like an unpressed button. Full suite green at 100/100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:02:20 +02:00
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_icon_button_press_fires.gd
# A browser on a phone delivers the touch DOWN and never delivers the
# release. Measured on the deployed web build: presses arrived with correct
# coordinates, landed inside the button, the button latched into its held
# state, and no release ever followed. IconButton emits `pressed` only on
# release, so on the web nothing ever fired - the "Play does nothing" bug.
#
# fire_on_press exists for exactly that platform. This test does not care
# whether THIS run is a web build; it exercises both settings directly, which
# is the only way a headless Linux run can cover the web path at all.
#
# Covers:
# A. with fire_on_press, a press alone emits - and emits ONCE even if a
# release does arrive after it
# B. with fire_on_press, the button does not stay latched in its held
# state (nothing is coming to clear it)
# C. without it, the release still drives the press, and dragging off
# still cancels - the behaviour every other platform keeps
# D. the default follows OS.has_feature("web")
Fix the web build: a wasm trap on integer % 0, and buttons that never fired The web export reached the main menu and went no further. Two separate bugs, one hiding the other. BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never the release. IconButton emits `pressed` only on release, deliberately, so that dragging off cancels - so on the web it never emitted at all, on every button in the game. fire_on_press (default OS.has_feature("web")) completes the press there instead, and does not latch, since nothing is coming to unlatch it. The cost is drag-off-to-cancel, which every other platform keeps. Measured on the device rather than guessed: an on-screen probe reported `touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down, the button latched through ordinary GUI routing, then nothing. THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and built Main.tscn and the whole engine stopped mid-frame, page still alive. An error overlay in a custom HTML shell finally showed why: Uncaught RuntimeError: remainder by zero An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So the bug was equally present on Android and in all 100 tests, doing nothing in both - which is why nothing runnable here could ever have caught it, and why reasoning from frozen screenshots got nowhere for several rounds. Every variable modulo is now guarded: the hangar grid's `i % columns`, planet naming's `% names.size()`, and two in the map editor. WHAT IS KEPT, AND WHY - web_shell.html mirrors console errors onto the page, hidden until one occurs. Godot's errors go to the browser console, which is unreachable on a phone; this found in one shot what inference had failed to find all evening. It costs nothing when there is nothing to report. - MainMenu._start does load -> instantiate -> add_child by hand instead of change_scene_to_file. That call does all three opaquely, so a failure is indistinguishable from an unpressed button; split, each step reports. - PlanetAtmosphereController defers the forced world populate out of the frame that builds Main.tscn. It did not fix the hang - the trap did - but 25 chunks of asteroids and 49 of planets do not belong in the one frame that already constructs 197 nodes, and nothing can paint until it ends. Removed the direct-input-routing experiment and its test: it was written on the theory that touches were not reaching Controls, which `held=y` later disproved, and its set_input_as_handled() swallowed the very events the probe was reading. Full suite green at 100/100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
#
# There is deliberately NO test for bypassing Godot's GUI routing. An earlier
# attempt did that, on the theory that touches were not reaching Controls at
# all - the device then reported `held=y`, proving the button had been
# receiving presses through ordinary routing the whole time. The bypass was
# solving a problem that did not exist, and its set_input_as_handled() call
# swallowed the very events the on-screen probe was reading.
Fire buttons on press where the browser never delivers a release Every button in the web build was dead - reported as "Play does nothing". Diagnosed on the deployed build with a temporary on-screen input probe, because none of it reproduces here: the suite passes, and pressing Play under the real gl_compatibility renderer (the same path WebGL2 takes) reaches Main.tscn fine. The probe reported, from a phone browser: #6 touch down @ 362,781 play rect 296,725 128x128 hit=YES Three taps, six events, every one a DOWN and not a single UP - and the button left drawn in its bright held state, pressed_down stuck true because nothing ever came to clear it. So touches arrive, their coordinates are correct, and they land inside the button; the browser simply never delivers the release, in _gui_input or in raw _input. IconButton emits `pressed` only on release, deliberately, so that dragging off a button cancels it. With no release that means it never emits at all - on every button in the game, not just Play. Which also explains why the earlier suspects went nowhere: the scene change was never reached, so it was never the scene change, and hit=YES ruled out coordinate mapping. fire_on_press defaults to OS.has_feature("web"). There, a press completes the tap itself and does NOT latch, since nothing is coming to unlatch it. Every other platform is untouched and keeps drag-off-to-cancel. The cost, stated plainly: a web tap can no longer be cancelled by sliding off before lifting, and that applies to destructive buttons too. The alternative - keeping release-firing and treating a new press as completing the previous one - preserves cancellation but leaves the very first tap dead, which is worse. tests/test_icon_button_press_fires.gd drives the flag directly, since a headless Linux run cannot BE a web build: one tap emits exactly once, a late release does not emit again, the button does not latch, and with the flag off the release still drives the press and dragging off still cancels. MainMenu keeps the load/start bisection introduced while chasing this - a failed change_scene_to_packed otherwise leaves the menu up, which looks exactly like an unpressed button. Full suite green at 100/100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:02:20 +02:00
var presses: int = 0
func _initialize() -> void:
var button_script: GDScript = load("res://IconButton.gd")
# --- D. the default is the platform rule --------------------------
var default_button: Control = button_script.new()
default_button.size = Vector2(120, 120)
if default_button.fire_on_press != OS.has_feature("web"):
return _fail("fire_on_press defaults to %s, expected OS.has_feature('web') = %s" \
% [default_button.fire_on_press, OS.has_feature("web")])
default_button.free()
print("fire_on_press defaults to the web feature flag (%s here)" % OS.has_feature("web"))
# --- A + B. the web path ------------------------------------------
var web_button: Control = button_script.new()
web_button.size = Vector2(120, 120)
web_button.fire_on_press = true
web_button.pressed.connect(_count)
root.add_child(web_button)
presses = 0
web_button._handle_press(true, Vector2(60, 60))
if presses != 1:
return _fail("a press alone emitted %d times with fire_on_press, expected 1" % presses)
if web_button.pressed_down:
return _fail("the button latched held with fire_on_press - nothing will ever clear it")
# A release may still arrive on a platform that sends one; it must not
# produce a second press for the same tap.
web_button._handle_press(false, Vector2(60, 60))
if presses != 1:
return _fail("a release after a fired press emitted again (%d total)" % presses)
print("fire_on_press: one tap emits exactly once and does not latch")
# --- C. every other platform --------------------------------------
var native_button: Control = button_script.new()
native_button.size = Vector2(120, 120)
native_button.fire_on_press = false
native_button.pressed.connect(_count)
root.add_child(native_button)
presses = 0
native_button._handle_press(true, Vector2(60, 60))
if presses != 0:
return _fail("a press emitted without a release while fire_on_press was off")
if not native_button.pressed_down:
return _fail("the button did not latch held while waiting for its release")
native_button._handle_press(false, Vector2(60, 60))
if presses != 1:
return _fail("the release did not emit (%d)" % presses)
# Dragging off before lifting still cancels.
presses = 0
native_button._handle_press(true, Vector2(60, 60))
native_button._handle_press(false, Vector2(400, 400))
if presses != 0:
return _fail("releasing outside the button still counted as a press")
print("release path unchanged: release fires, dragging off cancels")
print("TEST_ICON_BUTTON_PRESS_FIRES: PASS")
quit(0)
func _count() -> void:
presses += 1
func _fail(message: String) -> void:
print("TEST_ICON_BUTTON_PRESS_FIRES: FAIL (%s)" % message)
quit(1)