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.
This commit is contained in:
Martin Pitt 2024-07-26 16:13:10 +02:00
parent 9653a60fc2
commit 49317d7c19
3 changed files with 83 additions and 11 deletions

View file

@ -126,8 +126,25 @@ class Browser:
# TODO: wait for value # TODO: wait for value
time.sleep(0.2) 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.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)})", element = self.bidi("script.evaluate", expression=f"window.ph_find({jsquote(selector)})",
awaitPromise=False, target={"context": self.driver.context})["result"] awaitPromise=False, target={"context": self.driver.context})["result"]
@ -138,16 +155,13 @@ class Browser:
self.bidi("input.performActions", context=self.driver.context, actions=[ self.bidi("input.performActions", context=self.driver.context, actions=[
{ {
"id": "pointer-0", "id": f"pointer-{self.driver.last_id}",
"type": "pointer", "type": "pointer",
"parameters": {"pointerType": "mouse"}, "parameters": {"pointerType": "mouse"},
"actions": actions, "actions": actions,
} }
]) ])
def click(self, selector: str) -> None:
return self.mouse(selector)
def wait_text(self, selector: str, text: str) -> None: def wait_text(self, selector: str, text: str) -> None:
self.wait_visible(selector) self.wait_visible(selector)
self.wait_js_cond(f"window.ph_text({jsquote(selector)}) == {jsquote(text)}", 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-user-input", "admin")
b.set_input_text("#login-password-input", "foobar") b.set_input_text("#login-password-input", "foobar")
# either works # either works
# b.click("#login-button") b.click("#login-button")
b.key(KEY_ENTER) # b.key(KEY_ENTER)
b.wait_text("#super-user-indicator", "Limited access") b.wait_text("#super-user-indicator", "Limited access")
b.switch_to_frame("cockpit1:localhost/system") b.switch_to_frame("cockpit1:localhost/system")
b.wait_in_text(".system-configuration", "Join domain") 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: finally:
b.close() b.close()

View file

@ -140,10 +140,6 @@ class WebdriverBidi:
else: else:
raise WebdriverError("timed out waiting for default realm") 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): async def __aenter__(self):
await self.start_session() await self.start_session()
return self return self

View file

@ -73,3 +73,59 @@ window.ph_wait_cond = function (cond, timeout, error_description) {
step(); 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");
};