|
| 1 | +"""Physical-mouse coverage for the panel and for the screen-to-world contract. |
| 2 | +
|
| 3 | +Most scenarios reach the editors through the typed control channel, which never |
| 4 | +touches the engine's mouse callins. These two do the opposite: every input here |
| 5 | +is a real X11 event, and the assertions read the resulting state back through |
| 6 | +the control channel. They carry no goldens, so they stay cheap enough to run on |
| 7 | +every suite. |
| 8 | +""" |
| 9 | + |
| 10 | +from typing import TYPE_CHECKING |
| 11 | + |
| 12 | +from e2e.driver.timing import Delay |
| 13 | +from e2e.scenarios.helpers.camera import zoom_map |
| 14 | +from e2e.scenarios.helpers.geometry import ( |
| 15 | + MAP, |
| 16 | + OBJECTS, |
| 17 | + TAB_X, |
| 18 | + TAB_Y, |
| 19 | + editor_point, |
| 20 | + panel_left, |
| 21 | + panel_point, |
| 22 | + window_size, |
| 23 | +) |
| 24 | +from e2e.scenarios.helpers.objects import arm_tree as _arm_tree |
| 25 | +from e2e.scenarios.helpers.objects import open_object_editor as _open |
| 26 | +from e2e.scenarios.helpers.registry import scenario |
| 27 | +from e2e.scenarios.objects.common import _placed |
| 28 | + |
| 29 | +if TYPE_CHECKING: |
| 30 | + from e2e.driver.state import RunState |
| 31 | + |
| 32 | +# A click and the ray query for the same pixel describe the same spot. The |
| 33 | +# budget covers the brush's own rounding, not a different point on the map: a |
| 34 | +# mirrored y lands hundreds of units away even near the middle of the view. |
| 35 | +TRACE_TOLERANCE = 40.0 |
| 36 | + |
| 37 | + |
| 38 | +@scenario() |
| 39 | +def mouse_ui(run_state: "RunState") -> None: |
| 40 | + """The panel, driven only by the pointer. |
| 41 | +
|
| 42 | + Tab, editor button, field click, typed commit and a numeric drag are all |
| 43 | + real X11 input, and the editor model is read back to prove each one landed |
| 44 | + on the control it aimed at. Without this, every editor is reachable in tests |
| 45 | + only through the control channel, which no user has. |
| 46 | +
|
| 47 | + Note what this cannot cover: RmlUi consumes a press over the panel document |
| 48 | + inside the engine, so those clicks never reach the native mouse callin at |
| 49 | + all. The panel's own hit test therefore sees map presses only, and this |
| 50 | + scenario proves the user-facing path rather than that hit test. |
| 51 | + """ |
| 52 | + run_state.focus() |
| 53 | + left = panel_left(run_state) |
| 54 | + run_state.click(left + TAB_X["map"], TAB_Y, delay=Delay.FRAME) |
| 55 | + run_state.click(*editor_point(left, "map", "terrain"), delay=Delay.READY) |
| 56 | + |
| 57 | + terrain = run_state.control.editor("heightmapEditor") |
| 58 | + run_state.fill_text( |
| 59 | + *panel_point(left, MAP["terrain_size"]), |
| 60 | + "140", |
| 61 | + click_delay=Delay.FRAME, |
| 62 | + commit_delay=Delay.SETTLE, |
| 63 | + ) |
| 64 | + typed = terrain.get("size") |
| 65 | + assert typed == 140.0, f"typing into the Size field gave {typed!r}, want 140.0" |
| 66 | + |
| 67 | + # A numeric drag pins the pointer and warps it back, so the motion has to be |
| 68 | + # relative. The direction is what matters: the exact value depends on which |
| 69 | + # tick the release meets. |
| 70 | + run_state.press(*panel_point(left, MAP["terrain_size"])) |
| 71 | + run_state.move_relative(60) |
| 72 | + run_state.release(*panel_point(left, MAP["terrain_size"]), delay=Delay.SETTLE) |
| 73 | + dragged = terrain.get("size") |
| 74 | + assert isinstance(dragged, float), f"Size is {dragged!r}, want a number" |
| 75 | + assert dragged > typed, f"dragging right did not raise Size: {typed} -> {dragged}" |
| 76 | + |
| 77 | + # A press beside the panel belongs to the map, not to the control that was |
| 78 | + # last touched. This is the ownership half of the hit test. |
| 79 | + run_state.fill_text( |
| 80 | + *panel_point(left, MAP["terrain_rotation"]), |
| 81 | + "15", |
| 82 | + click_delay=Delay.FRAME, |
| 83 | + commit_delay=Delay.SETTLE, |
| 84 | + ) |
| 85 | + rotation = terrain.get("rotation") |
| 86 | + assert rotation == 15.0, f"the second field took {rotation!r}, want 15.0" |
| 87 | + assert terrain.get("size") == dragged, "editing Rotation moved Size" |
| 88 | + |
| 89 | + |
| 90 | +@scenario() |
| 91 | +def mouse_coordinates(run_state: "RunState") -> None: |
| 92 | + """A click places an object where that pixel points. |
| 93 | +
|
| 94 | + The engine reports mouse callbacks in its own bottom-origin screen space, |
| 95 | + while control clients address the window top-origin like a screenshot. Both |
| 96 | + paths are asked about the same two pixels here, so a future flip on either |
| 97 | + side fails immediately -- and says so, rather than moving a golden's trees. |
| 98 | + """ |
| 99 | + run_state.focus() |
| 100 | + left = _open(run_state, "features") |
| 101 | + run_state.click(*panel_point(left, OBJECTS["add"]), delay=Delay.DIALOG) |
| 102 | + _arm_tree(run_state, left) |
| 103 | + |
| 104 | + width, height = window_size(run_state) |
| 105 | + spot_x = width // 3 |
| 106 | + zoom_map(run_state, point=(spot_x, height // 2)) |
| 107 | + |
| 108 | + # Two points either side of the middle: a mirrored y is invisible at the |
| 109 | + # centre of the view and grows with the distance from it. |
| 110 | + for label, spot_y in (("upper", height // 3), ("lower", height * 2 // 3)): |
| 111 | + expected = run_state.control.camera.trace_screen_ray(spot_x, spot_y) |
| 112 | + assert expected["hit_type"] == 3, f"{label}: the ray missed the ground: {expected}" |
| 113 | + mark = len(run_state.commands()) |
| 114 | + run_state.click_settled(spot_x, spot_y, delay=Delay.SETTLE) |
| 115 | + placed = _placed(run_state, mark) |
| 116 | + assert len(placed) == 1, f"{label}: click placed {len(placed)} objects, want 1" |
| 117 | + |
| 118 | + want_x, _want_y, want_z = expected["position"] |
| 119 | + got = placed[0] |
| 120 | + drift = max(abs(got["x"] - want_x), abs(got["z"] - want_z)) |
| 121 | + assert drift <= TRACE_TOLERANCE, ( |
| 122 | + f"{label}: click at ({spot_x}, {spot_y}) placed the feature at " |
| 123 | + f"({got['x']:.0f}, {got['z']:.0f}), but that pixel traces to " |
| 124 | + f"({want_x:.0f}, {want_z:.0f}) -- {drift:.0f} units away" |
| 125 | + ) |
0 commit comments