class Browser: """A real web browser window that does what you want. This browser is modeled after Capybara: https://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL This DSL encourages single-call methods, to avoid races. It _discourages_ code like `browser.find('input[name="foo"]').click()`, because that has a race: what happens if the input disappears after `find()` and before `click()`? An exception -- which is not ideal. The DSL also encourages you to consider races with every line of code. `wait_for_element()` will stall until an element appears. And `not browser.exists(..., wait: true)` is different from `browser.not_exists(..., wait: true)`, since the latter will wait for the element to disappear. Keyword arguments: base_url -- automatic prefix to 'visit()' urls (default '') default_wait_timeout -- default timeout for 'wait_for_element()' etc, in s (default 9) """ def __init__(self, **kwargs): self.page = Session("selenium", None) self.base_url = _sanitize_base_url(kwargs.get('base_url') or '') # default wait timeout -- None means forever self.default_wait_timeout = kwargs.get('default_wait_timeout', 9) def _capybarize_kwargs(self, kwargs): """Modify kwargs in-place. Conversions: - Converts 'wait':True to 'wait':default_wait_timeout. """ if kwargs.get('wait') is True: kwargs['wait'] = self.default_wait_timeout def visit(self, url: str) -> None: """Type 'url' into the address bar, press Enter, and await onload.""" if url[0] == '/': url = self.base_url + url self.page.visit(url) def exec_js(self, js: str, *args: Any) -> Any: """Execute the given JavaScript on the page.""" self.page.execute_script(js, *args) def fill_in(self, locator: str, text: str, **kwargs) -> None: """Type 'text' into field with name/label/id 'locator'. Raises ValueError if text is empty. (Empty text is usually an error in test code.) Keyword arguments: wait -- True or number of seconds to wait until element appears """ if not text: raise ValueError("fill_in() called without text") kwargs['value'] = text self._capybarize_kwargs(kwargs) self.page.fill_in(locator, **kwargs) def fill_text_in_whatever(self, text: str, *selector, **kwargs) -> None: """Type 'text' into field matching selector. Raises ValueError if text is empty. (Empty text is usually an error in test code.) See 'assert_element()' for syntax. Prefer `fill_in`. All our HTML fields should have names or titles; anything else is an accessibility issue we should fix. Notice that this method's arguments are reversed from `fill_in`'s arguments. Keyword arguments: wait -- True or number of seconds to wait until element appears """ if not text: raise ValueError("fill_in() called without text") kwargs['value'] = text self._capybarize_kwargs(kwargs) # There's a race here between find() and fill_in(). If we get an error # about "missing element", write the exception handler we need. self.page.find(*selector, **kwargs).set(text) def send_keys(self, locator: str, *keys: str, **kwargs) -> None: """Press `keys` in field with name/label/id 'locator'. Raises ValueError if text is empty. (Empty text is usually an error in test code.) Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.find('fillable_field', locator, **kwargs).send_keys(*keys) def check(self, locator: str, **kwargs) -> None: """Check the checkbox with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.check(locator, **kwargs) def uncheck(self, locator: str, **kwargs) -> None: """Uncheck the checkbox with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.uncheck(locator, **kwargs) def select(self, locator: str, text: str, **kwargs) -> None: """Select 'text' in the select box with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) kwargs['field'] = locator self.page.select(text, **kwargs) def click_button(self, locator: str, **kwargs) -> None: """Click the button with name/id/text 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.click_button(locator, **kwargs) def click_link(self, locator: str, **kwargs) -> None: """Click the <a> with id/text/title 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.click_link(locator, **kwargs) def click_whatever(self, *selector, **kwargs) -> None: """Click the selected element. Raises unless 1 element matches the selector. Calling this method usually means the site has an accessibility problem. Prefer click_link() and click_button(): the user should be clicking on links and buttons to make things happen. See 'assert_element()' for syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) # There's a race here between find() and click(). If we get an error # about "missing element", write the exception handler we need. self.page.find(*selector, **kwargs).click() def double_click_whatever(self, *selector, **kwargs) -> None: """Double-click the selected element. Raises unless 1 element matches the selector. See 'assert_element()' for syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) # There's a race here between find() and execute(). If we get an # error about "missing element", write the exception handler we need. native_node = self.page.find(*selector, **kwargs).native # https://github.com/mozilla/geckodriver/issues/661 means we can't just # double_click(). We need to dispatch a JS event. # # [adamhooper, 2018-06-20] bug reproduced as late as today, which is # odd because the GitHub issue is marked resolved. I gave up # investigating. script = """ var ev = new MouseEvent( 'dblclick', { bubbles: true, cancelable: true, view: window } ) arguments[0].dispatchEvent(ev); """ self.page.driver.browser.execute_script(script, native_node) def hover_over_element(self, *selector, **kwargs) -> None: """Hover over the selected element. Raises unless 1 element matches the selector. Calling this method usually means the site has an accessibility problem. Not all users can hover. See 'assert_element()' for syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) # There's a race here between find() and hover(). If we get an error # about "missing element", write the exception handler we need. self.page.find(*selector, **kwargs).hover() def assert_element(self, *selector, **kwargs) -> None: """Test that 'selector' matches, or throws an error. Example selectors: - 'div.foo' - '#main' - 'xpath', '//h1[contains(text(), "foo")]' (two arguments, more complex than simply using the 'text' kwarg.) Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) self.page.assert_selector(*selector, **kwargs) def assert_no_element(self, *selector, **kwargs) -> None: """Test that 'selector' does _not_ match, or throws an error. Example selectors: - 'div.foo' - '#main' - 'xpath', '//h1[contains(text(), "foo")]' (two arguments, more complex than simply using the 'text' kwarg.) Keyword arguments: wait -- seconds to poll until the element goes away (default 0) text -- text the element we don't want to find must contain """ self._capybarize_kwargs(kwargs) self.page.assert_no_selector(*selector, **kwargs) def wait_for_element(self, *selector, **kwargs) -> None: """Polls until 'selector' matches; throws error on timeout. Keyword arguments: wait -- seconds to poll (default default_wait_timeout) text -- text the element must contain """ if 'wait' not in kwargs: kwargs['wait'] = self.default_wait_timeout self.assert_element(*selector, **kwargs) def text(self, *selector, **kwargs) -> str: """Returns text of element matching selector. See 'assert_element()' for selector syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) return self.page.find(*selector, **kwargs).all_text @contextmanager def scope(self, selector: str) -> None: """Within the given block, scopes all selectors within 'selector'. Example: with browser.scope('#root'): browser.assert_element('h2') """ with self.page.scope(selector): yield @contextmanager def iframe(self, *selector, **kwargs) -> None: """ Within the given block, `window` is the chosen iframe. Example: with browser.iframe('#root', wait=True): browser.assert_element('h2') """ # There's a race here: the iframe can disappear before we switch to it. # If this becomes an issue, find a fix. el = self.page.find(*selector, **kwargs) with self.page.frame(el): yield def clear_cookies(self) -> None: """Delete all cookies and navigates to a blank page.""" self.page.reset() def get_url(self) -> str: """Find the URL in the browser's address bar.""" return self.page.current_url def quit(self) -> None: """Destroys the browser and everything it created. """ self.page.driver.browser.quit() # hack Capybara's internals
class Browser: """A real web browser window that does what you want. This browser is modeled after Capybara: https://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL This DSL encourages single-call methods, to avoid races. It _discourages_ code like `browser.find('input[name="foo"]').click()`, because that has a race: what happens if the input disappears after `find()` and before `click()`? An exception -- which is not ideal. The DSL also encourages you to consider races with every line of code. `wait_for_element()` will stall until an element appears. And `not browser.exists(..., wait: true)` is different from `browser.not_exists(..., wait: true)`, since the latter will wait for the element to disappear. Keyword arguments: base_url -- automatic prefix to 'visit()' urls (default '') default_wait_timeout -- default timeout for 'wait_for_element()' etc, in s (default 5) """ def __init__(self, **kwargs): self.page = Session("selenium", None) self.base_url = _sanitize_base_url(kwargs.get('base_url') or '') # default wait timeout -- None means forever self.default_wait_timeout = kwargs.get('default_wait_timeout', 5) def _capybarize_kwargs(self, kwargs): """In-place modifies kwargs. Conversions: - Converts 'wait':True to 'wait':default_wait_timeout. """ if kwargs.get('wait') is True: kwargs['wait'] = self.default_wait_timeout def visit(self, url: str) -> None: """Types 'url' into the address bar, presses Enter, and awaits onload. """ if url[0] == '/': url = self.base_url + url self.page.visit(url) def fill_in(self, locator: str, text: str, **kwargs) -> None: """Types 'text' into field with name/label/id 'locator'. Raises ValueError if text is empty. (Empty text is usually an error in test code.) Keyword arguments: wait -- True or number of seconds to wait until element appears """ if not text: raise ValueError("fill_in() called without text") kwargs['value'] = text self._capybarize_kwargs(kwargs) self.page.fill_in(locator, **kwargs) def check(self, locator: str, **kwargs) -> None: """Check the checkbox with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.check(locator, **kwargs) def uncheck(self, locator: str, **kwargs) -> None: """Unheck the checkbox with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.uncheck(locator, **kwargs) def select(self, locator: str, text: str, **kwargs) -> None: """Selects 'text' in the select box with name/label/id 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) kwargs['field'] = locator self.page.select(text, **kwargs) def click_button(self, locator: str, **kwargs) -> None: """Clicks the button with name/id/text 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.click_button(locator, **kwargs) def click_link(self, locator: str, **kwargs) -> None: """Clicks the <a> with id/text/title 'locator'. Keyword arguments: wait -- True or number of seconds to wait until element appears """ self._capybarize_kwargs(kwargs) self.page.click_link(locator, **kwargs) def click_whatever(self, *selector, **kwargs) -> None: """Clicks the selected element. Raises unless 1 element matches the selector. Calling this method usually means the site has an accessibility problem. Prefer click_link() and click_button(): the user should be clicking on links and buttons to make things happen. See 'assert_element()' for syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) # There's a race here between find() and click(). If we get an error # about "missing element", write the exception handler we need. self.page.find(*selector, **kwargs).click() def hover_over_element(self, *selector, **kwargs) -> None: """Clicks the selected element. Raises unless 1 element matches the selector. Calling this method usually means the site has an accessibility problem. Not all users can hover. See 'assert_element()' for syntax. Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) # There's a race here between find() and click(). If we get an error # about "missing element", write the exception handler we need. self.page.find(*selector, **kwargs).click() def assert_element(self, *selector, **kwargs) -> None: """Tests that 'selector' matches, or throws an error. Example selectors: - 'div.foo' - '#main' - 'xpath', '//h1[contains(text(), "foo")]' (two arguments, more complex than simply using the 'text' kwarg.) Keyword arguments: wait -- seconds to poll (default 0) text -- text the element must contain """ self._capybarize_kwargs(kwargs) self.page.assert_selector(*selector, **kwargs) def assert_no_element(self, *selector, **kwargs) -> None: """Tests that 'selector' does _not_ match, or throws an error. Example selectors: - 'div.foo' - '#main' - 'xpath', '//h1[contains(text(), "foo")]' (two arguments, more complex than simply using the 'text' kwarg.) Keyword arguments: wait -- seconds to poll until the element goes away (default 0) text -- text the element we don't want to find must contain """ self._capybarize_kwargs(kwargs) self.page.assert_no_selector(*selector, **kwargs) def wait_for_element(self, *selector, **kwargs) -> None: """Polls until 'selector' matches; throws error on timeout. Keyword arguments: wait -- seconds to poll (default default_wait_timeout) text -- text the element must contain """ if 'wait' not in kwargs: kwargs['wait'] = self.default_wait_timeout self.assert_element(*selector, **kwargs) @contextmanager def scope(self, selector: str) -> None: """Within the given block, scopes all selectors within 'selector'. Example: with browser.scope('#root'): browser.assert_element('h2') """ with self.page.scope(selector): yield def quit(self) -> None: """Destroys the browser and everything it created. """ self.page.driver.browser.quit() # hack Capybara's internals