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' % (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' % (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.',