users.py 10.4 KB
Newer Older
1 2 3
"""
Page classes to test either the Course Team page or the Library Team page.
"""
Muddasser committed
4 5
import os
from opaque_keys.edx.locator import CourseLocator
6 7
from bok_choy.promise import EmptyPromise
from bok_choy.page_object import PageObject
8 9 10
from common.test.acceptance.tests.helpers import disable_animations
from common.test.acceptance.pages.studio.course_page import CoursePage
from common.test.acceptance.pages.studio import BASE_URL
Muddasser committed
11
from common.test.acceptance.pages.studio.utils import HelpMixin
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26


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


27 28 29
class UsersPageMixin(PageObject):
    """ Common functionality for course/library team pages """
    new_user_form_selector = '.form-create.create-user .user-email-input'
30 31 32 33 34 35 36 37 38

    def url(self):
        """
        URL to this page - override in subclass
        """
        raise NotImplementedError

    def is_browser_on_page(self):
        """
39
        Returns True if the browser has loaded the page.
40
        """
41
        return self.q(css='body.view-team').present and not self.q(css='.ui-loading').present
42 43 44 45 46 47

    @property
    def users(self):
        """
        Return a list of users listed on this page.
        """
E. Kolpakov committed
48 49 50
        return self.q(css='.user-list .user-item').map(
            lambda el: UserWrapper(self.browser, el.get_attribute('data-email'))
        ).results
51 52

    @property
53 54 55 56 57 58 59
    def usernames(self):
        """
        Returns a list of user names for users listed on this page
        """
        return [user.name for user in self.users]

    @property
60 61 62 63 64 65 66 67 68 69
    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
        """
70 71
        self.q(css='.create-user-button').first.click()
        self.wait_for(lambda: self.new_user_form_visible, "Add user form is visible")
72 73 74 75 76 77 78 79 80 81 82 83 84 85

    @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)
86
        self.wait_for_element_visibility('.user-list', 'wait for team to load')
87

88 89 90 91 92 93 94 95
    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 """
96
        self.wait_for_element_visibility('.create-user-button', "Add team member button is available")
97 98 99
        self.click_add_button()
        self.set_new_user_email(email)
        self.click_submit_new_user_form()
100
        self.wait_for_page()
101 102 103 104 105

    def delete_user_from_course(self, email):
        """ Deletes user from course/library """
        target_user = self.get_user(email)
        target_user.click_delete()
106
        self.wait_for_page()
107 108 109 110 111 112 113 114 115

    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]

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    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)

134 135 136 137 138
    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.
139

140
        This method is different from wait_until_no_loading_indicator because this expects
141 142 143
        the loading indicator to still exist on the page; it is just hidden.

        It also disables animations for improved test reliability.
144
        """
145

146 147
        self.wait_for_element_invisibility(
            '.ui-loading',
148
            'Wait for the page to complete its initial loading'
149 150 151 152
        )
        disable_animations(self)


Muddasser committed
153
class LibraryUsersPage(UsersPageMixin, HelpMixin):
154 155 156
    """
    Library Team page in Studio
    """
157 158 159
    def __init__(self, browser, locator):
        super(LibraryUsersPage, self).__init__(browser)
        self.locator = locator
160 161 162 163 164 165 166 167 168

    @property
    def url(self):
        """
        URL to the "User Access" page for the given library.
        """
        return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))


Muddasser committed
169
class CourseTeamPage(UsersPageMixin, CoursePage):
170 171 172 173 174
    """
    Course Team page in Studio.
    """
    url_path = "course_team"

Muddasser committed
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    @property
    def url(self):
        """
        Construct a URL to the page within the course.
        """
        # TODO - is there a better way to make this agnostic to the underlying default module store?
        default_store = os.environ.get('DEFAULT_STORE', 'draft')
        course_key = CourseLocator(
            self.course_info['course_org'],
            self.course_info['course_num'],
            self.course_info['course_run'],
            deprecated=(default_store == 'draft')
        )
        return "/".join([BASE_URL, self.url_path, unicode(course_key)])

190

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
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()
cahrens committed
277
        self.wait_for_element_absence('.page-prompt .is-shown', 'Confirmation prompt is hidden')
278 279 280 281 282 283 284 285 286 287 288
        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]