From 49317d7c191d8c80f19a77ef88edc5307dafd9aa Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 26 Jul 2024 16:13:10 +0200 Subject: [PATCH] scroll element into view for mouse actions This works fine with Firefox, and conforms to the spec. However, Chromium gets confused and clicks on the wrong position. Work around that for now by keeping our old `ph_mouse()` event synthesizer for Chromium. --- bidi-test.py | 34 ++++++++++++++++++++++------ bidi.py | 4 ---- test-functions.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/bidi-test.py b/bidi-test.py index 5a7ac42..fed7b20 100755 --- a/bidi-test.py +++ b/bidi-test.py @@ -126,8 +126,25 @@ class Browser: # TODO: wait for value time.sleep(0.2) - def mouse(self, selector: str, button: int = 0, click_count: int = 1) -> None: + def click(self, selector: str, button: int = 0, click_count: int = 1) -> None: self.wait_visible(selector) + self.bidi("script.evaluate", expression=f"window.ph_find({jsquote(selector)}).scrollIntoView()", + awaitPromise=False, target={"context": self.driver.context}) + + # HACK: Chromium mis-clicks to wrong position with iframes; use our old "synthesize MouseEvent" approach + # TODO: file/find bug + if isinstance(self.driver, bidi.ChromiumBidi): + if click_count == 1: + _type = "click" + elif click_count == 2: + _type = "dblclick" + else: + raise bidi.Error("only click_count=1 or 2 are supported with Chromium") + self.bidi("script.evaluate", + expression=f"window.ph_mouse({jsquote(selector)}, '{_type}', 0, 0, {button})", + awaitPromise=False, target={"context": self.driver.context}) + return + element = self.bidi("script.evaluate", expression=f"window.ph_find({jsquote(selector)})", awaitPromise=False, target={"context": self.driver.context})["result"] @@ -138,16 +155,13 @@ class Browser: self.bidi("input.performActions", context=self.driver.context, actions=[ { - "id": "pointer-0", + "id": f"pointer-{self.driver.last_id}", "type": "pointer", "parameters": {"pointerType": "mouse"}, "actions": actions, } ]) - def click(self, selector: str) -> None: - return self.mouse(selector) - def wait_text(self, selector: str, text: str) -> None: self.wait_visible(selector) self.wait_js_cond(f"window.ph_text({jsquote(selector)}) == {jsquote(text)}", @@ -177,11 +191,17 @@ try: b.set_input_text("#login-user-input", "admin") b.set_input_text("#login-password-input", "foobar") # either works - # b.click("#login-button") - b.key(KEY_ENTER) + b.click("#login-button") + # b.key(KEY_ENTER) b.wait_text("#super-user-indicator", "Limited access") b.switch_to_frame("cockpit1:localhost/system") b.wait_in_text(".system-configuration", "Join domain") + + b.switch_to_top() + b.click("#host-apps a[href='/system/services']") + b.switch_to_frame("cockpit1:localhost/system/services") + b.click("tr[data-goto-unit='virtqemud.service'] a") + b.wait_in_text("#service-details-unit", "Automatically starts") finally: b.close() diff --git a/bidi.py b/bidi.py index 7e46e49..3fa5c9e 100755 --- a/bidi.py +++ b/bidi.py @@ -140,10 +140,6 @@ class WebdriverBidi: else: raise WebdriverError("timed out waiting for default realm") - # avoid not seeing elements due to too small window - # await self.bidi("browsingContext.setViewport", context=self.top_context, - # viewport={"width": 1024, "height": 5000}) - async def __aenter__(self): await self.start_session() return self diff --git a/test-functions.js b/test-functions.js index 50e0c55..633e3fe 100644 --- a/test-functions.js +++ b/test-functions.js @@ -73,3 +73,59 @@ window.ph_wait_cond = function (cond, timeout, error_description) { step(); }); }; + +// we only need this for Chromium, which mis-handles pointerMove with frames +window.ph_mouse = function(sel, type, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) { + const el = window.ph_find(sel); + + /* The element has to be visible, and not collapsed */ + if (el.offsetWidth <= 0 && el.offsetHeight <= 0 && el.tagName !== 'svg') + throw new Error(sel + " is not visible"); + + /* The event has to actually work */ + let processed = false; + function handler() { + processed = true; + } + + el.addEventListener(type, handler, true); + + let elp = el; + let left = elp.offsetLeft || 0; + let top = elp.offsetTop || 0; + while (elp.offsetParent) { + elp = elp.offsetParent; + left += elp.offsetLeft; + top += elp.offsetTop; + } + + let detail = 0; + if (["click", "mousedown", "mouseup"].indexOf(type) > -1) + detail = 1; + else if (type === "dblclick") + detail = 2; + + const ev = new MouseEvent(type, { + bubbles: true, + cancelable: true, + view: window, + detail, + screenX: left + x, + screenY: top + y, + clientX: left + x, + clientY: top + y, + button: btn, + ctrlKey: ctrlKey || false, + shiftKey: shiftKey || false, + altKey: altKey || false, + metaKey: metaKey || false + }); + + el.dispatchEvent(ev); + + el.removeEventListener(type, handler, true); + + /* It really had to work */ + if (!processed) + throw new Error(sel + " is disabled or somehow doesn't process events"); +};