diff --git a/helium/__init__.py b/helium/__init__.py
index 2345c7c..86b71c4 100644
--- a/helium/__init__.py
+++ b/helium/__init__.py
@@ -18,1133 +18,1235 @@
import helium._impl
+
def start_chrome(
- url=None, headless=False, maximize=False, options=None, capabilities=None
+ url=None, headless=False, maximize=False, options=None, capabilities=None
):
- """
- :param url: URL to open.
- :type url: str
- :param headless: Whether to start Chrome in headless mode.
- :type headless: bool
- :param maximize: Whether to maximize the Chrome window.
- Ignored when `headless` is set to `True`.
- :type maximize: bool
- :param options: ChromeOptions to use for starting the browser
- :type options: :py:class:`selenium.webdriver.ChromeOptions`
- :param capabilities: DesiredCapabilities to use for starting the browser
- :type capabilities: :py:class:`selenium.webdriver.DesiredCapabilities`
-
- Starts an instance of Google Chrome::
-
- start_chrome()
-
- If this doesn't work for you, then it may be that Helium's copy of
- ChromeDriver is not compatible with your version of Chrome. To fix this,
- place a copy of ChromeDriver on your `PATH`.
-
- You can optionally open a URL::
-
- start_chrome("google.com")
-
- The `headless` switch lets you prevent the browser window from appearing on
- your screen::
-
- start_chrome(headless=True)
- start_chrome("google.com", headless=True)
-
- For more advanced configuration, use the `options` or `capabilities`
- parameters::
-
- from selenium.webdriver import ChromeOptions
- options = ChromeOptions()
- options.add_argument('--proxy-server=1.2.3.4:5678')
- start_chrome(options=options)
-
- from selenium.webdriver import DesiredCapabilities
- capabilities = DesiredCapabilities.CHROME
- capabilities["pageLoadStrategy"] = "none"
- capabilities["goog:loggingPrefs"] = {'performance': 'ALL'}
- start_chrome(capabilities=capabilities)
-
- On shutdown of the Python interpreter, Helium cleans up all resources used
- for controlling the browser (such as the ChromeDriver process), but does
- not close the browser itself. If you want to terminate the browser at the
- end of your script, use the following command::
-
- kill_browser()
- """
- return _get_api_impl().start_chrome_impl(
- url, headless, maximize, options, capabilities
- )
+ """
+ :param url: URL to open.
+ :type url: str
+ :param headless: Whether to start Chrome in headless mode.
+ :type headless: bool
+ :param maximize: Whether to maximize the Chrome window.
+ Ignored when `headless` is set to `True`.
+ :type maximize: bool
+ :param options: ChromeOptions to use for starting the browser
+ :type options: :py:class:`selenium.webdriver.ChromeOptions`
+ :param capabilities: DesiredCapabilities to use for starting the browser
+ :type capabilities: :py:class:`selenium.webdriver.DesiredCapabilities`
+
+ Starts an instance of Google Chrome::
+
+ start_chrome()
+
+ If this doesn't work for you, then it may be that Helium's copy of
+ ChromeDriver is not compatible with your version of Chrome. To fix this,
+ place a copy of ChromeDriver on your `PATH`.
+
+ You can optionally open a URL::
+
+ start_chrome("google.com")
+
+ The `headless` switch lets you prevent the browser window from appearing on
+ your screen::
+
+ start_chrome(headless=True)
+ start_chrome("google.com", headless=True)
+
+ For more advanced configuration, use the `options` or `capabilities`
+ parameters::
+
+ from selenium.webdriver import ChromeOptions
+ options = ChromeOptions()
+ options.add_argument('--proxy-server=1.2.3.4:5678')
+ start_chrome(options=options)
+
+ from selenium.webdriver import DesiredCapabilities
+ capabilities = DesiredCapabilities.CHROME
+ capabilities["pageLoadStrategy"] = "none"
+ capabilities["goog:loggingPrefs"] = {'performance': 'ALL'}
+ start_chrome(capabilities=capabilities)
+
+ On shutdown of the Python interpreter, Helium cleans up all resources used
+ for controlling the browser (such as the ChromeDriver process), but does
+ not close the browser itself. If you want to terminate the browser at the
+ end of your script, use the following command::
+
+ kill_browser()
+ """
+ return _get_api_impl().start_chrome_impl(
+ url, headless, maximize, options, capabilities
+ )
+
def start_firefox(url=None, headless=False, options=None):
- """
- :param url: URL to open.
- :type url: str
- :param headless: Whether to start Firefox in headless mode.
- :type headless: bool
- :param options: FirefoxOptions to use for starting the browser.
- :type options: :py:class:`selenium.webdriver.FirefoxOptions`
+ """
+ :param url: URL to open.
+ :type url: str
+ :param headless: Whether to start Firefox in headless mode.
+ :type headless: bool
+ :param options: FirefoxOptions to use for starting the browser.
+ :type options: :py:class:`selenium.webdriver.FirefoxOptions`
+
+ Starts an instance of Firefox::
- Starts an instance of Firefox::
+ start_firefox()
- start_firefox()
+ If this doesn't work for you, then it may be that Helium's copy of
+ geckodriver is not compatible with your version of Firefox. To fix this,
+ place a copy of geckodriver on your `PATH`.
- If this doesn't work for you, then it may be that Helium's copy of
- geckodriver is not compatible with your version of Firefox. To fix this,
- place a copy of geckodriver on your `PATH`.
+ You can optionally open a URL::
- You can optionally open a URL::
+ start_firefox("google.com")
- start_firefox("google.com")
+ The `headless` switch lets you prevent the browser window from appearing on
+ your screen::
- The `headless` switch lets you prevent the browser window from appearing on
- your screen::
+ start_firefox(headless=True)
+ start_firefox("google.com", headless=True)
- start_firefox(headless=True)
- start_firefox("google.com", headless=True)
+ For more advanced configuration, use the `options` parameter::
- For more advanced configuration, use the `options` parameter::
+ from selenium.webdriver import FirefoxOptions
+ options = FirefoxOptions()
+ options.add_argument("--width=2560")
+ options.add_argument("--height=1440")
+ start_firefox(options=options)
- from selenium.webdriver import FirefoxOptions
- options = FirefoxOptions()
- options.add_argument("--width=2560")
- options.add_argument("--height=1440")
- start_firefox(options=options)
+ On shutdown of the Python interpreter, Helium cleans up all resources used
+ for controlling the browser (such as the geckodriver process), but does
+ not close the browser itself. If you want to terminate the browser at the
+ end of your script, use the following command::
- On shutdown of the Python interpreter, Helium cleans up all resources used
- for controlling the browser (such as the geckodriver process), but does
- not close the browser itself. If you want to terminate the browser at the
- end of your script, use the following command::
+ kill_browser()
+ """
+ return _get_api_impl().start_firefox_impl(url, headless, options)
- kill_browser()
- """
- return _get_api_impl().start_firefox_impl(url, headless, options)
def go_to(url):
- """
- :param url: URL to open.
- :type url: str
+ """
+ :param url: URL to open.
+ :type url: str
- Opens the specified URL in the current web browser window. For instance::
+ Opens the specified URL in the current web browser window. For instance::
+
+ go_to("google.com")
+ """
+ _get_api_impl().go_to_impl(url)
- go_to("google.com")
- """
- _get_api_impl().go_to_impl(url)
def set_driver(driver):
- """
- Sets the Selenium WebDriver used to execute Helium commands. See also
- :py:func:`get_driver`.
- """
- _get_api_impl().set_driver_impl(driver)
+ """
+ Sets the Selenium WebDriver used to execute Helium commands. See also
+ :py:func:`get_driver`.
+ """
+ _get_api_impl().set_driver_impl(driver)
+
def get_driver():
- """
- Returns the Selenium WebDriver currently used by Helium to execute all
- commands. Each Helium command such as ``click("Login")`` is translated to a
- sequence of Selenium commands that are issued to this driver.
- """
- return _get_api_impl().get_driver_impl()
+ """
+ Returns the Selenium WebDriver currently used by Helium to execute all
+ commands. Each Helium command such as ``click("Login")`` is translated to a
+ sequence of Selenium commands that are issued to this driver.
+ """
+ return _get_api_impl().get_driver_impl()
+
def write(text, into=None):
- """
- :param text: The text to be written.
- :type text: one of str, unicode
- :param into: The element to write into.
- :type into: one of str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param text: The text to be written.
+ :type text: one of str, unicode
+ :param into: The element to write into.
+ :type into: one of str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement`, :py:class:`Alert`
- Types the given text into the active window. If parameter 'into' is given,
- writes the text into the text field or element identified by that parameter.
- Common examples of 'write' are::
+ Types the given text into the active window. If parameter 'into' is given,
+ writes the text into the text field or element identified by that parameter.
+ Common examples of 'write' are::
+
+ write("Hello World!")
+ write("user12345", into="Username:")
+ write("Michael", into=Alert("Please enter your name"))
+ """
+ _get_api_impl().write_impl(text, into)
- write("Hello World!")
- write("user12345", into="Username:")
- write("Michael", into=Alert("Please enter your name"))
- """
- _get_api_impl().write_impl(text, into)
def press(key):
- """
- :param key: Key or combination of keys to be pressed.
+ """
+ :param key: Key or combination of keys to be pressed.
- Presses the given key or key combination. To press a normal letter key such
- as 'a' simply call `press` for it::
+ Presses the given key or key combination. To press a normal letter key such
+ as 'a' simply call `press` for it::
- press('a')
+ press('a')
- You can also simulate the pressing of upper case characters that way::
+ You can also simulate the pressing of upper case characters that way::
- press('A')
+ press('A')
- The special keys you can press are those given by Selenium's class
- :py:class:`selenium.webdriver.common.keys.Keys`. Helium makes all those keys
- available through its namespace, so you can just use them without having to
- refer to :py:class:`selenium.webdriver.common.keys.Keys`. For instance, to
- press the Enter key::
+ The special keys you can press are those given by Selenium's class
+ :py:class:`selenium.webdriver.common.keys.Keys`. Helium makes all those keys
+ available through its namespace, so you can just use them without having to
+ refer to :py:class:`selenium.webdriver.common.keys.Keys`. For instance, to
+ press the Enter key::
- press(ENTER)
+ press(ENTER)
- To press multiple keys at the same time, concatenate them with `+`. For
- example, to press Control + a, call::
+ To press multiple keys at the same time, concatenate them with `+`. For
+ example, to press Control + a, call::
- press(CONTROL + 'a')
- """
- _get_api_impl().press_impl(key)
+ press(CONTROL + 'a')
+ """
+ _get_api_impl().press_impl(key)
-NULL = Keys.NULL
-CANCEL = Keys.CANCEL
-HELP = Keys.HELP
-BACK_SPACE = Keys.BACK_SPACE
-TAB = Keys.TAB
-CLEAR = Keys.CLEAR
-RETURN = Keys.RETURN
-ENTER = Keys.ENTER
-SHIFT = Keys.SHIFT
-LEFT_SHIFT = Keys.LEFT_SHIFT
-CONTROL = Keys.CONTROL
+
+NULL = Keys.NULL
+CANCEL = Keys.CANCEL
+HELP = Keys.HELP
+BACK_SPACE = Keys.BACK_SPACE
+TAB = Keys.TAB
+CLEAR = Keys.CLEAR
+RETURN = Keys.RETURN
+ENTER = Keys.ENTER
+SHIFT = Keys.SHIFT
+LEFT_SHIFT = Keys.LEFT_SHIFT
+CONTROL = Keys.CONTROL
LEFT_CONTROL = Keys.LEFT_CONTROL
-ALT = Keys.ALT
-LEFT_ALT = Keys.LEFT_ALT
-PAUSE = Keys.PAUSE
-ESCAPE = Keys.ESCAPE
-SPACE = Keys.SPACE
-PAGE_UP = Keys.PAGE_UP
-PAGE_DOWN = Keys.PAGE_DOWN
-END = Keys.END
-HOME = Keys.HOME
-LEFT = Keys.LEFT
-ARROW_LEFT = Keys.ARROW_LEFT
-UP = Keys.UP
-ARROW_UP = Keys.ARROW_UP
-RIGHT = Keys.RIGHT
-ARROW_RIGHT = Keys.ARROW_RIGHT
-DOWN = Keys.DOWN
-ARROW_DOWN = Keys.ARROW_DOWN
-INSERT = Keys.INSERT
-DELETE = Keys.DELETE
-SEMICOLON = Keys.SEMICOLON
-EQUALS = Keys.EQUALS
-NUMPAD0 = Keys.NUMPAD0
-NUMPAD1 = Keys.NUMPAD1
-NUMPAD2 = Keys.NUMPAD2
-NUMPAD3 = Keys.NUMPAD3
-NUMPAD4 = Keys.NUMPAD4
-NUMPAD5 = Keys.NUMPAD5
-NUMPAD6 = Keys.NUMPAD6
-NUMPAD7 = Keys.NUMPAD7
-NUMPAD8 = Keys.NUMPAD8
-NUMPAD9 = Keys.NUMPAD9
-MULTIPLY = Keys.MULTIPLY
-ADD = Keys.ADD
-SEPARATOR = Keys.SEPARATOR
-SUBTRACT = Keys.SUBTRACT
-DECIMAL = Keys.DECIMAL
-DIVIDE = Keys.DIVIDE
-F1 = Keys.F1
-F2 = Keys.F2
-F3 = Keys.F3
-F4 = Keys.F4
-F5 = Keys.F5
-F6 = Keys.F6
-F7 = Keys.F7
-F8 = Keys.F8
-F9 = Keys.F9
-F10 = Keys.F10
-F11 = Keys.F11
-F12 = Keys.F12
-META = Keys.META
-COMMAND = Keys.COMMAND
+ALT = Keys.ALT
+LEFT_ALT = Keys.LEFT_ALT
+PAUSE = Keys.PAUSE
+ESCAPE = Keys.ESCAPE
+SPACE = Keys.SPACE
+PAGE_UP = Keys.PAGE_UP
+PAGE_DOWN = Keys.PAGE_DOWN
+END = Keys.END
+HOME = Keys.HOME
+LEFT = Keys.LEFT
+ARROW_LEFT = Keys.ARROW_LEFT
+UP = Keys.UP
+ARROW_UP = Keys.ARROW_UP
+RIGHT = Keys.RIGHT
+ARROW_RIGHT = Keys.ARROW_RIGHT
+DOWN = Keys.DOWN
+ARROW_DOWN = Keys.ARROW_DOWN
+INSERT = Keys.INSERT
+DELETE = Keys.DELETE
+SEMICOLON = Keys.SEMICOLON
+EQUALS = Keys.EQUALS
+NUMPAD0 = Keys.NUMPAD0
+NUMPAD1 = Keys.NUMPAD1
+NUMPAD2 = Keys.NUMPAD2
+NUMPAD3 = Keys.NUMPAD3
+NUMPAD4 = Keys.NUMPAD4
+NUMPAD5 = Keys.NUMPAD5
+NUMPAD6 = Keys.NUMPAD6
+NUMPAD7 = Keys.NUMPAD7
+NUMPAD8 = Keys.NUMPAD8
+NUMPAD9 = Keys.NUMPAD9
+MULTIPLY = Keys.MULTIPLY
+ADD = Keys.ADD
+SEPARATOR = Keys.SEPARATOR
+SUBTRACT = Keys.SUBTRACT
+DECIMAL = Keys.DECIMAL
+DIVIDE = Keys.DIVIDE
+F1 = Keys.F1
+F2 = Keys.F2
+F3 = Keys.F3
+F4 = Keys.F4
+F5 = Keys.F5
+F6 = Keys.F6
+F7 = Keys.F7
+F8 = Keys.F8
+F9 = Keys.F9
+F10 = Keys.F10
+F11 = Keys.F11
+F12 = Keys.F12
+META = Keys.META
+COMMAND = Keys.COMMAND
+
def click(element):
- """
- :param element: The element or point to click.
- :type element: str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param element: The element or point to click.
+ :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- Clicks on the given element or point. Common examples are::
+ Clicks on the given element or point. Common examples are::
+
+ click("Sign in")
+ click(Button("OK"))
+ click(Point(200, 300))
+ click(ComboBox("File type").top_left + (50, 0))
+ """
+ _get_api_impl().click_impl(element)
- click("Sign in")
- click(Button("OK"))
- click(Point(200, 300))
- click(ComboBox("File type").top_left + (50, 0))
- """
- _get_api_impl().click_impl(element)
def doubleclick(element):
- """
- :param element: The element or point to click.
- :type element: str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param element: The element or point to click.
+ :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- Performs a double-click on the given element or point. For example::
+ Performs a double-click on the given element or point. For example::
+
+ doubleclick("Double click here")
+ doubleclick(Image("Directories"))
+ doubleclick(Point(200, 300))
+ doubleclick(TextField("Username").top_left - (0, 20))
+ """
+ _get_api_impl().doubleclick_impl(element)
- doubleclick("Double click here")
- doubleclick(Image("Directories"))
- doubleclick(Point(200, 300))
- doubleclick(TextField("Username").top_left - (0, 20))
- """
- _get_api_impl().doubleclick_impl(element)
def drag(element, to):
- """
- :param element: The element or point to drag.
- :type element: str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param element: The element or point to drag.
+ :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- :param to: The element or point to drag to.
- :type to: str, unicode, :py:class:`HTMLElement`, \
+ :param to: The element or point to drag to.
+ :type to: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- Drags the given element or point to the given location. For example::
+ Drags the given element or point to the given location. For example::
- drag("Drag me!", to="Drop here.")
+ drag("Drag me!", to="Drop here.")
- The dragging is performed by hovering the mouse cursor over ``element``,
- pressing and holding the left mouse button, moving the mouse cursor over
- ``to``, and then releasing the left mouse button again.
+ The dragging is performed by hovering the mouse cursor over ``element``,
+ pressing and holding the left mouse button, moving the mouse cursor over
+ ``to``, and then releasing the left mouse button again.
+
+ This function is exclusively used for dragging elements inside one web page.
+ If you wish to drag a file from the hard disk onto the browser window (eg.
+ to initiate a file upload), use function :py:func:`drag_file`.
+ """
+ _get_api_impl().drag_impl(element, to)
- This function is exclusively used for dragging elements inside one web page.
- If you wish to drag a file from the hard disk onto the browser window (eg.
- to initiate a file upload), use function :py:func:`drag_file`.
- """
- _get_api_impl().drag_impl(element, to)
def press_mouse_on(element):
- _get_api_impl().press_mouse_on_impl(element)
+ _get_api_impl().press_mouse_on_impl(element)
+
def release_mouse_over(element):
- _get_api_impl().release_mouse_over_impl(element)
+ _get_api_impl().release_mouse_over_impl(element)
+
def find_all(predicate):
- """
- Lets you find all occurrences of the given GUI element predicate. For
- instance, the following statement returns a list of all buttons with label
- "Open"::
+ """
+ Lets you find all occurrences of the given GUI element predicate. For
+ instance, the following statement returns a list of all buttons with label
+ "Open"::
+
+ find_all(Button("Open"))
- find_all(Button("Open"))
+ Other examples are::
- Other examples are::
+ find_all(Window())
+ find_all(TextField("Address line 1"))
- find_all(Window())
- find_all(TextField("Address line 1"))
+ The function returns a list of elements of the same type as the passed-in
+ parameter. For instance, ``find_all(Button(...))`` yields a list whose
+ elements are of type :py:class:`Button`.
- The function returns a list of elements of the same type as the passed-in
- parameter. For instance, ``find_all(Button(...))`` yields a list whose
- elements are of type :py:class:`Button`.
+ In a typical usage scenario, you want to pick out one of the occurrences
+ returned by :py:func:`find_all`. In such cases, :py:func:`list.sort` can
+ be very useful. For example, to find the leftmost "Open" button, you can
+ write::
- In a typical usage scenario, you want to pick out one of the occurrences
- returned by :py:func:`find_all`. In such cases, :py:func:`list.sort` can
- be very useful. For example, to find the leftmost "Open" button, you can
- write::
+ buttons = find_all(Button("Open"))
+ leftmost_button = sorted(buttons, key=lambda button: button.x)[0]
+ """
+ return _get_api_impl().find_all_impl(predicate)
- buttons = find_all(Button("Open"))
- leftmost_button = sorted(buttons, key=lambda button: button.x)[0]
- """
- return _get_api_impl().find_all_impl(predicate)
def scroll_down(num_pixels=100):
- """
- Scrolls down the page the given number of pixels.
- """
- _get_api_impl().scroll_down_impl(num_pixels)
+ """
+ Scrolls down the page the given number of pixels.
+ """
+ _get_api_impl().scroll_down_impl(num_pixels)
+
def scroll_up(num_pixels=100):
- """
- Scrolls the the page up the given number of pixels.
- """
- _get_api_impl().scroll_up_impl(num_pixels)
+ """
+ Scrolls the the page up the given number of pixels.
+ """
+ _get_api_impl().scroll_up_impl(num_pixels)
+
def scroll_right(num_pixels=100):
- """
- Scrolls the page to the right the given number of pixels.
- """
- _get_api_impl().scroll_right_impl(num_pixels)
+ """
+ Scrolls the page to the right the given number of pixels.
+ """
+ _get_api_impl().scroll_right_impl(num_pixels)
+
def scroll_left(num_pixels=100):
- """
- Scrolls the page to the left the given number of pixels.
- """
- _get_api_impl().scroll_left_impl(num_pixels)
+ """
+ Scrolls the page to the left the given number of pixels.
+ """
+ _get_api_impl().scroll_left_impl(num_pixels)
+
def hover(element):
- """
- :param element: The element or point to hover.
- :type element: str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param element: The element or point to hover.
+ :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- Hovers the mouse cursor over the given element or point. For example::
+ Hovers the mouse cursor over the given element or point. For example::
+
+ hover("File size")
+ hover(Button("OK"))
+ hover(Link("Download"))
+ hover(Point(200, 300))
+ hover(ComboBox("File type").top_left + (50, 0))
+ """
+ _get_api_impl().hover_impl(element)
- hover("File size")
- hover(Button("OK"))
- hover(Link("Download"))
- hover(Point(200, 300))
- hover(ComboBox("File type").top_left + (50, 0))
- """
- _get_api_impl().hover_impl(element)
def rightclick(element):
- """
- :param element: The element or point to click.
- :type element: str, unicode, :py:class:`HTMLElement`, \
+ """
+ :param element: The element or point to click.
+ :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`
- Performs a right click on the given element or point. For example::
+ Performs a right click on the given element or point. For example::
+
+ rightclick("Something")
+ rightclick(Point(200, 300))
+ rightclick(Image("captcha"))
+ """
+ _get_api_impl().rightclick_impl(element)
- rightclick("Something")
- rightclick(Point(200, 300))
- rightclick(Image("captcha"))
- """
- _get_api_impl().rightclick_impl(element)
def select(combo_box, value):
- """
- :param combo_box: The combo box whose value should be changed.
- :type combo_box: str, unicode or :py:class:`ComboBox`
- :param value: The visible value of the combo box to be selected.
+ """
+ :param combo_box: The combo box whose value should be changed.
+ :type combo_box: str, unicode or :py:class:`ComboBox`
+ :param value: The visible value of the combo box to be selected.
- Selects a value from a combo box. For example::
+ Selects a value from a combo box. For example::
+
+ select("Language", "English")
+ select(ComboBox("Language"), "English")
+ """
+ _get_api_impl().select_impl(combo_box, value)
- select("Language", "English")
- select(ComboBox("Language"), "English")
- """
- _get_api_impl().select_impl(combo_box, value)
def drag_file(file_path, to):
- """
- Simulates the dragging of a file from the computer over the browser window
- and dropping it over the given element. This allows, for example, to attach
- files to emails in Gmail::
-
- click("COMPOSE")
- write("example@gmail.com", into="To")
- write("Email subject", into="Subject")
- drag_file(r"C:\\Documents\\notes.txt", to="Drop files here")
- """
- _get_api_impl().drag_file_impl(file_path, to)
+ """
+ Simulates the dragging of a file from the computer over the browser window
+ and dropping it over the given element. This allows, for example, to attach
+ files to emails in Gmail::
+
+ click("COMPOSE")
+ write("example@gmail.com", into="To")
+ write("Email subject", into="Subject")
+ drag_file(r"C:\\Documents\\notes.txt", to="Drop files here")
+ """
+ _get_api_impl().drag_file_impl(file_path, to)
+
def attach_file(file_path, to=None):
- """
- :param file_path: The path of the file to be attached.
- :param to: The file input element to which the file should be attached.
+ """
+ :param file_path: The path of the file to be attached.
+ :param to: The file input element to which the file should be attached.
+
+ Allows attaching a file to a file input element. For instance::
- Allows attaching a file to a file input element. For instance::
+ attach_file("c:/test.txt", to="Please select a file:")
- attach_file("c:/test.txt", to="Please select a file:")
+ The file input element is identified by its label. If you omit the ``to=``
+ parameter, then Helium attaches the file to the first file input element it
+ finds on the page.
+ """
+ _get_api_impl().attach_file_impl(file_path, to=to)
- The file input element is identified by its label. If you omit the ``to=``
- parameter, then Helium attaches the file to the first file input element it
- finds on the page.
- """
- _get_api_impl().attach_file_impl(file_path, to=to)
def refresh():
- """
- Refreshes the current page. If an alert dialog is open, then Helium first
- closes it.
- """
- _get_api_impl().refresh_impl()
+ """
+ Refreshes the current page. If an alert dialog is open, then Helium first
+ closes it.
+ """
+ _get_api_impl().refresh_impl()
+
def wait_until(condition_fn, timeout_secs=10, interval_secs=0.5):
- """
- :param condition_fn: A function taking no arguments that represents the \
- condition to be waited for.
- :param timeout_secs: The timeout, in seconds, after which the condition is \
- deemed to have failed.
- :param interval_secs: The interval, in seconds, at which the condition \
- function is polled to determine whether the wait has succeeded.
+ """
+ :param condition_fn: A function taking no arguments that represents the \
+ condition to be waited for.
+ :param timeout_secs: The timeout, in seconds, after which the condition is \
+ deemed to have failed.
+ :param interval_secs: The interval, in seconds, at which the condition \
+ function is polled to determine whether the wait has succeeded.
+
+ Waits until the given condition function evaluates to true. This is most
+ commonly used to wait for an element to exist::
- Waits until the given condition function evaluates to true. This is most
- commonly used to wait for an element to exist::
+ wait_until(Text("Finished!").exists)
- wait_until(Text("Finished!").exists)
+ More elaborate conditions are also possible using Python lambda
+ expressions. For instance, to wait until a text no longer exists::
- More elaborate conditions are also possible using Python lambda
- expressions. For instance, to wait until a text no longer exists::
+ wait_until(lambda: not Text("Uploading...").exists())
- wait_until(lambda: not Text("Uploading...").exists())
+ ``wait_until`` raises
+ :py:class:`selenium.common.exceptions.TimeoutException` if the condition is
+ not satisfied within the given number of seconds. The parameter
+ ``interval_secs`` specifies the number of seconds Helium waits between
+ evaluating the condition function.
+ """
+ _get_api_impl().wait_until_impl(condition_fn, timeout_secs, interval_secs)
- ``wait_until`` raises
- :py:class:`selenium.common.exceptions.TimeoutException` if the condition is
- not satisfied within the given number of seconds. The parameter
- ``interval_secs`` specifies the number of seconds Helium waits between
- evaluating the condition function.
- """
- _get_api_impl().wait_until_impl(condition_fn, timeout_secs, interval_secs)
class Config:
- """
- This class contains Helium's run-time configuration. To modify Helium's
- behaviour, simply assign to the properties of this class. For instance::
+ """
+ This class contains Helium's run-time configuration. To modify Helium's
+ behaviour, simply assign to the properties of this class. For instance::
- Config.implicit_wait_secs = 0
- """
- implicit_wait_secs = 10
- """
- ``implicit_wait_secs`` is Helium's analogue to Selenium's
- ``.implicitly_wait(secs)``. Suppose you have a script that executes the
- following command::
+ Config.implicit_wait_secs = 0
+ """
+ implicit_wait_secs = 10
+ """
+ ``implicit_wait_secs`` is Helium's analogue to Selenium's
+ ``.implicitly_wait(secs)``. Suppose you have a script that executes the
+ following command::
- >>> click("Download")
+ >>> click("Download")
- If the "Download" element is not immediately available, then Helium waits up
- to ``implicit_wait_secs`` for it to appear before raising a
- ``LookupError``. This is useful in situations where the page takes slightly
- longer to load, or a GUI element only appears after a certain time.
+ If the "Download" element is not immediately available, then Helium waits up
+ to ``implicit_wait_secs`` for it to appear before raising a
+ ``LookupError``. This is useful in situations where the page takes slightly
+ longer to load, or a GUI element only appears after a certain time.
- To disable Helium's implicit waits, simply execute::
+ To disable Helium's implicit waits, simply execute::
- Config.implicit_wait_secs = 0
+ Config.implicit_wait_secs = 0
- Helium's implicit waits do not affect commands :py:func:`find_all` or
- :py:func:`GUIElement.exists`. Note also that setting
- ``implicit_wait_secs`` does not affect the underlying Selenium driver
- (see :py:func:`get_driver`).
+ Helium's implicit waits do not affect commands :py:func:`find_all` or
+ :py:func:`GUIElement.exists`. Note also that setting
+ ``implicit_wait_secs`` does not affect the underlying Selenium driver
+ (see :py:func:`get_driver`).
+
+ For the best results, it is recommended to not use Selenium's
+ ``.implicitly_wait(...)`` in conjunction with Helium.
+ """
- For the best results, it is recommended to not use Selenium's
- ``.implicitly_wait(...)`` in conjunction with Helium.
- """
class GUIElement:
- def __init__(self):
- self._driver = _get_api_impl().require_driver()
- self._args = []
- self._kwargs = OrderedDict()
- self._impl_cached = None
- def exists(self):
- """
- Evaluates to true if this GUI element exists.
- """
- return self._impl.exists()
- def with_impl(self, impl):
- result = copy(self)
- result._impl = impl
- return result
- @property
- def _impl(self):
- if self._impl_cached is None:
- impl_class = \
- getattr(helium._impl, self.__class__.__name__ + 'Impl')
- self._impl_cached = impl_class(
- self._driver, *self._args, **self._kwargs
- )
- return self._impl_cached
- @_impl.setter
- def _impl(self, value):
- self._impl_cached = value
- def __repr__(self):
- return self._repr_constructor_args(self._args, self._kwargs)
- def _repr_constructor_args(self, args=None, kwargs=None):
- if args is None:
- args = []
- if kwargs is None:
- kwargs = {}
- return '%s(%s)' % (
- self.__class__.__name__,
- repr_args(self.__init__, args, kwargs, repr)
- )
- def _is_bound(self):
- return self._impl_cached is not None and self._impl_cached._is_bound()
+ def __init__(self):
+ self._driver = _get_api_impl().require_driver()
+ self._args = []
+ self._kwargs = OrderedDict()
+ self._impl_cached = None
+
+ def exists(self):
+ """
+ Evaluates to true if this GUI element exists.
+ """
+ return self._impl.exists()
+
+ def with_impl(self, impl):
+ result = copy(self)
+ result._impl = impl
+ return result
+
+ @property
+ def _impl(self):
+ if self._impl_cached is None:
+ impl_class = \
+ getattr(helium._impl, self.__class__.__name__ + 'Impl')
+ self._impl_cached = impl_class(
+ self._driver, *self._args, **self._kwargs
+ )
+ return self._impl_cached
+
+ @_impl.setter
+ def _impl(self, value):
+ self._impl_cached = value
+
+ def __repr__(self):
+ return self._repr_constructor_args(self._args, self._kwargs)
+
+ def _repr_constructor_args(self, args=None, kwargs=None):
+ if args is None:
+ args = []
+ if kwargs is None:
+ kwargs = {}
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ repr_args(self.__init__, args, kwargs, repr)
+ )
+
+ def _is_bound(self):
+ return self._impl_cached is not None and self._impl_cached._is_bound()
+
class HTMLElement(GUIElement):
- def __init__(
- self, below=None, to_right_of=None, above=None, to_left_of=None
- ):
- super(HTMLElement, self).__init__()
- self._kwargs['below'] = below
- self._kwargs['to_right_of'] = to_right_of
- self._kwargs['above'] = above
- self._kwargs['to_left_of'] = to_left_of
- @property
- def width(self):
- """
- The width of this HTML element, in pixels.
- """
- return self._impl.width
- @property
- def height(self):
- """
- The height of this HTML element, in pixels.
- """
- return self._impl.height
- @property
- def x(self):
- """
- The x-coordinate on the page of the top-left point of this HTML element.
- """
- return self._impl.x
- @property
- def y(self):
- """
- The y-coordinate on the page of the top-left point of this HTML element.
- """
- return self._impl.y
- @property
- def top_left(self):
- """
- The top left corner of this element, as a :py:class:`helium.Point`.
- This point has exactly the coordinates given by this element's `.x` and
- `.y` properties. `top_left` is for instance useful for clicking at an
- offset of an element::
-
- click(Button("OK").top_left + (30, 15))
- """
- return self._impl.top_left
- @property
- def web_element(self):
- """
- The Selenium WebElement corresponding to this element.
- """
- return self._impl.web_element
- def __repr__(self):
- if self._is_bound():
- element_html = self.web_element.get_attribute('outerHTML')
- return get_easily_readable_snippet(element_html)
- else:
- return super(HTMLElement, self).__repr__()
+ def __init__(
+ self, below=None, to_right_of=None, above=None, to_left_of=None
+ ):
+ super(HTMLElement, self).__init__()
+ self._kwargs['below'] = below
+ self._kwargs['to_right_of'] = to_right_of
+ self._kwargs['above'] = above
+ self._kwargs['to_left_of'] = to_left_of
+
+ @property
+ def width(self):
+ """
+ The width of this HTML element, in pixels.
+ """
+ return self._impl.width
+
+ @property
+ def height(self):
+ """
+ The height of this HTML element, in pixels.
+ """
+ return self._impl.height
+
+ @property
+ def x(self):
+ """
+ The x-coordinate on the page of the top-left point of this HTML element.
+ """
+ return self._impl.x
+
+ @property
+ def y(self):
+ """
+ The y-coordinate on the page of the top-left point of this HTML element.
+ """
+ return self._impl.y
+
+ @property
+ def top_left(self):
+ """
+ The top left corner of this element, as a :py:class:`helium.Point`.
+ This point has exactly the coordinates given by this element's `.x` and
+ `.y` properties. `top_left` is for instance useful for clicking at an
+ offset of an element::
+
+ click(Button("OK").top_left + (30, 15))
+ """
+ return self._impl.top_left
+
+ @property
+ def web_element(self):
+ """
+ The Selenium WebElement corresponding to this element.
+ """
+ return self._impl.web_element
+
+ def __repr__(self):
+ if self._is_bound():
+ element_html = self.web_element.get_attribute('outerHTML')
+ return get_easily_readable_snippet(element_html)
+ else:
+ return super(HTMLElement, self).__repr__()
+
class S(HTMLElement):
- """
- :param selector: The selector used to identify the HTML element(s).
-
- A jQuery-style selector for identifying HTML elements by ID, name, CSS
- class, CSS selector or XPath. For example: Say you have an element with
- ID "myId" on a web page, such as ``
``.
- Then you can identify this element using ``S`` as follows::
-
- S("#myId")
-
- The parameter which you pass to ``S(...)`` is interpreted by Helium
- according to these rules:
-
- * If it starts with an ``@``, then it identifies elements by HTML ``name``.
- Eg. ``S("@btnName")`` identifies an element with ``name="btnName"``.
- * If it starts with ``//``, then Helium interprets it as an XPath.
- * Otherwise, Helium interprets it as a CSS selector. This in particular
- lets you write ``S("#myId")`` to identify an element with ``id="myId"``,
- or ``S(".myClass")`` to identify elements with ``class="myClass"``.
-
- ``S`` also makes it possible to read plain text data from a web page. For
- example, suppose you have a table of people's email addresses. Then you
- can read the list of email addresses as follows::
-
- email_cells = find_all(S("table > tr > td", below="Email"))
- emails = [cell.web_element.text for cell in email_cells]
-
- Where ``email`` is the column header (``
Email
``). Similarly to
- ``below`` and ``to_right_of``, the keyword parameters ``above`` and
- ``to_left_of`` can be used to search for elements above and to the left
- of other web elements.
- """
- def __init__(self, selector, below=None, to_right_of=None, above=None,
- to_left_of=None):
- super(S, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(selector)
+ """
+ :param selector: The selector used to identify the HTML element(s).
+
+ A jQuery-style selector for identifying HTML elements by ID, name, CSS
+ class, CSS selector or XPath. For example: Say you have an element with
+ ID "myId" on a web page, such as ````.
+ Then you can identify this element using ``S`` as follows::
+
+ S("#myId")
+
+ The parameter which you pass to ``S(...)`` is interpreted by Helium
+ according to these rules:
+
+ * If it starts with an ``@``, then it identifies elements by HTML ``name``.
+ Eg. ``S("@btnName")`` identifies an element with ``name="btnName"``.
+ * If it starts with ``//``, then Helium interprets it as an XPath.
+ * Otherwise, Helium interprets it as a CSS selector. This in particular
+ lets you write ``S("#myId")`` to identify an element with ``id="myId"``,
+ or ``S(".myClass")`` to identify elements with ``class="myClass"``.
+
+ ``S`` also makes it possible to read plain text data from a web page. For
+ example, suppose you have a table of people's email addresses. Then you
+ can read the list of email addresses as follows::
+
+ email_cells = find_all(S("table > tr > td", below="Email"))
+ emails = [cell.web_element.text for cell in email_cells]
+
+ Where ``email`` is the column header (``
Email
``). Similarly to
+ ``below`` and ``to_right_of``, the keyword parameters ``above`` and
+ ``to_left_of`` can be used to search for elements above and to the left
+ of other web elements.
+ """
+
+ def __init__(self, selector, below=None, to_right_of=None, above=None,
+ to_left_of=None):
+ super(S, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(selector)
+
class Text(HTMLElement):
- """
- Lets you identify any text or label on a web page. This is most useful for
- checking whether a particular text exists::
-
- if Text("Do you want to proceed?").exists():
- click("Yes")
-
- ``Text`` also makes it possible to read plain text data from a web page. For
- example, suppose you have a table of people's email addresses. Then you
- can read John's email addresses as follows::
-
- Text(below="Email", to_right_of="John").value
-
- Similarly to ``below`` and ``to_right_of``, the keyword parameters ``above``
- and ``to_left_of`` can be used to search for texts above and to the left of
- other web elements.
- """
- def __init__(
- self, text=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(Text, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(text)
- @property
- def value(self):
- """
- Returns the current value of this Text object.
- """
- return self._impl.value
+ """
+ Lets you identify any text or label on a web page. This is most useful for
+ checking whether a particular text exists::
+
+ if Text("Do you want to proceed?").exists():
+ click("Yes")
+
+ ``Text`` also makes it possible to read plain text data from a web page. For
+ example, suppose you have a table of people's email addresses. Then you
+ can read John's email addresses as follows::
+
+ Text(below="Email", to_right_of="John").value
+
+ Similarly to ``below`` and ``to_right_of``, the keyword parameters ``above``
+ and ``to_left_of`` can be used to search for texts above and to the left of
+ other web elements.
+ """
+
+ def __init__(
+ self, text=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(Text, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(text)
+
+ @property
+ def value(self):
+ """
+ Returns the current value of this Text object.
+ """
+ return self._impl.value
+
class Link(HTMLElement):
- """
- Lets you identify a link on a web page. A typical usage of ``Link`` is::
-
- click(Link("Sign in"))
-
- You can also read a ``Link``'s properties. This is most typically used to
- check for a link's existence before clicking on it::
-
- if Link("Sign in").exists():
- click(Link("Sign in"))
-
- When there are multiple occurrences of a link on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(Link("Block User", to_right_of="John Doe"))
- """
- def __init__(
- self, text=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(Link, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(text)
- @property
- def href(self):
- """
- Returns the URL of the page the link goes to.
- """
- return self._impl.href
+ """
+ Lets you identify a link on a web page. A typical usage of ``Link`` is::
+
+ click(Link("Sign in"))
+
+ You can also read a ``Link``'s properties. This is most typically used to
+ check for a link's existence before clicking on it::
+
+ if Link("Sign in").exists():
+ click(Link("Sign in"))
+
+ When there are multiple occurrences of a link on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(Link("Block User", to_right_of="John Doe"))
+ """
+
+ def __init__(
+ self, text=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(Link, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(text)
+
+ @property
+ def href(self):
+ """
+ Returns the URL of the page the link goes to.
+ """
+ return self._impl.href
+
class ListItem(HTMLElement):
- """
- Lets you identify a list item (HTML ``
`` element) on a web page. This is
- often useful for interacting with elements of a navigation bar::
-
- click(ListItem("News Feed"))
-
- In other cases such as an automated test, you might want to query the
- properties of a ``ListItem``. For example, the following line checks whether
- a list item with text "List item 1" exists, and raises an error if not::
-
- assert ListItem("List item 1").exists()
-
- When there are multiple occurrences of a list item on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(ListItem("List item 1", below="My first list:"))
- """
- def __init__(
- self, text=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(ListItem, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(text)
+ """
+ Lets you identify a list item (HTML ``
`` element) on a web page. This is
+ often useful for interacting with elements of a navigation bar::
+
+ click(ListItem("News Feed"))
+
+ In other cases such as an automated test, you might want to query the
+ properties of a ``ListItem``. For example, the following line checks whether
+ a list item with text "List item 1" exists, and raises an error if not::
+
+ assert ListItem("List item 1").exists()
+
+ When there are multiple occurrences of a list item on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(ListItem("List item 1", below="My first list:"))
+ """
+
+ def __init__(
+ self, text=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(ListItem, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(text)
+
class Button(HTMLElement):
- """
- Lets you identify a button on a web page. A typical usage of ``Button`` is::
-
- click(Button("Log In"))
-
- ``Button`` also lets you read a button's properties. For example, the
- following snippet clicks button "OK" only if it exists::
-
- if Button("OK").exists():
- click(Button("OK"))
-
- When there are multiple occurrences of a button on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(Button("Log In", below=TextField("Password")))
- """
- def __init__(
- self, text=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(Button, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(text)
- def is_enabled(self):
- """
- Returns true if this UI element can currently be interacted with.
- """
- return self._impl.is_enabled()
+ """
+ Lets you identify a button on a web page. A typical usage of ``Button`` is::
+
+ click(Button("Log In"))
+
+ ``Button`` also lets you read a button's properties. For example, the
+ following snippet clicks button "OK" only if it exists::
+
+ if Button("OK").exists():
+ click(Button("OK"))
+
+ When there are multiple occurrences of a button on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(Button("Log In", below=TextField("Password")))
+ """
+
+ def __init__(
+ self, text=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(Button, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(text)
+
+ def is_enabled(self):
+ """
+ Returns true if this UI element can currently be interacted with.
+ """
+ return self._impl.is_enabled()
+
class Image(HTMLElement):
- """
- Lets you identify an image (HTML ```` element) on a web page.
- Typically, this is done via the image's alt text. For instance::
-
- click(Image(alt="Helium Logo"))
-
- You can also query an image's properties. For example, the following snippet
- clicks on the image with alt text "Helium Logo" only if it exists::
-
- if Image("Helium Logo").exists():
- click(Image("Helium Logo"))
-
- When there are multiple occurrences of an image on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(Image("Helium Logo", to_left_of=ListItem("Download")))
- """
- def __init__(
- self, alt=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(Image, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(alt)
+ """
+ Lets you identify an image (HTML ```` element) on a web page.
+ Typically, this is done via the image's alt text. For instance::
+
+ click(Image(alt="Helium Logo"))
+
+ You can also query an image's properties. For example, the following snippet
+ clicks on the image with alt text "Helium Logo" only if it exists::
+
+ if Image("Helium Logo").exists():
+ click(Image("Helium Logo"))
+
+ When there are multiple occurrences of an image on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(Image("Helium Logo", to_left_of=ListItem("Download")))
+ """
+
+ def __init__(
+ self, alt=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(Image, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(alt)
+
class TextField(HTMLElement):
- """
- Lets you identify a text field on a web page. This is most typically done to
- read the value of a text field. For example::
-
- TextField("First name").value
-
- This returns the value of the "First name" text field. If it is empty, the
- empty string "" is returned.
-
- When there are multiple occurrences of a text field on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- TextField("Address line 1", below="Billing Address:").value
- """
- def __init__(
- self, label=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(TextField, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(label)
- @property
- def value(self):
- """
- Returns the current value of this text field. '' if there is no value.
- """
- return self._impl.value
- def is_enabled(self):
- """
- Returns true if this UI element can currently be interacted with.
-
- The difference between a text field being 'enabled' and 'editable' is
- mostly visual: If a text field is not enabled, it is usually greyed out,
- whereas if it is not editable it looks normal. See also ``is_editable``.
- """
- return self._impl.is_enabled()
- def is_editable(self):
- """
- Returns true if the value of this UI element can be modified.
-
- The difference between a text field being 'enabled' and 'editable' is
- mostly visual: If a text field is not enabled, it is usually greyed out,
- whereas if it is not editable it looks normal. See also ``is_enabled``.
- """
- return self._impl.is_editable()
+ """
+ Lets you identify a text field on a web page. This is most typically done to
+ read the value of a text field. For example::
+
+ TextField("First name").value
+
+ This returns the value of the "First name" text field. If it is empty, the
+ empty string "" is returned.
+
+ When there are multiple occurrences of a text field on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ TextField("Address line 1", below="Billing Address:").value
+ """
+
+ def __init__(
+ self, label=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(TextField, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(label)
+
+ @property
+ def value(self):
+ """
+ Returns the current value of this text field. '' if there is no value.
+ """
+ return self._impl.value
+
+ def is_enabled(self):
+ """
+ Returns true if this UI element can currently be interacted with.
+
+ The difference between a text field being 'enabled' and 'editable' is
+ mostly visual: If a text field is not enabled, it is usually greyed out,
+ whereas if it is not editable it looks normal. See also ``is_editable``.
+ """
+ return self._impl.is_enabled()
+
+ def is_editable(self):
+ """
+ Returns true if the value of this UI element can be modified.
+
+ The difference between a text field being 'enabled' and 'editable' is
+ mostly visual: If a text field is not enabled, it is usually greyed out,
+ whereas if it is not editable it looks normal. See also ``is_enabled``.
+ """
+ return self._impl.is_editable()
+
class ComboBox(HTMLElement):
- """
- Lets you identify a combo box on a web page. This can for instance be used
- to determine the current value of a combo box::
-
- ComboBox("Language").value
-
- A ComboBox may be *editable*, which means that it is possible to type in
- arbitrary values in addition to selecting from a predefined drop-down list
- of values. The property :py:func:`ComboBox.is_editable` can be used to
- determine whether this is the case for a particular combo box instance.
-
- When there are multiple occurrences of a combo box on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- select(ComboBox(to_right_of="John Doe", below="Status"), "Active")
-
- This sets the Status of John Doe to Active on the page.
- """
- def __init__(
- self, label=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(ComboBox, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(label)
- def is_editable(self):
- """
- Returns whether this combo box allows entering an arbitrary text in
- addition to selecting predefined values from a drop-down list.
- """
- return self._impl.is_editable()
- @property
- def value(self):
- """
- Returns the currently selected combo box value.
- """
- return self._impl.value
- @property
- def options(self):
- """
- Returns a list of all possible options available to choose from in the
- ComboBox.
- """
- return self._impl.options
+ """
+ Lets you identify a combo box on a web page. This can for instance be used
+ to determine the current value of a combo box::
+
+ ComboBox("Language").value
+
+ A ComboBox may be *editable*, which means that it is possible to type in
+ arbitrary values in addition to selecting from a predefined drop-down list
+ of values. The property :py:func:`ComboBox.is_editable` can be used to
+ determine whether this is the case for a particular combo box instance.
+
+ When there are multiple occurrences of a combo box on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ select(ComboBox(to_right_of="John Doe", below="Status"), "Active")
+
+ This sets the Status of John Doe to Active on the page.
+ """
+
+ def __init__(
+ self, label=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(ComboBox, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(label)
+
+ def is_editable(self):
+ """
+ Returns whether this combo box allows entering an arbitrary text in
+ addition to selecting predefined values from a drop-down list.
+ """
+ return self._impl.is_editable()
+
+ @property
+ def value(self):
+ """
+ Returns the currently selected combo box value.
+ """
+ return self._impl.value
+
+ @property
+ def options(self):
+ """
+ Returns a list of all possible options available to choose from in the
+ ComboBox.
+ """
+ return self._impl.options
+
class CheckBox(HTMLElement):
- """
- Lets you identify a check box on a web page. To tick a currently unselected
- check box, use::
-
- click(CheckBox("I agree"))
-
- ``CheckBox`` also lets you read the properties of a check box. For example,
- the method :py:func:`CheckBox.is_checked` can be used to only click a check
- box if it isn't already checked::
-
- if not CheckBox("I agree").is_checked():
- click(CheckBox("I agree"))
-
- When there are multiple occurrences of a check box on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(CheckBox("Stay signed in", below=Button("Sign in")))
- """
- def __init__(
- self, label=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(CheckBox, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(label)
- def is_enabled(self):
- """
- Returns True if this GUI element can currently be interacted with.
- """
- return self._impl.is_enabled()
- def is_checked(self):
- """
- Returns True if this GUI element is checked (selected).
- """
- return self._impl.is_checked()
+ """
+ Lets you identify a check box on a web page. To tick a currently unselected
+ check box, use::
+
+ click(CheckBox("I agree"))
+
+ ``CheckBox`` also lets you read the properties of a check box. For example,
+ the method :py:func:`CheckBox.is_checked` can be used to only click a check
+ box if it isn't already checked::
+
+ if not CheckBox("I agree").is_checked():
+ click(CheckBox("I agree"))
+
+ When there are multiple occurrences of a check box on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(CheckBox("Stay signed in", below=Button("Sign in")))
+ """
+
+ def __init__(
+ self, label=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(CheckBox, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(label)
+
+ def is_enabled(self):
+ """
+ Returns True if this GUI element can currently be interacted with.
+ """
+ return self._impl.is_enabled()
+
+ def is_checked(self):
+ """
+ Returns True if this GUI element is checked (selected).
+ """
+ return self._impl.is_checked()
+
class RadioButton(HTMLElement):
- """
- Lets you identify a radio button on a web page. To select a currently
- unselected radio button, use::
-
- click(RadioButton("Windows"))
-
- ``RadioButton`` also lets you read the properties of a radio button. For
- example, the method :py:func:`RadioButton.is_selected` can be used to only
- click a radio button if it isn't already selected::
-
- if not RadioButton("Windows").is_selected():
- click(RadioButton("Windows"))
-
- When there are multiple occurrences of a radio button on a page, you can
- disambiguate between them using the keyword parameters ``below``,
- ``to_right_of``, ``above`` and ``to_left_of``. For instance::
-
- click(RadioButton("I accept", below="License Agreement"))
- """
- def __init__(
- self, label=None, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(RadioButton, self).__init__(
- below=below, to_right_of=to_right_of, above=above,
- to_left_of=to_left_of
- )
- self._args.append(label)
- def is_selected(self):
- """
- Returns true if this radio button is selected.
- """
- return self._impl.is_selected()
+ """
+ Lets you identify a radio button on a web page. To select a currently
+ unselected radio button, use::
+
+ click(RadioButton("Windows"))
+
+ ``RadioButton`` also lets you read the properties of a radio button. For
+ example, the method :py:func:`RadioButton.is_selected` can be used to only
+ click a radio button if it isn't already selected::
+
+ if not RadioButton("Windows").is_selected():
+ click(RadioButton("Windows"))
+
+ When there are multiple occurrences of a radio button on a page, you can
+ disambiguate between them using the keyword parameters ``below``,
+ ``to_right_of``, ``above`` and ``to_left_of``. For instance::
+
+ click(RadioButton("I accept", below="License Agreement"))
+ """
+
+ def __init__(
+ self, label=None, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(RadioButton, self).__init__(
+ below=below, to_right_of=to_right_of, above=above,
+ to_left_of=to_left_of
+ )
+ self._args.append(label)
+
+ def is_selected(self):
+ """
+ Returns true if this radio button is selected.
+ """
+ return self._impl.is_selected()
+
class Window(GUIElement):
- """
- Lets you identify individual windows of the currently open browser session.
- """
- def __init__(self, title=None):
- super(Window, self).__init__()
- self._args.append(title)
- @property
- def title(self):
- """
- Returns the title of this Window.
- """
- return self._impl.title
- @property
- def handle(self):
- """
- Returns the Selenium driver window handle assigned to this window. Note
- that this window handle is simply an abstract identifier and bears no
- relationship to the corresponding operating system handle (HWND on
- Windows).
- """
- return self._impl.handle
- def __repr__(self):
- if self._is_bound():
- return self._repr_constructor_args([self.title])
- else:
- return super(Window, self).__repr__()
+ """
+ Lets you identify individual windows of the currently open browser session.
+ """
+
+ def __init__(self, title=None):
+ super(Window, self).__init__()
+ self._args.append(title)
+
+ @property
+ def title(self):
+ """
+ Returns the title of this Window.
+ """
+ return self._impl.title
+
+ @property
+ def handle(self):
+ """
+ Returns the Selenium driver window handle assigned to this window. Note
+ that this window handle is simply an abstract identifier and bears no
+ relationship to the corresponding operating system handle (HWND on
+ Windows).
+ """
+ return self._impl.handle
+
+ def __repr__(self):
+ if self._is_bound():
+ return self._repr_constructor_args([self.title])
+ else:
+ return super(Window, self).__repr__()
+
class Alert(GUIElement):
- """
- Lets you identify and interact with JavaScript alert boxes.
- """
- def __init__(self, search_text=None):
- super(Alert, self).__init__()
- self._args.append(search_text)
- @property
- def text(self):
- """
- The text displayed in the alert box.
- """
- return self._impl.text
- def accept(self):
- """
- Accepts this alert. This typically corresponds to clicking the "OK"
- button inside the alert. The typical way to use this method is::
-
- >>> Alert().accept()
-
- This accepts the currently open alert.
- """
- self._impl.accept()
- def dismiss(self):
- """
- Dismisses this alert. This typically corresponds to clicking the
- "Cancel" or "Close" button of the alert. The typical way to use this
- method is::
-
- >>> Alert().dismiss()
-
- This dismisses the currently open alert.
- """
- self._impl.dismiss()
- def __repr__(self):
- if self._is_bound():
- return self._repr_constructor_args([self.text])
- else:
- return super(Alert, self).__repr__()
+ """
+ Lets you identify and interact with JavaScript alert boxes.
+ """
+
+ def __init__(self, search_text=None):
+ super(Alert, self).__init__()
+ self._args.append(search_text)
+
+ @property
+ def text(self):
+ """
+ The text displayed in the alert box.
+ """
+ return self._impl.text
+
+ def accept(self):
+ """
+ Accepts this alert. This typically corresponds to clicking the "OK"
+ button inside the alert. The typical way to use this method is::
+
+ >>> Alert().accept()
+
+ This accepts the currently open alert.
+ """
+ self._impl.accept()
+
+ def dismiss(self):
+ """
+ Dismisses this alert. This typically corresponds to clicking the
+ "Cancel" or "Close" button of the alert. The typical way to use this
+ method is::
+
+ >>> Alert().dismiss()
+
+ This dismisses the currently open alert.
+ """
+ self._impl.dismiss()
+
+ def __repr__(self):
+ if self._is_bound():
+ return self._repr_constructor_args([self.text])
+ else:
+ return super(Alert, self).__repr__()
+
class Point(namedtuple('Point', ['x', 'y'])):
- """
- A clickable point. To create a ``Point`` at an offset of an existing point,
- use ``+`` and ``-``::
-
- >>> point = Point(x=10, y=25)
- >>> point + (10, 0)
- Point(x=20, y=25)
- >>> point - (0, 10)
- Point(x=10, y=15)
- """
- def __new__(cls, x=0, y=0):
- return cls.__bases__[0].__new__(cls, x, y)
- def __init__(self, x=0, y=0):
- # tuple is immutable so we can't do anything here. The initialization
- # happens in __new__(...) above.
- pass
- @property
- def x(self):
- """
- The x coordinate of the point.
- """
- return self[0]
- @property
- def y(self):
- """
- The y coordinate of the point.
- """
- return self[1]
- def __eq__(self, other):
- return (self.x, self.y) == other
- def __ne__(self, other):
- return not self == other
- def __hash__(self):
- return self.x + 7 * self.y
- def __add__(self, delta):
- dx, dy = delta
- return Point(self.x + dx, self.y + dy)
- def __radd__(self, delta):
- return self.__add__(delta)
- def __sub__(self, delta):
- dx, dy = delta
- return Point(self.x - dx, self.y - dy)
- def __rsub__(self, delta):
- x, y = delta
- return Point(x - self.x, y - self.y)
+ """
+ A clickable point. To create a ``Point`` at an offset of an existing point,
+ use ``+`` and ``-``::
+
+ >>> point = Point(x=10, y=25)
+ >>> point + (10, 0)
+ Point(x=20, y=25)
+ >>> point - (0, 10)
+ Point(x=10, y=15)
+ """
+
+ def __new__(cls, x=0, y=0):
+ return cls.__bases__[0].__new__(cls, x, y)
+
+ def __init__(self, x=0, y=0):
+ # tuple is immutable so we can't do anything here. The initialization
+ # happens in __new__(...) above.
+ pass
+
+ @property
+ def x(self):
+ """
+ The x coordinate of the point.
+ """
+ return self[0]
+
+ @property
+ def y(self):
+ """
+ The y coordinate of the point.
+ """
+ return self[1]
+
+ def __eq__(self, other):
+ return (self.x, self.y) == other
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __hash__(self):
+ return self.x + 7 * self.y
+
+ def __add__(self, delta):
+ dx, dy = delta
+ return Point(self.x + dx, self.y + dy)
+
+ def __radd__(self, delta):
+ return self.__add__(delta)
+
+ def __sub__(self, delta):
+ dx, dy = delta
+ return Point(self.x - dx, self.y - dy)
+
+ def __rsub__(self, delta):
+ x, y = delta
+ return Point(x - self.x, y - self.y)
+
def switch_to(window):
- """
- :param window: The title (string) of a browser window or a \
+ """
+ :param window: The title (string) of a browser window or a \
:py:class:`Window` object
- Switches to the given browser window. For example::
+ Switches to the given browser window. For example::
- switch_to("Google")
+ switch_to("Google")
- This searches for a browser window whose title contains "Google", and
- activates it.
+ This searches for a browser window whose title contains "Google", and
+ activates it.
- If there are multiple windows with the same title, then you can use
- :py:func:`find_all` to find all open windows, pick out the one you want and
- pass that to ``switch_to``. For example, the following snippet switches to
- the first window in the list of open windows::
+ If there are multiple windows with the same title, then you can use
+ :py:func:`find_all` to find all open windows, pick out the one you want and
+ pass that to ``switch_to``. For example, the following snippet switches to
+ the first window in the list of open windows::
+
+ switch_to(find_all(Window())[0])
+ """
+ _get_api_impl().switch_to_impl(window)
- switch_to(find_all(Window())[0])
- """
- _get_api_impl().switch_to_impl(window)
def kill_browser():
- """
- Closes the current browser with all associated windows and potentially open
- dialogs. Dialogs opened as a response to the browser closing (eg. "Are you
- sure you want to leave this page?") are also ignored and closed.
+ """
+ Closes the current browser with all associated windows and potentially open
+ dialogs. Dialogs opened as a response to the browser closing (eg. "Are you
+ sure you want to leave this page?") are also ignored and closed.
+
+ This function is most commonly used to close the browser at the end of an
+ automation run::
- This function is most commonly used to close the browser at the end of an
- automation run::
+ start_chrome()
+ ...
+ # Close Chrome:
+ kill_browser()
+ """
+ _get_api_impl().kill_browser_impl()
- start_chrome()
- ...
- # Close Chrome:
- kill_browser()
- """
- _get_api_impl().kill_browser_impl()
def highlight(element):
- """
- :param element: The element to highlight.
+ """
+ :param element: The element to highlight.
- Highlights the given element on the webpage by drawing a red rectangle
- around it. This is useful for debugging purposes. For example::
+ Highlights the given element on the webpage by drawing a red rectangle
+ around it. This is useful for debugging purposes. For example::
+
+ highlight("Helium")
+ highlight(Button("Sign in"))
+ """
+ _get_api_impl().highlight_impl(element)
- highlight("Helium")
- highlight(Button("Sign in"))
- """
- _get_api_impl().highlight_impl(element)
def _get_api_impl():
- global _API_IMPL
- if _API_IMPL is None:
- _API_IMPL = APIImpl()
- return _API_IMPL
+ global _API_IMPL
+ if _API_IMPL is None:
+ _API_IMPL = APIImpl()
+ return _API_IMPL
+
-_API_IMPL = None
\ No newline at end of file
+_API_IMPL = None
diff --git a/helium/_impl/__init__.py b/helium/_impl/__init__.py
index fd3c4f5..d8dfe3c 100644
--- a/helium/_impl/__init__.py
+++ b/helium/_impl/__init__.py
@@ -1,7 +1,7 @@
from copy import copy
from helium._impl.match_type import PREFIX_IGNORE_CASE
from helium._impl.selenium_wrappers import WebElementWrapper, \
- WebDriverWrapper, FrameIterator, FramesChangedWhileIterating
+ WebDriverWrapper, FrameIterator, FramesChangedWhileIterating
from helium._impl.util.dictionary import inverse
from helium._impl.util.os_ import make_executable
from helium._impl.util.system import is_windows, get_canonical_os_name
@@ -10,9 +10,9 @@
from os import access, X_OK
from os.path import join, dirname
from selenium.common.exceptions import UnexpectedAlertPresentException, \
- ElementNotVisibleException, MoveTargetOutOfBoundsException, \
- WebDriverException, StaleElementReferenceException, \
- NoAlertPresentException, NoSuchWindowException
+ ElementNotVisibleException, MoveTargetOutOfBoundsException, \
+ WebDriverException, StaleElementReferenceException, \
+ NoAlertPresentException, NoSuchWindowException
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.ui import Select
@@ -22,1245 +22,1449 @@
import atexit
import re
+
def might_spawn_window(f):
- def f_decorated(self, *args, **kwargs):
- driver = self.require_driver()
- if driver.is_ie() and AlertImpl(driver).exists():
- # Accessing .window_handles in IE when an alert is present raises an
- # UnexpectedAlertPresentException. When DesiredCapability
- # 'unexpectedAlertBehaviour' is not 'ignore' (the default is
- # 'dismiss'), this leads to the alert being closed. Since we don't
- # want to unintentionally close alert dialogs, we therefore do not
- # access .window_handles in IE when an alert is present.
- return f(self, *args, **kwargs)
- window_handles_before = driver.window_handles[:]
- result = f(self, *args, **kwargs)
- # As above, don't access .window_handles in IE if an alert is present:
- if not (driver.is_ie() and AlertImpl(driver).exists()):
- if driver.is_firefox():
- # Unlike Chrome, Firefox does not wait for new windows to open.
- # Give it a little time to do so:
- sleep(.2)
- new_window_handles = [
- h for h in driver.window_handles
- if h not in window_handles_before
- ]
- if new_window_handles:
- driver.switch_to.window(new_window_handles[0])
- return result
- return f_decorated
+ def f_decorated(self, *args, **kwargs):
+ driver = self.require_driver()
+ if driver.is_ie() and AlertImpl(driver).exists():
+ # Accessing .window_handles in IE when an alert is present raises an
+ # UnexpectedAlertPresentException. When DesiredCapability
+ # 'unexpectedAlertBehaviour' is not 'ignore' (the default is
+ # 'dismiss'), this leads to the alert being closed. Since we don't
+ # want to unintentionally close alert dialogs, we therefore do not
+ # access .window_handles in IE when an alert is present.
+ return f(self, *args, **kwargs)
+ window_handles_before = driver.window_handles[:]
+ result = f(self, *args, **kwargs)
+ # As above, don't access .window_handles in IE if an alert is present:
+ if not (driver.is_ie() and AlertImpl(driver).exists()):
+ if driver.is_firefox():
+ # Unlike Chrome, Firefox does not wait for new windows to open.
+ # Give it a little time to do so:
+ sleep(.2)
+ new_window_handles = [
+ h for h in driver.window_handles
+ if h not in window_handles_before
+ ]
+ if new_window_handles:
+ driver.switch_to.window(new_window_handles[0])
+ return result
+
+ return f_decorated
+
def handle_unexpected_alert(f):
- def f_decorated(*args, **kwargs):
- try:
- return f(*args, **kwargs)
- except UnexpectedAlertPresentException:
- raise UnexpectedAlertPresentException(
- "This command is not supported when an alert is present. To "
- "accept the alert (this usually corresponds to clicking 'OK') "
- "use `Alert().accept()`. To dismiss the alert (ie. 'cancel' "
- "it), use `Alert().dismiss()`. If the alert contains a text "
- "field, you can use write(...) to set its value. "
- "Eg.: `write('hi there!')`."
- )
- return f_decorated
+ def f_decorated(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except UnexpectedAlertPresentException:
+ raise UnexpectedAlertPresentException(
+ "This command is not supported when an alert is present. To "
+ "accept the alert (this usually corresponds to clicking 'OK') "
+ "use `Alert().accept()`. To dismiss the alert (ie. 'cancel' "
+ "it), use `Alert().dismiss()`. If the alert contains a text "
+ "field, you can use write(...) to set its value. "
+ "Eg.: `write('hi there!')`."
+ )
+
+ return f_decorated
+
class APIImpl:
- DRIVER_REQUIRED_MESSAGE = \
- "This operation requires a browser window. Please call one of " \
- "the following functions first:\n" \
- " * start_chrome()\n" \
- " * start_firefox()\n" \
- " * set_driver(...)"
- def __init__(self):
- self.driver = None
- def start_firefox_impl(self, url=None, headless=False, options=None):
- firefox_driver = self._start_firefox_driver(headless, options)
- return self._start(firefox_driver, url)
- def _start_firefox_driver(self, headless, options):
- firefox_options = FirefoxOptions() if options is None else options
- if headless:
- firefox_options.headless = True
- kwargs = {
- 'options': firefox_options,
- 'service_log_path': 'nul' if is_windows() else '/dev/null'
- }
- try:
- result = Firefox(**kwargs)
- except WebDriverException:
- # This usually happens when geckodriver is not on the PATH.
- driver_path = self._use_included_web_driver('geckodriver')
- result = Firefox(executable_path=driver_path, **kwargs)
- atexit.register(self._kill_service, result.service)
- return result
- def start_chrome_impl(
- self, url=None, headless=False, maximize=False, options=None,
- capabilities=None
- ):
- chrome_driver = \
- self._start_chrome_driver(headless, maximize, options, capabilities)
- return self._start(chrome_driver, url)
- def _start_chrome_driver(self, headless, maximize, options, capabilities):
- chrome_options = self._get_chrome_options(headless, maximize, options)
- try:
- result = Chrome(options=chrome_options, desired_capabilities=capabilities)
- except WebDriverException:
- # This usually happens when chromedriver is not on the PATH.
- driver_path = self._use_included_web_driver('chromedriver')
- result = Chrome(
- options=chrome_options, desired_capabilities=capabilities,
- executable_path=driver_path
- )
- atexit.register(self._kill_service, result.service)
- return result
- def _get_chrome_options(self, headless, maximize, options):
- result = ChromeOptions() if options is None else options
- # Prevent Chrome's debug logs from appearing in our console window:
- result.add_experimental_option('excludeSwitches', ['enable-logging'])
- if headless:
- result.add_argument('--headless')
- elif maximize:
- result.add_argument('--start-maximized')
- return result
- def _use_included_web_driver(self, driver_name):
- if is_windows():
- driver_name += '.exe'
- driver_path = join(
- dirname(__file__), 'webdrivers', get_canonical_os_name(),
- driver_name
- )
- if not access(driver_path, X_OK):
- try:
- make_executable(driver_path)
- except Exception:
- raise RuntimeError(
- "The driver located at %s is not executable." % driver_path
- ) from None
- return driver_path
- def _kill_service(self, service):
- old = service.send_remote_shutdown_command
- service.send_remote_shutdown_command = lambda: None
- try:
- service.stop()
- finally:
- service.send_remote_shutdown_command = old
- def _start(self, browser, url=None):
- self.set_driver_impl(browser)
- if url is not None:
- self.go_to_impl(url)
- return self.get_driver_impl()
- @might_spawn_window
- @handle_unexpected_alert
- def go_to_impl(self, url):
- if '://' not in url:
- url = 'http://' + url
- self.require_driver().get(url)
- def set_driver_impl(self, driver):
- self.driver = WebDriverWrapper(driver)
- def get_driver_impl(self):
- if self.driver is not None:
- return self.driver.unwrap()
- @might_spawn_window
- @handle_unexpected_alert
- def write_impl(self, text, into=None):
- if into is not None:
- from helium import GUIElement
- if isinstance(into, GUIElement):
- into = into._impl
- self._handle_alerts(
- self._write_no_alert, self._write_with_alert, text, into=into
- )
- def _write_no_alert(self, text, into=None):
- if into:
- if isinstance(into, str):
- into = TextFieldImpl(self.require_driver(), into)
- def _write(elt):
- if hasattr(elt, 'clear') and callable(elt.clear):
- elt.clear()
- elt.send_keys(text)
- self._manipulate(into, _write)
- else:
- self.require_driver().switch_to.active_element.send_keys(text)
- def _write_with_alert(self, text, into=None):
- if into is None:
- into = AlertImpl(self.require_driver())
- if not isinstance(into, AlertImpl):
- raise UnexpectedAlertPresentException(
- "into=%r is not allowed when an alert is present." % into
- )
- into._write(text)
- def _handle_alerts(self, no_alert, with_alert, *args, **kwargs):
- driver = self.require_driver()
- if not AlertImpl(driver).exists():
- return no_alert(*args, **kwargs)
- return with_alert(*args, **kwargs)
- @might_spawn_window
- @handle_unexpected_alert
- def press_impl(self, key):
- self.require_driver().switch_to.active_element.send_keys(key)
- def click_impl(self, element):
- self._perform_mouse_action(element, self._click)
- def doubleclick_impl(self, element):
- self._perform_mouse_action(element, self._doubleclick)
- def hover_impl(self, element):
- self._perform_mouse_action(element, self._hover)
- def rightclick_impl(self, element):
- self._perform_mouse_action(element, self._rightclick)
- def press_mouse_on_impl(self, element):
- self._perform_mouse_action(element, self._press_mouse_on)
- def release_mouse_over_impl(self, element):
- self._perform_mouse_action(element, self._release_mouse_over)
- def _click(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).click().perform()
- def _doubleclick(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).double_click().perform()
- def _hover(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).perform()
- def _rightclick(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).context_click().perform()
- def _press_mouse_on(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).click_and_hold().perform()
- def _release_mouse_over(self, selenium_elt, offset):
- self._move_to_element(selenium_elt, offset).release().perform()
- def _move_to_element(self, element, offset):
- result = self.require_driver().action()
- if offset is not None:
- result.move_to_element_with_offset(element, *offset)
- else:
- result.move_to_element(element)
- return result
- def drag_impl(self, element, to):
- with DragHelper(self) as drag_helper:
- self._perform_mouse_action(element, drag_helper.start_dragging)
- self._perform_mouse_action(to, drag_helper.drop_on_target)
- @might_spawn_window
- @handle_unexpected_alert
- def _perform_mouse_action(self, element, action):
- element, offset = self._unwrap_clickable_element(element)
- self._manipulate(element, lambda wew: action(wew.unwrap(), offset))
- def _unwrap_clickable_element(self, elt):
- from helium import HTMLElement, Point
- offset = None
- if isinstance(elt, str):
- elt = ClickableText(self.require_driver(), elt)
- elif isinstance(elt, HTMLElement):
- elt = elt._impl
- elif isinstance(elt, Point):
- elt, offset = self._point_to_element_and_offset(elt)
- return elt, offset
- def _point_to_element_and_offset(self, point):
- driver = self.require_driver()
- element = WebElementWrapper(driver.execute_script(
- 'return document.elementFromPoint(%r, %r);' % (point.x, point.y)
- ))
- offset = point - (element.location.left, element.location.top)
- if offset == (0, 0) and driver.is_firefox():
- # In some CSS settings (eg. test_point.html), the (0, 0) point of
- # buttons in Firefox is not clickable! The reason for this is that
- # Firefox styles buttons to not be perfect squares, but have an
- # indent in the corners. This workaround makes `click(btn.top_left)`
- # work even when this happens:
- offset = (1, 1)
- return element, offset
- @handle_unexpected_alert
- def find_all_impl(self, predicate):
- return [
- predicate.with_impl(bound_gui_elt_impl)
- for bound_gui_elt_impl in predicate._impl.find_all()
- ]
- def scroll_down_impl(self, num_pixels):
- self._scroll_by(0, num_pixels)
- def scroll_up_impl(self, num_pixels):
- self._scroll_by(0, -num_pixels)
- def scroll_right_impl(self, num_pixels):
- self._scroll_by(num_pixels, 0)
- def scroll_left_impl(self, num_pixels):
- self._scroll_by(-num_pixels, 0)
- @handle_unexpected_alert
- def _scroll_by(self, dx_pixels, dy_pixels):
- self.require_driver().execute_script(
- 'window.scrollBy(arguments[0], arguments[1]);', dx_pixels, dy_pixels
- )
- @might_spawn_window
- @handle_unexpected_alert
- def select_impl(self, combo_box, value):
- from helium import ComboBox
- if isinstance(combo_box, str):
- combo_box = ComboBoxImpl(self.require_driver(), combo_box)
- elif isinstance(combo_box, ComboBox):
- combo_box = combo_box._impl
- def _select(web_element):
- if isinstance(web_element, WebElementWrapper):
- web_element = web_element.unwrap()
- Select(web_element).select_by_visible_text(value)
- self._manipulate(combo_box, _select)
- def _manipulate(self, gui_or_web_elt, action):
- driver = self.require_driver()
- if hasattr(gui_or_web_elt, 'perform') \
- and callable(gui_or_web_elt.perform):
- driver.last_manipulated_element = gui_or_web_elt.perform(action)
- else:
- if isinstance(gui_or_web_elt, WebElement):
- gui_or_web_elt = WebElementWrapper(gui_or_web_elt)
- action(gui_or_web_elt)
- driver.last_manipulated_element = gui_or_web_elt
- @handle_unexpected_alert
- def drag_file_impl(self, file_path, to):
- to, _ = self._unwrap_clickable_element(to)
- drag_and_drop = DragAndDropFile(self.require_driver(), file_path)
- drag_and_drop.begin()
- try:
- # Some web apps (Gmail in particular) only register for the 'drop'
- # event when user has dragged the file over the document. We
- # therefore simulate this dragging over the document first:
- drag_and_drop.drag_over_document()
- self._manipulate(to, lambda elt: drag_and_drop.drop_on(elt))
- finally:
- drag_and_drop.end()
- @might_spawn_window
- @handle_unexpected_alert
- def attach_file_impl(self, file_path, to=None):
- from helium import Point
- driver = self.require_driver()
- if to is None:
- to = FileInput(driver)
- elif isinstance(to, str):
- to = FileInput(driver, to)
- elif isinstance(to, Point):
- to, _ = self._point_to_element_and_offset(to)
- self._manipulate(to, lambda elt: elt.send_keys(file_path))
- def refresh_impl(self):
- self._handle_alerts(
- self._refresh_no_alert, self._refresh_with_alert
- )
- def _refresh_no_alert(self):
- self.require_driver().refresh()
- def _refresh_with_alert(self):
- AlertImpl(self.require_driver()).accept()
- self._refresh_no_alert()
- def wait_until_impl(self, condition_fn, timeout_secs=10, interval_secs=0.5):
- if ismethod(condition_fn):
- is_bound = condition_fn.__self__ is not None
- args_spec = getfullargspec(condition_fn).args
- unfilled_args = len(args_spec) - (1 if is_bound else 0)
- else:
- if not isfunction(condition_fn):
- condition_fn = condition_fn.__call__
- args_spec = getfullargspec(condition_fn).args
- unfilled_args = len(args_spec)
- condition = \
- condition_fn if unfilled_args else lambda driver: condition_fn()
- wait = WebDriverWait(
- self.require_driver().unwrap(), timeout_secs,
- poll_frequency=interval_secs
- )
- wait.until(condition)
- @handle_unexpected_alert
- def switch_to_impl(self, window):
- driver = self.require_driver()
- from helium import Window
- if isinstance(window, str):
- window = WindowImpl(driver, window)
- elif isinstance(window, Window):
- window = window._impl
- driver.switch_to.window(window.handle)
- def kill_browser_impl(self):
- self.require_driver().quit()
- self.driver = None
- @handle_unexpected_alert
- def highlight_impl(self, element):
- driver = self.require_driver()
- from helium import HTMLElement, Text
- if isinstance(element, str):
- element = Text(element)
- if isinstance(element, HTMLElement):
- element = element._impl
- try:
- element = element.first_occurrence
- except AttributeError:
- pass
- previous_style = element.get_attribute("style")
- if isinstance(element, WebElementWrapper):
- element = element.unwrap()
- driver.execute_script(
- "arguments[0].setAttribute("
- "'style', 'border: 2px solid red; font-weight: bold;'"
- ");", element
- )
- driver.execute_script(
- "var target = arguments[0];"
- "var previousStyle = arguments[1];"
- "setTimeout("
- "function() {"
- "target.setAttribute('style', previousStyle);"
- "}, 2000"
- ");", element, previous_style
- )
- def require_driver(self):
- if not self.driver:
- raise RuntimeError(self.DRIVER_REQUIRED_MESSAGE)
- return self.driver
+ DRIVER_REQUIRED_MESSAGE = \
+ "This operation requires a browser window. Please call one of " \
+ "the following functions first:\n" \
+ " * start_chrome()\n" \
+ " * start_firefox()\n" \
+ " * set_driver(...)"
+
+ def __init__(self):
+ self.driver = None
+
+ def start_firefox_impl(self, url=None, headless=False, options=None):
+ firefox_driver = self._start_firefox_driver(headless, options)
+ return self._start(firefox_driver, url)
+
+ def _start_firefox_driver(self, headless, options):
+ firefox_options = FirefoxOptions() if options is None else options
+ if headless:
+ firefox_options.headless = True
+ kwargs = {
+ 'options': firefox_options,
+ 'service_log_path': 'nul' if is_windows() else '/dev/null'
+ }
+ try:
+ result = Firefox(**kwargs)
+ except WebDriverException:
+ # This usually happens when geckodriver is not on the PATH.
+ driver_path = self._use_included_web_driver('geckodriver')
+ result = Firefox(executable_path=driver_path, **kwargs)
+ atexit.register(self._kill_service, result.service)
+ return result
+
+ def start_chrome_impl(
+ self, url=None, headless=False, maximize=False, options=None,
+ capabilities=None
+ ):
+ chrome_driver = \
+ self._start_chrome_driver(headless, maximize, options, capabilities)
+ return self._start(chrome_driver, url)
+
+ def _start_chrome_driver(self, headless, maximize, options, capabilities):
+ chrome_options = self._get_chrome_options(headless, maximize, options)
+ try:
+ result = Chrome(options=chrome_options, desired_capabilities=capabilities)
+ except WebDriverException:
+ # This usually happens when chromedriver is not on the PATH.
+ driver_path = self._use_included_web_driver('chromedriver')
+ result = Chrome(
+ options=chrome_options, desired_capabilities=capabilities,
+ executable_path=driver_path
+ )
+ atexit.register(self._kill_service, result.service)
+ return result
+
+ def _get_chrome_options(self, headless, maximize, options):
+ result = ChromeOptions() if options is None else options
+ # Prevent Chrome's debug logs from appearing in our console window:
+ result.add_experimental_option('excludeSwitches', ['enable-logging'])
+ if headless:
+ result.add_argument('--headless')
+ elif maximize:
+ result.add_argument('--start-maximized')
+ return result
+
+ def _use_included_web_driver(self, driver_name):
+ if is_windows():
+ driver_name += '.exe'
+ driver_path = join(
+ dirname(__file__), 'webdrivers', get_canonical_os_name(),
+ driver_name
+ )
+ if not access(driver_path, X_OK):
+ try:
+ make_executable(driver_path)
+ except Exception:
+ raise RuntimeError(
+ "The driver located at %s is not executable." % driver_path
+ ) from None
+ return driver_path
+
+ def _kill_service(self, service):
+ old = service.send_remote_shutdown_command
+ service.send_remote_shutdown_command = lambda: None
+ try:
+ service.stop()
+ finally:
+ service.send_remote_shutdown_command = old
+
+ def _start(self, browser, url=None):
+ self.set_driver_impl(browser)
+ if url is not None:
+ self.go_to_impl(url)
+ return self.get_driver_impl()
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def go_to_impl(self, url):
+ if '://' not in url:
+ url = 'http://' + url
+ self.require_driver().get(url)
+
+ def set_driver_impl(self, driver):
+ self.driver = WebDriverWrapper(driver)
+
+ def get_driver_impl(self):
+ if self.driver is not None:
+ return self.driver.unwrap()
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def write_impl(self, text, into=None):
+ if into is not None:
+ from helium import GUIElement
+ if isinstance(into, GUIElement):
+ into = into._impl
+ self._handle_alerts(
+ self._write_no_alert, self._write_with_alert, text, into=into
+ )
+
+ def _write_no_alert(self, text, into=None):
+ if into:
+ if isinstance(into, str):
+ into = TextFieldImpl(self.require_driver(), into)
+
+ def _write(elt):
+ if hasattr(elt, 'clear') and callable(elt.clear):
+ elt.clear()
+ elt.send_keys(text)
+
+ self._manipulate(into, _write)
+ else:
+ self.require_driver().switch_to.active_element.send_keys(text)
+
+ def _write_with_alert(self, text, into=None):
+ if into is None:
+ into = AlertImpl(self.require_driver())
+ if not isinstance(into, AlertImpl):
+ raise UnexpectedAlertPresentException(
+ "into=%r is not allowed when an alert is present." % into
+ )
+ into._write(text)
+
+ def _handle_alerts(self, no_alert, with_alert, *args, **kwargs):
+ driver = self.require_driver()
+ if not AlertImpl(driver).exists():
+ return no_alert(*args, **kwargs)
+ return with_alert(*args, **kwargs)
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def press_impl(self, key):
+ self.require_driver().switch_to.active_element.send_keys(key)
+
+ def click_impl(self, element):
+ self._perform_mouse_action(element, self._click)
+
+ def doubleclick_impl(self, element):
+ self._perform_mouse_action(element, self._doubleclick)
+
+ def hover_impl(self, element):
+ self._perform_mouse_action(element, self._hover)
+
+ def rightclick_impl(self, element):
+ self._perform_mouse_action(element, self._rightclick)
+
+ def press_mouse_on_impl(self, element):
+ self._perform_mouse_action(element, self._press_mouse_on)
+
+ def release_mouse_over_impl(self, element):
+ self._perform_mouse_action(element, self._release_mouse_over)
+
+ def _click(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).click().perform()
+
+ def _doubleclick(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).double_click().perform()
+
+ def _hover(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).perform()
+
+ def _rightclick(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).context_click().perform()
+
+ def _press_mouse_on(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).click_and_hold().perform()
+
+ def _release_mouse_over(self, selenium_elt, offset):
+ self._move_to_element(selenium_elt, offset).release().perform()
+
+ def _move_to_element(self, element, offset):
+ result = self.require_driver().action()
+ if offset is not None:
+ result.move_to_element_with_offset(element, *offset)
+ else:
+ result.move_to_element(element)
+ return result
+
+ def drag_impl(self, element, to):
+ with DragHelper(self) as drag_helper:
+ self._perform_mouse_action(element, drag_helper.start_dragging)
+ self._perform_mouse_action(to, drag_helper.drop_on_target)
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def _perform_mouse_action(self, element, action):
+ element, offset = self._unwrap_clickable_element(element)
+ self._manipulate(element, lambda wew: action(wew.unwrap(), offset))
+
+ def _unwrap_clickable_element(self, elt):
+ from helium import HTMLElement, Point
+ offset = None
+ if isinstance(elt, str):
+ elt = ClickableText(self.require_driver(), elt)
+ elif isinstance(elt, HTMLElement):
+ elt = elt._impl
+ elif isinstance(elt, Point):
+ elt, offset = self._point_to_element_and_offset(elt)
+ return elt, offset
+
+ def _point_to_element_and_offset(self, point):
+ driver = self.require_driver()
+ element = WebElementWrapper(driver.execute_script(
+ 'return document.elementFromPoint(%r, %r);' % (point.x, point.y)
+ ))
+ offset = point - (element.location.left, element.location.top)
+ if offset == (0, 0) and driver.is_firefox():
+ # In some CSS settings (eg. test_point.html), the (0, 0) point of
+ # buttons in Firefox is not clickable! The reason for this is that
+ # Firefox styles buttons to not be perfect squares, but have an
+ # indent in the corners. This workaround makes `click(btn.top_left)`
+ # work even when this happens:
+ offset = (1, 1)
+ return element, offset
+
+ @handle_unexpected_alert
+ def find_all_impl(self, predicate):
+ return [
+ predicate.with_impl(bound_gui_elt_impl)
+ for bound_gui_elt_impl in predicate._impl.find_all()
+ ]
+
+ def scroll_down_impl(self, num_pixels):
+ self._scroll_by(0, num_pixels)
+
+ def scroll_up_impl(self, num_pixels):
+ self._scroll_by(0, -num_pixels)
+
+ def scroll_right_impl(self, num_pixels):
+ self._scroll_by(num_pixels, 0)
+
+ def scroll_left_impl(self, num_pixels):
+ self._scroll_by(-num_pixels, 0)
+
+ @handle_unexpected_alert
+ def _scroll_by(self, dx_pixels, dy_pixels):
+ self.require_driver().execute_script(
+ 'window.scrollBy(arguments[0], arguments[1]);', dx_pixels, dy_pixels
+ )
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def select_impl(self, combo_box, value):
+ from helium import ComboBox
+ if isinstance(combo_box, str):
+ combo_box = ComboBoxImpl(self.require_driver(), combo_box)
+ elif isinstance(combo_box, ComboBox):
+ combo_box = combo_box._impl
+
+ def _select(web_element):
+ if isinstance(web_element, WebElementWrapper):
+ web_element = web_element.unwrap()
+ Select(web_element).select_by_visible_text(value)
+
+ self._manipulate(combo_box, _select)
+
+ def _manipulate(self, gui_or_web_elt, action):
+ driver = self.require_driver()
+ if hasattr(gui_or_web_elt, 'perform') \
+ and callable(gui_or_web_elt.perform):
+ driver.last_manipulated_element = gui_or_web_elt.perform(action)
+ else:
+ if isinstance(gui_or_web_elt, WebElement):
+ gui_or_web_elt = WebElementWrapper(gui_or_web_elt)
+ action(gui_or_web_elt)
+ driver.last_manipulated_element = gui_or_web_elt
+
+ @handle_unexpected_alert
+ def drag_file_impl(self, file_path, to):
+ to, _ = self._unwrap_clickable_element(to)
+ drag_and_drop = DragAndDropFile(self.require_driver(), file_path)
+ drag_and_drop.begin()
+ try:
+ # Some web apps (Gmail in particular) only register for the 'drop'
+ # event when user has dragged the file over the document. We
+ # therefore simulate this dragging over the document first:
+ drag_and_drop.drag_over_document()
+ self._manipulate(to, lambda elt: drag_and_drop.drop_on(elt))
+ finally:
+ drag_and_drop.end()
+
+ @might_spawn_window
+ @handle_unexpected_alert
+ def attach_file_impl(self, file_path, to=None):
+ from helium import Point
+ driver = self.require_driver()
+ if to is None:
+ to = FileInput(driver)
+ elif isinstance(to, str):
+ to = FileInput(driver, to)
+ elif isinstance(to, Point):
+ to, _ = self._point_to_element_and_offset(to)
+ self._manipulate(to, lambda elt: elt.send_keys(file_path))
+
+ def refresh_impl(self):
+ self._handle_alerts(
+ self._refresh_no_alert, self._refresh_with_alert
+ )
+
+ def _refresh_no_alert(self):
+ self.require_driver().refresh()
+
+ def _refresh_with_alert(self):
+ AlertImpl(self.require_driver()).accept()
+ self._refresh_no_alert()
+
+ def wait_until_impl(self, condition_fn, timeout_secs=10, interval_secs=0.5):
+ if ismethod(condition_fn):
+ is_bound = condition_fn.__self__ is not None
+ args_spec = getfullargspec(condition_fn).args
+ unfilled_args = len(args_spec) - (1 if is_bound else 0)
+ else:
+ if not isfunction(condition_fn):
+ condition_fn = condition_fn.__call__
+ args_spec = getfullargspec(condition_fn).args
+ unfilled_args = len(args_spec)
+ condition = \
+ condition_fn if unfilled_args else lambda driver: condition_fn()
+ wait = WebDriverWait(
+ self.require_driver().unwrap(), timeout_secs,
+ poll_frequency=interval_secs
+ )
+ wait.until(condition)
+
+ @handle_unexpected_alert
+ def switch_to_impl(self, window):
+ driver = self.require_driver()
+ from helium import Window
+ if isinstance(window, str):
+ window = WindowImpl(driver, window)
+ elif isinstance(window, Window):
+ window = window._impl
+ driver.switch_to.window(window.handle)
+
+ def kill_browser_impl(self):
+ self.require_driver().quit()
+ self.driver = None
+
+ @handle_unexpected_alert
+ def highlight_impl(self, element):
+ driver = self.require_driver()
+ from helium import HTMLElement, Text
+ if isinstance(element, str):
+ element = Text(element)
+ if isinstance(element, HTMLElement):
+ element = element._impl
+ try:
+ element = element.first_occurrence
+ except AttributeError:
+ pass
+ previous_style = element.get_attribute("style")
+ if isinstance(element, WebElementWrapper):
+ element = element.unwrap()
+ driver.execute_script(
+ "arguments[0].setAttribute("
+ "'style', 'border: 2px solid red; font-weight: bold;'"
+ ");", element
+ )
+ driver.execute_script(
+ "var target = arguments[0];"
+ "var previousStyle = arguments[1];"
+ "setTimeout("
+ "function() {"
+ "target.setAttribute('style', previousStyle);"
+ "}, 2000"
+ ");", element, previous_style
+ )
+
+ def require_driver(self):
+ if not self.driver:
+ raise RuntimeError(self.DRIVER_REQUIRED_MESSAGE)
+ return self.driver
+
class DragHelper:
- def __init__(self, api_impl):
- self.api_impl = api_impl
- self.is_html_5_drag = None
- def __enter__(self):
- self._execute_script(
- "window.helium = {};"
- "window.helium.dragHelper = {"
- " createEvent: function(type) {"
- " var event = document.createEvent('CustomEvent');"
- " event.initCustomEvent(type, true, true, null);"
- " event.dataTransfer = {"
- " data: {},"
- " setData: function(type, val) {"
- " this.data[type] = val;"
- " },"
- " getData: function(type) {"
- " return this.data[type];"
- " }"
- " };"
- " return event;"
- " }"
- "};"
- )
- return self
- def start_dragging(self, element, offset):
- if self._attempt_html_5_drag(element):
- self.is_html_5_drag = True
- else:
- self.api_impl._press_mouse_on(element, offset)
- def drop_on_target(self, target, offset):
- if self.is_html_5_drag:
- self._complete_html_5_drag(target)
- else:
- self.api_impl._release_mouse_over(target, offset)
- def _attempt_html_5_drag(self, element_to_drag):
- return self._execute_script(
- "var source = arguments[0];"
- "function getDraggableParent(element) {"
- " var previousParent = null;"
- " while (element != null && element != previousParent) {"
- " previousParent = element;"
- " if ('draggable' in element) {"
- " var draggable = element.draggable;"
- " if (draggable === true)"
- " return element;"
- " if (typeof draggable == 'string' "
- " || draggable instanceof String)"
- " if (draggable.toLowerCase() == 'true')"
- " return element;"
- " }"
- " element = element.parentNode;"
- " }"
- " return null;"
- "}"
- "var draggableParent = getDraggableParent(source);"
- "if (draggableParent == null)"
- " return false;"
- "window.helium.dragHelper.draggedElement = draggableParent;"
- "var dragStart = window.helium.dragHelper.createEvent('dragstart');"
- "source.dispatchEvent(dragStart);"
- "window.helium.dragHelper.dataTransfer = dragStart.dataTransfer;"
- "return true;",
- element_to_drag
- )
- def _complete_html_5_drag(self, on):
- self._execute_script(
- "var target = arguments[0];"
- "var drop = window.helium.dragHelper.createEvent('drop');"
- "drop.dataTransfer = window.helium.dragHelper.dataTransfer;"
- "target.dispatchEvent(drop);"
- "var dragEnd = window.helium.dragHelper.createEvent('dragend');"
- "dragEnd.dataTransfer = window.helium.dragHelper.dataTransfer;"
- "window.helium.dragHelper.draggedElement.dispatchEvent(dragEnd);",
- on
- )
- def __exit__(self, *_):
- self._execute_script("delete window.helium;")
- def _execute_script(self, script, *args):
- return self.api_impl.require_driver().execute_script(script, *args)
+ def __init__(self, api_impl):
+ self.api_impl = api_impl
+ self.is_html_5_drag = None
+
+ def __enter__(self):
+ self._execute_script(
+ "window.helium = {};"
+ "window.helium.dragHelper = {"
+ " createEvent: function(type) {"
+ " var event = document.createEvent('CustomEvent');"
+ " event.initCustomEvent(type, true, true, null);"
+ " event.dataTransfer = {"
+ " data: {},"
+ " setData: function(type, val) {"
+ " this.data[type] = val;"
+ " },"
+ " getData: function(type) {"
+ " return this.data[type];"
+ " }"
+ " };"
+ " return event;"
+ " }"
+ "};"
+ )
+ return self
+
+ def start_dragging(self, element, offset):
+ if self._attempt_html_5_drag(element):
+ self.is_html_5_drag = True
+ else:
+ self.api_impl._press_mouse_on(element, offset)
+
+ def drop_on_target(self, target, offset):
+ if self.is_html_5_drag:
+ self._complete_html_5_drag(target)
+ else:
+ self.api_impl._release_mouse_over(target, offset)
+
+ def _attempt_html_5_drag(self, element_to_drag):
+ return self._execute_script(
+ "var source = arguments[0];"
+ "function getDraggableParent(element) {"
+ " var previousParent = null;"
+ " while (element != null && element != previousParent) {"
+ " previousParent = element;"
+ " if ('draggable' in element) {"
+ " var draggable = element.draggable;"
+ " if (draggable === true)"
+ " return element;"
+ " if (typeof draggable == 'string' "
+ " || draggable instanceof String)"
+ " if (draggable.toLowerCase() == 'true')"
+ " return element;"
+ " }"
+ " element = element.parentNode;"
+ " }"
+ " return null;"
+ "}"
+ "var draggableParent = getDraggableParent(source);"
+ "if (draggableParent == null)"
+ " return false;"
+ "window.helium.dragHelper.draggedElement = draggableParent;"
+ "var dragStart = window.helium.dragHelper.createEvent('dragstart');"
+ "source.dispatchEvent(dragStart);"
+ "window.helium.dragHelper.dataTransfer = dragStart.dataTransfer;"
+ "return true;",
+ element_to_drag
+ )
+
+ def _complete_html_5_drag(self, on):
+ self._execute_script(
+ "var target = arguments[0];"
+ "var drop = window.helium.dragHelper.createEvent('drop');"
+ "drop.dataTransfer = window.helium.dragHelper.dataTransfer;"
+ "target.dispatchEvent(drop);"
+ "var dragEnd = window.helium.dragHelper.createEvent('dragend');"
+ "dragEnd.dataTransfer = window.helium.dragHelper.dataTransfer;"
+ "window.helium.dragHelper.draggedElement.dispatchEvent(dragEnd);",
+ on
+ )
+
+ def __exit__(self, *_):
+ self._execute_script("delete window.helium;")
+
+ def _execute_script(self, script, *args):
+ return self.api_impl.require_driver().execute_script(script, *args)
+
class DragAndDropFile:
- def __init__(self, driver, file_path):
- self.driver = driver
- self.file_path = file_path
- self.file_input_element = None
- self.dragover_event = None
- def begin(self):
- self._create_file_input_element()
- try:
- self.file_input_element.send_keys(self.file_path)
- except:
- self.end()
- raise
- def _create_file_input_element(self):
- # The input needs to be visible to Selenium to allow sending keys to it
- # in Firefox and IE.
- # According to http://stackoverflow.com/questions/6101461/
- # Selenium criteria whether an element is visible or not are the
- # following:
- # - visibility != hidden
- # - display != none (is also checked against every parent element)
- # - opacity != 0
- # - height and width are both > 0
- # - for an input, the attribute type != hidden
- # So let's make sure its all good!
- self.file_input_element = self.driver.execute_script(
- "var input = document.createElement('input');"
- "input.type = 'file';"
- "input.style.display = 'block';"
- "input.style.opacity = '1';"
- "input.style.visibility = 'visible';"
- "input.style.height = '1px';"
- "input.style.width = '1px';"
- "if (document.body.childElementCount > 0) { "
- " document.body.insertBefore(input, document.body.childNodes[0]);"
- "} else { "
- " document.body.appendChild(input);"
- "}"
- "return input;"
- )
- def drag_over_document(self):
- # According to the HTML5 spec, we need to dispatch the dragenter event
- # once, and then the dragover event continuously, every 350+-200ms:
- # http://www.w3.org/html/wg/drafts/html/master/editing.html#current-drag
- # -operation
- # Especially IE implements this spec very tightly, and considers the
- # dragging to be over if no dragover event occurs for more than ~1sec.
- # We thus need to ensure that we keep dispatching the dragover event.
-
- # This line used to read `_dispatch_event(..., to='document')`. However,
- # this doesn't work when adding a photo to a tweet on Twitter.
- # Dispatching the event to document.body fixes this, and also works for
- # Gmail:
- self._dispatch_event('dragenter', to='document.body')
- self.dragover_event = self._prepare_continuous_event(
- 'dragover', 'document', interval_msecs=300
- )
- self.dragover_event.start()
- def _dispatch_event(self, event_name, to):
- script, args = self._prepare_dispatch_event(event_name, to)
- self.driver.execute_script(script, *args)
- def _prepare_continuous_event(self, event_name, to, interval_msecs):
- script, args = self._prepare_dispatch_event(event_name, to)
- return JavaScriptInterval(self.driver, script, args, interval_msecs)
- def _prepare_dispatch_event(self, event_name, to):
- script = \
- "var files = arguments[0].files;" \
- "var items = [];" \
- "var types = [];" \
- "for (var i = 0; i < files.length; i++) {" \
- " items[i] = {kind: 'file', type: files[i].type};" \
- " types[i] = 'Files';" \
- "}" \
- "var event = document.createEvent('CustomEvent');" \
- "event.initCustomEvent(arguments[1], true, true, 0);" \
- "event.dataTransfer = {" \
- " files: files," \
- " items: items," \
- " types: types" \
- "};" \
- "arguments[2].dispatchEvent(event);"
- if isinstance(to, str):
- script = script.replace('arguments[2]', to)
- args = self.file_input_element, event_name,
- else:
- args = self.file_input_element, event_name, to.unwrap()
- return script, args
- def drop_on(self, target):
- self.dragover_event.stop()
- self._dispatch_event('drop', to=target)
- def end(self):
- if self.file_input_element is not None:
- self.driver.execute_script(
- "arguments[0].parentNode.removeChild(arguments[0]);",
- self.file_input_element
- )
- self.file_input_element = None
+ def __init__(self, driver, file_path):
+ self.driver = driver
+ self.file_path = file_path
+ self.file_input_element = None
+ self.dragover_event = None
+
+ def begin(self):
+ self._create_file_input_element()
+ try:
+ self.file_input_element.send_keys(self.file_path)
+ except:
+ self.end()
+ raise
+
+ def _create_file_input_element(self):
+ # The input needs to be visible to Selenium to allow sending keys to it
+ # in Firefox and IE.
+ # According to http://stackoverflow.com/questions/6101461/
+ # Selenium criteria whether an element is visible or not are the
+ # following:
+ # - visibility != hidden
+ # - display != none (is also checked against every parent element)
+ # - opacity != 0
+ # - height and width are both > 0
+ # - for an input, the attribute type != hidden
+ # So let's make sure its all good!
+ self.file_input_element = self.driver.execute_script(
+ "var input = document.createElement('input');"
+ "input.type = 'file';"
+ "input.style.display = 'block';"
+ "input.style.opacity = '1';"
+ "input.style.visibility = 'visible';"
+ "input.style.height = '1px';"
+ "input.style.width = '1px';"
+ "if (document.body.childElementCount > 0) { "
+ " document.body.insertBefore(input, document.body.childNodes[0]);"
+ "} else { "
+ " document.body.appendChild(input);"
+ "}"
+ "return input;"
+ )
+
+ def drag_over_document(self):
+ # According to the HTML5 spec, we need to dispatch the dragenter event
+ # once, and then the dragover event continuously, every 350+-200ms:
+ # http://www.w3.org/html/wg/drafts/html/master/editing.html#current-drag
+ # -operation
+ # Especially IE implements this spec very tightly, and considers the
+ # dragging to be over if no dragover event occurs for more than ~1sec.
+ # We thus need to ensure that we keep dispatching the dragover event.
+
+ # This line used to read `_dispatch_event(..., to='document')`. However,
+ # this doesn't work when adding a photo to a tweet on Twitter.
+ # Dispatching the event to document.body fixes this, and also works for
+ # Gmail:
+ self._dispatch_event('dragenter', to='document.body')
+ self.dragover_event = self._prepare_continuous_event(
+ 'dragover', 'document', interval_msecs=300
+ )
+ self.dragover_event.start()
+
+ def _dispatch_event(self, event_name, to):
+ script, args = self._prepare_dispatch_event(event_name, to)
+ self.driver.execute_script(script, *args)
+
+ def _prepare_continuous_event(self, event_name, to, interval_msecs):
+ script, args = self._prepare_dispatch_event(event_name, to)
+ return JavaScriptInterval(self.driver, script, args, interval_msecs)
+
+ def _prepare_dispatch_event(self, event_name, to):
+ script = \
+ "var files = arguments[0].files;" \
+ "var items = [];" \
+ "var types = [];" \
+ "for (var i = 0; i < files.length; i++) {" \
+ " items[i] = {kind: 'file', type: files[i].type};" \
+ " types[i] = 'Files';" \
+ "}" \
+ "var event = document.createEvent('CustomEvent');" \
+ "event.initCustomEvent(arguments[1], true, true, 0);" \
+ "event.dataTransfer = {" \
+ " files: files," \
+ " items: items," \
+ " types: types" \
+ "};" \
+ "arguments[2].dispatchEvent(event);"
+ if isinstance(to, str):
+ script = script.replace('arguments[2]', to)
+ args = self.file_input_element, event_name,
+ else:
+ args = self.file_input_element, event_name, to.unwrap()
+ return script, args
+
+ def drop_on(self, target):
+ self.dragover_event.stop()
+ self._dispatch_event('drop', to=target)
+
+ def end(self):
+ if self.file_input_element is not None:
+ self.driver.execute_script(
+ "arguments[0].parentNode.removeChild(arguments[0]);",
+ self.file_input_element
+ )
+ self.file_input_element = None
+
class JavaScriptInterval:
- def __init__(self, driver, script, args, interval_msecs):
- self.driver = driver
- self.script = script
- self.args = args
- self.interval_msecs = interval_msecs
- self._interval_id = None
- def start(self):
- setinterval_script = (
- "var originalArguments = arguments;"
- "return setInterval(function() {"
- " arguments = originalArguments;"
- " %s"
- "}, %d);"
- ) % (self.script, self.interval_msecs)
- self._interval_id = \
- self.driver.execute_script(setinterval_script, *self.args)
- def stop(self):
- self.driver.execute_script(
- "clearInterval(arguments[0]);", self._interval_id
- )
- self._interval_id = None
+ def __init__(self, driver, script, args, interval_msecs):
+ self.driver = driver
+ self.script = script
+ self.args = args
+ self.interval_msecs = interval_msecs
+ self._interval_id = None
+
+ def start(self):
+ setinterval_script = (
+ "var originalArguments = arguments;"
+ "return setInterval(function() {"
+ " arguments = originalArguments;"
+ " %s"
+ "}, %d);"
+ ) % (self.script, self.interval_msecs)
+ self._interval_id = \
+ self.driver.execute_script(setinterval_script, *self.args)
+
+ def stop(self):
+ self.driver.execute_script(
+ "clearInterval(arguments[0]);", self._interval_id
+ )
+ self._interval_id = None
+
class GUIElementImpl:
- def __init__(self, driver):
- self._bound_occurrence = None
- self._driver = driver
- def find_all(self):
- if self._is_bound():
- yield self
- else:
- for occurrence in self.find_all_occurrences():
- yield self.bound_to_occurrence(occurrence)
- def _is_bound(self):
- return self._bound_occurrence is not None
- def find_all_occurrences(self):
- raise NotImplementedError()
- def bound_to_occurrence(self, occurrence):
- result = copy(self)
- result._bound_occurrence = occurrence
- return result
- def exists(self):
- try:
- next(self.find_all())
- except StopIteration:
- return False
- else:
- return True
- @property
- def first_occurrence(self):
- if not self._is_bound():
- self._bind_to_first_occurrence()
- return self._bound_occurrence
- def _bind_to_first_occurrence(self):
- self.perform(lambda _: None)
- # _perform_no_wait(...) below now sets _bound_occurrence.
- def perform(self, action):
- from helium import Config
- end_time = time() + Config.implicit_wait_secs
- # Try to perform `action` at least once:
- result = self._perform_no_wait(action)
- while result is None and time() < end_time:
- result = self._perform_no_wait(action)
- if result is not None:
- return result
- raise LookupError()
- def _perform_no_wait(self, action):
- for bound_gui_elt_impl in self.find_all():
- occurrence = bound_gui_elt_impl.first_occurrence
- try:
- action(occurrence)
- except Exception as e:
- if self.should_ignore_exception(e):
- continue
- else:
- raise
- else:
- self._bound_occurrence = occurrence
- return occurrence
- def should_ignore_exception(self, exception):
- if isinstance(exception, ElementNotVisibleException):
- return True
- if isinstance(exception, MoveTargetOutOfBoundsException):
- return True
- if isinstance(exception, StaleElementReferenceException):
- return True
- if isinstance(exception, WebDriverException):
- msg = exception.msg
- if 'is not clickable at point' in msg \
- and 'Other element would receive the click' in msg:
- # This can happen when the element has moved.
- return True
- return False
+ def __init__(self, driver):
+ self._bound_occurrence = None
+ self._driver = driver
+
+ def find_all(self):
+ if self._is_bound():
+ yield self
+ else:
+ for occurrence in self.find_all_occurrences():
+ yield self.bound_to_occurrence(occurrence)
+
+ def _is_bound(self):
+ return self._bound_occurrence is not None
+
+ def find_all_occurrences(self):
+ raise NotImplementedError()
+
+ def bound_to_occurrence(self, occurrence):
+ result = copy(self)
+ result._bound_occurrence = occurrence
+ return result
+
+ def exists(self):
+ try:
+ next(self.find_all())
+ except StopIteration:
+ return False
+ else:
+ return True
+
+ @property
+ def first_occurrence(self):
+ if not self._is_bound():
+ self._bind_to_first_occurrence()
+ return self._bound_occurrence
+
+ def _bind_to_first_occurrence(self):
+ self.perform(lambda _: None)
+
+ # _perform_no_wait(...) below now sets _bound_occurrence.
+ def perform(self, action):
+ from helium import Config
+ end_time = time() + Config.implicit_wait_secs
+ # Try to perform `action` at least once:
+ result = self._perform_no_wait(action)
+ while result is None and time() < end_time:
+ result = self._perform_no_wait(action)
+ if result is not None:
+ return result
+ raise LookupError()
+
+ def _perform_no_wait(self, action):
+ for bound_gui_elt_impl in self.find_all():
+ occurrence = bound_gui_elt_impl.first_occurrence
+ try:
+ action(occurrence)
+ except Exception as e:
+ if self.should_ignore_exception(e):
+ continue
+ else:
+ raise
+ else:
+ self._bound_occurrence = occurrence
+ return occurrence
+
+ def should_ignore_exception(self, exception):
+ if isinstance(exception, ElementNotVisibleException):
+ return True
+ if isinstance(exception, MoveTargetOutOfBoundsException):
+ return True
+ if isinstance(exception, StaleElementReferenceException):
+ return True
+ if isinstance(exception, WebDriverException):
+ msg = exception.msg
+ if 'is not clickable at point' in msg \
+ and 'Other element would receive the click' in msg:
+ # This can happen when the element has moved.
+ return True
+ return False
+
class HTMLElementImpl(GUIElementImpl):
- def __init__(
- self, driver, below=None, to_right_of=None, above=None,
- to_left_of=None
- ):
- super(HTMLElementImpl, self).__init__(driver)
- self.below = self._unwrap_element(below)
- self.to_right_of = self._unwrap_element(to_right_of)
- self.above = self._unwrap_element(above)
- self.to_left_of = self._unwrap_element(to_left_of)
- self.matches = PREFIX_IGNORE_CASE()
- def _unwrap_element(self, element):
- if isinstance(element, str):
- return TextImpl(self._driver, element)
- from helium import HTMLElement
- if isinstance(element, HTMLElement):
- return element._impl
- return element
- @property
- def width(self):
- return self.first_occurrence.location.width
- @property
- def height(self):
- return self.first_occurrence.location.height
- @property
- def x(self):
- return self.first_occurrence.location.left
- @property
- def y(self):
- return self.first_occurrence.location.top
- @property
- def top_left(self):
- from helium import Point
- return Point(self.x, self.y)
- @property
- def web_element(self):
- return self.first_occurrence.unwrap()
- def find_all_occurrences(self):
- self._handle_closed_window()
- self._driver.switch_to.default_content()
- try:
- for frame_index in FrameIterator(self._driver):
- search_regions = self._get_search_regions_in_curr_frame()
- for occurrence in self.find_all_in_curr_frame():
- if self._should_yield(occurrence, search_regions):
- occurrence.frame_index = frame_index
- yield occurrence
- except FramesChangedWhileIterating:
- # Abort this search.
- pass
- def _handle_closed_window(self):
- window_handles = self._driver.window_handles
- try:
- curr_window_handle = self._driver.current_window_handle
- except NoSuchWindowException:
- window_has_been_closed = True
- else:
- window_has_been_closed = curr_window_handle not in window_handles
- if window_has_been_closed:
- self._driver.switch_to.window(window_handles[0])
- def _get_search_regions_in_curr_frame(self):
- result = []
- if self.below:
- result.append([
- elt.location.is_above
- for elt in self.below.find_all_in_curr_frame()
- ])
- if self.to_right_of:
- result.append([
- elt.location.is_to_left_of
- for elt in self.to_right_of.find_all_in_curr_frame()
- ])
- if self.above:
- result.append([
- elt.location.is_below
- for elt in self.above.find_all_in_curr_frame()
- ])
- if self.to_left_of:
- result.append([
- elt.location.is_to_right_of
- for elt in self.to_left_of.find_all_in_curr_frame()
- ])
- return result
- def _should_yield(self, occurrence, search_regions):
- return occurrence.is_displayed() and \
- self._is_in_any_search_region(occurrence, search_regions)
- def _is_in_any_search_region(self, element, search_regions):
- for direction in search_regions:
- found = False
- for search_region in direction:
- if search_region(element.location):
- found = True
- break
- if not found:
- return False
- return True
- def find_all_in_curr_frame(self):
- raise NotImplementedError()
- def _is_enabled(self):
- """
- Useful for subclasses.
- """
- return self.first_occurrence.get_attribute('disabled') is None
+ def __init__(
+ self, driver, below=None, to_right_of=None, above=None,
+ to_left_of=None
+ ):
+ super(HTMLElementImpl, self).__init__(driver)
+ self.below = self._unwrap_element(below)
+ self.to_right_of = self._unwrap_element(to_right_of)
+ self.above = self._unwrap_element(above)
+ self.to_left_of = self._unwrap_element(to_left_of)
+ self.matches = PREFIX_IGNORE_CASE()
+
+ def _unwrap_element(self, element):
+ if isinstance(element, str):
+ return TextImpl(self._driver, element)
+ from helium import HTMLElement
+ if isinstance(element, HTMLElement):
+ return element._impl
+ return element
+
+ @property
+ def width(self):
+ return self.first_occurrence.location.width
+
+ @property
+ def height(self):
+ return self.first_occurrence.location.height
+
+ @property
+ def x(self):
+ return self.first_occurrence.location.left
+
+ @property
+ def y(self):
+ return self.first_occurrence.location.top
+
+ @property
+ def top_left(self):
+ from helium import Point
+ return Point(self.x, self.y)
+
+ @property
+ def web_element(self):
+ return self.first_occurrence.unwrap()
+
+ def find_all_occurrences(self):
+ self._handle_closed_window()
+ self._driver.switch_to.default_content()
+ try:
+ for frame_index in FrameIterator(self._driver):
+ search_regions = self._get_search_regions_in_curr_frame()
+ for occurrence in self.find_all_in_curr_frame():
+ if self._should_yield(occurrence, search_regions):
+ occurrence.frame_index = frame_index
+ yield occurrence
+ except FramesChangedWhileIterating:
+ # Abort this search.
+ pass
+
+ def _handle_closed_window(self):
+ window_handles = self._driver.window_handles
+ try:
+ curr_window_handle = self._driver.current_window_handle
+ except NoSuchWindowException:
+ window_has_been_closed = True
+ else:
+ window_has_been_closed = curr_window_handle not in window_handles
+ if window_has_been_closed:
+ self._driver.switch_to.window(window_handles[0])
+
+ def _get_search_regions_in_curr_frame(self):
+ result = []
+ if self.below:
+ result.append([
+ elt.location.is_above
+ for elt in self.below.find_all_in_curr_frame()
+ ])
+ if self.to_right_of:
+ result.append([
+ elt.location.is_to_left_of
+ for elt in self.to_right_of.find_all_in_curr_frame()
+ ])
+ if self.above:
+ result.append([
+ elt.location.is_below
+ for elt in self.above.find_all_in_curr_frame()
+ ])
+ if self.to_left_of:
+ result.append([
+ elt.location.is_to_right_of
+ for elt in self.to_left_of.find_all_in_curr_frame()
+ ])
+ return result
+
+ def _should_yield(self, occurrence, search_regions):
+ return occurrence.is_displayed() and \
+ self._is_in_any_search_region(occurrence, search_regions)
+
+ def _is_in_any_search_region(self, element, search_regions):
+ for direction in search_regions:
+ found = False
+ for search_region in direction:
+ if search_region(element.location):
+ found = True
+ break
+ if not found:
+ return False
+ return True
+
+ def find_all_in_curr_frame(self):
+ raise NotImplementedError()
+
+ def _is_enabled(self):
+ """
+ Useful for subclasses.
+ """
+ return self.first_occurrence.get_attribute('disabled') is None
+
class SImpl(HTMLElementImpl):
- def __init__(self, driver, selector, **kwargs):
- super(SImpl, self).__init__(driver, **kwargs)
- self.selector = selector
- def find_all_in_curr_frame(self):
- wrap = lambda web_elements: list(map(WebElementWrapper, web_elements))
- if self.selector.startswith('@'):
- return wrap(self._driver.find_elements_by_name(self.selector[1:]))
- if self.selector.startswith('//'):
- return wrap(self._driver.find_elements_by_xpath(self.selector))
- return wrap(self._driver.find_elements_by_css_selector(self.selector))
+ def __init__(self, driver, selector, **kwargs):
+ super(SImpl, self).__init__(driver, **kwargs)
+ self.selector = selector
+
+ def find_all_in_curr_frame(self):
+ wrap = lambda web_elements: list(map(WebElementWrapper, web_elements))
+ if self.selector.startswith('@'):
+ return wrap(self._driver.find_elements_by_name(self.selector[1:]))
+ if self.selector.startswith('//'):
+ return wrap(self._driver.find_elements_by_xpath(self.selector))
+ return wrap(self._driver.find_elements_by_css_selector(self.selector))
+
class HTMLElementIdentifiedByXPath(HTMLElementImpl):
- def find_all_in_curr_frame(self):
- x_path = self.get_xpath()
- return self._sort_search_result(
- list(map(
- WebElementWrapper, self._driver.find_elements_by_xpath(x_path)
- ))
- )
- def _sort_search_result(self, search_result):
- keys_to_result_items = []
- for web_elt in search_result:
- try:
- key = self.get_sort_index(web_elt)
- except StaleElementReferenceException:
- pass
- else:
- keys_to_result_items.append((key, web_elt))
- sort_key = lambda tpl: tpl[0]
- keys_to_result_items.sort(key=sort_key)
- result_item = lambda tpl: tpl[1]
- return list(map(result_item, keys_to_result_items))
- def get_xpath(self):
- raise NotImplementedError()
- def get_sort_index(self, web_element):
- return self._driver.get_distance_to_last_manipulated(web_element) + 1
+ def find_all_in_curr_frame(self):
+ x_path = self.get_xpath()
+ return self._sort_search_result(
+ list(map(
+ WebElementWrapper, self._driver.find_elements_by_xpath(x_path)
+ ))
+ )
+
+ def _sort_search_result(self, search_result):
+ keys_to_result_items = []
+ for web_elt in search_result:
+ try:
+ key = self.get_sort_index(web_elt)
+ except StaleElementReferenceException:
+ pass
+ else:
+ keys_to_result_items.append((key, web_elt))
+ sort_key = lambda tpl: tpl[0]
+ keys_to_result_items.sort(key=sort_key)
+ result_item = lambda tpl: tpl[1]
+ return list(map(result_item, keys_to_result_items))
+
+ def get_xpath(self):
+ raise NotImplementedError()
+
+ def get_sort_index(self, web_element):
+ return self._driver.get_distance_to_last_manipulated(web_element) + 1
+
class HTMLElementContainingText(HTMLElementIdentifiedByXPath):
- def __init__(self, driver, text=None, **kwargs):
- super(HTMLElementContainingText, self).__init__(driver, **kwargs)
- self.search_text = text
- def get_xpath(self):
- xpath_base = "//" + self.get_xpath_node_selector() + \
- predicate(self.matches.xpath('.', self.search_text))
- return '%s[not(self::script)][not(.%s)]' % (xpath_base, xpath_base)
- def get_xpath_node_selector(self):
- return '*'
+ def __init__(self, driver, text=None, **kwargs):
+ super(HTMLElementContainingText, self).__init__(driver, **kwargs)
+ self.search_text = text
+
+ def get_xpath(self):
+ xpath_base = "//" + self.get_xpath_node_selector() + \
+ predicate(self.matches.xpath('.', self.search_text))
+ return '%s[not(self::script)][not(.%s)]' % (xpath_base, xpath_base)
+
+ def get_xpath_node_selector(self):
+ return '*'
+
class TextImpl(HTMLElementContainingText):
- def __init__(self, driver, text=None, include_free_text=True, **kwargs):
- super(TextImpl, self).__init__(driver, text, **kwargs)
- self.include_free_text = include_free_text
- @property
- def value(self):
- return self.first_occurrence.text
- def get_xpath(self):
- button_impl = ButtonImpl(self._driver, self.search_text)
- link_impl = LinkImpl(self._driver, self.search_text)
- components = [
- self._get_search_text_xpath(),
- button_impl.get_input_button_xpath(),
- link_impl.get_xpath()
- ]
- if self.search_text and self.include_free_text:
- components.append(
- FreeText(self._driver, self.search_text).get_xpath()
- )
- return ' | '.join(components)
- def _get_search_text_xpath(self):
- if self.search_text:
- result = super(TextImpl, self).get_xpath()
- else:
- no_descendant_with_same_text = \
- "not(.//*[normalize-space(.)=normalize-space(self::*)])"
- result = '//*[text() and %s]' % no_descendant_with_same_text
- return result + "[not(self::option)]" + \
- ("" if self.include_free_text else "[count(*) <= 1]")
+ def __init__(self, driver, text=None, include_free_text=True, **kwargs):
+ super(TextImpl, self).__init__(driver, text, **kwargs)
+ self.include_free_text = include_free_text
+
+ @property
+ def value(self):
+ return self.first_occurrence.text
+
+ def get_xpath(self):
+ button_impl = ButtonImpl(self._driver, self.search_text)
+ link_impl = LinkImpl(self._driver, self.search_text)
+ components = [
+ self._get_search_text_xpath(),
+ button_impl.get_input_button_xpath(),
+ link_impl.get_xpath()
+ ]
+ if self.search_text and self.include_free_text:
+ components.append(
+ FreeText(self._driver, self.search_text).get_xpath()
+ )
+ return ' | '.join(components)
+
+ def _get_search_text_xpath(self):
+ if self.search_text:
+ result = super(TextImpl, self).get_xpath()
+ else:
+ no_descendant_with_same_text = \
+ "not(.//*[normalize-space(.)=normalize-space(self::*)])"
+ result = '//*[text() and %s]' % no_descendant_with_same_text
+ return result + "[not(self::option)]" + \
+ ("" if self.include_free_text else "[count(*) <= 1]")
+
class FreeText(HTMLElementContainingText):
- def get_xpath_node_selector(self):
- return 'text()'
- def get_xpath(self):
- return super(FreeText, self).get_xpath() + '/..'
+ def get_xpath_node_selector(self):
+ return 'text()'
+
+ def get_xpath(self):
+ return super(FreeText, self).get_xpath() + '/..'
+
class LinkImpl(HTMLElementContainingText):
- def get_xpath_node_selector(self):
- return 'a'
- def get_xpath(self):
- return super(LinkImpl, self).get_xpath() + ' | ' + \
- "//a" + \
- predicate(self.matches.xpath('@title', self.search_text)) + \
- ' | ' + "//*[@role='link']" + \
- predicate(self.matches.xpath('.', self.search_text))
- @property
- def href(self):
- return self.web_element.get_attribute('href')
+ def get_xpath_node_selector(self):
+ return 'a'
+
+ def get_xpath(self):
+ return super(LinkImpl, self).get_xpath() + ' | ' + \
+ "//a" + \
+ predicate(self.matches.xpath('@title', self.search_text)) + \
+ ' | ' + "//*[@role='link']" + \
+ predicate(self.matches.xpath('.', self.search_text))
+
+ @property
+ def href(self):
+ return self.web_element.get_attribute('href')
+
class ListItemImpl(HTMLElementContainingText):
- def get_xpath_node_selector(self):
- return 'li'
+ def get_xpath_node_selector(self):
+ return 'li'
+
class ButtonImpl(HTMLElementContainingText):
- def get_xpath_node_selector(self):
- return 'button'
- def is_enabled(self):
- aria_disabled = self.first_occurrence.get_attribute('aria-disabled')
- return self._is_enabled() \
- and (not aria_disabled or aria_disabled.lower() == 'false')
- def get_xpath(self):
- has_aria_label = self.matches.xpath('@aria-label', self.search_text)
- has_text = self.matches.xpath('.', self.search_text)
- has_text_or_aria_label = predicate_or(has_aria_label, has_text)
- return ' | '.join([
- super(ButtonImpl, self).get_xpath(), self.get_input_button_xpath(),
- "//*[@role='button']" + has_text_or_aria_label,
- "//button" + predicate(has_aria_label)
- ])
- def get_input_button_xpath(self):
- if self.search_text:
- has_value = self.matches.xpath('@value', self.search_text)
- has_label = self.matches.xpath('@label', self.search_text)
- has_aria_label = self.matches.xpath('@aria-label', self.search_text)
- has_title = self.matches.xpath('@title', self.search_text)
- has_text = \
- predicate_or(has_value, has_label, has_aria_label, has_title)
- else:
- has_text = ''
- return "//input[@type='submit' or @type='button']" + has_text
+ def get_xpath_node_selector(self):
+ return 'button'
+
+ def is_enabled(self):
+ aria_disabled = self.first_occurrence.get_attribute('aria-disabled')
+ return self._is_enabled() \
+ and (not aria_disabled or aria_disabled.lower() == 'false')
+
+ def get_xpath(self):
+ has_aria_label = self.matches.xpath('@aria-label', self.search_text)
+ has_text = self.matches.xpath('.', self.search_text)
+ has_text_or_aria_label = predicate_or(has_aria_label, has_text)
+ return ' | '.join([
+ super(ButtonImpl, self).get_xpath(), self.get_input_button_xpath(),
+ "//*[@role='button']" + has_text_or_aria_label,
+ "//button" + predicate(has_aria_label)
+ ])
+
+ def get_input_button_xpath(self):
+ if self.search_text:
+ has_value = self.matches.xpath('@value', self.search_text)
+ has_label = self.matches.xpath('@label', self.search_text)
+ has_aria_label = self.matches.xpath('@aria-label', self.search_text)
+ has_title = self.matches.xpath('@title', self.search_text)
+ has_text = \
+ predicate_or(has_value, has_label, has_aria_label, has_title)
+ else:
+ has_text = ''
+ return "//input[@type='submit' or @type='button']" + has_text
+
class ImageImpl(HTMLElementIdentifiedByXPath):
- def __init__(self, driver, alt, **kwargs):
- super(ImageImpl, self).__init__(driver, **kwargs)
- self.alt = alt
- def get_xpath(self):
- return "//img" + predicate(self.matches.xpath('@alt', self.alt))
+ def __init__(self, driver, alt, **kwargs):
+ super(ImageImpl, self).__init__(driver, **kwargs)
+ self.alt = alt
+
+ def get_xpath(self):
+ return "//img" + predicate(self.matches.xpath('@alt', self.alt))
+
class LabelledElement(HTMLElementImpl):
- SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR = 1.5
- def __init__(self, driver, label=None, **kwargs):
- super(LabelledElement, self).__init__(driver, **kwargs)
- self.label = label
- def find_all_in_curr_frame(self):
- if not self.label:
- result = self._find_elts()
- else:
- labels = TextImpl(
- self._driver, self.label, include_free_text=False
- ).find_all_in_curr_frame()
- if labels:
- result = list(self._filter_elts_belonging_to_labels(
- self._find_elts(), labels
- ))
- else:
- result = self._find_elts_by_free_text()
- return sorted(result, key=self._driver.get_distance_to_last_manipulated)
- def _find_elts(self, xpath=None):
- if xpath is None:
- xpath = self.get_xpath()
- return list(map(
- WebElementWrapper, self._driver.find_elements_by_xpath(xpath)
- ))
- def _find_elts_by_free_text(self):
- elt_types = [
- xpath.strip().lstrip('/') for xpath in self.get_xpath().split('|')
- ]
- labels = '//text()' + predicate(self.matches.xpath('.', self.label))
- xpath = ' | '.join(
- [(labels + '/%s::' + elt_type + '[1]')
- % ('preceding-sibling'
- if 'checkbox' in elt_type or 'radio' in elt_type
- else 'following')
- for elt_type in elt_types]
- )
- return self._find_elts(xpath)
- def get_xpath(self):
- raise NotImplementedError()
- def get_primary_search_direction(self):
- return 'to_right_of'
- def get_secondary_search_direction(self):
- return 'below'
- def _filter_elts_belonging_to_labels(self, all_elts, labels):
- for label, elt in self._get_labels_with_explicit_elts(all_elts, labels):
- yield elt
- labels.remove(label)
- all_elts.remove(elt)
- labels_to_elts = self._get_related_elts(all_elts, labels)
- labels_to_elts = self._ensure_at_most_one_label_per_elt(labels_to_elts)
- self._retain_closest(labels_to_elts)
- for elts_for_label in list(labels_to_elts.values()):
- assert len(elts_for_label) <= 1
- if elts_for_label:
- yield next(iter(elts_for_label))
- def _get_labels_with_explicit_elts(self, all_elts, labels):
- for label in labels:
- if label.tag_name == 'label':
- label_target = label.get_attribute('for')
- if label_target:
- for elt in all_elts:
- elt_id = elt.get_attribute('id')
- if elt_id.lower() == label_target.lower():
- yield label, elt
- def _get_related_elts(self, all_elts, labels):
- result = {}
- for label in labels:
- for elt in all_elts:
- if self._are_related(elt, label):
- if label not in result:
- result[label] = set()
- result[label].add(elt)
- return result
- def _are_related(self, elt, label):
- if elt.location.intersects(label.location):
- return True
- prim_search_dir = self.get_primary_search_direction()
- sec_search_dir = self.get_secondary_search_direction()
- return label.location.distance_to(elt.location) <= 150 and (
- elt.location.is_in_direction(prim_search_dir, label.location) or
- elt.location.is_in_direction(sec_search_dir, label.location)
- )
- def _ensure_at_most_one_label_per_elt(self, labels_to_elts):
- elts_to_labels = inverse(labels_to_elts)
- self._retain_closest(elts_to_labels)
- return inverse(elts_to_labels)
- def _retain_closest(self, pivots_to_elts):
- for pivot, elts in list(pivots_to_elts.items()):
- if elts:
- # Would like to use a set literal {...} here, but this is not
- # supported in Python 2.6. Thus we need to use set([...]).
- pivots_to_elts[pivot] = set([self._find_closest(pivot, elts)])
- def _find_closest(self, to_pivot, among_elts):
- remaining_elts = iter(among_elts)
- result = next(remaining_elts)
- result_distance = self._compute_distance(result, to_pivot)
- for element in remaining_elts:
- element_distance = self._compute_distance(element, to_pivot)
- if element_distance < result_distance:
- result = element
- result_distance = element_distance
- return result
- def _compute_distance(self, elt_1, elt_2):
- loc_1 = elt_1.location
- loc_2 = elt_2.location
- if loc_1.is_in_direction(self.get_secondary_search_direction(), loc_2):
- factor = self.SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR
- else:
- factor = 1
- return factor * loc_1.distance_to(loc_2)
+ SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR = 1.5
+
+ def __init__(self, driver, label=None, **kwargs):
+ super(LabelledElement, self).__init__(driver, **kwargs)
+ self.label = label
+
+ def find_all_in_curr_frame(self):
+ if not self.label:
+ result = self._find_elts()
+ else:
+ labels = TextImpl(
+ self._driver, self.label, include_free_text=False
+ ).find_all_in_curr_frame()
+ if labels:
+ result = list(self._filter_elts_belonging_to_labels(
+ self._find_elts(), labels
+ ))
+ else:
+ result = self._find_elts_by_free_text()
+ return sorted(result, key=self._driver.get_distance_to_last_manipulated)
+
+ def _find_elts(self, xpath=None):
+ if xpath is None:
+ xpath = self.get_xpath()
+ return list(map(
+ WebElementWrapper, self._driver.find_elements_by_xpath(xpath)
+ ))
+
+ def _find_elts_by_free_text(self):
+ elt_types = [
+ xpath.strip().lstrip('/') for xpath in self.get_xpath().split('|')
+ ]
+ labels = '//text()' + predicate(self.matches.xpath('.', self.label))
+ xpath = ' | '.join(
+ [(labels + '/%s::' + elt_type + '[1]')
+ % ('preceding-sibling'
+ if 'checkbox' in elt_type or 'radio' in elt_type
+ else 'following')
+ for elt_type in elt_types]
+ )
+ return self._find_elts(xpath)
+
+ def get_xpath(self):
+ raise NotImplementedError()
+
+ def get_primary_search_direction(self):
+ return 'to_right_of'
+
+ def get_secondary_search_direction(self):
+ return 'below'
+
+ def _filter_elts_belonging_to_labels(self, all_elts, labels):
+ for label, elt in self._get_labels_with_explicit_elts(all_elts, labels):
+ yield elt
+ labels.remove(label)
+ all_elts.remove(elt)
+ labels_to_elts = self._get_related_elts(all_elts, labels)
+ labels_to_elts = self._ensure_at_most_one_label_per_elt(labels_to_elts)
+ self._retain_closest(labels_to_elts)
+ for elts_for_label in list(labels_to_elts.values()):
+ assert len(elts_for_label) <= 1
+ if elts_for_label:
+ yield next(iter(elts_for_label))
+
+ def _get_labels_with_explicit_elts(self, all_elts, labels):
+ for label in labels:
+ if label.tag_name == 'label':
+ label_target = label.get_attribute('for')
+ if label_target:
+ for elt in all_elts:
+ elt_id = elt.get_attribute('id')
+ if elt_id.lower() == label_target.lower():
+ yield label, elt
+
+ def _get_related_elts(self, all_elts, labels):
+ result = {}
+ for label in labels:
+ for elt in all_elts:
+ if self._are_related(elt, label):
+ if label not in result:
+ result[label] = set()
+ result[label].add(elt)
+ return result
+
+ def _are_related(self, elt, label):
+ if elt.location.intersects(label.location):
+ return True
+ prim_search_dir = self.get_primary_search_direction()
+ sec_search_dir = self.get_secondary_search_direction()
+ return label.location.distance_to(elt.location) <= 150 and (
+ elt.location.is_in_direction(prim_search_dir, label.location) or
+ elt.location.is_in_direction(sec_search_dir, label.location)
+ )
+
+ def _ensure_at_most_one_label_per_elt(self, labels_to_elts):
+ elts_to_labels = inverse(labels_to_elts)
+ self._retain_closest(elts_to_labels)
+ return inverse(elts_to_labels)
+
+ def _retain_closest(self, pivots_to_elts):
+ for pivot, elts in list(pivots_to_elts.items()):
+ if elts:
+ # Would like to use a set literal {...} here, but this is not
+ # supported in Python 2.6. Thus we need to use set([...]).
+ pivots_to_elts[pivot] = set([self._find_closest(pivot, elts)])
+
+ def _find_closest(self, to_pivot, among_elts):
+ remaining_elts = iter(among_elts)
+ result = next(remaining_elts)
+ result_distance = self._compute_distance(result, to_pivot)
+ for element in remaining_elts:
+ element_distance = self._compute_distance(element, to_pivot)
+ if element_distance < result_distance:
+ result = element
+ result_distance = element_distance
+ return result
+
+ def _compute_distance(self, elt_1, elt_2):
+ loc_1 = elt_1.location
+ loc_2 = elt_2.location
+ if loc_1.is_in_direction(self.get_secondary_search_direction(), loc_2):
+ factor = self.SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR
+ else:
+ factor = 1
+ return factor * loc_1.distance_to(loc_2)
+
class CompositeElement(HTMLElementImpl):
- def __init__(self, driver, *args, **kwargs):
- super(CompositeElement, self).__init__(driver, **kwargs)
- self.args = [driver] + list(args)
- self.kwargs = kwargs
- self._first_element = None
- @property
- def first_element(self):
- if self._first_element is None:
- self._bind_to_first_occurrence()
- # find_all_in_curr_frame() below now sets _first_element
- return self._first_element
- def find_all_in_curr_frame(self):
- already_yielded = []
- for element in self.get_elements():
- for bound_gui_elt_impl in element.find_all_in_curr_frame():
- if self._first_element is None:
- self._first_element = element
- if bound_gui_elt_impl not in already_yielded:
- yield bound_gui_elt_impl
- already_yielded.append(bound_gui_elt_impl)
- def get_elements(self):
- for element_type in self.get_element_types():
- yield element_type(*self.args, **self.kwargs)
- def get_element_types(self):
- raise NotImplementedError()
+ def __init__(self, driver, *args, **kwargs):
+ super(CompositeElement, self).__init__(driver, **kwargs)
+ self.args = [driver] + list(args)
+ self.kwargs = kwargs
+ self._first_element = None
+
+ @property
+ def first_element(self):
+ if self._first_element is None:
+ self._bind_to_first_occurrence()
+ # find_all_in_curr_frame() below now sets _first_element
+ return self._first_element
+
+ def find_all_in_curr_frame(self):
+ already_yielded = []
+ for element in self.get_elements():
+ for bound_gui_elt_impl in element.find_all_in_curr_frame():
+ if self._first_element is None:
+ self._first_element = element
+ if bound_gui_elt_impl not in already_yielded:
+ yield bound_gui_elt_impl
+ already_yielded.append(bound_gui_elt_impl)
+
+ def get_elements(self):
+ for element_type in self.get_element_types():
+ yield element_type(*self.args, **self.kwargs)
+
+ def get_element_types(self):
+ raise NotImplementedError()
+
class ClickableText(CompositeElement):
- def get_element_types(self):
- return [ButtonImpl, TextImpl, ImageImpl]
+ def get_element_types(self):
+ return [ButtonImpl, TextImpl, ImageImpl]
+
class TextFieldImpl(CompositeElement):
- def get_element_types(self):
- return [
- StandardTextFieldWithPlaceholder, StandardTextFieldWithLabel,
- AriaTextFieldWithLabel
- ]
- @property
- def value(self):
- return self.first_element.value
- def is_enabled(self):
- return self.first_element.is_enabled()
- def is_editable(self):
- return self.first_element.is_editable()
+ def get_element_types(self):
+ return [
+ StandardTextFieldWithPlaceholder, StandardTextFieldWithLabel,
+ AriaTextFieldWithLabel
+ ]
+
+ @property
+ def value(self):
+ return self.first_element.value
+
+ def is_enabled(self):
+ return self.first_element.is_enabled()
+
+ def is_editable(self):
+ return self.first_element.is_editable()
+
class StandardTextFieldWithLabel(LabelledElement):
- @property
- def value(self):
- return self.first_occurrence.get_attribute('value') or ''
- def is_enabled(self):
- return self._is_enabled()
- def is_editable(self):
- return self.first_occurrence.get_attribute('readOnly') is None
- def get_xpath(self):
- return \
- "//input[%s='text' or %s='email' or %s='password' or %s='number' " \
- "or %s='tel' or string-length(@type)=0]" % ((lower('@type'), ) * 5)\
- + " | //textarea | //*[@contenteditable='true']"
+ @property
+ def value(self):
+ return self.first_occurrence.get_attribute('value') or ''
+
+ def is_enabled(self):
+ return self._is_enabled()
+
+ def is_editable(self):
+ return self.first_occurrence.get_attribute('readOnly') is None
+
+ def get_xpath(self):
+ return \
+ "//input[%s='text' or %s='email' or %s='password' or %s='number' " \
+ "or %s='tel' or string-length(@type)=0]" % ((lower('@type'),) * 5) \
+ + " | //textarea | //*[@contenteditable='true']"
+
class AriaTextFieldWithLabel(LabelledElement):
- @property
- def value(self):
- return self.first_occurrence.text
- def is_enabled(self):
- return self._is_enabled()
- def is_editable(self):
- return self.first_occurrence.get_attribute('readOnly') is None
- def get_xpath(self):
- return "//*[@role='textbox']"
+ @property
+ def value(self):
+ return self.first_occurrence.text
+
+ def is_enabled(self):
+ return self._is_enabled()
+
+ def is_editable(self):
+ return self.first_occurrence.get_attribute('readOnly') is None
+
+ def get_xpath(self):
+ return "//*[@role='textbox']"
+
class StandardTextFieldWithPlaceholder(HTMLElementIdentifiedByXPath):
- def __init__(self, driver, label, **kwargs):
- super(StandardTextFieldWithPlaceholder, self).__init__(driver, **kwargs)
- self.label = label
- @property
- def value(self):
- return self.first_occurrence.get_attribute('value') or ''
- def is_enabled(self):
- return self._is_enabled()
- def is_editable(self):
- return self.first_occurrence.get_attribute('readOnly') is None
- def get_xpath(self):
- return "(%s)%s" % (
- StandardTextFieldWithLabel(self.label).get_xpath(),
- predicate(self.matches.xpath('@placeholder', self.label))
- )
+ def __init__(self, driver, label, **kwargs):
+ super(StandardTextFieldWithPlaceholder, self).__init__(driver, **kwargs)
+ self.label = label
+
+ @property
+ def value(self):
+ return self.first_occurrence.get_attribute('value') or ''
+
+ def is_enabled(self):
+ return self._is_enabled()
+
+ def is_editable(self):
+ return self.first_occurrence.get_attribute('readOnly') is None
+
+ def get_xpath(self):
+ return "(%s)%s" % (
+ StandardTextFieldWithLabel(self.label).get_xpath(),
+ predicate(self.matches.xpath('@placeholder', self.label))
+ )
+
class FileInput(LabelledElement):
- def get_xpath(self):
- return "//input[@type='file']"
+ def get_xpath(self):
+ return "//input[@type='file']"
+
class ComboBoxImpl(CompositeElement):
- def get_element_types(self):
- return [ComboBoxIdentifiedByDisplayedValue, ComboBoxIdentifiedByLabel]
- def is_editable(self):
- return self.first_occurrence.tag_name != 'select'
- @property
- def value(self):
- selected_value = self._select_driver.first_selected_option
- if selected_value:
- return selected_value.text
- return None
- @property
- def options(self):
- return [option.text for option in self._select_driver.options]
- @property
- def _select_driver(self):
- return Select(self.web_element)
+ def get_element_types(self):
+ return [ComboBoxIdentifiedByDisplayedValue, ComboBoxIdentifiedByLabel]
+
+ def is_editable(self):
+ return self.first_occurrence.tag_name != 'select'
+
+ @property
+ def value(self):
+ selected_value = self._select_driver.first_selected_option
+ if selected_value:
+ return selected_value.text
+ return None
+
+ @property
+ def options(self):
+ return [option.text for option in self._select_driver.options]
+
+ @property
+ def _select_driver(self):
+ return Select(self.web_element)
+
class ComboBoxIdentifiedByLabel(LabelledElement):
- def get_xpath(self):
- return "//select | //input[@list]"
+ def get_xpath(self):
+ return "//select | //input[@list]"
+
class ComboBoxIdentifiedByDisplayedValue(HTMLElementContainingText):
- def get_xpath_node_selector(self):
- return 'option'
- def get_xpath(self):
- option_xpath = \
- super(ComboBoxIdentifiedByDisplayedValue, self).get_xpath()
- return option_xpath + '/ancestor::select[1]'
- def find_all_in_curr_frame(self):
- all_cbs_with_a_matching_value = super(
- ComboBoxIdentifiedByDisplayedValue, self
- ).find_all_in_curr_frame()
- result = []
- for cb in all_cbs_with_a_matching_value:
- for selected_option in Select(cb.unwrap()).all_selected_options:
- if self.matches.text(selected_option.text, self.search_text):
- result.append(cb)
- break
- return result
+ def get_xpath_node_selector(self):
+ return 'option'
+
+ def get_xpath(self):
+ option_xpath = \
+ super(ComboBoxIdentifiedByDisplayedValue, self).get_xpath()
+ return option_xpath + '/ancestor::select[1]'
+
+ def find_all_in_curr_frame(self):
+ all_cbs_with_a_matching_value = super(
+ ComboBoxIdentifiedByDisplayedValue, self
+ ).find_all_in_curr_frame()
+ result = []
+ for cb in all_cbs_with_a_matching_value:
+ for selected_option in Select(cb.unwrap()).all_selected_options:
+ if self.matches.text(selected_option.text, self.search_text):
+ result.append(cb)
+ break
+ return result
+
class CheckBoxImpl(LabelledElement):
- def is_enabled(self):
- return self._is_enabled()
- def is_checked(self):
- return self.first_occurrence.get_attribute('checked') is not None
- def get_xpath(self):
- return "//input[@type='checkbox']"
- def get_primary_search_direction(self):
- return 'to_left_of'
- def get_secondary_search_direction(self):
- return 'to_right_of'
+ def is_enabled(self):
+ return self._is_enabled()
+
+ def is_checked(self):
+ return self.first_occurrence.get_attribute('checked') is not None
+
+ def get_xpath(self):
+ return "//input[@type='checkbox']"
+
+ def get_primary_search_direction(self):
+ return 'to_left_of'
+
+ def get_secondary_search_direction(self):
+ return 'to_right_of'
+
class RadioButtonImpl(LabelledElement):
- def is_selected(self):
- return self.first_occurrence.get_attribute('checked') is not None
- def get_xpath(self):
- return "//input[@type='radio']"
- def get_primary_search_direction(self):
- return 'to_left_of'
- def get_secondary_search_direction(self):
- return 'to_right_of'
+ def is_selected(self):
+ return self.first_occurrence.get_attribute('checked') is not None
+
+ def get_xpath(self):
+ return "//input[@type='radio']"
+
+ def get_primary_search_direction(self):
+ return 'to_left_of'
+
+ def get_secondary_search_direction(self):
+ return 'to_right_of'
+
class WindowImpl(GUIElementImpl):
- def __init__(self, driver, title=None):
- super(WindowImpl, self).__init__(driver)
- self.search_title = title
- def find_all_occurrences(self):
- result_scores = []
- for handle in self._driver.window_handles:
- window = WindowImpl.SeleniumWindow(self._driver, handle)
- if self.search_title is None:
- result_scores.append((0, window))
- else:
- title = window.title
- if title.startswith(self.search_title):
- score = len(title) - len(self.search_title)
- result_scores.append((score, window))
- score = lambda tpl: tpl[0]
- result_scores.sort(key=score)
- for score, window in result_scores:
- yield window
- @property
- def title(self):
- return self.first_occurrence.title
- @property
- def handle(self):
- return self.first_occurrence.handle
- class SeleniumWindow:
- def __init__(self, driver, handle):
- self.driver = driver
- self.handle = handle
- self._window_handle_before = None
- @property
- def title(self):
- with self:
- return self.driver.title
- def __enter__(self):
- try:
- self._window_handle_before = self.driver.current_window_handle
- except NoSuchWindowException as window_closed:
- do_switch = True
- else:
- do_switch = self._window_handle_before != self.handle
- if do_switch:
- self.driver.switch_to.window(self.handle)
- def __exit__(self, *_):
- if self._window_handle_before and \
- self.driver.current_window_handle != self._window_handle_before:
- self.driver.switch_to.window(self._window_handle_before)
+ def __init__(self, driver, title=None):
+ super(WindowImpl, self).__init__(driver)
+ self.search_title = title
+
+ def find_all_occurrences(self):
+ result_scores = []
+ for handle in self._driver.window_handles:
+ window = WindowImpl.SeleniumWindow(self._driver, handle)
+ if self.search_title is None:
+ result_scores.append((0, window))
+ else:
+ title = window.title
+ if title.startswith(self.search_title):
+ score = len(title) - len(self.search_title)
+ result_scores.append((score, window))
+ score = lambda tpl: tpl[0]
+ result_scores.sort(key=score)
+ for score, window in result_scores:
+ yield window
+
+ @property
+ def title(self):
+ return self.first_occurrence.title
+
+ @property
+ def handle(self):
+ return self.first_occurrence.handle
+
+ class SeleniumWindow:
+ def __init__(self, driver, handle):
+ self.driver = driver
+ self.handle = handle
+ self._window_handle_before = None
+
+ @property
+ def title(self):
+ with self:
+ return self.driver.title
+
+ def __enter__(self):
+ try:
+ self._window_handle_before = self.driver.current_window_handle
+ except NoSuchWindowException as window_closed:
+ do_switch = True
+ else:
+ do_switch = self._window_handle_before != self.handle
+ if do_switch:
+ self.driver.switch_to.window(self.handle)
+
+ def __exit__(self, *_):
+ if self._window_handle_before and \
+ self.driver.current_window_handle != self._window_handle_before:
+ self.driver.switch_to.window(self._window_handle_before)
+
class AlertImpl(GUIElementImpl):
- def __init__(self, driver, search_text=None):
- super(AlertImpl, self).__init__(driver)
- self.search_text = search_text
- def find_all_occurrences(self):
- try:
- result = self._driver.switch_to.alert
- text = result.text
- if self.search_text is None or text.startswith(self.search_text):
- yield result
- except NoAlertPresentException:
- pass
- @property
- def text(self):
- return self.first_occurrence.text
- def accept(self):
- first_occurrence = self.first_occurrence
- try:
- first_occurrence.accept()
- except WebDriverException as e:
- # Attempt to work around Selenium issue 3544:
- # https://code.google.com/p/selenium/issues/detail?id=3544
- msg = e.msg
- if msg and re.match(
- r"a\.document\.getElementsByTagName\([^\)]*\)\[0\] is "
- r"undefined", msg
- ):
- sleep(0.25)
- first_occurrence.accept()
- else:
- raise
- def dismiss(self):
- self.first_occurrence.dismiss()
- def _write(self, text):
- self.first_occurrence.send_keys(text)
+ def __init__(self, driver, search_text=None):
+ super(AlertImpl, self).__init__(driver)
+ self.search_text = search_text
+
+ def find_all_occurrences(self):
+ try:
+ result = self._driver.switch_to.alert
+ text = result.text
+ if self.search_text is None or text.startswith(self.search_text):
+ yield result
+ except NoAlertPresentException:
+ pass
+
+ @property
+ def text(self):
+ return self.first_occurrence.text
+
+ def accept(self):
+ first_occurrence = self.first_occurrence
+ try:
+ first_occurrence.accept()
+ except WebDriverException as e:
+ # Attempt to work around Selenium issue 3544:
+ # https://code.google.com/p/selenium/issues/detail?id=3544
+ msg = e.msg
+ if msg and re.match(
+ r"a\.document\.getElementsByTagName\([^\)]*\)\[0\] is "
+ r"undefined", msg
+ ):
+ sleep(0.25)
+ first_occurrence.accept()
+ else:
+ raise
+
+ def dismiss(self):
+ self.first_occurrence.dismiss()
+
+ def _write(self, text):
+ self.first_occurrence.send_keys(text)
diff --git a/helium/_impl/match_type.py b/helium/_impl/match_type.py
index d4d053d..5558adf 100644
--- a/helium/_impl/match_type.py
+++ b/helium/_impl/match_type.py
@@ -1,36 +1,40 @@
from helium._impl.util.xpath import lower, replace_nbsp
+
class MatchType:
- def xpath(self, value, text):
- raise NotImplementedError()
- def text(self, value, text):
- raise NotImplementedError()
+ def xpath(self, value, text):
+ raise NotImplementedError()
+
+ def text(self, value, text):
+ raise NotImplementedError()
+
class PREFIX_IGNORE_CASE(MatchType):
- def xpath(self, value, text):
- if not text:
- return ''
- # Asterisks '*' are sometimes used to mark required fields. Eg.:
- #
- # The starts-with filter below would be too strict to include such
- # matches. To get around this, we ignore asterisks unless the searched
- # text itself contains one.
- if '*' in text:
- strip_asterisks = value
- else:
- strip_asterisks = "translate(%s, '*', '')" % value
-
- # if text contains apostrophes (single quotes) then they need to be
- # treated with care
- if "'" in text:
- text = "concat('%s')" % ("',\"'\",'".join(text.split("'")))
- else:
- text = "'%s'" % text
-
- return "starts-with(normalize-space(%s), %s)" % (
- lower(replace_nbsp(strip_asterisks)), text.lower()
- )
- def text(self, value, text):
- if not text:
- return True
- return value.lower().lstrip().startswith(text.lower())
\ No newline at end of file
+ def xpath(self, value, text):
+ if not text:
+ return ''
+ # Asterisks '*' are sometimes used to mark required fields. Eg.:
+ #
+ # The starts-with filter below would be too strict to include such
+ # matches. To get around this, we ignore asterisks unless the searched
+ # text itself contains one.
+ if '*' in text:
+ strip_asterisks = value
+ else:
+ strip_asterisks = "translate(%s, '*', '')" % value
+
+ # if text contains apostrophes (single quotes) then they need to be
+ # treated with care
+ if "'" in text:
+ text = "concat('%s')" % ("',\"'\",'".join(text.split("'")))
+ else:
+ text = "'%s'" % text
+
+ return "starts-with(normalize-space(%s), %s)" % (
+ lower(replace_nbsp(strip_asterisks)), text.lower()
+ )
+
+ def text(self, value, text):
+ if not text:
+ return True
+ return value.lower().lstrip().startswith(text.lower())
diff --git a/helium/_impl/selenium_wrappers.py b/helium/_impl/selenium_wrappers.py
index e642d12..933e31a 100644
--- a/helium/_impl/selenium_wrappers.py
+++ b/helium/_impl/selenium_wrappers.py
@@ -1,169 +1,203 @@
from helium._impl.util.geom import Rectangle
from selenium.common.exceptions import StaleElementReferenceException, \
- NoSuchFrameException, WebDriverException
+ NoSuchFrameException, WebDriverException
from selenium.webdriver.common.action_chains import ActionChains
from urllib.error import URLError
import sys
+
class Wrapper:
- def __init__(self, target):
- self.target = target
- def __getattr__(self, item):
- return getattr(self.target, item)
- def unwrap(self):
- return self.target
- def __hash__(self):
- return hash(self.target)
- def __eq__(self, other):
- return self.target == other.target
- def __ne__(self, other):
- return not self == other
+ def __init__(self, target):
+ self.target = target
+
+ def __getattr__(self, item):
+ return getattr(self.target, item)
+
+ def unwrap(self):
+ return self.target
+
+ def __hash__(self):
+ return hash(self.target)
+
+ def __eq__(self, other):
+ return self.target == other.target
+
+ def __ne__(self, other):
+ return not self == other
+
class WebDriverWrapper(Wrapper):
- def __init__(self, target):
- super(WebDriverWrapper, self).__init__(target)
- self.last_manipulated_element = None
- def action(self):
- return ActionChains(self.target)
- def get_distance_to_last_manipulated(self, web_element):
- if not self.last_manipulated_element:
- return 0
- try:
- if hasattr(self.last_manipulated_element, 'location'):
- last_location = self.last_manipulated_element.location
- return last_location.distance_to(web_element.location)
- except StaleElementReferenceException:
- return 0
- else:
- # No .location. This happens when last_manipulated_element is an
- # Alert or a Window.
- return 0
- def find_elements_by_name(self, name):
- # Selenium sometimes returns None. For robustness, we turn this into []:
- return self.target.find_elements_by_name(name) or []
- def find_elements_by_xpath(self, xpath):
- # Selenium sometimes returns None. For robustness, we turn this into []:
- return self.target.find_elements_by_xpath(xpath) or []
- def find_elements_by_css_selector(self, selector):
- # Selenium sometimes returns None. For robustness, we turn this into []:
- return self.target.find_elements_by_css_selector(selector) or []
- def is_firefox(self):
- return self.browser_name == 'firefox'
- @property
- def browser_name(self):
- return self.target.capabilities['browserName']
- def is_ie(self):
- return self.browser_name == 'internet explorer'
+ def __init__(self, target):
+ super(WebDriverWrapper, self).__init__(target)
+ self.last_manipulated_element = None
+
+ def action(self):
+ return ActionChains(self.target)
+
+ def get_distance_to_last_manipulated(self, web_element):
+ if not self.last_manipulated_element:
+ return 0
+ try:
+ if hasattr(self.last_manipulated_element, 'location'):
+ last_location = self.last_manipulated_element.location
+ return last_location.distance_to(web_element.location)
+ except StaleElementReferenceException:
+ return 0
+ else:
+ # No .location. This happens when last_manipulated_element is an
+ # Alert or a Window.
+ return 0
+
+ def find_elements_by_name(self, name):
+ # Selenium sometimes returns None. For robustness, we turn this into []:
+ return self.target.find_elements_by_name(name) or []
+
+ def find_elements_by_xpath(self, xpath):
+ # Selenium sometimes returns None. For robustness, we turn this into []:
+ return self.target.find_elements_by_xpath(xpath) or []
+
+ def find_elements_by_css_selector(self, selector):
+ # Selenium sometimes returns None. For robustness, we turn this into []:
+ return self.target.find_elements_by_css_selector(selector) or []
+
+ def is_firefox(self):
+ return self.browser_name == 'firefox'
+
+ @property
+ def browser_name(self):
+ return self.target.capabilities['browserName']
+
+ def is_ie(self):
+ return self.browser_name == 'internet explorer'
+
def _translate_url_errors_caused_by_server_shutdown(f):
- def f_decorated(*args, **kwargs):
- try:
- return f(*args, **kwargs)
- except URLError as url_error:
- if _is_caused_by_server_shutdown(url_error):
- raise StaleElementReferenceException(
- 'The Selenium server this element belonged to is no longer '
- 'available.'
- )
- else:
- raise
- return f_decorated
+ def f_decorated(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except URLError as url_error:
+ if _is_caused_by_server_shutdown(url_error):
+ raise StaleElementReferenceException(
+ 'The Selenium server this element belonged to is no longer '
+ 'available.'
+ )
+ else:
+ raise
+
+ return f_decorated
+
def _is_caused_by_server_shutdown(url_error):
- try:
- CONNECTION_REFUSED = 10061
- return url_error.args[0][0] == CONNECTION_REFUSED
- except (IndexError, TypeError):
- return False
+ try:
+ CONNECTION_REFUSED = 10061
+ return url_error.args[0][0] == CONNECTION_REFUSED
+ except (IndexError, TypeError):
+ return False
+
def handle_element_being_in_other_frame(f):
- def f_decorated(self, *args, **kwargs):
- if not self.frame_index:
- return f(self, *args, **kwargs)
- try:
- return f(self, *args, **kwargs)
- except StaleElementReferenceException as original_exc:
- try:
- frame_iterator = FrameIterator(self.target.parent)
- frame_iterator.switch_to_frame(self.frame_index)
- except NoSuchFrameException:
- raise original_exc
- else:
- return f(self, *args, **kwargs)
- return f_decorated
+ def f_decorated(self, *args, **kwargs):
+ if not self.frame_index:
+ return f(self, *args, **kwargs)
+ try:
+ return f(self, *args, **kwargs)
+ except StaleElementReferenceException as original_exc:
+ try:
+ frame_iterator = FrameIterator(self.target.parent)
+ frame_iterator.switch_to_frame(self.frame_index)
+ except NoSuchFrameException:
+ raise original_exc
+ else:
+ return f(self, *args, **kwargs)
+
+ return f_decorated
+
class WebElementWrapper:
- def __init__(self, target, frame_index=None):
- self.target = target
- self.frame_index = frame_index
- self._cached_location = None
- @property
- @handle_element_being_in_other_frame
- @_translate_url_errors_caused_by_server_shutdown
- def location(self):
- if self._cached_location is None:
- # Cache access to web_element.location as it's expensive:
- location = self.target.location
- x, y = location['x'], location['y']
- # Cache access to web_element.size as it's expensive:
- size = self.target.size
- width, height = size['width'], size['height']
- self._cached_location = Rectangle(x, y, width, height)
- return self._cached_location
- def is_displayed(self):
- try:
- return self.target.is_displayed() and self.location.intersects(
- Rectangle(0, 0, sys.maxsize, sys.maxsize)
- )
- except StaleElementReferenceException:
- return False
- @handle_element_being_in_other_frame
- def get_attribute(self, attr_name):
- return self.target.get_attribute(attr_name)
- @property
- @handle_element_being_in_other_frame
- def text(self):
- return self.target.text
- @handle_element_being_in_other_frame
- def clear(self):
- self.target.clear()
- @handle_element_being_in_other_frame
- def send_keys(self, keys):
- self.target.send_keys(keys)
- @property
- @handle_element_being_in_other_frame
- def tag_name(self):
- return self.target.tag_name
- def unwrap(self):
- return self.target
- def __repr__(self):
- return '<%s>%s%s>' % (self.tag_name, self.target.text, self.tag_name)
+ def __init__(self, target, frame_index=None):
+ self.target = target
+ self.frame_index = frame_index
+ self._cached_location = None
+
+ @property
+ @handle_element_being_in_other_frame
+ @_translate_url_errors_caused_by_server_shutdown
+ def location(self):
+ if self._cached_location is None:
+ # Cache access to web_element.location as it's expensive:
+ location = self.target.location
+ x, y = location['x'], location['y']
+ # Cache access to web_element.size as it's expensive:
+ size = self.target.size
+ width, height = size['width'], size['height']
+ self._cached_location = Rectangle(x, y, width, height)
+ return self._cached_location
+
+ def is_displayed(self):
+ try:
+ return self.target.is_displayed() and self.location.intersects(
+ Rectangle(0, 0, sys.maxsize, sys.maxsize)
+ )
+ except StaleElementReferenceException:
+ return False
+
+ @handle_element_being_in_other_frame
+ def get_attribute(self, attr_name):
+ return self.target.get_attribute(attr_name)
+
+ @property
+ @handle_element_being_in_other_frame
+ def text(self):
+ return self.target.text
+
+ @handle_element_being_in_other_frame
+ def clear(self):
+ self.target.clear()
+
+ @handle_element_being_in_other_frame
+ def send_keys(self, keys):
+ self.target.send_keys(keys)
+
+ @property
+ @handle_element_being_in_other_frame
+ def tag_name(self):
+ return self.target.tag_name
+
+ def unwrap(self):
+ return self.target
+
+ def __repr__(self):
+ return '<%s>%s%s>' % (self.tag_name, self.target.text, self.tag_name)
+
class FrameIterator:
- def __init__(self, driver, start_frame=None):
- if start_frame is None:
- start_frame = []
- self.driver = driver
- self.start_frame = start_frame
- def __iter__(self):
- yield []
- for new_frame in range(sys.maxsize):
- try:
- self.driver.switch_to.frame(new_frame)
- except WebDriverException:
- break
- else:
- new_start_frame = self.start_frame + [new_frame]
- for result in FrameIterator(self.driver, new_start_frame):
- yield [new_frame] + result
- try:
- self.switch_to_frame(self.start_frame)
- except NoSuchFrameException:
- raise FramesChangedWhileIterating()
- def switch_to_frame(self, frame_index_path):
- self.driver.switch_to.default_content()
- for frame_index in frame_index_path:
- self.driver.switch_to.frame(frame_index)
+ def __init__(self, driver, start_frame=None):
+ if start_frame is None:
+ start_frame = []
+ self.driver = driver
+ self.start_frame = start_frame
+
+ def __iter__(self):
+ yield []
+ for new_frame in range(sys.maxsize):
+ try:
+ self.driver.switch_to.frame(new_frame)
+ except WebDriverException:
+ break
+ else:
+ new_start_frame = self.start_frame + [new_frame]
+ for result in FrameIterator(self.driver, new_start_frame):
+ yield [new_frame] + result
+ try:
+ self.switch_to_frame(self.start_frame)
+ except NoSuchFrameException:
+ raise FramesChangedWhileIterating()
+
+ def switch_to_frame(self, frame_index_path):
+ self.driver.switch_to.default_content()
+ for frame_index in frame_index_path:
+ self.driver.switch_to.frame(frame_index)
+
class FramesChangedWhileIterating(Exception):
- pass
\ No newline at end of file
+ pass
diff --git a/helium/_impl/util/dictionary.py b/helium/_impl/util/dictionary.py
index 05b1b41..02a0a92 100644
--- a/helium/_impl/util/dictionary.py
+++ b/helium/_impl/util/dictionary.py
@@ -1,11 +1,11 @@
def inverse(dictionary):
- """
- {a: {b}} -> {b: {a}}
- """
- result = {}
- for key, values in dictionary.items():
- for value in values:
- if value not in result:
- result[value] = set()
- result[value].add(key)
- return result
\ No newline at end of file
+ """
+ {a: {b}} -> {b: {a}}
+ """
+ result = {}
+ for key, values in dictionary.items():
+ for value in values:
+ if value not in result:
+ result[value] = set()
+ result[value].add(key)
+ return result
diff --git a/helium/_impl/util/geom.py b/helium/_impl/util/geom.py
index e7136e8..848db29 100644
--- a/helium/_impl/util/geom.py
+++ b/helium/_impl/util/geom.py
@@ -1,200 +1,256 @@
from collections import namedtuple
from math import sqrt
+
class Rectangle:
- def __init__(self, left=0, top=0, width=0, height=0):
- self.left = left
- self.top = top
- self.right = left + width
- self.bottom = top + height
- @classmethod
- def from_w_h(cls, width, height):
- return cls(0, 0, width, height)
- @classmethod
- def from_tuple_l_t_w_h(cls, l_t_w_h=None):
- if l_t_w_h is None:
- l_t_w_h = (0, 0, 0, 0)
- return cls(*l_t_w_h)
- @classmethod
- def from_tuple_w_h(cls, w_h):
- return cls.from_w_h(*w_h)
- @classmethod
- def from_struct_l_t_r_b(cls, struct):
- return cls.from_l_t_r_b(
- struct.left, struct.top, struct.right, struct.bottom
- )
- @classmethod
- def from_l_t_r_b(cls, left, top, right, bottom):
- return cls(left, top, right - left, bottom - top)
- @property
- def width(self):
- return self.right - self.left
- @property
- def height(self):
- return self.bottom - self.top
- @property
- def center(self):
- return Point(self.left + self.width / 2, self.top + self.height / 2)
- @property
- def east(self):
- return self.clip(Point(self.right - 1, self.center.y))
- @property
- def west(self):
- return Point(self.left, self.center.y)
- @property
- def north(self):
- return Point(self.center.x, self.top)
- @property
- def south(self):
- return self.clip(Point(self.center.x, self.bottom - 1))
- @property
- def northeast(self):
- return Point(self.east.x, self.north.y)
- @property
- def southeast(self):
- return Point(self.east.x, self.south.y)
- @property
- def southwest(self):
- return Point(self.west.x, self.south.y)
- @property
- def northwest(self):
- return Point(self.west.x, self.north.y)
- @property
- def area(self):
- if not self:
- return 0
- return self.width * self.height
- def __contains__(self, point):
- return self.left <= point.x < self.right and \
- self.top <= point.y < self.bottom
- def translate(self, dx, dy):
- self.left += dx
- self.right += dx
- self.top += dy
- self.bottom += dy
- return self
- def clip(self, point):
- return Point(
- min(max(point[0], self.left), max(self.left, self.right - 1)),
- min(max(point[1], self.top), max(self.top, self.bottom - 1))
- )
- def intersect(self, rectangle):
- left = max(self.left, rectangle.left)
- top = max(self.top, rectangle.top)
- right = min(self.right, rectangle.right)
- bottom = min(self.bottom, rectangle.bottom)
- return self.from_l_t_r_b(left, top, right, bottom) or Rectangle()
- def intersects(self, rectangle):
- return bool(self.intersect(rectangle))
- def as_numpy_slice(self):
- return slice(self.top, self.bottom), slice(self.left, self.right)
- def is_to_left_of(self, other):
- self_starts_to_left_of_other = self.left < other.left
- self_overlaps_other_top = self.top <= other.top < self.bottom
- other_overlaps_self_top = other.top <= self.top < other.bottom
- return self_starts_to_left_of_other and (
- self_overlaps_other_top or
- other_overlaps_self_top
- )
- def is_to_right_of(self, other):
- return other.is_to_left_of(self)
- def is_above(self, other):
- self_starts_above_other = self.top < other.top
- self_overlaps_other_left = self.left <= other.left < self.right
- other_overlaps_self_left = other.left <= self.left < other.right
- return self_starts_above_other and (
- self_overlaps_other_left or
- other_overlaps_self_left
- )
- def is_below(self, other):
- return other.is_above(self)
- def is_in_direction(self, in_direction, of_other):
- return getattr(self, 'is_' + in_direction)(of_other)
- def distance_to(self, other):
- leftmost = self if self.left < other.left else other
- rightmost = self if leftmost == other else other
- distance_x = max(0, rightmost.left - leftmost.right)
- topmost = self if self.top < other.top else other
- bottommost = self if topmost == other else other
- distance_y = max(0, bottommost.top - topmost.bottom)
- return sqrt(distance_x ** 2 + distance_y ** 2)
- def __eq__(self, other):
- if not isinstance(other, Rectangle):
- return False
- return self.left == other.left and self.top == other.top and \
- self.right == other.right and self.bottom == other.bottom
- def __ne__(self, other):
- return not self.__eq__(other)
- def __bool__(self):
- return bool(self.width > 0 and self.height > 0)
- def __repr__(self):
- return type(self).__name__ + '(left=%d, top=%d, width=%d, height=%d)' \
- % (self.left, self.top, self.width, self.height)
- def __hash__(self):
- return self.left + 7 * self.top + 11 * self.right + 13 * self.bottom
+ def __init__(self, left=0, top=0, width=0, height=0):
+ self.left = left
+ self.top = top
+ self.right = left + width
+ self.bottom = top + height
+
+ @classmethod
+ def from_w_h(cls, width, height):
+ return cls(0, 0, width, height)
+
+ @classmethod
+ def from_tuple_l_t_w_h(cls, l_t_w_h=None):
+ if l_t_w_h is None:
+ l_t_w_h = (0, 0, 0, 0)
+ return cls(*l_t_w_h)
+
+ @classmethod
+ def from_tuple_w_h(cls, w_h):
+ return cls.from_w_h(*w_h)
+
+ @classmethod
+ def from_struct_l_t_r_b(cls, struct):
+ return cls.from_l_t_r_b(
+ struct.left, struct.top, struct.right, struct.bottom
+ )
+
+ @classmethod
+ def from_l_t_r_b(cls, left, top, right, bottom):
+ return cls(left, top, right - left, bottom - top)
+
+ @property
+ def width(self):
+ return self.right - self.left
+
+ @property
+ def height(self):
+ return self.bottom - self.top
+
+ @property
+ def center(self):
+ return Point(int(self.left + self.width / 2), int(self.top + self.height / 2))
+
+ @property
+ def east(self):
+ return self.clip(Point(self.right - 1, self.center.y))
+
+ @property
+ def west(self):
+ return Point(self.left, self.center.y)
+
+ @property
+ def north(self):
+ return Point(self.center.x, self.top)
+
+ @property
+ def south(self):
+ return self.clip(Point(self.center.x, self.bottom - 1))
+
+ @property
+ def northeast(self):
+ return Point(self.east.x, self.north.y)
+
+ @property
+ def southeast(self):
+ return Point(self.east.x, self.south.y)
+
+ @property
+ def southwest(self):
+ return Point(self.west.x, self.south.y)
+
+ @property
+ def northwest(self):
+ return Point(self.west.x, self.north.y)
+
+ @property
+ def area(self):
+ if not self:
+ return 0
+ return self.width * self.height
+
+ def __contains__(self, point):
+ return self.left <= point.x < self.right and \
+ self.top <= point.y < self.bottom
+
+ def translate(self, dx, dy):
+ self.left += dx
+ self.right += dx
+ self.top += dy
+ self.bottom += dy
+ return self
+
+ def clip(self, point):
+ return Point(
+ min(max(point[0], self.left), max(self.left, self.right - 1)),
+ min(max(point[1], self.top), max(self.top, self.bottom - 1))
+ )
+
+ def intersect(self, rectangle):
+ left = max(self.left, rectangle.left)
+ top = max(self.top, rectangle.top)
+ right = min(self.right, rectangle.right)
+ bottom = min(self.bottom, rectangle.bottom)
+ return self.from_l_t_r_b(left, top, right, bottom) or Rectangle()
+
+ def intersects(self, rectangle):
+ return bool(self.intersect(rectangle))
+
+ def as_numpy_slice(self):
+ return slice(self.top, self.bottom), slice(self.left, self.right)
+
+ def is_to_left_of(self, other):
+ self_starts_to_left_of_other = self.left < other.left
+ self_overlaps_other_top = self.top <= other.top < self.bottom
+ other_overlaps_self_top = other.top <= self.top < other.bottom
+ return self_starts_to_left_of_other and (
+ self_overlaps_other_top or
+ other_overlaps_self_top
+ )
+
+ def is_to_right_of(self, other):
+ return other.is_to_left_of(self)
+
+ def is_above(self, other):
+ self_starts_above_other = self.top < other.top
+ self_overlaps_other_left = self.left <= other.left < self.right
+ other_overlaps_self_left = other.left <= self.left < other.right
+ return self_starts_above_other and (
+ self_overlaps_other_left or
+ other_overlaps_self_left
+ )
+
+ def is_below(self, other):
+ return other.is_above(self)
+
+ def is_in_direction(self, in_direction, of_other):
+ return getattr(self, 'is_' + in_direction)(of_other)
+
+ def distance_to(self, other):
+ leftmost = self if self.left < other.left else other
+ rightmost = self if leftmost == other else other
+ distance_x = max(0, rightmost.left - leftmost.right)
+ topmost = self if self.top < other.top else other
+ bottommost = self if topmost == other else other
+ distance_y = max(0, bottommost.top - topmost.bottom)
+ return sqrt(distance_x ** 2 + distance_y ** 2)
+
+ def __eq__(self, other):
+ if not isinstance(other, Rectangle):
+ return False
+ return self.left == other.left and self.top == other.top and \
+ self.right == other.right and self.bottom == other.bottom
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __bool__(self):
+ return bool(self.width > 0 and self.height > 0)
+
+ def __repr__(self):
+ return type(self).__name__ + '(left=%d, top=%d, width=%d, height=%d)' \
+ % (self.left, self.top, self.width, self.height)
+
+ def __hash__(self):
+ return self.left + 7 * self.top + 11 * self.right + 13 * self.bottom
+
class Point(namedtuple('Point', ['x', 'y'])):
- def __new__(cls, x=0, y=0):
- return cls.__bases__[0].__new__(cls, x, y)
- def __init__(self, x=0, y=0):
- # tuple is immutable so can't do anything here. The initialization
- # happens in __new__(...) above.
- pass
- @classmethod
- def from_tuple(cls, tpl):
- return cls(*tpl)
- def __eq__(self, other):
- return (self.x, self.y) == other
- def __ne__(self, other):
- return not self == other
- def __add__(self, other):
- dx, dy = other
- return Point(self.x + dx, self.y + dy)
- def __radd__(self, other):
- return self.__add__(other)
- def __sub__(self, other):
- dx, dy = other
- return Point(self.x - dx, self.y - dy)
- def __rsub__(self, other):
- x, y = other
- dx, dy = self
- return Point(x - dx, y - dy)
- def __mul__(self, scalar):
- if isinstance(scalar, (int, float)):
- return Point(self.x * scalar, self.y * scalar)
- else:
- raise ValueError("Invalid argument")
- def __rmul__(self, scalar):
- return self.__mul__(scalar)
- def __div__(self, scalar):
- if isinstance(scalar, (int, float)):
- return Point(self.x / scalar, self.y / scalar)
- else:
- raise ValueError("Invalid argument")
- def __bool__(self):
- return bool(self.x) or bool(self.y)
+ def __new__(cls, x=0, y=0):
+ return cls.__bases__[0].__new__(cls, x, y)
+
+ def __init__(self, x=0, y=0):
+ # tuple is immutable so can't do anything here. The initialization
+ # happens in __new__(...) above.
+ pass
+
+ @classmethod
+ def from_tuple(cls, tpl):
+ return cls(*tpl)
+
+ def __eq__(self, other):
+ return (self.x, self.y) == other
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __add__(self, other):
+ dx, dy = other
+ return Point(self.x + dx, self.y + dy)
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+ dx, dy = other
+ return Point(self.x - dx, self.y - dy)
+
+ def __rsub__(self, other):
+ x, y = other
+ dx, dy = self
+ return Point(x - dx, y - dy)
+
+ def __mul__(self, scalar):
+ if isinstance(scalar, (int, float)):
+ return Point(self.x * scalar, self.y * scalar)
+ else:
+ raise ValueError("Invalid argument")
+
+ def __rmul__(self, scalar):
+ return self.__mul__(scalar)
+
+ def __div__(self, scalar):
+ if isinstance(scalar, (int, float)):
+ return Point(self.x / scalar, self.y / scalar)
+ else:
+ raise ValueError("Invalid argument")
+
+ def __bool__(self):
+ return bool(self.x) or bool(self.y)
+
class Direction:
- def __init__(self, unit_vector):
- self.unit_vector = unit_vector
- def iterate_points_starting_at(self, point, offsets):
- for offset in offsets:
- yield point + offset * self.unit_vector
- def is_horizontal(self):
- return bool(self.unit_vector.x)
- def is_vertical(self):
- return not self.is_horizontal()
- @property
- def orthog_vector(self):
- return Point(-self.unit_vector[1], self.unit_vector[0])
- def __eq__(self, other):
- return self.unit_vector == other.unit_vector
- def __repr__(self):
- for module_element in dir(self.__module__):
- if self == getattr(self.__module__, module_element):
- return module_element
+ def __init__(self, unit_vector):
+ self.unit_vector = unit_vector
+
+ def iterate_points_starting_at(self, point, offsets):
+ for offset in offsets:
+ yield point + offset * self.unit_vector
+
+ def is_horizontal(self):
+ return bool(self.unit_vector.x)
+
+ def is_vertical(self):
+ return not self.is_horizontal()
+
+ @property
+ def orthog_vector(self):
+ return Point(-self.unit_vector[1], self.unit_vector[0])
+
+ def __eq__(self, other):
+ return self.unit_vector == other.unit_vector
+
+ def __repr__(self):
+ for module_element in dir(self.__module__):
+ if self == getattr(self.__module__, module_element):
+ return module_element
+
NORTH = Direction(Point(0, -1))
EAST = Direction(Point(1, 0))
SOUTH = Direction(Point(0, 1))
-WEST = Direction(Point(-1, 0))
\ No newline at end of file
+WEST = Direction(Point(-1, 0))
diff --git a/helium/_impl/util/html.py b/helium/_impl/util/html.py
index 15b87bd..d53783b 100644
--- a/helium/_impl/util/html.py
+++ b/helium/_impl/util/html.py
@@ -1,40 +1,46 @@
from html.parser import HTMLParser
import re
+
def strip_tags(html):
- s = TagStripper()
- s.feed(html)
- return s.get_data()
+ s = TagStripper()
+ s.feed(html)
+ return s.get_data()
+
class TagStripper(HTMLParser):
- def __init__(self):
- HTMLParser.__init__(self)
- self.reset()
- self.fed = []
- def handle_data(self, d):
- self.fed.append(d)
- def get_data(self):
- return ''.join(self.fed)
+ def __init__(self):
+ HTMLParser.__init__(self)
+ self.reset()
+ self.fed = []
+
+ def handle_data(self, d):
+ self.fed.append(d)
+
+ def get_data(self):
+ return ''.join(self.fed)
+
def get_easily_readable_snippet(html):
- html = normalize_whitespace(html)
- try:
- inner_start = html.index('>') + 1
- inner_end = html.rindex('<', inner_start)
- except ValueError:
- return html
- opening_tag = html[:inner_start]
- closing_tag = html[inner_end:]
- inner = html[inner_start:inner_end]
- if '<' in inner or len(inner) > 60:
- return '%s...%s' % (opening_tag, closing_tag)
- else:
- return html
+ html = normalize_whitespace(html)
+ try:
+ inner_start = html.index('>') + 1
+ inner_end = html.rindex('<', inner_start)
+ except ValueError:
+ return html
+ opening_tag = html[:inner_start]
+ closing_tag = html[inner_end:]
+ inner = html[inner_start:inner_end]
+ if '<' in inner or len(inner) > 60:
+ return '%s...%s' % (opening_tag, closing_tag)
+ else:
+ return html
+
def normalize_whitespace(html):
- result = html.strip()
- # Remove multiple spaces:
- result = re.sub(r'\s+', ' ', result)
- # Remove spaces after opening or before closing tags:
- result = result.replace('> ', '>').replace(' <', '<')
- return result
\ No newline at end of file
+ result = html.strip()
+ # Remove multiple spaces:
+ result = re.sub(r'\s+', ' ', result)
+ # Remove spaces after opening or before closing tags:
+ result = result.replace('> ', '>').replace(' <', '<')
+ return result
diff --git a/helium/_impl/util/inspect_.py b/helium/_impl/util/inspect_.py
index b9fbf1a..4fb11a0 100644
--- a/helium/_impl/util/inspect_.py
+++ b/helium/_impl/util/inspect_.py
@@ -1,37 +1,38 @@
from helium._impl.util.lang import isbound
import inspect
+
def repr_args(f, args=None, kwargs=None, repr_fn=repr):
- if args is None:
- args = []
- if kwargs is None:
- kwargs = {}
- arg_names, _, _, defaults = inspect.getfullargspec(f)[:4]
- if isbound(f):
- # Skip 'self' parameter:
- arg_names = arg_names[1:]
- num_defaults = 0 if defaults is None else len(defaults)
- num_requireds = len(arg_names) - num_defaults
- result = []
- for i, arg_name in enumerate(arg_names):
- has_default = i >= len(arg_names) - num_defaults
- if has_default:
- default_value = defaults[i - num_requireds]
- if i < len(args): # Normal arg
- value = args[i]
- prefix = ''
- value_is_default = has_default and value == default_value
- elif arg_name in kwargs: # Keyword arg
- value = kwargs[arg_name]
- prefix = arg_name + '='
- value_is_default = has_default and value == default_value
- else: # Optional arg without given value
- value_is_default = True
- if not value_is_default:
- result.append(prefix + repr_fn(value))
- for vararg in args[len(arg_names):]:
- result.append(repr_fn(vararg))
- for kwarg in kwargs:
- if kwarg not in arg_names:
- result.append(kwarg + '=' + repr_fn(kwargs[kwarg]))
- return ', '.join(result)
\ No newline at end of file
+ if args is None:
+ args = []
+ if kwargs is None:
+ kwargs = {}
+ arg_names, _, _, defaults = inspect.getfullargspec(f)[:4]
+ if isbound(f):
+ # Skip 'self' parameter:
+ arg_names = arg_names[1:]
+ num_defaults = 0 if defaults is None else len(defaults)
+ num_requireds = len(arg_names) - num_defaults
+ result = []
+ for i, arg_name in enumerate(arg_names):
+ has_default = i >= len(arg_names) - num_defaults
+ if has_default:
+ default_value = defaults[i - num_requireds]
+ if i < len(args): # Normal arg
+ value = args[i]
+ prefix = ''
+ value_is_default = has_default and value == default_value
+ elif arg_name in kwargs: # Keyword arg
+ value = kwargs[arg_name]
+ prefix = arg_name + '='
+ value_is_default = has_default and value == default_value
+ else: # Optional arg without given value
+ value_is_default = True
+ if not value_is_default:
+ result.append(prefix + repr_fn(value))
+ for vararg in args[len(arg_names):]:
+ result.append(repr_fn(vararg))
+ for kwarg in kwargs:
+ if kwarg not in arg_names:
+ result.append(kwarg + '=' + repr_fn(kwargs[kwarg]))
+ return ', '.join(result)
diff --git a/helium/_impl/util/lang.py b/helium/_impl/util/lang.py
index b5be5f2..c6b2417 100644
--- a/helium/_impl/util/lang.py
+++ b/helium/_impl/util/lang.py
@@ -1,21 +1,24 @@
class TemporaryAttrValue:
- def __init__(self, obj, attr, value):
- self.obj = obj
- self.attr = attr
- self.value = value
- self.value_before = None
- def __enter__(self):
- self.value_before = getattr(self.obj, self.attr)
- setattr(self.obj, self.attr, self.value)
- def __exit__(self, *_):
- setattr(self.obj, self.attr, self.value_before)
- self.value_before = None
+ def __init__(self, obj, attr, value):
+ self.obj = obj
+ self.attr = attr
+ self.value = value
+ self.value_before = None
+
+ def __enter__(self):
+ self.value_before = getattr(self.obj, self.attr)
+ setattr(self.obj, self.attr, self.value)
+
+ def __exit__(self, *_):
+ setattr(self.obj, self.attr, self.value_before)
+ self.value_before = None
+
def isbound(method_or_fn):
- try:
- return method_or_fn.__self__ is not None
- except AttributeError: # Python 3
- try:
- return method_or_fn.__self__ is not None
- except AttributeError:
- return False
\ No newline at end of file
+ try:
+ return method_or_fn.__self__ is not None
+ except AttributeError: # Python 3
+ try:
+ return method_or_fn.__self__ is not None
+ except AttributeError:
+ return False
diff --git a/helium/_impl/util/os_.py b/helium/_impl/util/os_.py
index 56c740c..d449a50 100644
--- a/helium/_impl/util/os_.py
+++ b/helium/_impl/util/os_.py
@@ -1,5 +1,6 @@
from os import chmod, stat
from stat import S_IEXEC
+
def make_executable(file_path):
- chmod(file_path, stat(file_path).st_mode | S_IEXEC)
\ No newline at end of file
+ chmod(file_path, stat(file_path).st_mode | S_IEXEC)
diff --git a/helium/_impl/util/path.py b/helium/_impl/util/path.py
index ce4c6ed..25a2495 100644
--- a/helium/_impl/util/path.py
+++ b/helium/_impl/util/path.py
@@ -2,26 +2,29 @@
from os.path import split, isdir
from os import makedirs
+
def get_components(path):
- folders = []
- while True:
- path, folder = split(path)
- if folder != "":
- folders.append(folder)
- else:
- if path != "":
- folders.append(path)
- break
- return list(reversed(folders))
+ folders = []
+ while True:
+ path, folder = split(path)
+ if folder != "":
+ folders.append(folder)
+ else:
+ if path != "":
+ folders.append(path)
+ break
+ return list(reversed(folders))
+
def ensure_exists(path):
- """http://stackoverflow.com/a/600612/190597 (tzot)"""
- try:
- makedirs(path, exist_ok=True) # Python>3.2
- except TypeError:
- try:
- makedirs(path)
- except OSError as exc: # Python >2.5
- if exc.errno == EEXIST and isdir(path):
- pass
- else: raise
\ No newline at end of file
+ """http://stackoverflow.com/a/600612/190597 (tzot)"""
+ try:
+ makedirs(path, exist_ok=True) # Python>3.2
+ except TypeError:
+ try:
+ makedirs(path)
+ except OSError as exc: # Python >2.5
+ if exc.errno == EEXIST and isdir(path):
+ pass
+ else:
+ raise
diff --git a/helium/_impl/util/system.py b/helium/_impl/util/system.py
index 1d3c970..1e4b25e 100644
--- a/helium/_impl/util/system.py
+++ b/helium/_impl/util/system.py
@@ -2,20 +2,31 @@
Gives information about the current operating system.
"""
import sys
+import subprocess
+
def is_windows():
- return sys.platform in ('win32', 'cygwin')
+ return sys.platform in ('win32', 'cygwin')
+
def is_mac():
- return sys.platform == 'darwin'
+ return sys.platform == 'darwin'
+
def is_linux():
- return sys.platform.startswith('linux')
+ return sys.platform.startswith('linux')
+
def get_canonical_os_name():
- if is_windows():
- return 'windows'
- elif is_mac():
- return 'mac'
- elif is_linux():
- return 'linux'
\ No newline at end of file
+ if is_windows():
+ return 'windows'
+ elif is_mac():
+ # check apple cpu
+ result = subprocess.run(["sysctl", "-a", "machdep.cpu.brand_string"], stdout=subprocess.PIPE)
+ brand_string: str = result.stdout.decode('utf-8').strip().lower()
+ if 'apple' in brand_string:
+ return 'mac_m1'
+ else:
+ return 'mac'
+ elif is_linux():
+ return 'linux'
diff --git a/helium/_impl/util/xpath.py b/helium/_impl/util/xpath.py
index 68536b1..a3c20a0 100644
--- a/helium/_impl/util/xpath.py
+++ b/helium/_impl/util/xpath.py
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
def lower(text):
- alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝ'
- return "translate(%s, '%s', '%s')" % (text, alphabet, alphabet.lower())
+ alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝ'
+ return "translate(%s, '%s', '%s')" % (text, alphabet, alphabet.lower())
+
def replace_nbsp(text, by=' '):
- return "translate(%s, '\u00a0', %r)" % (text, by)
+ return "translate(%s, '\u00a0', %r)" % (text, by)
+
def predicate(condition):
- return '[%s]' % condition if condition else ''
+ return '[%s]' % condition if condition else ''
+
def predicate_or(*conditions):
- return predicate(' or '.join([c for c in conditions if c]))
\ No newline at end of file
+ return predicate(' or '.join([c for c in conditions if c]))
diff --git a/helium/_impl/webdrivers/linux/chromedriver b/helium/_impl/webdrivers/linux/chromedriver
index 4419ca5..b8b43e5 100755
Binary files a/helium/_impl/webdrivers/linux/chromedriver and b/helium/_impl/webdrivers/linux/chromedriver differ
diff --git a/helium/_impl/webdrivers/linux/geckodriver b/helium/_impl/webdrivers/linux/geckodriver
index ff08a41..1bd1945 100755
Binary files a/helium/_impl/webdrivers/linux/geckodriver and b/helium/_impl/webdrivers/linux/geckodriver differ
diff --git a/helium/_impl/webdrivers/mac/chromedriver b/helium/_impl/webdrivers/mac/chromedriver
index afa705f..21bbdc8 100755
Binary files a/helium/_impl/webdrivers/mac/chromedriver and b/helium/_impl/webdrivers/mac/chromedriver differ
diff --git a/helium/_impl/webdrivers/mac/geckodriver b/helium/_impl/webdrivers/mac/geckodriver
index 83a86e1..a94809b 100755
Binary files a/helium/_impl/webdrivers/mac/geckodriver and b/helium/_impl/webdrivers/mac/geckodriver differ
diff --git a/helium/_impl/webdrivers/mac_m1/chromedriver b/helium/_impl/webdrivers/mac_m1/chromedriver
new file mode 100755
index 0000000..0c635c9
Binary files /dev/null and b/helium/_impl/webdrivers/mac_m1/chromedriver differ
diff --git a/helium/_impl/webdrivers/mac_m1/geckodriver b/helium/_impl/webdrivers/mac_m1/geckodriver
new file mode 100755
index 0000000..cb62164
Binary files /dev/null and b/helium/_impl/webdrivers/mac_m1/geckodriver differ
diff --git a/helium/_impl/webdrivers/windows/chromedriver.exe b/helium/_impl/webdrivers/windows/chromedriver.exe
index d51602d..ffd8baa 100644
Binary files a/helium/_impl/webdrivers/windows/chromedriver.exe and b/helium/_impl/webdrivers/windows/chromedriver.exe differ
diff --git a/helium/_impl/webdrivers/windows/geckodriver.exe b/helium/_impl/webdrivers/windows/geckodriver.exe
old mode 100755
new mode 100644
index 9fae8e0..87b2b58
Binary files a/helium/_impl/webdrivers/windows/geckodriver.exe and b/helium/_impl/webdrivers/windows/geckodriver.exe differ
diff --git a/setup.py b/setup.py
index 73d2ee6..0d36ec2 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
setup(
name = 'helium',
# Also update docs/conf.py when you change this:
- version = '3.0.9-SNAPSHOT',
+ version = '3.0.10-RC',
author = 'Michael Herrmann',
author_email = 'michael+removethisifyouarehuman@herrmann.io',
description = 'Lighter browser automation based on Selenium.',