Firefox 42 is faster than the version we're currently using for platform tests. This failure occurs in the brief amount of time where the teams page is reloading. Waiting for the actual team list to appear before moving on with tests solves the issue. Approx 4 testcases are fixed. Below is an example of error that was appearing in firefox 42 tests: ``` Error Message Not on the correct page to use '<common.test.acceptance.pages.studio.users.CourseTeamPage object at 0x7feb9fdb0490>' at URL 'http://localhost:8031/course_team/course-v1:test_org+321550032104190792981383915127220335686+test_run' -------------------- >> begin captured logging << -------------------- bok_choy.browser: INFO: Using local browser: firefox [Default is firefox] --------------------- >> end captured logging << --------------------- Stacktrace File "/usr/lib/python2.7/unittest/case.py", line 331, in run testMethod() File "/home/jenkins/workspace/edx-platform-test-subset/common/test/acceptance/tests/studio/test_studio_course_team.py", line 147, in test_added_users_cannot_add_or_delete_other_users self._assert_user_present(self.other_user, present=True) File "/home/jenkins/workspace/edx-platform-test-subset/common/test/acceptance/tests/studio/test_studio_course_team.py", line 78, in _assert_user_present description="Wait for user to be present" File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/page_object.py", line 490, in wait_for return EmptyPromise(promise_check_func, description, timeout=timeout).fulfill() File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/promise.py", line 92, in fulfill is_fulfilled, result = self._check_fulfilled() File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/promise.py", line 118, in _check_fulfilled is_fulfilled, result = self._check_func() File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/promise.py", line 173, in <lambda> full_check_func = lambda: (check_func(), None) File "/home/jenkins/workspace/edx-platform-test-subset/common/test/acceptance/tests/studio/test_studio_course_team.py", line 77, in <lambda> lambda: user.get('username') in self.page.usernames, File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/page_object.py", line 66, in wrapper self._verify_page() # pylint: disable=protected-access File "/home/jenkins/edx-venv/local/lib/python2.7/site-packages/bok_choy/page_object.py", line 331, in _verify_page raise WrongPageError(msg) "Not on the correct page to use '<common.test.acceptance.pages.studio.users.CourseTeamPage object at 0x7feb9fdb0490>' at URL 'http://localhost:8031/course_team/course-v1:test_org+321550032104190792981383915127220335686+test_run'\n-------------------- >> begin captured logging << --------------------\nbok_choy.browser: INFO: Using local browser: firefox [Default is firefox]\n--------------------- >> end captured logging << ---------------------" ```
271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
"""
|
|
Page classes to test either the Course Team page or the Library Team page.
|
|
"""
|
|
from bok_choy.promise import EmptyPromise
|
|
from bok_choy.page_object import PageObject
|
|
from ...tests.helpers import disable_animations
|
|
from .course_page import CoursePage
|
|
from . import BASE_URL
|
|
|
|
|
|
def wait_for_ajax_or_reload(browser):
|
|
"""
|
|
Wait for all ajax requests to finish, OR for the page to reload.
|
|
Normal wait_for_ajax() chokes on occasion if the pages reloads,
|
|
giving "WebDriverException: Message: u'jQuery is not defined'"
|
|
"""
|
|
def _is_ajax_finished():
|
|
""" Wait for jQuery to finish all AJAX calls, if it is present. """
|
|
return browser.execute_script("return typeof(jQuery) == 'undefined' || jQuery.active == 0")
|
|
|
|
EmptyPromise(_is_ajax_finished, "Finished waiting for ajax requests.").fulfill()
|
|
|
|
|
|
class UsersPageMixin(PageObject):
|
|
""" Common functionality for course/library team pages """
|
|
new_user_form_selector = '.form-create.create-user .user-email-input'
|
|
|
|
def url(self):
|
|
"""
|
|
URL to this page - override in subclass
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def is_browser_on_page(self):
|
|
"""
|
|
Returns True if the browser has loaded the page.
|
|
"""
|
|
return self.q(css='body.view-team').present and not self.q(css='.ui-loading').present
|
|
|
|
@property
|
|
def users(self):
|
|
"""
|
|
Return a list of users listed on this page.
|
|
"""
|
|
return self.q(css='.user-list .user-item').map(
|
|
lambda el: UserWrapper(self.browser, el.get_attribute('data-email'))
|
|
).results
|
|
|
|
@property
|
|
def usernames(self):
|
|
"""
|
|
Returns a list of user names for users listed on this page
|
|
"""
|
|
return [user.name for user in self.users]
|
|
|
|
@property
|
|
def has_add_button(self):
|
|
"""
|
|
Is the "New Team Member" button present?
|
|
"""
|
|
return self.q(css='.create-user-button').present
|
|
|
|
def click_add_button(self):
|
|
"""
|
|
Click on the "New Team Member" button
|
|
"""
|
|
self.q(css='.create-user-button').first.click()
|
|
self.wait_for(lambda: self.new_user_form_visible, "Add user form is visible")
|
|
|
|
@property
|
|
def new_user_form_visible(self):
|
|
""" Is the new user form visible? """
|
|
return self.q(css='.form-create.create-user .user-email-input').visible
|
|
|
|
def set_new_user_email(self, email):
|
|
""" Set the value of the "New User Email Address" field. """
|
|
self.q(css='.form-create.create-user .user-email-input').fill(email)
|
|
|
|
def click_submit_new_user_form(self):
|
|
""" Submit the "New User" form """
|
|
self.q(css='.form-create.create-user .action-primary').click()
|
|
wait_for_ajax_or_reload(self.browser)
|
|
self.wait_for_element_visibility('.user-list', 'wait for team to load')
|
|
|
|
def get_user(self, email):
|
|
""" Gets user wrapper by email """
|
|
target_users = [user for user in self.users if user.email == email]
|
|
assert len(target_users) == 1
|
|
return target_users[0]
|
|
|
|
def add_user_to_course(self, email):
|
|
""" Adds user to a course/library """
|
|
self.wait_for_element_visibility('.create-user-button', "Add team member button is available")
|
|
self.click_add_button()
|
|
self.set_new_user_email(email)
|
|
self.click_submit_new_user_form()
|
|
|
|
def delete_user_from_course(self, email):
|
|
""" Deletes user from course/library """
|
|
target_user = self.get_user(email)
|
|
target_user.click_delete()
|
|
self.wait_for_page()
|
|
|
|
def modal_dialog_visible(self, dialog_type):
|
|
""" Checks if modal dialog of specified class is displayed """
|
|
return self.q(css='.prompt.{dialog_type}'.format(dialog_type=dialog_type)).visible
|
|
|
|
def modal_dialog_text(self, dialog_type):
|
|
""" Gets modal dialog text """
|
|
return self.q(css='.prompt.{dialog_type} .message'.format(dialog_type=dialog_type)).text[0]
|
|
|
|
def wait_until_no_loading_indicator(self):
|
|
"""
|
|
When the page first loads, there is a loading indicator and most
|
|
functionality is not yet available. This waits for that loading to finish
|
|
and be removed from the DOM.
|
|
|
|
This method is different from wait_until_ready because the loading element
|
|
is removed from the DOM, rather than hidden.
|
|
|
|
It also disables animations for improved test reliability.
|
|
"""
|
|
|
|
self.wait_for(
|
|
lambda: not self.q(css='.ui-loading').present,
|
|
"Wait for page to complete its initial loading"
|
|
)
|
|
disable_animations(self)
|
|
|
|
def wait_until_ready(self):
|
|
"""
|
|
When the page first loads, there is a loading indicator and most
|
|
functionality is not yet available. This waits for that loading to
|
|
finish.
|
|
|
|
This method is different from wait_until_no_loading_indicator because this expects
|
|
the loading indicator to still exist on the page; it is just hidden.
|
|
|
|
It also disables animations for improved test reliability.
|
|
"""
|
|
|
|
self.wait_for_element_invisibility(
|
|
'.ui-loading',
|
|
'Wait for the page to complete its initial loading'
|
|
)
|
|
disable_animations(self)
|
|
|
|
|
|
class LibraryUsersPage(UsersPageMixin):
|
|
"""
|
|
Library Team page in Studio
|
|
"""
|
|
def __init__(self, browser, locator):
|
|
super(LibraryUsersPage, self).__init__(browser)
|
|
self.locator = locator
|
|
|
|
@property
|
|
def url(self):
|
|
"""
|
|
URL to the "User Access" page for the given library.
|
|
"""
|
|
return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
|
|
|
|
|
|
class CourseTeamPage(CoursePage, UsersPageMixin):
|
|
"""
|
|
Course Team page in Studio.
|
|
"""
|
|
|
|
url_path = "course_team"
|
|
|
|
|
|
class UserWrapper(PageObject):
|
|
"""
|
|
A PageObject representing a wrapper around a user listed on the course/library team page.
|
|
"""
|
|
url = None
|
|
COMPONENT_BUTTONS = {
|
|
'basic_tab': '.editor-tabs li.inner_tab_wrap:nth-child(1) > a',
|
|
'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a',
|
|
'save_settings': '.action-save',
|
|
}
|
|
|
|
def __init__(self, browser, email):
|
|
super(UserWrapper, self).__init__(browser)
|
|
self.email = email
|
|
self.selector = '.user-list .user-item[data-email="{}"]'.format(self.email)
|
|
|
|
def is_browser_on_page(self):
|
|
"""
|
|
Sanity check that our wrapper element is on the page.
|
|
"""
|
|
return self.q(css=self.selector).present
|
|
|
|
def _bounded_selector(self, selector):
|
|
"""
|
|
Return `selector`, but limited to this particular user entry's context
|
|
"""
|
|
return '{} {}'.format(self.selector, selector)
|
|
|
|
@property
|
|
def name(self):
|
|
""" Get this user's username, as displayed. """
|
|
return self.q(css=self._bounded_selector('.user-username')).text[0]
|
|
|
|
@property
|
|
def role_label(self):
|
|
""" Get this user's role, as displayed. """
|
|
return self.q(css=self._bounded_selector('.flag-role .value')).text[0]
|
|
|
|
@property
|
|
def is_current_user(self):
|
|
""" Does the UI indicate that this is the current user? """
|
|
return self.q(css=self._bounded_selector('.flag-role .msg-you')).present
|
|
|
|
@property
|
|
def can_promote(self):
|
|
""" Can this user be promoted to a more powerful role? """
|
|
return self.q(css=self._bounded_selector('.add-admin-role')).present
|
|
|
|
@property
|
|
def promote_button_text(self):
|
|
""" What does the promote user button say? """
|
|
return self.q(css=self._bounded_selector('.add-admin-role')).text[0]
|
|
|
|
def click_promote(self):
|
|
""" Click on the button to promote this user to the more powerful role """
|
|
self.q(css=self._bounded_selector('.add-admin-role')).click()
|
|
wait_for_ajax_or_reload(self.browser)
|
|
|
|
@property
|
|
def can_demote(self):
|
|
""" Can this user be demoted to a less powerful role? """
|
|
return self.q(css=self._bounded_selector('.remove-admin-role')).present
|
|
|
|
@property
|
|
def demote_button_text(self):
|
|
""" What does the demote user button say? """
|
|
return self.q(css=self._bounded_selector('.remove-admin-role')).text[0]
|
|
|
|
def click_demote(self):
|
|
""" Click on the button to demote this user to the less powerful role """
|
|
self.q(css=self._bounded_selector('.remove-admin-role')).click()
|
|
wait_for_ajax_or_reload(self.browser)
|
|
|
|
@property
|
|
def can_delete(self):
|
|
""" Can this user be deleted? """
|
|
return self.q(css=self._bounded_selector('.action-delete:not(.is-disabled) .remove-user')).present
|
|
|
|
def click_delete(self):
|
|
""" Click the button to delete this user. """
|
|
disable_animations(self)
|
|
self.q(css=self._bounded_selector('.remove-user')).click()
|
|
# We can't use confirm_prompt because its wait_for_ajax is flaky when the page is expected to reload.
|
|
self.wait_for_element_visibility('.prompt', 'Prompt is visible')
|
|
self.wait_for_element_visibility('.prompt .action-primary', 'Confirmation button is visible')
|
|
self.q(css='.prompt .action-primary').click()
|
|
self.wait_for_element_absence('.page-prompt .is-shown', 'Confirmation prompt is hidden')
|
|
wait_for_ajax_or_reload(self.browser)
|
|
|
|
@property
|
|
def has_no_change_warning(self):
|
|
""" Does this have a warning in place of the promote/demote buttons? """
|
|
return self.q(css=self._bounded_selector('.notoggleforyou')).present
|
|
|
|
@property
|
|
def no_change_warning_text(self):
|
|
""" Text of the warning seen in place of the promote/demote buttons. """
|
|
return self.q(css=self._bounded_selector('.notoggleforyou')).text[0]
|