1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-03 00:07:05 +00:00

qa: add support for Nextcloud 25

This commit is contained in:
downtownallday 2022-11-10 10:20:54 -05:00
parent b4624b35eb
commit 49bcf7ba59
5 changed files with 147 additions and 49 deletions

View File

@ -0,0 +1,60 @@
#####
##### 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.common.exceptions import (
NoSuchElementException,
)
class NcContactsAutomation(object):
def __init__(self, nc):
''' `nc` is a NextcloudAutomation object '''
self.nc = nc
self.d = nc.d
def click_contact(self, contact):
d = self.d
d.say("Click contact %s", contact['email'])
found = False
# .list-item-content (nc 25+)
# .option__details (nc <25)
els = d.find_els('div.contacts-list div.list-item-content,div.option__details')
d.say_verbose('found %s contacts' % len(els))
for el in els:
# .line-one (nc 25+)
# .option__lineone (nc <25)
fullname = el.find_el('.line-one,.option__lineone').content().strip()
email = el.find_el('.line-two,.option__linetwo').content().strip()
d.say_verbose('contact: "%s" <%s>', fullname, email)
if fullname.lower() == "%s %s" % (contact['givenname'].lower(), contact['surname'].lower()) and email.lower() == contact['email'].lower():
found = True
el.click()
break
if not found: raise NoSuchElementException()
def wait_contact_loaded(self, secs=5):
d = self.d
d.say("Wait for contact to load")
d.wait_for_el('section.contact-details', secs=secs)
def delete_current_contact(self):
d = self.d
d.say("Delete current contact")
# Click ... menu
d.find_el('.contact-header__actions button.action-item__menutoggle').click()
# .v-popper__popper (nc 25+)
# .popover (nc <25)
el = d.wait_for_el(
'.v-popper__popper,.popover',
must_be_displayed=True,
secs=2
)
# click "delete"
# .delete-icon (nc 25+)
# .icon-delete (nc <25)
delete = el.find_el('span.delete-icon,span.icon-delete').click()

View File

@ -10,6 +10,7 @@
from selenium.common.exceptions import (
NoSuchElementException,
)
from .NcContactsAutomation import NcContactsAutomation
class NextcloudAutomation(object):
def __init__(self, d):
@ -26,56 +27,52 @@ class NextcloudAutomation(object):
d.say("Login %s to Nextcloud", login)
d.find_el('input#user').send_text(login)
d.find_el('input#password').send_text(pw)
d.find_el('#submit-wrapper').click()
submit = d.find_el('button[type="submit"]', throws=False)
# submit button for nextcloud < 25 (jquery)
if not submit: submit = d.find_el('#submit-wrapper') # nc<25
submit.click()
def logout(self):
d = self.d
d.say("Logout of Nextcloud")
d.find_el('header .avatardiv').click()
d.find_el('#settings .avatardiv').click()
d.find_el('[data-id="logout"] a').click()
def open_contacts(self):
d = self.d
d.say("Open contacts")
d.find_el('header [data-id="contacts"]').click()
# nc 25+
el = d.find_el('header [data-app-id="contacts"]', throws=False)
if not el:
# nc < 25
el = d.find_el('header [data-id="contacts"]')
self.close_first_run_wizard()
el.click()
return NcContactsAutomation(self)
def wait_for_app_load(self, secs=7):
d = self.d
d.say("Wait for app to load")
# some apps are vue, some jquery
# some apps are vue, some jquery (legacy)
vue = d.find_el('#app-content-vue', throws=False)
if not vue: vue = d.find_el('#app-dashboard', throws=False)
jquery = d.find_el('#app-content', throws=False)
if vue:
d.say_verbose('Waiting on a vue app')
d.execute_script('window.qa_app_loaded=false; window.setTimeout(() => { window.qa_app_loaded=true; }, 1000)');
d.wait_until_true('return window.qa_app_loaded === true', secs=secs)
d.wait_tick(1000)
elif jquery:
d.say_verbose('Waiting on a jquery app')
d.wait_until_true('return window.$.active == 0', secs=secs)
else:
raise NoSuchElementException('#app-content or #app-content-vue')
else:
raise NoSuchElementException('#app-dashboard, #app-content or #app-content-vue')
def click_contact(self, contact):
def close_first_run_wizard(self):
d = self.d
d.say("Click contact %s", contact['email'])
found = False
els = d.find_els('div.contacts-list div.option__details')
d.say_verbose('found %s contacts' % len(els))
for el in els:
fullname = el.find_el('.option__lineone').content().strip()
email = el.find_el('.option__linetwo').content().strip()
d.say_verbose('contact: "%s" <%s>', fullname, email)
if fullname.lower() == "%s %s" % (contact['givenname'].lower(), contact['surname'].lower()) and email.lower() == contact['email'].lower():
found = True
el.click()
break
if not found: raise NoSuchElementException()
def wait_contact_loaded(self, secs=5):
d = self.d
d.wait_for_el('section.contact-details', secs=secs)
firstrunwiz = d.find_el('#firstrunwizard', throws=False, quiet=True)
if firstrunwiz and firstrunwiz.is_displayed():
d.say_verbose("closing first run wizard")
d.find_el('#firstrunwizard span.close-icon').click()
d.wait_tick(1)

