## 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") # T-1156 wave 1 round 3: AtlasWindowNatureOverlay has no class_name (matching # atlas_overlay_bar.gd/atlas_window_request.gd's own no-class_name precedent, # review #8) — subclassing it (the _CountingNatureOverlay spy below) needs # the preloaded script's PATH via `extends`, not a global class name. const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.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() ## PR #195 review (Hoshe): named default-visibility pins for the T-1156 ## nature overlays at fresh-viewer construction, per Araminta's ruling — ## RVR ON (rivers are load-bearing geography, a first-time Atlas viewer sees ## them without hunting for a toggle: the single most player-visible behavior ## the feature ships), BAS and ATR OFF (secondary/dev-facing analytical ## layers, opt-in). Without these, a refactor of the _ready() visibility-init ## loop could silently flip the defaults and only a live capture would ## notice — gen_basins was previously covered only incidentally by the ## toggle-redraw spy test above. func test_nature_overlay_defaults_rivers_on_basins_and_attractors_off() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) assert_bool(v.is_overlay_visible("gen_rivers")).override_failure_message( "gen_rivers must default ON (Araminta's RVR-on ruling, T-1156 wave 1)" ).is_true() assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message( "gen_basins must default OFF (opt-in analytical layer)" ).is_false() assert_bool(v.is_overlay_visible("gen_attractors")).override_failure_message( "gen_attractors must default OFF (dev-facing detail, opt-in)" ).is_false() ## Counts real _draw() invocations on AtlasWindowNatureOverlay — CanvasItem ## exposes no public "is a redraw pending" query in this Godot version ## (confirmed directly: is_queued_for_redraw() does not exist on Node2D here ## — an earlier version of this test assumed it did and failed with ## "Invalid call. Nonexistent function"), so the only reliable signal that ## queue_redraw() actually had an effect is the engine calling _draw() again ## on a subsequent frame. Matches test_atlas_cold_start.gd's own ## _CountingOverlay precedent exactly (same file's own doc: "the only ## reliable signal... is the engine calling _draw() again") — subclasses the ## REAL AtlasWindowNatureOverlay so drawing still runs through genuine ## production code, this spy only adds counting. class _CountingNatureOverlay extends AtlasWindowNatureOverlay: var draw_count := 0 func _draw() -> void: draw_count += 1 super._draw() ## Coordinator live-eyeball round 3 (2026-07-23): R1/R2 captures were ## byte-identical because a scratch drive script called ## set_overlay_visible("BAS", true) — the button LABEL, not the overlay id ## ("gen_basins") — which set_overlay_visible()'s own `not ## _overlay_visibility.has(overlay_id): push_warning(...); return` guard ## silently no-ops on. Real callers (atlas_overlay_bar.gd's ## _on_toggle_changed()) always pass def["id"], never the label, so product ## code was never actually broken — but this pins the EXACT gate the ## coordinator asked to verify: toggling gen_basins via the real viewer API ## must (a) flip is_overlay_visible("gen_basins") — the nature overlay's OWN ## draw gate, read live via viewer.is_overlay_visible() at _draw() time, not ## a stale copy — AND (b) actually cause the NATURE overlay (not just the ## terrain overlay/viewer) to redraw on the next frame, proven by swapping in ## a _draw()-counting spy (matching test_atlas_cold_start.gd's ## _CountingOverlay pattern) and confirming draw_count advances past a ## settled baseline after the toggle, with no other gesture. func test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_overlay() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message( "gen_basins must default to OFF (Araminta's ruling — BAS defaults off)" ).is_false() # Swap in the counting spy (matching _CountingOverlay's own swap-after- # construction shape) so entry's own queue_redraw() calls don't pollute # the baseline, then let it settle before touching the toggle. var spy := _CountingNatureOverlay.new(v) v._nature_overlay.queue_free() v._nature_overlay = spy v._canvas.add_child(spy) await get_tree().process_frame await get_tree().process_frame var baseline: int = spy.draw_count assert_int(baseline).override_failure_message( "sanity: the spy must have drawn at least once before the toggle, or" + " this test can't distinguish 'redrawn BY the toggle' from 'never" + " drawn at all'" ).is_greater(0) v.set_overlay_visible("gen_basins", true) await get_tree().process_frame assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message( "the toggle must flip the draw gate is_overlay_visible() reads live" ).is_true() assert_int(spy.draw_count).override_failure_message( "the toggle must queue_redraw() the NATURE overlay specifically —" + " queue_redraw() on the viewer/terrain overlay alone leaves the" + " nature node's last frame cached (a Node2D child does not redraw" + " because its sibling did) — draw_count must have advanced past the" + " baseline (%d)" % baseline ).is_greater(baseline) # ============================================================================= # 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)) ## T-1153: MIN_ZOOM widened to 0.0005 (from the pre-ladder 0.5) so a ## gas-giant-scale body's enter_orbital() fit zoom is never itself clamped — ## see MIN_ZOOM's own doc. Values here are chosen well outside the new wide ## range on both ends, not the old range's boundary values. func test_set_view_clamps_to_min_max_zoom() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.set_view(0.0000001, Vector2.ZERO) assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.0001) 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-1145 item 2: WASD/edge-scroll pan REPLACES drag-pan entirely (Jeroen's # input-model ruling — LMB-drag broke click semantics with future map # objects). Testable-shape choice (per the ticket's explicit either/or): # _apply_pan_delta(direction, delta) is the extracted, testable pan-tick — # calling it DIRECTLY with a synthetic direction/delta is preferred over # synthesizing InputEventKey events through _gui_input, because WASD panning # is NOT event-routed at all (it is Input.is_key_pressed() polling inside # _process(), see _held_pan_direction()'s own doc) — synthesizing a key EVENT # would exercise nothing (no _gui_input branch reads WASD), and driving it # through Godot's actual global Input singleton state (Input.action_press() # et al) would work but couples every test to mutating engine-global state # that must then be carefully reset, for zero additional coverage over # calling the already-extracted pure-ish tick function directly. This # confirms the HANDLER/tick logic itself (offset movement, pole wall, wrap, # _user_adjusted, refetch) exactly as the old drag tests did; a live human # drive (WASD held down, edge-scroll near a real screen edge) is the ## lead's own stated live-verification step for what a real key-repeat/mouse- ## position sequence produces end to end. # ============================================================================= ## _apply_pan_delta() must move _view_offset — the WASD-input-model ## equivalent of the old test_drag_pan_moves_view_offset. func test_wasd_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 # pan-delta math itself from item 5's clamp in this test. var offset_before: Vector2 = v.get_view_offset() v._apply_pan_delta(Vector2(1.0, 0.0), 0.1) # "D"/east held for one tick assert_that(v.get_view_offset()).override_failure_message( "a pan tick must move _view_offset away from its pre-pan value" ).is_not_equal(offset_before) ## Frame-rate independence (T-1145's explicit requirement): the SAME held ## direction over a LONGER delta must move the view FARTHER — proportionally, ## not by some fixed per-tick step. Two short ticks must (within float ## rounding) equal one long tick of the combined duration. func test_wasd_pan_is_frame_rate_independent() -> void: var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v1) v1.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32) v1._apply_pan_delta(Vector2(1.0, 0.0), 0.02) v1._apply_pan_delta(Vector2(1.0, 0.0), 0.02) var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v2) v2.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32) v2._apply_pan_delta(Vector2(1.0, 0.0), 0.04) assert_vector(v1.get_view_offset()).override_failure_message( "two 0.02s ticks must move the view the same distance as one 0.04s tick" ).is_equal_approx(v2.get_view_offset(), Vector2(0.01, 0.01)) ## Diagonal input (e.g. W+D held together) must NOT pan faster than a single ## axis — _apply_pan_delta() normalizes the direction before applying speed. func test_wasd_diagonal_pan_is_not_faster_than_single_axis() -> void: var v_diag: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v_diag) v_diag.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32) var before_diag: Vector2 = v_diag.get_view_offset() v_diag._apply_pan_delta(Vector2(1.0, -1.0), 0.1) # D+W (east+north) held together var diag_distance: float = before_diag.distance_to(v_diag.get_view_offset()) var v_axis: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v_axis) v_axis.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32) var before_axis: Vector2 = v_axis.get_view_offset() v_axis._apply_pan_delta(Vector2(1.0, 0.0), 0.1) # D (east) alone var axis_distance: float = before_axis.distance_to(v_axis.get_view_offset()) assert_float(diag_distance).override_failure_message( "diagonal WASD must travel the SAME distance per tick as a single axis, not faster" ).is_equal_approx(axis_distance, 0.01) ## The real scene-tree path: RegionalScreen -> AtlasWindowViewer (T-1153 — ## RegionalScreen is now the WHOLE ladder's nav entry, superseding the ## retired DistrictScreen nav hop; see atlas_app.gd's own doc for why the ## separate "district" screen retired). Unlike drag (which needed ## _gui_input event delivery, hence the old "does an ancestor eat the ## event" test), WASD pan lives in _process() — Godot delivers _process() ## to every node in the tree regardless of Control mouse_filter/ancestry ## (there is no "topmost control" routing for per-frame process callbacks ## the way there is for _gui_input), so there is no equivalent "does the ## screen eat it" question for _process() itself. What DOES still matter ## through the real chain is _is_over_ui()'s edge-scroll suppression and ## visibility gating — pinned directly below instead. func test_wasd_pan_reaches_viewer_through_regional_screen_chain() -> void: var screen: RegionalScreen = auto_free(RegionalScreen.new()) add_child(screen) screen.enter({"body": {"body_id": "GJ380c"}, "system": {}}) var offset_before: Vector2 = screen._viewer.get_view_offset() screen._viewer._apply_pan_delta(Vector2(1.0, 0.0), 0.1) assert_that(screen._viewer.get_view_offset()).override_failure_message( "a pan tick driven through RegionalScreen's child viewer must still move" + " _view_offset — no ancestor in the real screen chain blocks it" ).is_not_equal(offset_before) # ============================================================================= # T-1142 item 5: pole hard wall wired into the (now WASD) pan handler # ============================================================================= ## A window already near the pole, panned FAR toward it, must have its ## offset clamped by the real _apply_pan_delta() 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 far away that even a long held-key tick never reaches it — ## the wall is real but the test would need an implausibly long hold to ## trigger it. A small synthetic radius (-> a small rows_half) keeps the ## wall reachable by an ordinary tick while exercising the exact same code ## path. func test_wasd_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 # pan tick. 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) # An absurdly long single tick (500s — no real frame is ever this long, # deliberately so the UNCLAMPED delta is orders of magnitude larger than # any plausible wall position, making "was it actually clamped" an # unambiguous check rather than a fragile near-boundary comparison). var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom() v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held var message: String = ( "a pan toward the pole with an unclamped magnitude of %.0f must land" + " nowhere near that far — the wall must have clamped it" ) % unclamped_magnitude assert_float(absf(v.get_view_offset().y)).override_failure_message(message).is_less( unclamped_magnitude * 0.5 ) # ============================================================================= # 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. # T-1145: the ORIGINAL version drove this through a synthetic drag sequence # (_gui_input); drag is gone (item 2), so this now drives a WASD press # instead — via _apply_pan_delta() directly, same testable-shape choice # documented at the top of the WASD section above (a real key-repeat # sequence through _gui_input would exercise nothing, since WASD panning # never goes through _gui_input at all). # ============================================================================= func test_resize_after_manual_wasd_press_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) v._apply_pan_delta(Vector2(1.0, -1.0), 0.1) # a single "D+W" tick — sets _user_adjusted 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) # ============================================================================= # T-1145 item 2: edge-scroll suppression — over UI (_is_over_ui reuse) and # unfocused-window (_app_has_focus, NOTIFICATION_APPLICATION_FOCUS_OUT/IN). # ============================================================================= ## Cursor within EDGE_SCROLL_MARGIN_PX of the left edge -> edge-scrolling. func test_edge_scroll_detects_cursor_near_the_left_edge() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(10.0, 300.0) # within 24px of x=0 assert_bool(v._is_cursor_edge_scrolling()).is_true() ## Cursor well inside the viewport (nowhere near any edge) -> NOT edge-scrolling. func test_edge_scroll_does_not_trigger_away_from_any_edge() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(400.0, 300.0) # dead center assert_bool(v._is_cursor_edge_scrolling()).is_false() ## Cursor near the RIGHT edge (not just left) also triggers — all four edges ## are live, not just one. func test_edge_scroll_detects_cursor_near_the_right_edge() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(795.0, 300.0) # within 24px of x=800 assert_bool(v._is_cursor_edge_scrolling()).is_true() ## The direction produced when edge-scrolling near the left edge must point ## WEST (negative X) — toward the edge the cursor is near, matching WASD's ## own "A pans toward more western content" semantics exactly (same sign ## convention, same _apply_pan_delta() consumer). func test_edge_scroll_direction_points_toward_the_near_edge() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(5.0, 300.0) var direction: Vector2 = v._edge_scroll_direction() assert_float(direction.x).override_failure_message( "edge-scroll near the LEFT edge must produce a WESTWARD (negative x) direction" ).is_less(0.0) assert_float(direction.y).is_equal_approx(0.0, 0.001) ## Reuses _is_over_ui() (the ticket's explicit instruction) — this screen's ## own _is_over_ui() always returns false today (no city panel yet, see its ## own doc), so edge-scroll near an edge must still trigger; the POINT of ## this test is pinning that the suppression call-site exists and reads ## _is_over_ui's real return value, not that it currently suppresses ## anything (nothing to suppress against yet on this screen). func test_edge_scroll_over_ui_uses_is_over_ui() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(5.0, 300.0) assert_bool(v._is_over_ui(v._last_mouse_pos)).override_failure_message( "AtlasWindowViewer._is_over_ui() has no UI surface yet (see its own doc) —" + " this pins that baseline so a future sidebar addition's test failure here" + " signals the edge-scroll suppression wiring needs a look, not a silent pass" ).is_false() assert_bool(v._is_cursor_edge_scrolling()).is_true() ## _app_has_focus defaults true (a freshly-entered screen assumes OS focus). func test_app_focus_defaults_true() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) assert_bool(v._app_has_focus).is_true() ## NOTIFICATION_APPLICATION_FOCUS_OUT flips _app_has_focus false, and edge- ## scroll must stop triggering even with the cursor still parked at an edge ## — "if detectable" per the ticket; Godot's own focus notification IS ## directly detectable, so this pins that it is actually wired. func test_app_focus_out_suppresses_edge_scroll() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(5.0, 300.0) assert_bool(v._is_cursor_edge_scrolling()).override_failure_message( "sanity: edge-scroll must be live before focus-out" ).is_true() v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT) assert_bool(v._app_has_focus).is_false() assert_bool(v._is_cursor_edge_scrolling()).override_failure_message( "edge-scroll must be suppressed while the OS window lacks focus" ).is_false() ## NOTIFICATION_APPLICATION_FOCUS_IN restores edge-scroll after a focus-out. func test_app_focus_in_restores_edge_scroll() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 600.0) v._last_mouse_pos = Vector2(5.0, 300.0) v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT) v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_IN) assert_bool(v._app_has_focus).is_true() assert_bool(v._is_cursor_edge_scrolling()).is_true() # ============================================================================= # T-1145 item 2: WASD reads the PHYSICAL keycode, independent of the # gameplay move_north/move_south/move_east/move_west InputMap actions those # SAME keys are already bound to project-wide (D-054). This is a structural # check, not a live-input one (gdUnit's headless mode does not transport real # InputEvents, per this suite's own established note) — it pins that ## _held_pan_direction() calls Input.is_key_pressed() (physical keycode), NOT ## Input.is_action_pressed("move_north") or similar, by inspecting that no ## project Input Map action name appears anywhere in this function's own ## reachable behavior. The live independence claim itself (holding W pans ## the map AND does not also queue a gameplay move) is the lead's own ## live-verification step. # ============================================================================= ## project.godot's move_north/move_south/move_east/move_west actions are ## ALREADY bound to W/S/A/D physical keys (confirmed by direct inspection of ## project.godot's [input] section during T-1145 implementation) — this test ## exists purely as a living pin of that fact, so the rationale in ## _held_pan_direction()'s own doc comment (why raw keycodes, not the shared ## action) stays true if the project's key bindings are ever edited. func test_wasd_keys_are_the_same_physical_keys_as_gameplay_movement_actions() -> void: var action_to_key: Dictionary = { "move_north": KEY_W, "move_west": KEY_A, "move_south": KEY_S, "move_east": KEY_D } for action: String in action_to_key.keys(): assert_bool(InputMap.has_action(action)).override_failure_message( "expected gameplay action '%s' to exist in the project InputMap" % action ).is_true() var bound_to_key: bool = false for input_event: InputEvent in InputMap.action_get_events(action): if input_event is InputEventKey and (input_event as InputEventKey).physical_keycode == action_to_key[action]: bound_to_key = true break assert_bool(bound_to_key).override_failure_message( ( "expected '%s' to be bound to physical keycode %d — if this ever" + " stops being true, _held_pan_direction()'s own doc comment" + " (why it reads Input.is_key_pressed() instead of the shared" + " action) should be re-checked, not silently left stale" ) % [action, action_to_key[action]] ).is_true()