##### ##### 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)