View File

@ -24,6 +24,7 @@ from selenium.common.exceptions import (
import os
import subprocess
import time
#
@ -96,7 +97,10 @@ class FirefoxTestDriver(Firefox):
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:
@ -139,7 +143,6 @@ class TestDriver(object):
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):
@ -163,6 +166,13 @@ class TestDriver(object):
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):
@ -264,6 +274,22 @@ class TestDriver(object):
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):
@ -278,12 +304,13 @@ class TestDriver(object):
if throws: raise e
else: return None
def find_el(self, css_selector, nth=0, throws=True):
self.say_verbose("find element: '%s'", css_selector)
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
@ -336,12 +363,14 @@ class TestDriver(object):
except TimeoutException as e:
pass
def execute_script(self, script, *args):
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):
@ -356,7 +385,7 @@ class TestDriver(object):
pass
def test_fn(driver):
nonlocal script, args
p = driver.execute_script(script, *args)
p = driver.execute_script(script, quiet=True, *args)
driver.say_verbose("script returned: %s", p)
if not p: raise NotTrue()
return True
@ -364,7 +393,12 @@ class TestDriver(object):
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)
@ -372,6 +406,10 @@ class TestDriver(object):
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):
@ -395,12 +433,13 @@ class ElWrapper(object):
self.driver = driver
self.el = el
def find_el(self, css_selector, nth=0, throws=True):
self.driver.say_verbose("find element: '%s'", css_selector)
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

View File

@ -41,11 +41,16 @@ try:
d.say_verbose('url: ' + d.current_url())
#
# login, then open the contacts app
# login
#
nc.login(login, pw)
nc.wait_for_app_load()
nc.open_contacts()
#
# open Contacts
#
d.start("Open contacts app")
contacts = nc.open_contacts()
nc.wait_for_app_load()
#
@ -53,18 +58,13 @@ try:
#
if op=='exists':
d.start("Check that contact %s exists", contact['email'])
nc.click_contact(contact) # raises NoSuchElementException if not found
contacts.click_contact(contact) # raises NoSuchElementException if not found
elif op=='delete':
d.start("Delete contact %s", contact['email'])
nc.click_contact(contact)
nc.wait_contact_loaded()
# click "..." menu
d.find_el('.contact-header__actions button.action-item__menutoggle').click()
d.wait_for_el('.popover', must_be_displayed=True, secs=2)
# click "delete"
d.find_el('.popover span.icon-delete').parent().click()
d.wait_for_el('div.empty-content', secs=2)
contacts.click_contact(contact)
contacts.wait_contact_loaded()
contacts.delete_current_contact()
else:
raise ValueError('Invalid operation: %s' % op)
@ -72,13 +72,14 @@ try:
#
# logout
#
d.start("Logout")
nc.logout()
nc.wait_for_login_screen()
#
# done
#
d.say("Success!")
d.start("Success!")
except Exception as e:
d.fail(e)

View File

@ -112,6 +112,7 @@ test_create_contact() {
fi
fi
delete_user "$alice"
test_end
}