1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 from urllib
.parse
import quote
11 from webdriver
.bidi
.modules
.script
import ContextTarget
15 def __init__(self
, session
, event_loop
):
16 self
.session
= session
17 self
.event_loop
= event_loop
18 self
.content_blocker_loaded
= False
21 def current_url(self
):
22 return self
.session
.url
26 return self
.session
.alert
30 return self
.session
.send_session_command("GET", "moz/context")
33 def context(self
, context
):
34 self
.session
.send_session_command("POST", "moz/context", {"context": context
})
36 @contextlib.contextmanager
37 def using_context(self
, context
):
38 orig_context
= self
.context
39 needs_change
= context
!= orig_context
42 self
.context
= context
48 self
.context
= orig_context
50 def wait_for_content_blocker(self
):
51 if not self
.content_blocker_loaded
:
52 with self
.using_context("chrome"):
53 self
.session
.execute_async_script(
55 const done = arguments[0],
56 signal = "safebrowsing-update-finished";
58 Services.obs.removeObserver(finish, signal);
61 Services.obs.addObserver(finish, signal);
64 self
.content_blocker_loaded
= True
68 return self
.session
.actions
.sequence("key", "keyboard_id")
72 return self
.session
.actions
.sequence(
73 "pointer", "pointer_id", {"pointerType": "mouse"}
78 return self
.session
.actions
.sequence(
79 "pointer", "pointer_id", {"pointerType": "pen"}
84 return self
.session
.actions
.sequence(
85 "pointer", "pointer_id", {"pointerType": "touch"}
90 return self
.session
.actions
.sequence("wheel", "wheel_id")
93 def modifier_key(self
):
94 if self
.session
.capabilities
["platformName"] == "mac":
95 return "\ue03d" # meta (command)
97 return "\ue009" # control
99 def inline(self
, doc
):
100 return "data:text/html;charset=utf-8,{}".format(quote(doc
))
102 async def top_context(self
):
103 contexts
= await self
.session
.bidi_session
.browsing_context
.get_tree()
106 async def navigate(self
, url
, timeout
=None, **kwargs
):
107 return await asyncio
.wait_for(
108 asyncio
.ensure_future(self
._navigate
(url
, **kwargs
)), timeout
=timeout
111 async def _navigate(self
, url
, wait
="complete", await_console_message
=None):
112 if self
.session
.test_config
.get("use_pbm") or self
.session
.test_config
.get(
115 print("waiting for content blocker...")
116 self
.wait_for_content_blocker()
117 if await_console_message
is not None:
118 console_message
= await self
.promise_console_message_listener(
119 await_console_message
122 page_load
= await self
.promise_readystate_listener("load", url
=url
)
124 await self
.session
.bidi_session
.browsing_context
.navigate(
125 context
=(await self
.top_context())["context"],
127 wait
=wait
if wait
!= "load" else None,
129 except webdriver
.bidi
.error
.UnknownErrorException
as u
:
132 "NS_BINDING_ABORTED" not in m
133 and "NS_ERROR_ABORT" not in m
134 and "NS_ERROR_WONT_HANDLE_CONTENT" not in m
139 if await_console_message
is not None:
140 await console_message
142 async def promise_event_listener(self
, events
, check_fn
=None, timeout
=20):
143 if type(events
) is not list:
146 await self
.session
.bidi_session
.session
.subscribe(events
=events
)
148 future
= self
.event_loop
.create_future()
150 listener_removers
= []
152 def remove_listeners():
153 for listener_remover
in listener_removers
:
159 async def on_event(method
, data
):
160 print("on_event", method
, data
)
162 if check_fn
is not None:
163 val
= check_fn(method
, data
)
166 future
.set_result(val
)
169 r
= self
.session
.bidi_session
.add_event_listener(event
, on_event
)
170 listener_removers
.append(r
)
174 return await asyncio
.wait_for(future
, timeout
=timeout
)
178 await asyncio
.wait_for(
179 self
.session
.bidi_session
.session
.unsubscribe(events
=events
),
182 except asyncio
.exceptions
.TimeoutError
:
183 print("Unexpectedly timed out unsubscribing", events
)
186 return asyncio
.create_task(task())
188 async def promise_console_message_listener(self
, msg
, **kwargs
):
189 def check(method
, data
):
191 if msg
in data
["text"]:
193 if "args" in data
and len(data
["args"]):
194 for arg
in data
["args"]:
195 if "value" in arg
and msg
in arg
["value"]:
198 return await self
.promise_event_listener("log.entryAdded", check
, **kwargs
)
200 async def is_console_message(self
, message
):
202 await (await self
.promise_console_message_listener(message
, timeout
=2))
204 except asyncio
.exceptions
.TimeoutError
:
207 async def promise_readystate_listener(self
, state
, url
=None, **kwargs
):
208 event
= f
"browsingContext.{state}"
210 def check(method
, data
):
211 if url
is None or url
in data
["url"]:
214 return await self
.promise_event_listener(event
, check
, **kwargs
)
216 async def promise_frame_listener(self
, url
, state
="domContentLoaded", **kwargs
):
217 event
= f
"browsingContext.{state}"
219 def check(method
, data
):
220 if url
is None or url
in data
["url"]:
221 return Client
.Context(self
, data
["context"])
223 return await self
.promise_event_listener(event
, check
, **kwargs
)
225 async def find_frame_context_by_url(self
, url
):
226 def find_in(arr
, url
):
228 if url
in context
["url"]:
231 found
= find_in(context
["children"], url
)
235 return find_in([await self
.top_context()], url
)
238 def __init__(self
, client
, id):
240 self
.target
= ContextTarget(id)
242 async def find_css(self
, selector
, all
=False):
243 all
= "All" if all
else ""
244 return await self
.client
.session
.bidi_session
.script
.evaluate(
245 expression
=f
"document.querySelector{all}('{selector}')",
250 def timed_js(self
, timeout
, poll
, fn
, is_displayed
=False):
251 return f
"""() => new Promise((_good, _bad) => {{
252 {self.is_displayed_js()}
253 var _poll = {poll} * 1000;
254 var _time = {timeout} * 1000;
256 var resolve = val => {{
257 if ({is_displayed}) {{
259 val = val.filter(v = is_displayed(v));
261 val = is_displayed(val) && val;
263 if (!val.length && !val.matches) {{
271 var reject = str => {{
276 var _int = setInterval(() => {{
287 def is_displayed_js(self
):
289 function is_displayed(e) {
290 const s = window.getComputedStyle(e),
291 v = s.visibility === "visible",
292 o = Math.abs(parseFloat(s.opacity));
293 return e.getClientRects().length > 0 && v && (isNaN(o) || o === 1.0);
306 all
= "All" if all
else ""
308 f
"var elem=arguments[0]; if ({condition})" if condition
else False
310 return await self
.client
.session
.bidi_session
.script
.evaluate(
311 expression
=self
.timed_js(
315 var ele = document.querySelector{all}('{selector}')";
316 if (ele && (!"length" in ele || ele.length > 0)) {{
326 async def await_text(self
, text
, **kwargs
):
327 xpath
= f
"//*[contains(text(),'{text}')]"
328 return await self
.await_xpath(self
, xpath
, **kwargs
)
330 async def await_xpath(
331 self
, xpath
, all
=False, timeout
=10, poll
=0.25, is_displayed
=False
333 all
= "true" if all
else "false"
334 return await self
.client
.session
.bidi_session
.script
.evaluate(
335 expression
=self
.timed_js(
340 var r, res = document.evaluate(`{xpath}`, document, null, 4);
341 while (r = res.iterateNext()) {
344 resolve({all} ? ret : ret[0]);
351 def wrap_script_args(self
, args
):
357 out
.append({"type": "undefined"})
360 if t
== int or t
== float:
361 out
.append({"type": "number", "value": arg
})
363 out
.append({"type": "boolean", "value": arg
})
365 out
.append({"type": "string", "value": arg
})
370 raise ValueError(f
"Unhandled argument type: {t}")
374 def __init__(self
, client
, script
, target
):
377 if type(target
) == list:
378 self
.target
= target
[0]
383 return self
.client
.session
.bidi_session
.script
.remove_preload_script(
387 async def run(self
, fn
, *args
, await_promise
=False):
388 val
= await self
.client
.session
.bidi_session
.script
.call_function(
389 arguments
=self
.client
.wrap_script_args(args
),
390 await_promise
=await_promise
,
391 function_declaration
=fn
,
394 if val
and "value" in val
:
398 async def make_preload_script(self
, text
, sandbox
, args
=None, context
=None):
400 context
= (await self
.top_context())["context"]
401 target
= ContextTarget(context
, sandbox
)
403 text
= f
"() => {{ {text} }}"
404 script
= await self
.session
.bidi_session
.script
.add_preload_script(
405 function_declaration
=text
,
406 arguments
=self
.wrap_script_args(args
),
409 return Client
.PreloadScript(self
, script
, target
)
411 async def await_alert(self
, text
):
412 if not hasattr(self
, "alert_preload_script"):
413 self
.alert_preload_script
= await self
.make_preload_script(
415 window.__alerts = [];
416 window.wrappedJSObject.alert = function(text) {
417 window.__alerts.push(text);
422 return self
.alert_preload_script
.run(
423 """(msg) => new Promise(done => {
424 const to = setInterval(() => {
425 if (window.__alerts.includes(msg)) {
436 async def await_popup(self
, url
=None):
437 if not hasattr(self
, "popup_preload_script"):
438 self
.popup_preload_script
= await self
.make_preload_script(
440 window.__popups = [];
441 window.wrappedJSObject.open = function(url) {
442 window.__popups.push(url);
447 return self
.popup_preload_script
.run(
448 """(url) => new Promise(done => {
449 const to = setInterval(() => {
450 if (url === undefined && window.__popups.length) {
452 return done(window.__popups[0]);
454 const found = window.__popups.find(u => u.includes(url));
455 if (found !== undefined) {
466 async def track_listener(self
, type, selector
):
467 if not hasattr(self
, "listener_preload_script"):
468 self
.listener_preload_script
= await self
.make_preload_script(
470 window.__listeners = {};
471 var proto = EventTarget.wrappedJSObject.prototype;
472 var def = Object.getOwnPropertyDescriptor(proto, "addEventListener");
474 def.value = function(type, fn, opts) {
475 if ("matches" in this) {
476 if (!window.__listeners[type]) {
477 window.__listeners[type] = new Set();
479 window.__listeners[type].add(this);
481 return old.call(this, type, fn, opts)
483 Object.defineProperty(proto, "addEventListener", def);
487 return Client
.ListenerTracker(self
.listener_preload_script
, type, selector
)
489 @contextlib.asynccontextmanager
490 async def preload_script(self
, text
, *args
):
491 script
= await self
.make_preload_script(text
, "preload", args
=args
)
498 def switch_to_frame(self
, frame
):
499 return self
.session
.transport
.send(
501 "session/{session_id}/frame".format(**vars(self
.session
)),
503 encoder
=webdriver
.protocol
.Encoder
,
504 decoder
=webdriver
.protocol
.Decoder
,
505 session
=self
.session
,
508 def switch_frame(self
, frame
):
509 self
.session
.switch_frame(frame
)
511 async def load_page_and_wait_for_iframe(
512 self
, url
, finder
, loads
=1, timeout
=None, **kwargs
515 await self
.navigate(url
, **kwargs
)
516 frame
= self
.await_element(finder
, timeout
=timeout
)
518 self
.switch_frame(frame
)
521 def execute_script(self
, script
, *args
):
522 return self
.session
.execute_script(script
, args
=args
)
524 def execute_async_script(self
, script
, *args
, **kwargs
):
525 return self
.session
.execute_async_script(script
, args
, **kwargs
)
527 def clear_all_cookies(self
):
528 self
.session
.transport
.send(
529 "DELETE", "session/%s/cookie" % self
.session
.session_id
532 def send_element_command(self
, element
, method
, uri
, body
=None):
533 url
= "element/%s/%s" % (element
.id, uri
)
534 return self
.session
.send_session_command(method
, url
, body
)
536 def get_element_attribute(self
, element
, name
):
537 return self
.send_element_command(element
, "GET", "attribute/%s" % name
)
539 def _do_is_displayed_check(self
, ele
, is_displayed
):
543 if type(ele
) in [list, tuple]:
544 return [x
for x
in ele
if self
._do
_is
_displayed
_check
(x
, is_displayed
)]
546 if is_displayed
is False and ele
and self
.is_displayed(ele
):
548 if is_displayed
is True and ele
and not self
.is_displayed(ele
):
552 def find_css(self
, *args
, all
=False, is_displayed
=None, **kwargs
):
554 ele
= self
.session
.find
.css(*args
, all
=all
, **kwargs
)
555 return self
._do
_is
_displayed
_check
(ele
, is_displayed
)
556 except webdriver
.error
.NoSuchElementException
:
559 def find_xpath(self
, xpath
, all
=False, is_displayed
=None):
560 route
= "elements" if all
else "element"
561 body
= {"using": "xpath", "value": xpath
}
563 ele
= self
.session
.send_session_command("POST", route
, body
)
564 return self
._do
_is
_displayed
_check
(ele
, is_displayed
)
565 except webdriver
.error
.NoSuchElementException
:
568 def find_text(self
, text
, is_displayed
=None, **kwargs
):
570 ele
= self
.find_xpath(f
"//*[contains(text(),'{text}')]", **kwargs
)
571 return self
._do
_is
_displayed
_check
(ele
, is_displayed
)
572 except webdriver
.error
.NoSuchElementException
:
575 def find_element(self
, finder
, is_displayed
=None, **kwargs
):
576 ele
= finder
.find(self
, **kwargs
)
577 return self
._do
_is
_displayed
_check
(ele
, is_displayed
)
579 def await_css(self
, selector
, **kwargs
):
580 return self
.await_element(self
.css(selector
), **kwargs
)
582 def await_xpath(self
, selector
, **kwargs
):
583 return self
.await_element(self
.xpath(selector
), **kwargs
)
585 def await_text(self
, selector
, *args
, **kwargs
):
586 return self
.await_element(self
.text(selector
), **kwargs
)
588 def await_element(self
, finder
, **kwargs
):
589 return self
.await_first_element_of([finder
], **kwargs
)[0]
592 def __init__(self
, selector
):
593 self
.selector
= selector
595 def find(self
, client
, **kwargs
):
596 return client
.find_css(self
.selector
, **kwargs
)
599 def __init__(self
, selector
):
600 self
.selector
= selector
602 def find(self
, client
, **kwargs
):
603 return client
.find_xpath(self
.selector
, **kwargs
)
606 def __init__(self
, selector
):
607 self
.selector
= selector
609 def find(self
, client
, **kwargs
):
610 return client
.find_text(self
.selector
, **kwargs
)
612 def await_first_element_of(
613 self
, finders
, timeout
=None, delay
=0.25, condition
=False, **kwargs
616 condition
= f
"var elem=arguments[0]; return {condition}" if condition
else False
621 found
= [None for finder
in finders
]
624 while time
.time() < t0
+ timeout
:
625 for i
, finder
in enumerate(finders
):
627 result
= finder
.find(self
, **kwargs
)
630 or self
.session
.execute_script(condition
, [result
])
634 except webdriver
.error
.NoSuchElementException
as e
:
637 raise exc
if exc
is not None else webdriver
.error
.NoSuchElementException
640 async def dom_ready(self
, timeout
=None):
645 return self
.session
.execute_async_script(
647 const cb = arguments[0];
649 if (document.readyState === "complete") {
656 task
= asyncio
.create_task(wait())
657 return await asyncio
.wait_for(task
, timeout
)
659 def is_float_cleared(self
, elem1
, elem2
):
660 return self
.session
.execute_script(
661 """return (function(a, b) {
662 // Ensure that a is placed under b (and not to its right)
663 return a?.offsetTop >= b?.offsetTop + b?.offsetHeight &&
664 a?.offsetLeft < b?.offsetLeft + b?.offsetWidth;
665 }(arguments[0], arguments[1]));""",
670 @contextlib.contextmanager
671 def assert_getUserMedia_called(self
):
674 navigator.mediaDevices.getUserMedia =
675 navigator.mozGetUserMedia =
676 navigator.getUserMedia =
677 () => { window.__gumCalled = true; };
681 assert self
.execute_script("return window.__gumCalled === true;")
683 def await_element_hidden(self
, finder
, timeout
=None, delay
=0.25):
689 elem
= finder
.find(self
)
690 while time
.time() < t0
+ timeout
:
692 if not self
.is_displayed(elem
):
695 except webdriver
.error
.StaleElementReferenceException
:
698 def soft_click(self
, element
):
699 self
.execute_script("arguments[0].click()", element
)
701 def remove_element(self
, element
):
702 self
.execute_script("arguments[0].remove()", element
)
704 def scroll_into_view(self
, element
):
706 "arguments[0].scrollIntoView({block:'center', inline:'center', behavior: 'instant'})",
710 @contextlib.asynccontextmanager
711 async def ensure_fastclick_activates(self
):
712 fastclick_preload_script
= await self
.make_preload_script(
714 var _ = document.createElement("webcompat_test");
715 _.style = "position:absolute;right:-1px;width:1px;height:1px";
716 document.documentElement.appendChild(_);
721 fastclick_preload_script
.stop()
723 def test_for_fastclick(self
, element
):
724 # FastClick cancels touchend, breaking default actions on Fenix.
725 # It instead fires a mousedown or click, which we can detect.
728 const sel = arguments[0];
729 window.fastclicked = false;
730 const evt = sel.nodeName === "SELECT" ? "mousedown" : "click";
731 document.addEventListener(evt, e => {
732 if (e.target === sel && !e.isTrusted) {
733 window.fastclicked = true;
739 self
.scroll_into_view(element
)
740 # tap a few times in case the site's other code interferes
741 self
.touch
.click(element
=element
).perform()
742 self
.touch
.click(element
=element
).perform()
743 self
.touch
.click(element
=element
).perform()
744 return self
.execute_script("return window.fastclicked")
746 def is_displayed(self
, element
):
750 return self
.session
.execute_script(
752 const e = arguments[0],
753 s = window.getComputedStyle(e),
754 v = s.visibility === "visible",
755 o = Math.abs(parseFloat(s.opacity));
756 return e.getClientRects().length > 0 && v && (isNaN(o) || o === 1.0);