summaryrefslogtreecommitdiff
path: root/service/test/functional/features
diff options
context:
space:
mode:
Diffstat (limited to 'service/test/functional/features')
-rw-r--r--service/test/functional/features/attachments.feature27
-rw-r--r--service/test/functional/features/checkboxes_and_mailboxes.feature3
-rw-r--r--service/test/functional/features/environment.py32
-rw-r--r--service/test/functional/features/steps/attachments.py55
-rw-r--r--service/test/functional/features/steps/common.py69
-rw-r--r--service/test/functional/features/steps/compose.py5
-rw-r--r--service/test/functional/features/steps/data_setup.py10
-rw-r--r--service/test/functional/features/steps/mail_list.py51
-rw-r--r--service/test/functional/features/steps/search.py9
-rw-r--r--service/test/functional/features/steps/tag_list.py31
10 files changed, 238 insertions, 54 deletions
diff --git a/service/test/functional/features/attachments.feature b/service/test/functional/features/attachments.feature
new file mode 100644
index 00000000..19834a9d
--- /dev/null
+++ b/service/test/functional/features/attachments.feature
@@ -0,0 +1,27 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it 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.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+Feature: Attachments
+ As a user of Pixelated
+ I want to download attachments of mails I received
+ So that my peers are able to send me any kind of content, not just text
+
+ Scenario: User opens a mail attachment
+ Given I have a mail with an attachment in my inbox
+ When I open the first mail in the 'inbox'
+ Then I see the mail has an attachment
+ #When I open click on the first attachment
+ #Then the browser downloaded a file
diff --git a/service/test/functional/features/checkboxes_and_mailboxes.feature b/service/test/functional/features/checkboxes_and_mailboxes.feature
index 47ea806d..09710040 100644
--- a/service/test/functional/features/checkboxes_and_mailboxes.feature
+++ b/service/test/functional/features/checkboxes_and_mailboxes.feature
@@ -21,7 +21,8 @@ Feature: Checkboxes
Scenario: User has a list of emails in each mailboxes that needs to be managed
Given I have a mail in my inbox
- When I mark the first unread email as read
+ When I select the tag 'inbox'
+ And I mark the first unread email as read
And I delete the email
When I select the tag 'trash'
Then the deleted mail is there
diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py
index 437529b8..53b13047 100644
--- a/service/test/functional/features/environment.py
+++ b/service/test/functional/features/environment.py
@@ -16,25 +16,37 @@
import logging
import uuid
+from crochet import setup, wait_for
+from leap.common.events.server import ensure_server
+from twisted.internet import defer
from test.support.dispatcher.proxy import Proxy
from test.support.integration import AppTestClient
from selenium import webdriver
from pixelated.resources.features_resource import FeaturesResource
+from steps.common import *
+
+setup()
+
+
+@wait_for(timeout=5.0)
+def start_app_test_client(client):
+ ensure_server()
+ return client.start_client()
def before_all(context):
logging.disable('INFO')
client = AppTestClient()
+ start_app_test_client(client)
+ client.listenTCP()
proxy = Proxy(proxy_port='8889', app_port='4567')
FeaturesResource.DISABLED_FEATURES.append('autoRefresh')
context.client = client
context.call_to_terminate_proxy = proxy.run_on_a_thread()
- context.call_to_terminate = client.run_on_a_thread(logfile='/tmp/behave-tests.log')
def after_all(context):
- context.call_to_terminate()
context.call_to_terminate_proxy()
@@ -42,7 +54,7 @@ def before_feature(context, feature):
# context.browser = webdriver.Firefox()
context.browser = webdriver.PhantomJS()
context.browser.set_window_size(1280, 1024)
- context.browser.implicitly_wait(10)
+ context.browser.implicitly_wait(DEFAULT_IMPLICIT_WAIT_TIMEOUT_IN_S)
context.browser.set_page_load_timeout(60) # wait for data
context.browser.get('http://localhost:8889/')
@@ -57,6 +69,20 @@ def after_step(context, step):
def after_feature(context, feature):
context.browser.quit()
+ cleanup_all_mails(context)
+ context.last_mail = None
+
+
+@wait_for(timeout=10.0)
+def cleanup_all_mails(context):
+ @defer.inlineCallbacks
+ def _delete_all_mails():
+ mails = yield context.client.mail_store.all_mails()
+ for mail in mails:
+ yield context.client.mail_store.delete_mail(mail.ident)
+
+ return _delete_all_mails()
+
def save_source(context, filename='/tmp/source.html'):
with open(filename, 'w') as out:
diff --git a/service/test/functional/features/steps/attachments.py b/service/test/functional/features/steps/attachments.py
new file mode 100644
index 00000000..066683bf
--- /dev/null
+++ b/service/test/functional/features/steps/attachments.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it 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.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from email.mime.application import MIMEApplication
+from time import sleep
+from leap.mail.mail import Message
+from common import *
+from test.support.integration import MailBuilder
+from behave import given
+from crochet import wait_for
+from uuid import uuid4
+from email.MIMEMultipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+
+@given(u'I have a mail with an attachment in my inbox')
+def add_mail_with_attachment_impl(context):
+ subject = 'Hi! This the subject %s' % uuid4()
+ mail = build_mail_with_attachment(subject)
+ load_mail_into_soledad(context, mail)
+ context.last_subject = subject
+
+
+def build_mail_with_attachment(subject):
+ mail = MIMEMultipart()
+ mail['Subject'] = subject
+ mail.attach(MIMEText(u'a utf8 message', _charset='utf-8'))
+ attachment = MIMEApplication('pretend to be binary attachment data')
+ attachment.add_header('Content-Disposition', 'attachment', filename='filename.txt')
+ mail.attach(attachment)
+
+ return mail
+
+
+@wait_for(timeout=10.0)
+def load_mail_into_soledad(context, mail):
+ return context.client.mail_store.add_mail('INBOX', mail.as_string())
+
+
+@then(u'I see the mail has an attachment')
+def step_impl(context):
+ attachments_list = find_elements_by_css_selector(context, '.attachmentsArea li')
+ assert len(attachments_list) == 1
diff --git a/service/test/functional/features/steps/common.py b/service/test/functional/features/steps/common.py
index 93f4cb1f..9a547375 100644
--- a/service/test/functional/features/steps/common.py
+++ b/service/test/functional/features/steps/common.py
@@ -16,50 +16,77 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
-from selenium.common.exceptions import TimeoutException
-
+from selenium.common.exceptions import TimeoutException, StaleElementReferenceException
+import time
from test.support.integration import MailBuilder
+LOADING = 'loading'
+
+TIMEOUT_IN_S = 20
+
+DEFAULT_IMPLICIT_WAIT_TIMEOUT_IN_S = 10.0
+
+
+class ImplicitWait(object):
+ def __init__(self, context, timeout=5.0):
+ self._context = context
+ self._timeout = timeout
-def wait_until_element_is_invisible_by_locator(context, locator_tuple, timeout=10):
+ def __enter__(self):
+ self._context.browser.implicitly_wait(self._timeout)
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self._context.browser.implicitly_wait(DEFAULT_IMPLICIT_WAIT_TIMEOUT_IN_S)
+
+
+def wait_until_element_is_invisible_by_locator(context, locator_tuple, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
wait.until(EC.invisibility_of_element_located(locator_tuple))
-def wait_until_element_is_deleted(context, locator_tuple, timeout=10):
+def wait_until_element_is_deleted(context, locator_tuple, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
wait.until(lambda s: len(s.find_elements(locator_tuple[0], locator_tuple[1])) == 0)
-def wait_for_user_alert_to_disapear(context, timeout=10):
+def wait_for_loading_to_finish(context, timeout=TIMEOUT_IN_S):
+ wait_until_element_is_invisible_by_locator(context, (By.ID, 'loading'), timeout)
+
+
+def wait_for_user_alert_to_disapear(context, timeout=TIMEOUT_IN_S):
wait_until_element_is_invisible_by_locator(context, (By.ID, 'user-alerts'), timeout)
-def wait_until_elements_are_visible_by_locator(context, locator_tuple, timeout=10):
+def wait_until_elements_are_visible_by_locator(context, locator_tuple, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
wait.until(EC.presence_of_all_elements_located(locator_tuple))
return context.browser.find_elements(locator_tuple[0], locator_tuple[1])
-def wait_until_elements_are_visible_by_xpath(context, locator_tuple, timeout=10):
+def wait_until_elements_are_visible_by_xpath(context, locator_tuple, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
wait.until(EC.presence_of_all_elements_located(locator_tuple))
return context.browser.find_elements(locator_tuple[0], locator_tuple[1])
-def wait_until_element_is_visible_by_locator(context, locator_tuple, timeout=10):
+def wait_until_element_is_visible_by_locator(context, locator_tuple, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
wait.until(EC.visibility_of_element_located(locator_tuple))
return context.browser.find_element(locator_tuple[0], locator_tuple[1])
+def wait_for_condition(context, predicate_func, timeout=TIMEOUT_IN_S, poll_frequency=0.1):
+ wait = WebDriverWait(context.browser, timeout, poll_frequency=poll_frequency)
+ wait.until(predicate_func)
+
+
def fill_by_xpath(context, xpath, text):
field = context.browser.find_element_by_xpath(xpath)
field.send_keys(text)
def fill_by_css_selector(context, css_selector, text):
- field = context.browser.find_element_by_css_selector(css_selector)
+ field = find_element_by_css_selector(context, css_selector)
field.send_keys(text)
@@ -92,8 +119,12 @@ def find_element_by_css_selector(context, css_selector):
return wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, css_selector))
-def find_elements_by_css_selector(context, css_selector):
- return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, css_selector))
+def find_element_by_class_name(context, class_name):
+ return wait_until_element_is_visible_by_locator(context, (By.CLASS_NAME, class_name))
+
+
+def find_elements_by_css_selector(context, css_selector, timeout=TIMEOUT_IN_S):
+ return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, css_selector), timeout=timeout)
def find_elements_by_xpath(context, xpath):
@@ -109,17 +140,31 @@ def element_should_have_content(context, css_selector, content):
assert e.text == content
-def wait_until_button_is_visible(context, title, timeout=10):
+def wait_until_button_is_visible(context, title, timeout=TIMEOUT_IN_S):
wait = WebDriverWait(context.browser, timeout)
locator_tuple = (By.XPATH, ("//%s[contains(.,'%s')]" % ('button', title)))
wait.until(EC.visibility_of_element_located(locator_tuple))
+def execute_ignoring_staleness(func, timeout=TIMEOUT_IN_S):
+ end_time = time.time() + timeout
+ while time.time() <= end_time:
+ try:
+ return func()
+ except StaleElementReferenceException:
+ pass
+ raise TimeoutException('did not solve stale state until timeout %f' % timeout)
+
+
def click_button(context, title, element='button'):
button = find_element_containing_text(context, title, element_type=element)
button.click()
+def mail_list_with_subject_exists(context, subject):
+ return find_element_by_xpath(context, "//*[@class='subject-and-tags' and contains(.,'%s')]" % subject)
+
+
def mail_subject(context):
e = find_element_by_css_selector(context, '#mail-view .subject')
return e.text
diff --git a/service/test/functional/features/steps/compose.py b/service/test/functional/features/steps/compose.py
index 668579b1..e93468a2 100644
--- a/service/test/functional/features/steps/compose.py
+++ b/service/test/functional/features/steps/compose.py
@@ -49,9 +49,8 @@ def save_impl(context):
@when('I send it')
def send_impl(context):
- assert page_has_css(context, '#send-button[disabled]') is False
- context.browser.find_element(By.ID, 'send-button').click()
- # wait_until_element_is_deleted(context, (By.ID, 'send-button'), timeout=120)
+ send_button = wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, '#send-button:enabled'))
+ send_button.click()
def _enter_recipient(context, recipients_field, to_type):
diff --git a/service/test/functional/features/steps/data_setup.py b/service/test/functional/features/steps/data_setup.py
index 2a3876fc..fb825aba 100644
--- a/service/test/functional/features/steps/data_setup.py
+++ b/service/test/functional/features/steps/data_setup.py
@@ -13,11 +13,19 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from uuid import uuid4
from test.support.integration import MailBuilder
from behave import given
+from common import wait_for_condition
@given('I have a mail in my inbox')
def add_mail_impl(context):
- input_mail = MailBuilder().build_input_mail()
+ subject = 'Hi! This the subject %s' % uuid4()
+
+ input_mail = MailBuilder().with_subject(subject).build_input_mail()
context.client.add_mail_to_inbox(input_mail)
+
+ wait_for_condition(context, lambda _: context.client.search_engine.search(subject)[1] > 0, poll_frequency=0.1)
+
+ context.last_subject = subject
diff --git a/service/test/functional/features/steps/mail_list.py b/service/test/functional/features/steps/mail_list.py
index 5f0a0116..1b850578 100644
--- a/service/test/functional/features/steps/mail_list.py
+++ b/service/test/functional/features/steps/mail_list.py
@@ -15,10 +15,10 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
from common import *
from selenium.common.exceptions import NoSuchElementException
-from time import sleep
def find_current_mail(context):
+ print 'searching for mail [%s]' % context.current_mail_id
return find_element_by_id(context, '%s' % context.current_mail_id)
@@ -27,11 +27,14 @@ def check_current_mail_is_visible(context):
def open_current_mail(context):
- sleep(2)
e = find_current_mail(context)
e.click()
+def get_first_email(context):
+ return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li span a'))[0]
+
+
@then('I see that mail under the \'{tag}\' tag')
def impl(context, tag):
context.execute_steps("when I select the tag '%s'" % tag)
@@ -40,16 +43,14 @@ def impl(context, tag):
@when('I open that mail')
def impl(context):
- sleep(3)
find_current_mail(context).click()
@when('I open the first mail in the mail list')
def impl(context):
- first_email = wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li span a'))[0]
- context.current_mail_id = 'mail-' + first_email.get_attribute('href').split('/')[-1]
- first_email.click()
- sleep(5)
+ # it seems page is often still loading so staleness exceptions happen often
+ context.current_mail_id = 'mail-' + execute_ignoring_staleness(lambda: get_first_email(context).get_attribute('href').split('/')[-1])
+ execute_ignoring_staleness(lambda: get_first_email(context).click())
@when('I open the first mail in the \'{tag}\'')
@@ -71,7 +72,7 @@ def impl(context):
@then('the deleted mail is there')
def impl(context):
- find_current_mail(context)
+ mail_list_with_subject_exists(context, context.last_subject)
@given('I have mails')
@@ -86,22 +87,35 @@ def impl(context):
for email in emails:
if 'status-read' not in email.get_attribute('class'):
+ context.current_mail_id = email.get_attribute('id') # we need to get the mail id before manipulating the page
email.find_element_by_tag_name('input').click()
find_element_by_id(context, 'mark-selected-as-read').click()
- context.current_mail_id = email.get_attribute('id')
break
- sleep(2)
- assert 'status-read' in context.browser.find_element_by_id(context.current_mail_id).get_attribute('class')
+ wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#%s.status-read' % context.current_mail_id))
@when('I delete the email')
def impl(context):
def last_email():
- return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li'))[0]
- context.current_mail_id = last_email().get_attribute('id')
- last_email().find_element_by_tag_name('input').click()
+ return wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li'))
+ mail = last_email()
+ context.current_mail_id = mail.get_attribute('id')
+ mail.find_element_by_tag_name('input').click()
find_element_by_id(context, 'delete-selected').click()
- assert context.current_mail_id != find_elements_by_css_selector(context, '#mail-list li span a')[0]
+ _wait_for_mail_list_to_be_empty(context)
+
+
+def _wait_for_mail_list_to_be_empty(context):
+ wait_for_loading_to_finish(context)
+
+ def mail_list_is_empty(_):
+ with ImplicitWait(context, timeout=0.1):
+ try:
+ return 0 == len(context.browser.find_elements_by_css_selector('#mail-list li'))
+ except TimeoutException:
+ return False
+
+ wait_for_condition(context, mail_list_is_empty)
@when('I check all emails')
@@ -116,9 +130,4 @@ def impl(context):
@then('I should not see any email')
def impl(context):
- try:
- context.browser.find_element(By.CSS_SELECTOR, '#mail-list li span a')
- except NoSuchElementException:
- assert True
- except:
- assert False
+ _wait_for_mail_list_to_be_empty(context)
diff --git a/service/test/functional/features/steps/search.py b/service/test/functional/features/steps/search.py
index e653c3ed..879e834d 100644
--- a/service/test/functional/features/steps/search.py
+++ b/service/test/functional/features/steps/search.py
@@ -13,7 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-from time import sleep
+from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from common import *
@@ -22,9 +22,10 @@ from common import *
@when('I search for a mail with the words "{search_term}"')
def impl(context, search_term):
search_field = find_element_by_css_selector(context, '#search-trigger input[type="search"]')
- search_field.send_keys(search_term)
- search_field.send_keys(Keys.ENTER)
- sleep(1)
+ ActionChains(context.browser)\
+ .send_keys_to_element(search_field, search_term)\
+ .send_keys_to_element(search_field, Keys.ENTER)\
+ .perform()
@then('I see one or more mails in the search results')
diff --git a/service/test/functional/features/steps/tag_list.py b/service/test/functional/features/steps/tag_list.py
index 443c5173..a3315835 100644
--- a/service/test/functional/features/steps/tag_list.py
+++ b/service/test/functional/features/steps/tag_list.py
@@ -17,20 +17,20 @@ from common import *
def click_first_element_with_class(context, classname):
- elements = context.browser.find_elements_by_class_name(classname)
- elements[0].click()
+ element = find_element_by_class_name(context, classname)
+ element.click()
def is_side_nav_expanded(context):
- e = context.browser.find_elements_by_class_name('content')[0].get_attribute('class').count(u'move-right') == 1
- return e
+ e = find_element_by_class_name(context, 'content')
+ return u'move-right' in e.get_attribute("class")
def expand_side_nav(context):
if is_side_nav_expanded(context):
return
- toggle = context.browser.find_elements_by_class_name('side-nav-toggle')[0]
+ toggle = find_element_by_class_name(context, 'side-nav-toggle')
toggle.click()
@@ -39,11 +39,24 @@ def impl(context, tag):
wait_for_user_alert_to_disapear(context)
expand_side_nav(context)
- wait_until_element_is_visible_by_locator(context, (By.ID, 'tag-%s' % tag), 20)
+ # try this multiple times as there are some race conditions
+ try_again = 2
+ success = False
+ while (not success) and (try_again > 0):
+ try:
+ wait_until_element_is_visible_by_locator(context, (By.ID, 'tag-%s' % tag), timeout=20)
- e = find_element_by_id(context, 'tag-%s' % tag)
- e.click()
- wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, "#mail-list li span a[href*='%s']" % tag))
+ e = find_element_by_id(context, 'tag-%s' % tag)
+ e.click()
+
+ wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, "#mail-list li span a[href*='%s']" % tag), timeout=20)
+ success = True
+ except TimeoutException:
+ pass
+ finally:
+ try_again -= 1
+
+ assert success
@when('I am in \'{tag}\'')