modules/page_base.py (573 lines of code) (raw):
import json
import logging
import os
import platform
import re
import sys
import time
from copy import deepcopy
from functools import wraps
from pathlib import Path
from typing import List, Union
from pynput.keyboard import Controller, Key
from pypom import Page
from selenium.common import NoAlertPresentException
from selenium.common.exceptions import (
NoSuchElementException,
NoSuchWindowException,
TimeoutException,
)
from selenium.webdriver import ActionChains, Firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from modules.util import PomUtils
# Convert "strategy" from the components json to Selenium By vals
STRATEGY_MAP = {
"css": By.CSS_SELECTOR,
"class": By.CLASS_NAME,
"id": By.ID,
"link_text": By.LINK_TEXT,
"partial_link_text": By.PARTIAL_LINK_TEXT,
"xpath": By.XPATH,
"tag": By.TAG_NAME,
"name": By.NAME,
}
class BasePage(Page):
"""
This class extends pypom.Page with a constructor and methods to support our testing.
Page objects will now expect a JSON entry in ./modules/data with info about elements'
selectors, locations in shadow DOM, and other categorizations. This JSON file should
be name filename.components.json, where filename is the snake_case version of the
class name. E.g. AboutPrefs has ./modules/data/about_prefs.components.json.
Elements in the "requiredForPage" group will be checked for presence before self.loaded
can return True.
...
Attributes
----------
driver: selenium.webdriver.Firefox
The browser instance under test
utils: modules.utils.PomUtils
POM utilities for the object
elements: dict
Parse of the elements JSON file
"""
def __init__(self, driver: Firefox, **kwargs):
super().__init__(driver, **kwargs)
self.utils = PomUtils(self.driver)
# JSON files should be labelled with snake_cased versions of the Class name
qualname = self.__class__.__qualname__
logging.info("======")
logging.info(f"Loading POM for {qualname}...")
manifest_name = qualname[0].lower()
for char in qualname[1:]:
if char == char.lower():
manifest_name += char
else:
manifest_name += f"_{char.lower()}"
sys_platform = self.sys_platform()
if sys_platform == "Windows":
root_dir = Path(os.getcwd())
json_path = root_dir.joinpath("modules", "data")
self.load_element_manifest(rf"{json_path}\{manifest_name}.components.json")
else:
self.load_element_manifest(
f"./modules/data/{manifest_name}.components.json"
)
self.actions = ActionChains(self.driver)
self.instawait = WebDriverWait(self.driver, 0)
_xul_source_snippet = (
'xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"'
)
def sys_platform(self):
"""Return the system platform name"""
return platform.system()
def set_chrome_context(self):
"""Make sure the Selenium driver is using CONTEXT_CHROME"""
if self._xul_source_snippet not in self.driver.page_source:
self.driver.set_context(self.driver.CONTEXT_CHROME)
@staticmethod
def context_chrome(func):
"""Decorator to switch to CONTEXT_CHROME"""
@wraps(func)
def wrapper(self, *args, **kwargs):
with self.driver.context(self.driver.CONTEXT_CHROME):
return func(self, *args, **kwargs)
return wrapper
@staticmethod
def context_content(func):
"""Decorator to switch to CONTEXT_CONTENT"""
@wraps(func)
def wrapper(self, *args, **kwargs):
with self.driver.context(self.driver.CONTEXT_CONTENT):
return func(self, *args, **kwargs)
return wrapper
def set_content_context(self):
"""Make sure the Selenium driver is using CONTEXT_CONTENT"""
if self._xul_source_snippet in self.driver.page_source:
self.driver.set_context(self.driver.CONTEXT_CONTENT)
def opposite_context(self):
"""Return the context that is *not* in use"""
return (
self.driver.CONTEXT_CONTENT
if self._xul_source_snippet in self.driver.page_source
else self.driver.CONTEXT_CHROME
)
def is_private(self):
"""Determine if current browsing context is private"""
with self.driver.context(self.driver.CONTEXT_CHROME):
return "Private Browsing" in self.driver.title
def custom_wait(self, **kwargs) -> WebDriverWait:
"""
Create a custom WebDriverWait object, refer to Selenium docs
for explanations of the arguments.
Examples:
self.custom_wait(timeout=45).until(<condition>)
self.custom_wait(poll_frequency=1).until(<condition>)
"""
return WebDriverWait(self.driver, **kwargs)
def expect(self, condition) -> Page:
"""Use the Page's wait object to assert a condition or wait until timeout"""
with self.driver.context(self.context_id):
logging.info(f"Expecting in {self.context_id}...")
self.wait.until(condition)
return self
def expect_not(self, condition) -> Page:
"""Use the Page's to wait until assert a condition is not true or wait until timeout"""
with self.driver.context(self.context_id):
logging.info(f"Expecting NOT in {self.context_id}...")
self.wait.until_not(condition)
return self
def perform_key_combo(self, *keys) -> Page:
"""
Use ActionChains to perform key combos. Modifier keys should come first in the function call.
Usage example: perform_key_combo(Keys.CONTROL, Keys.ALT, "c") presses CTRL+ALT+c.
"""
while Keys.CONTROL in keys and self.sys_platform == "Darwin":
keys[keys.index(Keys.CONTROL)] = Keys.COMMAND
for k in keys[:-1]:
self.actions.key_down(k)
self.actions.send_keys(keys[-1])
for k in keys[:-1]:
self.actions.key_up(k)
self.actions.perform()
return self
def load_element_manifest(self, manifest_loc):
"""Populate self.elements with the parse of the elements JSON"""
logging.info(f"Loading element manifest: {manifest_loc}")
with open(manifest_loc) as fh:
self.elements = json.load(fh)
# We should expect an key-value pair of "context": "chrome" for Browser Objs
if "context" in self.elements:
self.context = self.elements["context"]
self.context_id = (
self.driver.CONTEXT_CHROME
if self.context == "chrome"
else self.driver.CONTEXT_CONTENT
)
del self.elements["context"]
else:
self.context = "content"
self.context_id = self.driver.CONTEXT_CONTENT
# If we find a key-value pair for "do-not-cache", add all elements to that group
doNotCache = self.elements.get("do-not-cache")
if "do-not-cache" in self.elements:
del self.elements["do-not-cache"]
if doNotCache:
for key in self.elements.keys():
logging.info(f"adding do-not-cache to {key}")
self.elements[key]["groups"].append("doNotCache")
def get_selector(self, name: str, labels=[]) -> list:
"""
Given a key for a self.elements dict entry, return the Selenium selector tuple.
If there are items in `labels`, replace instances of {.*} in the "selectorData"
with items from `labels`, in the order they are given. (Think Rust format macros.)
...
Arguments
---------
name: str
The key of the entry in self.elements, parsed from the elements JSON
labels: list[str]
Strings that replace instances of {.*} in the "selectorData" subentry of
self.elements[name]
Returns
-------
list
The Selenium selector tuple (as a list)
"""
logging.info(f"Get selector for {name}...")
element_data = self.elements[name]
selector = [
STRATEGY_MAP[element_data["strategy"]],
element_data["selectorData"],
]
if not labels:
logging.info("Returned selector.")
return selector
braces = re.compile(r"(\{.*?\})")
match = braces.findall(selector[1])
for i in range(len(labels)):
logging.info(f"Replace {match[i]} with {labels[i]}")
selector[1] = selector[1].replace(match[i], labels[i], 1)
logging.info("Returned selector.")
return selector
def get_element(
self, name: str, multiple=False, parent_element=None, labels=[]
) -> Union[list[WebElement], WebElement]:
"""
Given a key for a self.elements dict entry, return the Selenium WebElement(s).
If multiple is set to True, use find_elements instead of find_element.
If there are items in `labels`, replace instances of {.*} in the "selectorData"
with items from `labels`, in the order they are given. (Think Rust format macros.)
Note: This method currently does not support finding a child under a parent (given in the JSON) if it has a shadow parent.
...
Arguments
---------
name: str
The key of the entry in self.elements, parsed from the elements JSON
multiple: bool
Do we expect a list of WebElements?
parent_element: WebElement
The parent WebElement to search under to narrow the scope instead of searching the entire page
labels: list[str]
Strings that replace instances of {.*} in the "selectorData" subentry of
self.elements[name]
Returns
-------
selenium.webdriver.remote.webelement.WebElement
The WebElement object referred to by the element dict.
"""
logging.info("====")
if not multiple:
logging.info(f"Getting element {name}")
else:
logging.info(f"Getting multiple elements by name {name}")
if labels:
logging.info(f"Labels: {labels}")
logging.info(f"Groups: {self.elements[name]['groups']}")
cache_name = name
if labels:
labelscode = "".join(labels)
cache_name = f"{name}{labelscode}"
if cache_name not in self.elements:
self.elements[cache_name] = deepcopy(self.elements[name])
if multiple:
logging.info(f"Multiples: Not caching {cache_name}...")
if not multiple and "seleniumObject" in self.elements[cache_name]:
# no caching for multiples
cached_element = self.elements[cache_name]["seleniumObject"]
try:
self.instawait.until_not(EC.staleness_of(cached_element))
logging.info(f"Returned {cache_name} from object cache!")
return self.elements[cache_name]["seleniumObject"]
except (TimeoutError, TimeoutException):
# Because we have a timeout of 0, this should not cause delays
pass
element_data = self.elements[cache_name]
selector = self.get_selector(cache_name, labels)
if "shadowParent" in element_data:
logging.info(f"Found shadow parent {element_data['shadowParent']}...")
shadow_parent = self.get_element(element_data["shadowParent"])
if not multiple:
shadow_element = self.utils.find_shadow_element(
shadow_parent, selector, context=self.context
)
if "doNotCache" not in element_data["groups"]:
logging.info(f"Not caching {cache_name}...")
self.elements[cache_name]["seleniumObject"] = shadow_element
return shadow_element
else:
# no caching for multiples
return self.utils.find_shadow_element(
shadow_parent, selector, multiple=multiple, context=self.context
)
# if the child has a parent tag
if parent_element is not None:
logging.info("A WebElement parent was detected.")
if not multiple:
child_element = parent_element.find_element(*selector)
if "doNotCache" not in element_data["groups"]:
self.elements[cache_name]["seleniumObject"] = child_element
logging.info(f"Returning element {cache_name}.\n")
return child_element
else:
return parent_element.find_elements(*selector)
if not multiple:
found_element = self.driver.find_element(*selector)
if "doNotCache" not in element_data["groups"]:
logging.info(f"Caching {cache_name}...")
self.elements[cache_name]["seleniumObject"] = found_element
logging.info(f"Returning element {cache_name}.\n")
return found_element
else:
return self.driver.find_elements(*selector)
def get_elements(self, name: str, labels=[]):
"""
Get multiple elements using get_element()
Arguments
---------
name: str
The key of the entry in self.elements, parsed from the elements JSON
labels: list[str]
Strings that replace instances of {.*} in the "selectorData" subentry of
self.elements[name]
Returns
-------
list[selenium.webdriver.remote.webelement.WebElement]
The WebElement objects referred to by the element dict.
"""
return self.get_element(name, multiple=True, labels=labels)
def get_parent_of(
self, reference: Union[str, tuple, WebElement], labels=[]
) -> WebElement:
"""
Given a name + labels, a WebElement, or a tuple, return the direct parent node of the element.
"""
child = self.fetch(reference, labels=labels)
return child.find_element(By.XPATH, "..")
def element_exists(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element exists or timeout"""
self.expect(lambda _: self.get_element(name, labels=labels))
return self
def element_does_not_exist(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element exists or timeout"""
self.instawait.until_not(lambda _: self.get_elements(name, labels=labels))
return self
def element_visible(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element is visible or timeout"""
self.expect(
lambda _: self.get_element(name, labels=labels)
and self.get_element(name, labels=labels).is_displayed()
)
return self
def element_not_visible(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element is not visible or timeout"""
self.expect(
lambda _: self.get_elements(name, labels=labels) == []
or not self.get_element(name, labels=labels).is_displayed()
)
return self
def element_clickable(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element is clickable or timeout"""
self.element_visible(name, labels=labels)
self.expect(lambda _: self.get_element(name, labels=labels).is_enabled())
return self
def element_selected(self, name: str, labels=[]) -> Page:
"""Expect helper: wait until element is selected or timeout"""
self.expect(
lambda _: self.get_element(name, labels=labels)
and self.get_element(name, labels=labels).is_selected()
)
return self
def element_has_text(self, name: str, text: str, labels=[]) -> Page:
"""Expect helper: wait until element has given text"""
self.expect(lambda _: text in self.get_element(name, labels=labels).text)
return self
def expect_element_attribute_contains(
self, name: str, attr_name: str, attr_value: Union[str, float, int], labels=[]
) -> Page:
"""Expect helper: wait until element attribute contains certain value"""
self.expect(
lambda _: self.get_element(name, labels=labels)
and str(attr_value)
in self.get_element(name, labels=labels).get_attribute(attr_name)
)
return self
def url_contains(self, url_part: str) -> Page:
"""Expect helper: wait until driver URL contains given text or timeout"""
self.context_id = self.driver.CONTEXT_CONTENT
self.expect(EC.url_contains(url_part))
return self
def title_contains(self, url_part: str) -> Page:
"""Expect helper: wait until driver URL contains given text or timeout"""
self.expect(EC.title_contains(url_part))
return self
def verify_opened_image_url(self, url_substr: str, pattern: str) -> Page:
"""
Given a part of a URL and a regex, wait for that substring to exist in
the current URL, then match the regex against the current URL.
(This gives us the benefit of fast failure.)
"""
self.url_contains(url_substr)
current_url = self.driver.current_url
assert re.match(pattern, current_url), (
f"URL does not match the expected pattern: {current_url}"
)
return self
def fill(
self, name: str, term: str, clear_first=True, press_enter=True, labels=[]
) -> Page:
"""
Get a fillable element and fill it with text. Return self.
...
Arguments
---------
name: str
The key of the entry in self.elements, parsed from the elements JSON
labels: list[str]
Strings that replace instances of {.*} in the "selectorData" subentry of
self.elements[name]
term: str
The text to enter into the element
clear_first: bool
Call .clear() on the element first. Default True
press_enter: bool
Press Keys.ENTER after filling the element. Default True
"""
if self.context == "chrome":
self.set_chrome_context()
el = self.get_element(name, labels=labels)
self.element_clickable(name, labels=labels)
if clear_first:
el.clear()
end = Keys.ENTER if press_enter else ""
el.send_keys(f"{term}{end}")
return self
def fetch(self, reference: Union[str, tuple, WebElement], labels=[]) -> WebElement:
"""
Given an element name, a selector, or a WebElement, return the
corresponding WebElement.
"""
if isinstance(reference, str):
return self.get_element(reference, labels=labels)
elif isinstance(reference, tuple):
return self.find_element(*reference)
elif isinstance(reference, WebElement):
return reference
assert False, (
"Bad fetch: only selectors, selector names, or WebElements allowed."
)
def click_on(self, reference: Union[str, tuple, WebElement], labels=[]) -> Page:
"""Click on an element, no matter the context, return the page"""
with self.driver.context(self.context_id):
self.fetch(reference, labels).click()
logging.info(f"{reference} clicked")
return self
def multi_click(
self, iters: int, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Perform multiple clicks at once on an element by name, selector, or WebElement"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
def execute_multi_click():
if iters == 2:
self.actions.double_click(el).perform()
else:
for _ in range(iters):
self.actions.click(el)
self.actions.perform()
# Little cheat: if element doesn't exist in one context, try the other
try:
execute_multi_click()
except NoSuchElementException:
with self.driver.context(self.opposite_context()):
execute_multi_click()
return self
def double_click(self, reference: Union[str, tuple, WebElement], labels=[]) -> Page:
"""Actions helper: perform double-click on given element"""
return self.multi_click(2, reference, labels)
def triple_click(self, reference: Union[str, tuple, WebElement], labels=[]) -> Page:
"""Actions helper: perform triple-click on a given element"""
return self.multi_click(3, reference, labels)
def context_click(
self, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Context (right-) click on an element"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.actions.context_click(el).perform()
return self
def copy(self) -> Page:
"""Copy the selected item"""
mod_key = Keys.COMMAND if self.sys_platform() == "Darwin" else Keys.CONTROL
self.actions.key_down(mod_key)
self.actions.send_keys("c")
self.actions.key_up(mod_key).perform()
time.sleep(0.5)
return self
def paste(self) -> Page:
"""Paste the copied item"""
mod_key = Keys.COMMAND if self.sys_platform() == "Darwin" else Keys.CONTROL
self.actions.key_down(mod_key)
self.actions.send_keys("v")
self.actions.key_up(mod_key).perform()
time.sleep(0.5)
return self
def undo(self) -> Page:
"""Undo last action"""
mod_key = Keys.COMMAND if self.sys_platform() == "Darwin" else Keys.CONTROL
self.actions.key_down(mod_key)
self.actions.send_keys("z")
self.actions.key_up(mod_key).perform()
time.sleep(0.5)
return self
def paste_to_element(
self, sys_platform, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Paste the copied item into the element"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.scroll_to_element(el)
mod_key = Keys.COMMAND if sys_platform == "Darwin" else Keys.CONTROL
self.actions.key_down(mod_key)
self.actions.send_keys_to_element(el, "v")
self.actions.key_up(mod_key).perform()
return self
def copy_image_from_element(
self, keyboard, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Copy from the given element using right click (pynput)"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.scroll_to_element(el)
self.context_click(el)
keyboard.tap(Key.down)
keyboard.tap(Key.down)
keyboard.tap(Key.down)
keyboard.tap(Key.enter)
time.sleep(0.5)
return self
def copy_selection(
self, keyboard, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Copy from the current selection using right click (pynput)"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.scroll_to_element(el)
self.context_click(el)
keyboard.tap(Key.down)
keyboard.tap(Key.enter)
time.sleep(0.5)
return self
def click_and_hide_menu(
self, reference: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""Click an option in a context menu, then hide it"""
with self.driver.context(self.driver.CONTEXT_CHROME):
self.fetch(reference, labels=labels).click()
self.hide_popup_by_child_node(reference, labels=labels)
return self
def hover(self, reference: Union[str, tuple, WebElement], labels=[]):
"""
Hover over the specified element.
Parameters: element (str): The element to hover over.
Default tries to hover something in the chrome context
"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.actions.move_to_element(el).perform()
return self
def scroll_to_element(self, reference: Union[str, tuple, WebElement], labels=[]):
"""
Scroll towards the specified element which may be out of frame.
Parameters: element (str): The element to hover over.
"""
with self.driver.context(self.context_id):
el = self.fetch(reference, labels)
self.driver.execute_script("arguments[0].scrollIntoView();", el)
return self
def get_all_children(
self, reference: Union[str, tuple, WebElement], labels=[]
) -> List[WebElement]:
"""
Gets all the children of a webelement
"""
children = None
with self.driver.context(self.context_id):
element = self.fetch(reference, labels)
children = element.find_elements(By.XPATH, "./*")
return children
def wait_for_no_children(
self, parent: Union[str, tuple, WebElement], labels=[]
) -> Page:
"""
Waits for 0 children under the given parent, the wait is instant (note, this changes the driver implicit wait and changes it back)
"""
driver_wait = self.driver.timeouts.implicit_wait
self.driver.implicitly_wait(0)
try:
assert len(self.get_all_children(self.fetch(parent, labels))) == 0
finally:
self.driver.implicitly_wait(driver_wait)
def wait_for_num_tabs(self, num_tabs: int) -> Page:
"""
Waits for the driver.window_handles to be updated accordingly with the number of tabs requested
"""
try:
self.wait.until(lambda _: len(self.driver.window_handles) == num_tabs)
except TimeoutException:
logging.warning(
"Timeout waiting for the number of windows to be:", num_tabs
)
return self
def switch_to_default_frame(self) -> Page:
"""Switch to default content frame"""
self.driver.switch_to.default_content()
return self
def switch_to_iframe(self, index: int):
"""Switch to frame of given index"""
self.driver.switch_to.frame(index)
return self
def switch_to_new_tab(self) -> Page:
"""Get list of all window handles, switch to the newly opened tab"""
with self.driver.context(self.driver.CONTEXT_CONTENT):
self.driver.switch_to.window(self.driver.window_handles[-1])
return self
def switch_to_new_window(self) -> Page:
"""Switch to the most recently opened window. Can be a standard or private window"""
with self.driver.context(self.driver.CONTEXT_CONTENT):
all_window_handles = self.driver.window_handles
self.driver.switch_to.window(all_window_handles[-1])
return self
def wait_for_num_windows(self, num: int) -> Page:
"""Wait for the number of open tabs + windows to equal given int"""
with self.driver.context(self.driver.CONTEXT_CONTENT):
return self.wait_for_num_tabs(num)
def open_and_switch_to_new_window(self, browser_window: str) -> Page:
"""
Opens a new browser window of the given type, then switches to it.
Parameters:
browser_window: Can be a standard 'window', 'tab' or 'private' browser window.
"""
if browser_window == "private":
self.open_and_switch_to_private_window_via_keyboard()
else:
self.driver.switch_to.new_window(browser_window)
return self
def open_and_switch_to_private_window_via_keyboard(self) -> Page:
"""
Opens a new private browsing window via keyboard shortcut and switch to it
"""
# Keep track of window count to ensure we get a new one to switch to
window_count = len(self.driver.window_handles)
with self.driver.context(self.driver.CONTEXT_CHROME):
os_name = sys.platform
mod_key = Keys.COMMAND if os_name == "darwin" else Keys.CONTROL
self.actions.key_down(mod_key)
self.actions.key_down(Keys.SHIFT)
self.actions.send_keys("p")
self.actions.key_up(Keys.SHIFT)
self.actions.key_up(mod_key).perform()
expected_window_count = window_count + 1
self.wait_for_num_windows(expected_window_count)
self.switch_to_new_window()
self.title_contains("Private")
self.driver.get("about:blank")
return self
def switch_to_frame(self, frame: str, labels=[]) -> Page:
"""Switch to inline document frame"""
with self.driver.context(self.driver.CONTEXT_CHROME):
self.expect(
EC.frame_to_be_available_and_switch_to_it(
self.get_selector(frame, labels=labels)
)
)
return self
def hide_popup(self, context_menu: str, chrome=True) -> Page:
"""
Given the ID of the context menu, it will dismiss the menu.
For example, the tab context menu corresponds to the id of tabContextMenu. Usage would be: tabs.hide_popup("tabContextMenu")
"""
script = f'document.querySelector("#{context_menu}").hidePopup();'
if chrome:
with self.driver.context(self.driver.CONTEXT_CHROME):
self.driver.execute_script(script)
else:
with self.driver.context(self.driver.CONTEXT_CONTENT):
self.driver.execute_script(script)
def hide_popup_by_class(self, class_name: str, retry=False) -> None:
"""
Given the class name of the context menu, it will dismiss the menu.
For example, if the context menu corresponds to the class name of 'context-menu',
usage would be: tabs.hide_popup_by_class("context-menu")
"""
try:
with self.driver.context(self.context_id):
script = f"""var element = document.querySelector(".{class_name}");
if (element && element.hidePopup) {{
element.hidePopup();
}}
"""
self.driver.execute_script(script)
except NoSuchWindowException:
if not retry:
with self.driver.context(self.opposite_context()):
self.hide_popup_by_class(class_name, True)
else:
raise NoSuchWindowException
def handle_os_download_confirmation(self, keyboard: Controller, sys_platform: str):
"""
This function handles the keyboard shortcuts. If on Linux, it simulates switching
to OK. On other platforms, it directly presses enter.
"""
if sys_platform == "Linux":
# Perform the series of ALT+TAB key presses on Linux
keyboard.press(Key.alt)
keyboard.press(Key.tab)
keyboard.release(Key.tab)
keyboard.release(Key.alt)
time.sleep(1)
keyboard.press(Key.alt)
keyboard.press(Key.tab)
keyboard.release(Key.tab)
keyboard.release(Key.alt)
time.sleep(1)
keyboard.press(Key.tab)
keyboard.release(Key.tab)
time.sleep(1)
keyboard.press(Key.tab)
keyboard.release(Key.tab)
# Press enter to confirm the download on all platforms
keyboard.press(Key.enter)
keyboard.release(Key.enter)
def hide_popup_by_child_node(
self, reference: Union[str, tuple, WebElement], labels=[], retry=False
) -> Page:
try:
with self.driver.context(self.context_id):
logging.info("hide popup child: start")
node = self.fetch(reference, labels=labels)
logging.info("hide popup child: fetched")
script = """var element = arguments[0].parentNode;
if (element && element.hidePopup) {
element.hidePopup();
}"""
self.driver.execute_script(script, node)
except NoSuchWindowException:
if not retry:
with self.driver.context(self.opposite_context()):
self.hide_popup_by_child_node(reference, labels, True)
else:
raise NoSuchWindowException
def get_localstorage_item(self, key: str):
return self.driver.execute_script(f"return window.localStorage.getItem({key});")
def _get_alert(self):
try:
alert = self.driver.switch_to.alert
except NoAlertPresentException:
return False
return alert
def get_alert(self):
alert = self.wait.until(lambda _: self._get_alert())
return alert
@property
def loaded(self):
"""
Here, we're using our own get_elements to ensure that all elements that
are requiredForPage are gettable before we return loaded as True
"""
_loaded = False
try:
if self.context == "chrome":
self.set_chrome_context()
for name in self.elements:
if "requiredForPage" in self.elements[name]["groups"]:
logging.info(f"ensuring {name} in DOM...")
self.get_element(name)
_loaded = True
except (TimeoutError, TimeoutException):
pass
self.set_content_context()
return _loaded
def get_css_zoom(self):
"""
Checks the CSS zoom and transform scale to determine the current zoom level.
"""
# Retrieve the CSS zoom property on the body element
css_zoom = self.driver.execute_script(
"return window.getComputedStyle(document.body).zoom"
)
if css_zoom:
return float(css_zoom)
# If zoom property is not explicitly set, check the transform scale
css_transform_scale = self.driver.execute_script("""
const transform = window.getComputedStyle(document.body).transform;
if (transform && transform !== 'none') {
return transform;
} else {
return null;
""")
# Parse the transform matrix to extract the scale factor (e.g., matrix(a, b, c, d, e, f))
if css_transform_scale:
scale_factor = float(css_transform_scale.split("(")[1].split(",")[0])
return scale_factor
# Default return if neither zoom nor transform is set
return 1.0