##### ##### This file is part of Mail-in-a-Box-LDAP which is released under the ##### terms of the GNU Affero General Public License as published by the ##### Free Software Foundation, either version 3 of the License, or (at ##### your option) any later version. See file LICENSE or go to ##### https://github.com/downtownallday/mailinabox-ldap for full license ##### details. ##### from selenium.webdriver import ( Chrome, ChromeOptions, Firefox, FirefoxOptions ) from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.common.by import By from selenium.common.exceptions import ( NoSuchElementException, TimeoutException, ElementNotInteractableException ) import os import subprocess import time # # chrome: # snap install chromium # # firefox: # apt-get install firefox # get the latest compiled geckodriver from: # https://github.com/mozilla/geckodriver/releases # copy into /usr/local/bin # # all: # pip3 install selenium (python 3.7 is required by selenium) # # OLD: for headless firefox (before firefox supported headless) # apt-get -y install xorg xvfb gtk2-engines-pixbuf # apt-get -y install dbus-x11 xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable # apt-get -y install imagemagick x11-apps # # before running tests, create an X frame buffer display: # Xvfb -ac :99 -screen 0 1280x1024x16 & export DISPLAY=:99 # class ChromeTestDriver(Chrome): def __init__(self, options=None): '''Initialze headless chrome. If problems arise, try running from the command line: `chromium --headless http://localhost/mail/` ''' if not options: options = ChromeOptions() options.headless = True # set a window size options.add_argument("--window-size=1200x600") # deal with ssl certificates since chrome has its own # trusted ca list and does not use the system's options.add_argument('--allow-insecure-localhost') options.add_argument('--ignore-certificate-errors') # required to run chromium as root options.add_argument('--no-sandbox') super(ChromeTestDriver, self).__init__( executable_path='/snap/bin/chromium.chromedriver', options=options ) self.delete_all_cookies() class FirefoxTestDriver(Firefox): ''' TODO: untested ''' def __init__(self, options=None): if not options: options = FirefoxOptions() options.headless = True super(FirefoxTestDriver, self).__init__( executable_path='/usr/local/bin/geckodriver', options=options ) self.delete_all_cookies() class TestDriver(object): def __init__(self, driver=None, verbose=None, base_url=None, output_path=None): self.first_start_time = None self.start_time = None self.start_msg = [] self.next_tick_id = 0 if driver is None: if 'BROWSER_TESTS_BROWSER' in os.environ: driver = os.environ['BROWSER_TESTS_BROWSER'] else: driver = 'chrome' if isinstance(driver, str): driver = TestDriver.createByName(driver) self.driver = driver if verbose is None: if 'BROWSER_TESTS_VERBOSITY' in os.environ: verbose = int(os.environ['BROWSER_TESTS_VERBOSITY']) else: verbose = 1 self.verbose = verbose if base_url is None: if 'BROWSER_TESTS_BASE_URL' in os.environ: base_url = os.environ['BROWSER_TESTS_BASE_URL'] else: hostname = subprocess.check_output(['/bin/hostname','--fqdn']) base_url = "https://%s" % hostname.decode('utf-8').strip() self.base_url = base_url if output_path is None: if 'BROWSER_TESTS_OUTPUT_PATH' in os.environ: output_path = os.environ['BROWSER_TESTS_OUTPUT_PATH'] else: output_path= "./" self.output_path = output_path @staticmethod def createByName(name): if name == 'chrome': return ChromeTestDriver() elif name == 'firefox': return FirefoxTestDriver() raise ValueError('no such driver named "%s"' % name) def _say(self, loglevel, heirarchy_level, *args): if self.verbose >= loglevel: for i in range(len(self.start_msg), heirarchy_level+1): self.start_msg.append(None) self.start_msg = self.start_msg[0:heirarchy_level] indent = 0 for item in self.start_msg: if item is not None: indent += 1 msg = args[0] % (args[1:]) self.start_msg.append(msg) print(' '*indent + msg + ' ') def is_verbose(self): return self.verbose >= 2 def say_verbose(self, *args): self._say(2, 2, *args) def say(self, *args): self._say(1, 1, *args) def start(self, *args): now = time.time() if self.start_time is not None: elapsed = format(now - self.start_time, '.1f') self._say(2, 0, '[%s: %s seconds]\n', self.start_msg[0], elapsed) else: self.first_start_time = now self.start_time = now self._say(1, 0, *args) def last_start(self): msg = [] for item in self.start_msg: if item is not None: msg.append(item) return " / ".join(msg) def get(self, url): ''' load a web page in the current browser session ''' if not url.startswith('http'): url = self.base_url + url self.say_verbose('get %s', url) self.driver.get(url) return self def title(self): return self.driver.title def current_url(self): return self.driver.current_url def refresh(self): self.driver.refresh() return self def get_current_window_handle(self): ''' returns the string id of the current window/tab ''' return self.driver.current_window_handle def get_window_handles(self): ''' returns an array of strings, one for each window or tab open ''' return self.driver.window_handles def switch_to_window(self, handle): ''' returns the current window handle ''' cur = self.get_current_window_handle() self.driver.switch_to.window(handle) return cur def switch_to_frame(self, iframe_el): self.driver.switch_to.frame(iframe_el.el) def switch_to_parent_frame(self, iframe_el): # untested self.driver.switch_to.parent_frame(iframe_el.el) def save_screenshot(self, where, ignore_errors=False, quiet=True): ''' where - path and file name of screen shotfile eg: "out/screenshot.png". ''' if not where.startswith('/'): where = os.path.join(self.output_path, where) try: os.makedirs(os.path.dirname(where), exist_ok=True) self._say(1 if quiet else 2, 2, "save screenshot: '%s'", where) self.driver.save_screenshot(where) except Exception as e: if not ignore_errors: raise e def delete_cookie(self, name): self.driver.delete_cookie(name) def wait_for_id(self, id, secs=5, throws=True): return self.wait_for_el('#' + id, secs=secs, throws=throws) def wait_for_el(self, css_selector, secs=5, throws=True, must_be_enabled=False, must_be_displayed=None): msg=[] if must_be_enabled: msg.append('enabled') if must_be_displayed is True: msg.append('displayed') elif must_be_displayed is False: msg.append('hidden') if len(msg)==0: self.say_verbose("wait for selector '%s' (%ss)", css_selector, secs) else: self.say_verbose("wait for selector '%s' to be %s (%ss)", css_selector, ",".join(msg), secs) def test_fn(driver): found_el = driver.find_element(By.CSS_SELECTOR, css_selector) if must_be_enabled and not found_el.is_enabled(): raise NoSuchElementException() if must_be_displayed is not None: if must_be_displayed and not found_el.is_displayed(): raise NoSuchElementException() if not must_be_displayed and found_el.is_displayed(): raise NoSuchElementException() return found_el wait = WebDriverWait(self.driver, secs, ignored_exceptions= ( NoSuchElementException )) try: rtn = wait.until(test_fn) return ElWrapper(self, rtn) except TimeoutException as e: if throws: raise e else: return None def wait_for_el_not_exists(self, css_selector, secs=5, throws=True): self.say_verbose("wait for selector '%s' (%ss) to not exist", css_selector, secs) def test_fn(driver): found_el = driver.find_element(By.CSS_SELECTOR, css_selector) if found_el: raise NoSuchElementException() wait = WebDriverWait(self.driver, secs, ignored_exceptions= ( NoSuchElementException )) try: wait.until(test_fn) return True except TimeoutException as e: if throws: raise e else: return None def wait_for_text(self, text, tag='*', secs=5, exact=False, throws=True, case_sensitive=False): self.say_verbose("wait for text '%s'", text) def test_fn(driver): return self.find_text(text, tag=tag, exact=exact, throws=False, quiet=True, case_sensitive=case_sensitive) wait = WebDriverWait(self.driver, secs, ignored_exceptions= ( NoSuchElementException )) try: rtn = wait.until(test_fn) return rtn except TimeoutException as e: if throws: raise e else: return None def find_el(self, css_selector, nth=0, throws=True, quiet=False): try: els = self.driver.find_elements(By.CSS_SELECTOR, css_selector) if len(els)==0: if not quiet: self.say_verbose("find element: '%s' (not found)", css_selector) raise NoSuchElementException("selector=%s" % css_selector) if not quiet: self.say_verbose("find element: '%s' (returning #%s/%s)", css_selector, nth+1, len(els)) return ElWrapper(self, els[nth]) except (IndexError, NoSuchElementException) as e: if throws: raise e else: return None def find_els(self, css_selector, throws=True, displayed=False): self.say_verbose("find elements: '%s'", css_selector) try: els = self.driver.find_elements(By.CSS_SELECTOR, css_selector) return [ ElWrapper(self, el) for el in els if not displayed or el.is_displayed() ] except (IndexError, NoSuchElementException) as e: if throws: raise e else: return None def find_text(self, text, tag='*', exact=False, throws=True, quiet=False, case_sensitive=False): if not quiet: self.say_verbose("find text: '%s' tag=%s exact=%s", text, tag, exact) try: if exact: if case_sensitive: xpath = "//%s[normalize-space(text()) = '%s']" % (tag, text) else: uc = text.upper() lc = text.lower() xpath = "//%s[normalize-space(translate(text(), '%s', '%s')) = '%s']" % (tag, lc, uc, uc) else: if case_sensitive: xpath = "//%s[contains(text(),'%s')]" % (tag, text) else: uc = text.upper() lc = text.lower() xpath = "//%s[contains(translate(text(),'%s','%s'),'%s')]" % (tag, lc, uc, uc) el = self.driver.find_element(by=By.XPATH, value=xpath) return ElWrapper(self, el) except NoSuchElementException as e: if throws: raise e else: return None def sleep(self, secs): self.say_verbose('sleep %s secs', secs) def test_fn(driver): raise NoSuchElementException wait = WebDriverWait(self.driver, secs, ignored_exceptions= ( NoSuchElementException )) try: wait.until(test_fn) except TimeoutException as e: pass def execute_script(self, script, quiet=False, *args): ''' Synchronously Executes JavaScript in the current window/frame ''' newargs = [] for arg in args: if isinstance(arg, ElWrapper): newargs.append(arg.el) else: newargs.append(arg) if not quiet: self.say_verbose('execute script: %s', script.replace('\n',' ')) return self.driver.execute_script(script, *newargs) def execute_async_script(self, script, secs=5, *args): ''' Asynchronously Executes JavaScript in the current window/frame ''' self.driver.set_script_timeout(secs) self.driver.execute_async_script(script, *args) def wait_until_true(self, script, secs=5, *args): self.say_verbose('run script until true: %s', script) d = self class NotTrue(Exception): pass def test_fn(driver): nonlocal script, args p = driver.execute_script(script, quiet=True, *args) driver.say_verbose("script returned: %s", p) if not p: raise NotTrue() return True wait = WebDriverWait(self, secs, ignored_exceptions= ( NotTrue )) wait.until(test_fn) # throws TimeoutException def wait_tick(self, delay_ms, secs=5): # allow time for vue to render (delay_ms>=1) cancel_id = self.execute_script('window.qa_ticked=false; return window.setTimeout(() => { window.qa_ticked=true; }, %s)' % delay_ms); self.wait_until_true('return window.qa_ticked === true', secs=secs) def close(self): ''' close the window/tab ''' self.say_verbose("closing %s", self.driver.current_url) self.driver.close() def quit(self): ''' closes the browser and shuts down the chromedriver executable ''' now = time.time() if self.first_start_time is not None: elapsed = format(now - self.first_start_time, '.1f') self._say(2, 0, '[TOTAL TIME: %s seconds]\n', elapsed) self.driver.quit() def fail(self, exception): last_start = self.last_start() self.start("Failure!") self.save_screenshot('screenshot.png', ignore_errors=False, quiet=False) if hasattr(exception, 'msg') and exception.msg != '': exception.msg = "Error during '%s': %s" % (last_start, exception.msg) else: exception.msg = "Error during '%s'" % last_start class ElWrapper(object): '''see: https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/remote/webelement.py ''' def __init__(self, driver, el): self.driver = driver self.el = el def find_el(self, css_selector, nth=0, throws=True, quiet=False): try: els = self.el.find_elements(By.CSS_SELECTOR, css_selector) if len(els)==0: if not quiet: self.driver.say_verbose("find element: '%s' (not found)", css_selector) raise NoSuchElementException("selector=%s" % css_selector) if not quiet: self.driver.say_verbose("find element: '%s' (returning #%s/%s)", css_selector, nth+1, len(els)) return ElWrapper(self.driver, els[nth]) except (IndexError, NoSuchElementException) as e: if throws: raise e else: return None def find_els(self, css_selector, throws=True, displayed=False): self.driver.say_verbose("find elements: '%s'", css_selector) try: els = self.el.find_elements(By.CSS_SELECTOR, css_selector) return [ ElWrapper(self.driver, el) for el in els if not displayed or el.is_displayed() ] except (IndexError, NoSuchElementException) as e: if throws: raise e else: return None def is_enabled(self): return self.el.is_enabled() def is_checked(self): """ a checkbox or radio button is checked """ return self.el.is_selected() def is_displayed(self): """Whether the self.element is visible to a user.""" return self.el.is_displayed() def get_attribute(self, name): return self.el.get_attribute(name) def get_property(self, expr): self.driver.say_verbose('get property %s', expr) prefix = '.' if expr.startswith('.') or expr.startswith('['): prefix='' p = self.driver.execute_script( "return arguments[0]%s%s;" % ( prefix, expr ), self.el ) if isinstance(p, WebElement): p = ElWrapper(self.driver, p) if isinstance(p, bool): self.driver.say_verbose('property result: %s', p) else: self.driver.say_verbose('property result: %s', p.__class__) return p def content(self, max_length=None, ellipses=True): txt = self.el.text if not max_length or len(txt)<max_length: return txt if ellipses: return txt[0:max_length] + '...' return txt[0:max_length] def tag(self): return self.el.tag_name def location(self): """ returns dictionary {x:N, y:N} """ return self.el.location() def rect(self): return self.el.rect() def parent(self): # get the parent element p = self.driver.execute_script( "return arguments[0].parentNode;", self.el ) return ElWrapper(self.driver, p) def send_text(self, *value): self.driver.say_verbose("send text '%s'", "/".join(value)) self.send_keys(*value) return self def send_keys(self, *value): self.el.send_keys(*value) return self def clear_text(self): self.el.clear() return self def click(self): if self.driver.is_verbose(): content = self.content(max_length=40).replace('\n',' ').strip() tag = self.tag() if tag=='a': tag='link' if content == '': content=self.get_attribute('href') if content != '': self.driver.say_verbose("click %s '%s'", tag, content) else: self.driver.say_verbose("click %s", tag) self.el.click() return self #dir(el) #['__abstractmethods__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_abc_impl', '_execute', '_id', '_parent', '_upload', 'accessible_name', 'aria_role', 'clear', 'click', 'find_element', 'find_elements', 'get_attribute', 'get_dom_attribute', 'get_property', 'id', 'is_displayed', 'is_enabled', 'is_selected', 'location', 'location_once_scrolled_into_view', 'parent', 'rect', 'screenshot', 'screenshot_as_base64', 'screenshot_as_png', 'send_keys', 'shadow_root', 'size', 'submit', 'tag_name', 'text', 'value_of_css_property'] #dir(driver) #['__abstractmethods__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_abc_impl', '_authenticator_id', '_file_detector', '_get_cdp_details', '_is_remote', '_mobile', '_shadowroot_cls', '_switch_to', '_unwrap_value', '_web_element_cls', '_wrap_value', 'add_cookie', 'add_credential', 'add_virtual_authenticator', 'application_cache', 'back', 'bidi_connection', 'capabilities', 'caps', 'close', 'command_executor', 'create_options', 'create_web_element', 'current_url', 'current_window_handle', 'delete_all_cookies', 'delete_cookie', 'delete_network_conditions', 'desired_capabilities', 'error_handler', 'execute', 'execute_async_script', 'execute_cdp_cmd', 'execute_script', 'file_detector', 'file_detector_context', 'find_element', 'find_elements', 'forward', 'fullscreen_window', 'get', 'get_cookie', 'get_cookies', 'get_credentials', 'get_issue_message', 'get_log', 'get_network_conditions', 'get_pinned_scripts', 'get_screenshot_as_base64', 'get_screenshot_as_file', 'get_screenshot_as_png', 'get_sinks', 'get_window_position', 'get_window_rect', 'get_window_size', 'implicitly_wait', 'launch_app', 'log_types', 'maximize_window', 'minimize_window', 'mobile', 'name', 'orientation', 'page_source', 'pin_script', 'pinned_scripts', 'port', 'print_page', 'quit', 'refresh', 'remove_all_credentials', 'remove_credential', 'remove_virtual_authenticator', 'save_screenshot', 'service', 'session_id', 'set_network_conditions', 'set_page_load_timeout', 'set_permissions', 'set_script_timeout', 'set_sink_to_use', 'set_user_verified', 'set_window_position', 'set_window_rect', 'set_window_size', 'start_client', 'start_desktop_mirroring', 'start_session', 'start_tab_mirroring', 'stop_casting', 'stop_client', 'switch_to', 'timeouts', 'title', 'unpin', 'vendor_prefix', 'virtual_authenticator_id', 'window_handles']