Tyre finding: the orphaned AtlasViewer cluster is now actually deleted (atlas_viewer, atlas_marker_overlay, atlas_descend_geometry, atlas_legend_panel — a sixth orphan found beyond the review list — atlas_generation_proxy, atlas_generation_state; ~2,497 lines), with reachability re-verified across preload/class_name/res:// strings, every .tscn, and the standalone companion app. Test suites triaged, not blanket-deleted: 5 pure AtlasOverlayColors tests relocated into test_atlas_window_colors, the live type-identity regression guard relocated into test_step_canvas_viewer, dead coverage deleted. A real harness gap surfaced during diligence and RULED, not patched: visual_scenarios/visual_capture golden shots call retired continuous-zoom API — no shim (would resurrect what D-255 kills); inventory recorded on re-scoped T-1157 (gate-invisible, manual targets only). Hoshe finding 1: decode_png_field now detects the [Error, PackedByteArray] bin-shape from messagepack.gd explicitly — a genuine msgpack bin payload decodes correctly instead of silently collapsing to [0,0]; test built from a real round-tripped bin decode. Hoshe finding 2: the hard zoom-out reset is wired — ascend at rung 0 with a drifted view triggers _reset_to_global (the restored HARD condition), behavioral tests through the real input path. Notes folded: refloat + edge-scroll test coverage, legend smoke suite, Vector2i narrowing-safety comment with computed headroom. gdlint clean on touched files; full client suite 3364/3364. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
328 lines
14 KiB
GDScript
328 lines
14 KiB
GDScript
## T-1182 tests: StepCanvasViewer — the rung transport state machine
|
|
## (enter() lands on the Global opener, scroll steps through the ladder,
|
|
## overlay toggle wiring, pan-edge re-request, edge-scroll) and
|
|
## RegionalScreen's re-entry guard against the new viewer. test_mode
|
|
## (SimBridge default outside SR_LIVE=1) means request_step_canvas() is a
|
|
## silent no-op — these tests exercise client-side state only, no live
|
|
## server needed.
|
|
class_name TestStepCanvasViewer
|
|
extends GdUnitTestSuite
|
|
|
|
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
|
|
|
|
|
func test_enter_lands_on_the_global_opener() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
func test_get_body_id_reflects_the_entered_body() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
assert_str(v.get_body_id()).is_equal("")
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_body_id()).is_equal("GJ380c")
|
|
|
|
|
|
## Scrolling one notch descends the ladder — cursor-anchored, so a cursor
|
|
## position must be supplied; the rung index advances by exactly one.
|
|
func test_scroll_rung_descends_one_notch() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
|
|
func test_scroll_rung_clamps_at_the_deepest_rung() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
for _i in range(10):
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_CHUNK)
|
|
|
|
|
|
func test_reset_to_global_returns_from_a_deep_rung() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._reset_to_global()
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
## PR #203 review (Hoshe finding 2): the hard full-zoom-out reset — a
|
|
## scroll-out gesture while ALREADY at Global (rung 0), with the view
|
|
## drifted from the canonical un-panned frame, must snap the view back to
|
|
## center (Jeroen's explicit HARD condition, carried from the retired
|
|
## viewer's own _maybe_reset_to_canonical_frame()). Behavioral, through the
|
|
## real input entry point (_scroll_rung with direction=-1), not a direct
|
|
## _reset_to_global() call — this is what would have caught the dead-code
|
|
## regression (scroll_step() clamping at index 0 meant _scroll_rung()
|
|
## returned before ever reaching a reset call).
|
|
func test_scroll_out_at_global_after_a_pan_resets_the_view() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
v._apply_pan_delta(Vector2(1.0, 0.0), 1.0) # drift the view off-center
|
|
assert_bool(v._is_global_view_drifted()).override_failure_message(
|
|
"test setup: a pan at Global must actually drift the view"
|
|
).is_true()
|
|
|
|
v._scroll_rung(-1, Vector2(400.0, 300.0)) # scroll OUT — already at rung 0
|
|
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
assert_bool(v._is_global_view_drifted()).override_failure_message(
|
|
"a scroll-out past the top of the ladder must hard-reset the drifted"
|
|
+ " Global view back to its canonical (centered) frame"
|
|
).is_false()
|
|
|
|
|
|
## The inverse guard: scrolling out while ALREADY at the canonical
|
|
## (un-drifted) Global frame must stay a no-op — the reset is edge-triggered
|
|
## on genuine drift, not a per-scroll unconditional reset.
|
|
func test_scroll_out_at_undrifted_global_is_a_no_op() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(-1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
assert_bool(v._is_global_view_drifted()).is_false()
|
|
|
|
|
|
func test_overlay_visibility_defaults_to_off_for_every_toggle() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
for def: Dictionary in v.get_overlay_defs():
|
|
assert_bool(v.is_overlay_visible(def["id"])).is_false()
|
|
|
|
|
|
func test_set_overlay_visible_updates_state() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.set_overlay_visible("gen_dw_temp", true)
|
|
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
|
|
|
|
|
|
func test_set_overlay_visible_unknown_id_is_a_no_op() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.set_overlay_visible("not_a_real_overlay", true)
|
|
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# RegionalScreen re-entry guard (BUG 2 lineage, carried forward from the
|
|
# retired AtlasWindowViewer-era regression) — now against StepCanvasViewer.
|
|
# =============================================================================
|
|
|
|
|
|
func test_regional_screen_repeat_enter_for_the_same_body_is_a_no_op() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
var body: Dictionary = {"body_id": "GJ380c", "body_radius_km": 6238.4}
|
|
screen.enter({"body": body, "system": {}})
|
|
screen._viewer._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(screen._viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
screen.enter({"body": body, "system": {}})
|
|
|
|
# A no-op re-entry must NOT reset the held rung back to Global — that
|
|
# would be the exact "repeat enter tears down in-flight state" class the
|
|
# retired viewer's own cold-start guard existed to prevent.
|
|
assert_str(screen._viewer.get_held_rung()).override_failure_message(
|
|
"a repeat enter() for the SAME body must not reset the held rung"
|
|
).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
|
|
func test_regional_screen_different_body_still_re_enters() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
screen.enter({"body": {"body_id": "GJ380c", "body_radius_km": 6238.4}, "system": {}})
|
|
|
|
screen.enter({"body": {"body_id": "OtherBody", "body_radius_km": 100.0}, "system": {}})
|
|
|
|
assert_str(screen._viewer.get_body_id()).is_equal("OtherBody")
|
|
|
|
|
|
## Relocated from test_atlas_descend_entry.gd (T-1182 PR #203 review — the
|
|
## AtlasViewer cluster orphan retirement, Tyre finding). This is the one live
|
|
## regression guard from that suite: RegionalScreen must wrap StepCanvasViewer
|
|
## (the stepped ladder), never fall back to the now-deleted AtlasViewer
|
|
## heightmap-texture display — a direct type-identity check, distinct from
|
|
## the behavioral tests above (which would only fail indirectly, via a
|
|
## missing method, if this ever regressed).
|
|
func test_regional_screen_wraps_step_canvas_viewer_not_atlas_viewer() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
assert_object(screen._viewer).override_failure_message(
|
|
"RegionalScreen must wrap StepCanvasViewer (the stepped ladder) since T-1182,"
|
|
+ " not the retired AtlasViewer heightmap-texture display"
|
|
).is_instanceof(StepCanvasViewer)
|
|
|
|
|
|
# =============================================================================
|
|
# PR #203 review (Hoshe notes): pan-edge re-request (_maybe_refloat) and
|
|
# edge-scroll pan — previously untested. _maybe_refloat() only does anything
|
|
# once the terrain layer holds a real texture (get_footprint_px() is
|
|
# ZERO/inert until then), so these tests drive StepCanvasTerrainLayer.
|
|
# rebuild_from_canvas() directly (bypassing the network — a decoded canvas
|
|
# dict is all it needs) to put the viewer into the "holding a real canvas"
|
|
# state _maybe_refloat's early-out guards against.
|
|
# =============================================================================
|
|
|
|
|
|
static func _synthetic_canvas(width: int, height: int) -> Dictionary:
|
|
return {
|
|
"width": width,
|
|
"height": height,
|
|
"morphology": null,
|
|
"elev_q": null,
|
|
"moisture_q": null,
|
|
"vegetation": null,
|
|
"glaciation": null,
|
|
"temp_dc": [],
|
|
"settlement_id": [],
|
|
"courses": [],
|
|
"cliffs": [],
|
|
}
|
|
|
|
|
|
func test_maybe_refloat_is_inert_before_any_canvas_has_arrived() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District — footprint still ZERO, nothing arrived
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._maybe_refloat()
|
|
assert_that(v._world_center).is_equal(world_center_before)
|
|
|
|
|
|
## A small pan (well under half the canvas footprint) must NOT re-float —
|
|
## the held canvas keeps drawing, no re-request (§4/§5's "only when a pan
|
|
## carries the view past the held window's edge").
|
|
func test_maybe_refloat_does_not_refloat_on_a_small_pan() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0) # a real viewport size — _maybe_refloat's
|
|
# drift math is relative to get_rect().size's own center; leaving this at
|
|
# the default ZERO would make screen_center ZERO too, so even a tiny
|
|
# view_offset reads as "drifted past the canvas's own half-footprint"
|
|
# (drift = view_offset + half, threshold = half*0.5) — a test-harness
|
|
# artifact, not the behavior under test.
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "")
|
|
# _scroll_rung() re-centers view_offset to ZERO on arrival, which under a
|
|
# real 800x600 viewport already puts the canvas center near screen center
|
|
# (both small relative to the viewport) — center the canvas explicitly so
|
|
# "small pan" starts from a known-centered baseline.
|
|
v._view_offset = v.size * 0.5 - v._terrain_layer.get_footprint_px() * 0.5
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._view_offset += Vector2(2.0, 0.0) # tiny drift, far under half the footprint
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a small pan must not re-float the held canvas"
|
|
).is_equal(world_center_before)
|
|
|
|
|
|
## A large pan (past half the canvas footprint) DOES re-float — new
|
|
## world_center, view_offset reset to ZERO (the canvas re-centers under the
|
|
## new request).
|
|
func test_maybe_refloat_refloats_once_the_pan_crosses_the_edge_threshold() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "")
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
var footprint: Vector2 = v._terrain_layer.get_footprint_px()
|
|
v._view_offset = Vector2(footprint.x, 0.0) # far past half the footprint
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a pan past the edge threshold must re-float (new world_center)"
|
|
).is_not_equal(world_center_before)
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"re-floating resets view_offset to ZERO (the canvas re-centers)"
|
|
).is_equal(Vector2.ZERO)
|
|
|
|
|
|
## Global never re-floats on pan (D-255(a): its canvas is the whole body,
|
|
## no edge to cross) — even with a real texture held and a huge drift.
|
|
func test_maybe_refloat_is_a_no_op_at_global_rung() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(200, 100), v.get_held_rung(), "")
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._view_offset = Vector2(9_999.0, 9_999.0)
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).is_equal(world_center_before)
|
|
|
|
|
|
# =============================================================================
|
|
# Edge-scroll: suppression conditions + direction.
|
|
# =============================================================================
|
|
|
|
|
|
func test_edge_scroll_suppressed_without_application_focus() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = false
|
|
v._last_mouse_pos = Vector2(2.0, 300.0) # well inside the edge margin
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
|
|
|
|
|
func test_edge_scroll_suppressed_when_cursor_has_never_moved_over_the_control() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
# _last_mouse_pos defaults to (-1, -1) — an impossible in-bounds position,
|
|
# so edge-scroll never fires before the mouse has moved over the control
|
|
# at least once (matches the retired viewer's own documented contract).
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
|
|
|
|
|
func test_edge_scroll_active_near_the_left_edge() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(2.0, 300.0)
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_true()
|
|
var direction: Vector2 = v._edge_scroll_direction()
|
|
assert_float(direction.x).is_less(0.0)
|
|
assert_float(direction.y).is_equal(0.0)
|
|
|
|
|
|
func test_edge_scroll_active_near_the_right_edge() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(798.0, 300.0)
|
|
var direction: Vector2 = v._edge_scroll_direction()
|
|
assert_float(direction.x).is_greater(0.0)
|
|
|
|
|
|
func test_edge_scroll_inactive_well_inside_the_viewport() -> void:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(400.0, 300.0) # dead center — far from any edge
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|