From c50bf49ff08553ca05424e0ea881a4d719796675 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Wed, 14 Aug 2024 12:40:22 +0200 Subject: [PATCH] test: Use real mouse events in tests on Firefox Our years-old `ph_mouse()` helper is cheating: It completely side-steps the browser UI and directly synthesizes `MouseEvent`s in JavaScript. This allowed funny things like clicking on a main page element while a dialog is open (which you can't normally do as the dialog is modal), clicking an element which is disabled in some non-standard way, or clicking through a tooltip. Make this more realistic by using the BiDi API for synthesizing mouse events. This works fine with Firefox, but is unfortunately completely broken in iframes with Chromium: https://issues.chromium.org/issues/359616812 Specifying precise coordinates is not currently implemented. `ph_mouse()` always clicks on the top left corner, while webdriver's `pointerMove` puts (0,0) in the center of the element. This can be done with a little extra calculation of the `getBoundingRect()`, but only very few tests like `TestStorageLvm2.testMaxLayoutGrowth` rely on that. For these tests, continue to use `ph_mouse()`. https://issues.redhat.com/browse/COCKPIT-1158 --- test/common/test-functions.js | 13 ++++++++ test/common/testlib.py | 58 ++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/test/common/test-functions.js b/test/common/test-functions.js index cdb4d174c15..3f944a7796c 100644 --- a/test/common/test-functions.js +++ b/test/common/test-functions.js @@ -28,6 +28,19 @@ window.ph_find = function(sel) { return window.ph_only(els, sel); }; +window.ph_find_scroll_into_view = function(sel) { + const el = window.ph_find(sel); + /* we cannot make this conditional, as there is no way to find out whether + an element is currently visible -- the usual trick to compare getBoundingClientRect() against + window.innerHeight does not work if the element is in e.g. a scrollable dialog area */ + return new Promise(resolve => { + el.scrollIntoView({ behaviour: 'instant', block: 'center', inline: 'center' }); + // scrolling needs a little bit of time to stabilize, and it's not predictable + // in particular, 'scrollend' is not reliably emitted + window.setTimeout(() => resolve(el), 200); + }); +}; + window.ph_count = function(sel) { const els = window.ph_select(sel); return els.length; diff --git a/test/common/testlib.py b/test/common/testlib.py index d4692d1bceb..290c6deee52 100644 --- a/test/common/testlib.py +++ b/test/common/testlib.py @@ -114,6 +114,7 @@ "ArrowDown": "\uE015", "Insert": "\uE016", "Delete": "\uE017", + "Meta": "\uE03D", } @@ -502,7 +503,62 @@ def mouse( :param metaKey: press the meta key """ self.wait_visible(selector) - self.call_js_func('ph_mouse', selector, event, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) + + # HACK: Chromium clicks don't work with iframes; use our old "synthesize MouseEvent" approach + # https://issues.chromium.org/issues/359616812 + # TODO: x and y are not currently implemented: webdriver (0, 0) is the element's center, not top left corner + if self.browser == "chromium" or x != 0 or y != 0: + self.call_js_func('ph_mouse', selector, event, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) + return + + # For Firefox and regular clicks, use the BiDi API, which is more realistic -- it doesn't + # sidestep the browser + element = self.call_js_func('ph_find_scroll_into_view', selector) + + actions = [{"type": "pointerMove", "x": 0, "y": 0, "origin": {"type": "element", "element": element}}] + down = {"type": "pointerDown", "button": btn} + up = {"type": "pointerUp", "button": btn} + if event == "click": + actions.extend([down, up]) + elif event == "dblclick": + actions.extend([down, up, down, up]) + elif event == "mouseenter": + actions.insert(0, {"type": "pointerMove", "x": 0, "y": 0, "origin": "viewport"}) + elif event == "mouseleave": + actions.append({"type": "pointerMove", "x": 0, "y": 0, "origin": "viewport"}) + else: + raise NotImplementedError(f"unknown event {event}") + + # modifier keys + ev_id = f"pointer-{self.driver.last_id}" + keys_pre = [] + keys_post = [] + + def key(type_: str, name: str) -> JsonObject: + return {"type": "key", "id": ev_id + type_, "actions": [{"type": type_, "value": WEBDRIVER_KEYS[name]}]} + + if altKey: + keys_pre.append(key("keyDown", "Alt")) + keys_post.append(key("keyUp", "Alt")) + if ctrlKey: + keys_pre.append(key("keyDown", "Control")) + keys_post.append(key("keyUp", "Control")) + if shiftKey: + keys_pre.append(key("keyDown", "Shift")) + keys_post.append(key("keyUp", "Shift")) + if metaKey: + keys_pre.append(key("keyDown", "Meta")) + keys_post.append(key("keyUp", "Meta")) + + # the actual mouse event + actions = [{ + "id": ev_id, + "type": "pointer", + "parameters": {"pointerType": "mouse"}, + "actions": actions, + }] + + self.bidi("input.performActions", context=self.driver.context, actions=keys_pre + actions + keys_post) def click(self, selector: str) -> None: """Click on a ui element