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>
496 lines
21 KiB
GDScript
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)
|