Files
settled-reach/client/tests/test_atlas_window_viewer.gd
T
jpmschweitzerandClaude Fable 5 7ce6cc08fd test(ui): pin the _user_adjusted resize guard both ways; fix coalescing doc overstatement (PR #188 review)
Hoshe's finding: the flag introduced so auto-fit never fights a manual
view had zero coverage on exactly that branch. Two tests drive the
REAL _gui_input path (synthetic drag), then fire NOTIFICATION_RESIZED:
user-adjusted view survives a resize untouched (zoom AND offset);
an unadjusted view re-fits to the new viewport. 52/52.

Non-blocking doc note also taken: layer_proxy's normalization comment
claimed the twins would otherwise 'coalesce independently' — the
coalescing key is (ConnectionId, body_id) and never carried center;
rewritten to say what normalization actually buys on that path (the
work item derives and echoes the canonical center).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:22:24 +02:00

496 lines
21 KiB
GDScript

## T-1138 (D-226 T-1124 amendment §1-§5): tests for AtlasWindowViewer + its
## companion request/cache orchestration (atlas_window_request.gd) — pure
## logic against hand-built AtlasLayerResponse-shaped dicts, matching the
## ticket's "unit tests against hand-built response dicts" instruction. Live
## end-to-end verification against a real spawned server is separate
## (companion-run evidence, not gdUnit — this file never touches SimBridge's
## live-mode path, only the response-handling/cache/overlay logic that path
## eventually feeds).
class_name TestAtlasWindowViewer
extends GdUnitTestSuite
# atlas_window_request.gd has no class_name (review #8 precedent throughout
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
# T-1142: district_extent()/canonicalize_district_center() — used to derive
# real (cols/rows_half) bounds for the wrap/pole-wall tests below.
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape
## district_grid/region_grid fixtures already use elsewhere in this suite).
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# AtlasWindowViewer — entry + overlay defs
# =============================================================================
func test_enter_with_no_response_leaves_window_null_and_pending() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20))
assert_that(v.get_district_window()).is_null()
## Feeding a matching Ready response (via the SAME SimBridge.atlas_layers_received
## routing path the viewer subscribes to in _ready()) must populate the window.
func test_enter_then_matching_response_populates_window() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).is_equal(window)
## A response for a DIFFERENT body must not populate the window — the
## body_id scoping AtlasWindowRequest.on_response() checks.
func test_response_for_different_body_is_ignored() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window))
assert_that(v.get_district_window()).is_null()
## A response whose echoed (center, n) does NOT match what was last asked for
## is stale — §2's race-condition guard. Simulates a superseded-by-a-later-pan
## response arriving after the fact.
func test_response_with_mismatched_echo_is_discarded_as_stale() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var stale_window: Dictionary = _mock_window(Vector2i(99, 99), 2) # wrong center
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window))
assert_that(v.get_district_window()).is_null()
## §1: an as-yet-underived window rides as `district_window: None` inside a
## Ready response — this is NOT an error, the viewer just keeps waiting
## (get_district_window() stays null, no crash, no window content shown).
func test_ready_response_with_null_district_window_keeps_waiting() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", null))
assert_that(v.get_district_window()).is_null()
func test_overlay_defs_include_the_three_toggle_ids() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).contains(["gen_dw_temp", "gen_dw_moisture", "gen_dw_veg"])
## Glaciation is explicitly NOT a toggle id (§5: "an always-on modifier, not
## a toggle") — a regression here would silently re-introduce it as a switch.
func test_overlay_defs_do_not_include_glaciation() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).not_contains(["gen_dw_glaciation", "gen_dw_ice"])
func test_set_overlay_visible_toggles_and_is_overlay_visible_reflects_it() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_false()
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_noop() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.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()
# =============================================================================
# T-1120 capture-API parity (the ticket's explicit note: must survive here too)
# =============================================================================
func test_set_view_and_getters_round_trip() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(2.5, Vector2(30.0, -10.0))
assert_that(v.get_view_zoom()).is_equal_approx(2.5, 0.001)
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
func test_set_view_clamps_to_min_max_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(0.01, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001)
v.set_view(1000.0, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
# =============================================================================
# AtlasWindowRequest — cache reuse (§4's Esc-then-re-enter / pan-back hit)
# =============================================================================
func test_window_request_cache_hit_emits_synchronously_no_pending() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
# Prime the cache directly (bypassing the network path) — the ticket's
# own instruction: unit test against hand-built response dicts.
req.get_cache().put("GJ380c", Vector2i(1, 1), 2, _mock_window(Vector2i(1, 1), 2))
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(1, 1), 2)
assert_int(received.size()).is_equal(1)
assert_bool(req.is_pending()).override_failure_message(
"a cache hit must never leave the request pending"
).is_false()
func test_window_request_cache_miss_leaves_pending_true() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(5, 5), 2)
assert_bool(req.is_pending()).is_true()
## on_response() with a matching Ready+window response resolves the pending
## request AND populates the cache — verified by a second request_now() call
## for the same (center, n) becoming a cache hit with zero additional pending.
func test_on_response_resolves_and_populates_cache_for_next_request() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
req.on_response(_mock_response("GJ380c", window))
assert_bool(req.is_pending()).is_false()
# Re-request the SAME (body, center, n) — must be a cache hit, no pending.
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).override_failure_message(
"a second request for an already-resolved window must hit the cache"
).is_false()
# =============================================================================
# T-1142 item 2: fit-and-center on entry (Jeroen's "postage stamp" finding)
# =============================================================================
## enter() must fit-and-center, NOT reset to the old zoom=1.0/offset=ZERO.
## With a real viewport size set on the Control, the fitted zoom for an
## n=32 default window must scale up past 1.0 (matches
## test_atlas_window_geometry.gd's own fit math, exercised here through the
## real enter() call path instead of the pure function directly).
func test_enter_fits_and_centers_instead_of_resetting_to_zoom_one() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_float(v.get_view_zoom()).override_failure_message(
"an n=32 (512px native) composite in a 1920x1080 viewport must be fitted"
+ " (zoom > 1.0), not left at the old zoom=1.0 postage-stamp default"
).is_greater(1.0)
## After enter()'s fit, the offset must not be Vector2.ZERO (the old
## behavior) — it must be the CENTERING offset the fit produces.
func test_enter_offset_is_not_the_old_zero_default() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_that(v.get_view_offset()).override_failure_message(
"a fitted+centered composite in a 1920x1080 viewport should not sit at (0,0)"
).is_not_equal(Vector2.ZERO)
# =============================================================================
# T-1142 item 3: header carries the body's proper name (cheap half of T-1141)
# =============================================================================
func test_header_location_label_includes_body_proper_name() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("Lendel")
## No proper_name on the body dict -> falls back to body_id (matches
## AtlasViewer's own _refresh_screen_header fallback chain exactly).
func test_header_location_label_falls_back_to_body_id() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ903b"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("GJ903b")
func test_header_location_label_still_includes_the_coordinates() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(42, -7), 2)
var label: String = v._location_label()
assert_str(label).contains("42")
assert_str(label).contains("-7")
# =============================================================================
# T-1142 item 4: drag-pan through the real input chain (DistrictScreen ->
# AtlasWindowViewer._gui_input) — verifies no ancestor eats the event.
# =============================================================================
## Drives _gui_input DIRECTLY on the viewer (the same call gdUnit's own
## headless-mode InputEvent limitation forces every other _gui_input test in
## this cluster to use — see test_atlas_descend_entry.gd's own note on real
## mouse events not being transported in headless mode). This confirms the
## HANDLER logic itself moves _view_offset on a drag; the "does the Control
## TREE deliver the event to this handler at all" question is the separate,
## real concern item 4 raises (DistrictScreen's own mouse_filter=STOP could
## theoretically intercept) — that is checked by the follow-up test below,
## which drives the SAME sequence starting from DistrictScreen's root.
func test_drag_pan_moves_view_offset() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
# body_radius_km absent -> no pole wall (identity clamp), isolating the
# drag-delta math itself from item 5's clamp in this test.
var offset_before: Vector2 = v.get_view_offset()
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 300.0)
v._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(500.0, 350.0) # +100, +50 drag delta
v._gui_input(motion)
assert_that(v.get_view_offset()).override_failure_message(
"a drag must move _view_offset away from its pre-drag value"
).is_not_equal(offset_before)
assert_that(v.get_view_offset()).is_equal(offset_before + Vector2(100.0, 50.0))
## The real scene-tree path: DistrictScreen (mouse_filter=STOP, no
## _gui_input override) -> AtlasWindowViewer (mouse_filter=STOP, HAS
## _gui_input). Godot delivers _gui_input to the DEEPEST/topmost Control
## under the mouse first — DistrictScreen having no _gui_input override
## means it never intercepts before AtlasWindowViewer gets the event; this
## test confirms that structurally by driving the event through
## DistrictScreen's own child and checking the SAME state change reaches
## AtlasWindowViewer, exactly as if the event had arrived organically through
## the real app -> nav-stack -> DistrictScreen chain.
func test_drag_pan_reaches_viewer_through_district_screen_chain() -> void:
var screen: DistrictScreen = auto_free(DistrictScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c"}, "district_center": Vector2i(0, 0)})
var offset_before: Vector2 = screen._viewer.get_view_offset()
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 300.0)
screen._viewer._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(460.0, 300.0)
screen._viewer._gui_input(motion)
assert_that(screen._viewer.get_view_offset()).override_failure_message(
"a drag driven through DistrictScreen's child viewer must still move"
+ " _view_offset — no ancestor in the real screen chain eats the event"
).is_not_equal(offset_before)
# =============================================================================
# T-1142 item 5: pole hard wall wired into the real drag handler
# =============================================================================
## A window already near the pole, dragged FAR toward it, must have its
## offset clamped by the real _gui_input path (not just the pure function in
## isolation — this confirms the wiring, not just the math).
## A synthetic small body (NOT GJ380c's real ~6238km radius) is used
## deliberately: with a real body's huge rows_half (~4785 for GJ380c), the
## wall sits so many screen-pixels away that even an "absurd" mouse-motion
## delta (bounded by a real screen's pixel dimensions) never reaches it —
## the wall is real but the test would need a physically-impossible mouse
## position to trigger it. A small synthetic radius (-> a small rows_half)
## keeps the wall reachable by an ordinary drag delta while exercising the
## exact same code path.
func test_drag_pan_is_clamped_by_the_pole_wall_when_wired() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
# A tiny synthetic radius -> district_extent().rows_half is small (a few
# hundred districts), so the pole wall is within reach of an ordinary
# drag delta. Center 10 districts from the north pole.
var radius_km := 50.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, -rows_half + 10), 32)
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 400.0)
v._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(400.0, -50000.0) # an absurd upward drag
v._gui_input(motion)
assert_float(v.get_view_offset().y).override_failure_message(
"an absurd drag toward the pole must be clamped by the real input path"
).is_greater(-50000.0)
# =============================================================================
# T-1142 item 6: east-west wrap — canonicalization on entry + cache reuse
# =============================================================================
## enter() canonicalizes an out-of-range center BEFORE it becomes
## _held_center — a column past the body's circumference wraps into range.
func test_enter_canonicalizes_an_out_of_range_center() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols + 50, 0), 32)
var window: Dictionary = _mock_window(Vector2i(50, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).override_failure_message(
"the response must be adopted under the CANONICALIZED center (50, 0),"
+ " matching what the server would echo back for the wrapped request"
).is_equal(window)
## A center ONE column past the seam (item 6b): the SAME cache key as its
## twin at column 0 — a full-circumnavigation pan back to the seam must hit
## cache, not re-derive, because both requests canonicalize to the same
## (body, center, n) key.
func test_center_one_column_past_the_seam_shares_a_cache_key_with_its_twin() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v1)
v1.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols, 50), 32)
var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v2)
v2.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, 50), 32)
# Both must adopt the SAME server response (keyed on the same
# canonicalized center) — proves the cache key (and the outbound
# request) canonicalize identically for the seam and its twin.
var window: Dictionary = _mock_window(Vector2i(0, 50), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v1.get_district_window()).is_equal(window)
assert_that(v2.get_district_window()).is_equal(window)
# =============================================================================
# _user_adjusted guard (PR #188 review) — the flag exists so auto-fit NEVER
# fights a manually-adjusted view. The one branch that makes that true
# (resize while user-adjusted) had no coverage; both directions pinned here,
# driving the REAL _gui_input path (synthetic events), not the flag directly.
# =============================================================================
func _drag_viewer(v: AtlasWindowViewer, from: Vector2, to: Vector2) -> void:
var down := InputEventMouseButton.new()
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
down.position = from
down.global_position = from
v._gui_input(down)
var move := InputEventMouseMotion.new()
move.position = to
move.global_position = to
v._gui_input(move)
var up := InputEventMouseButton.new()
up.button_index = MOUSE_BUTTON_LEFT
up.pressed = false
up.position = to
up.global_position = to
v._gui_input(up)
func test_resize_after_manual_drag_keeps_user_view() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
_drag_viewer(v, Vector2(600.0, 400.0), Vector2(540.0, 380.0))
var user_zoom: float = v.get_view_zoom()
var user_offset: Vector2 = v.get_view_offset()
v.size = Vector2(1600.0, 900.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize while user-adjusted must NOT re-fit — zoom belongs to the user"
).is_equal_approx(user_zoom, 0.0001)
assert_vector(v.get_view_offset()).override_failure_message(
"resize while user-adjusted must NOT re-center — offset belongs to the user"
).is_equal_approx(user_offset, Vector2(0.001, 0.001))
func test_resize_without_user_adjustment_refits() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
var fitted_zoom: float = v.get_view_zoom()
v.size = Vector2(640.0, 360.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize with no manual adjustment must re-fit to the new viewport"
).is_not_equal(fitted_zoom)