library.py 8.79 KB
Newer Older
1 2 3
"""
Library edit page in Studio
"""
4
from bok_choy.javascript import js_defined, wait_for_js
5
from bok_choy.page_object import PageObject
6 7
from bok_choy.promise import EmptyPromise
from selenium.webdriver.support.select import Select
8 9 10 11
from common.test.acceptance.pages.studio.component_editor import ComponentEditorView
from common.test.acceptance.pages.studio.container import XBlockWrapper
from common.test.acceptance.pages.studio.users import UsersPageMixin
from common.test.acceptance.pages.studio.pagination import PaginatedMixin
12
from selenium.webdriver.common.keys import Keys
Muddasser committed
13
from common.test.acceptance.pages.studio.utils import HelpMixin
14
from common.test.acceptance.pages.common.utils import confirm_prompt, sync_on_notification
15

16
from common.test.acceptance.pages.studio import BASE_URL
17 18


Muddasser committed
19
class LibraryPage(PageObject, HelpMixin):
20
    """
21
    Base page for Library pages. Defaults URL to the edit page.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
    """
    def __init__(self, browser, locator):
        super(LibraryPage, self).__init__(browser)
        self.locator = locator

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

    def is_browser_on_page(self):
        """
        Returns True iff the browser has loaded the library edit page.
        """
        return self.q(css='body.view-library').present

40

41
class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin):
42 43 44 45
    """
    Library edit page in Studio
    """

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
    def get_header_title(self):
        """
        The text of the main heading (H1) visible on the page.
        """
        return self.q(css='h1.page-header-title').text

    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.

        Always call this before using the page. It also disables animations
        for improved test reliability.
        """
        self.wait_for_ajax()
62
        super(LibraryEditPage, self).wait_until_ready()
63 64 65 66 67 68 69 70

    @property
    def xblocks(self):
        """
        Return a list of xblocks loaded on the container page.
        """
        return self._get_xblocks()

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
    def are_previews_showing(self):
        """
        Determines whether or not previews are showing for XBlocks
        """
        return all([not xblock.is_placeholder() for xblock in self.xblocks])

    def toggle_previews(self):
        """
        Clicks the preview toggling button and waits for the previews to appear or disappear.
        """
        toggle = not self.are_previews_showing()
        self.q(css='.toggle-preview-button').click()
        EmptyPromise(
            lambda: self.are_previews_showing() == toggle,
            'Preview is visible: %s' % toggle,
            timeout=30
        ).fulfill()
        self.wait_until_ready()

90 91 92 93 94
    def click_duplicate_button(self, xblock_id):
        """
        Click on the duplicate button for the given XBlock
        """
        self._action_btn_for_xblock_id(xblock_id, "duplicate").click()
95
        sync_on_notification(self)
96 97 98 99 100 101 102 103
        self.wait_for_ajax()

    def click_delete_button(self, xblock_id, confirm=True):
        """
        Click on the delete button for the given XBlock
        """
        self._action_btn_for_xblock_id(xblock_id, "delete").click()
        if confirm:
104
            confirm_prompt(self)  # this will also sync_on_notification()
105 106 107 108 109 110 111
            self.wait_for_ajax()

    def _get_xblocks(self):
        """
        Create an XBlockWrapper for each XBlock div found on the page.
        """
        prefix = '.wrapper-xblock.level-page '
112 113 114
        return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map(
            lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))
        ).results
115 116 117 118 119 120

    def _div_for_xblock_id(self, xblock_id):
        """
        Given an XBlock's usage locator as a string, return the WebElement for
        that block's wrapper div.
        """
121 122 123
        return self.q(css='.wrapper-xblock.level-page .studio-xblock-wrapper').filter(
            lambda el: el.get_attribute('data-locator') == xblock_id
        )
124 125 126 127 128 129 130

    def _action_btn_for_xblock_id(self, xblock_id, action):
        """
        Given an XBlock's usage locator as a string, return one of its action
        buttons.
        action is 'edit', 'duplicate', or 'delete'
        """
131 132 133 134 135
        return self._div_for_xblock_id(xblock_id)[0].find_element_by_css_selector(
            '.header-actions .{action}-button.action-button'.format(action=action)
        )


136
class StudioLibraryContentEditor(ComponentEditorView):
137 138 139 140
    """
    Library Content XBlock Modal edit window
    """
    # Labels used to identify the fields on the edit modal:
141
    LIBRARY_LABEL = "Library"
142
    COUNT_LABEL = "Count"
E. Kolpakov committed
143
    PROBLEM_TYPE_LABEL = "Problem Type"
144 145

    @property
146
    def library_name(self):
147
        """ Gets name of library """
148 149 150 151
        return self.get_selected_option_text(self.LIBRARY_LABEL)

    @library_name.setter
    def library_name(self, library_name):
152
        """
153
        Select a library from the library select box
154
        """
155 156
        self.set_select_value(self.LIBRARY_LABEL, library_name)
        EmptyPromise(lambda: self.library_name == library_name, "library_name is updated in modal.").fulfill()
157 158 159 160 161 162

    @property
    def count(self):
        """
        Gets value of children count input
        """
163
        return int(self.get_setting_element(self.COUNT_LABEL).get_attribute('value'))
164 165 166 167 168 169

    @count.setter
    def count(self, count):
        """
        Sets value of children count input
        """
170
        count_text = self.get_setting_element(self.COUNT_LABEL)
171 172
        count_text.send_keys(Keys.CONTROL, "a")
        count_text.send_keys(Keys.BACK_SPACE)
173 174 175 176
        count_text.send_keys(count)
        EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill()

    @property
E. Kolpakov committed
177 178 179 180
    def capa_type(self):
        """
        Gets value of CAPA type select
        """
181
        return self.get_setting_element(self.PROBLEM_TYPE_LABEL).get_attribute('value')
E. Kolpakov committed
182 183 184 185 186 187

    @capa_type.setter
    def capa_type(self, value):
        """
        Sets value of CAPA type select
        """
188
        self.set_select_value(self.PROBLEM_TYPE_LABEL, value)
E. Kolpakov committed
189 190
        EmptyPromise(lambda: self.capa_type == value, "problem type is updated in modal.").fulfill()

191
    def set_select_value(self, label, value):
192
        """
193
        Sets the select with given label (display name) to the specified value
194
        """
195 196 197
        elem = self.get_setting_element(label)
        select = Select(elem)
        select.select_by_value(value)
198 199


200
@js_defined('window.LibraryContentAuthorView')
201 202 203 204 205 206
class StudioLibraryContainerXBlockWrapper(XBlockWrapper):
    """
    Wraps :class:`.container.XBlockWrapper` for use with LibraryContent blocks
    """
    url = None

207 208 209 210 211 212
    def is_browser_on_page(self):
        """
        Returns true iff the library content area has been loaded
        """
        return self.q(css='article.content-primary').visible

213 214 215 216 217 218
    def is_finished_loading(self):
        """
        Returns true iff the Loading indicator is not visible
        """
        return not self.q(css='div.ui-loading').visible

219 220 221 222 223 224 225 226 227 228 229 230 231
    @classmethod
    def from_xblock_wrapper(cls, xblock_wrapper):
        """
        Factory method: creates :class:`.StudioLibraryContainerXBlockWrapper` from :class:`.container.XBlockWrapper`
        """
        return cls(xblock_wrapper.browser, xblock_wrapper.locator)

    def get_body_paragraphs(self):
        """
        Gets library content body paragraphs
        """
        return self.q(css=self._bounded_selector(".xblock-message-area p"))

232
    @wait_for_js  # Wait for the fragment.initialize_js('LibraryContentAuthorView') call to finish
233 234 235 236
    def refresh_children(self):
        """
        Click "Update now..." button
        """
237
        btn_selector = self._bounded_selector(".library-update-btn")
238 239 240 241
        self.wait_for_element_presence(btn_selector, 'Update now button is present.')
        self.q(css=btn_selector).first.click()

        # This causes a reload (see cms/static/xmodule_js/public/js/library_content_edit.js)
Christine Lytwynec committed
242 243 244
        # Check that the ajax request that caused the reload is done.
        self.wait_for_ajax()
        # Then check that we are still on the right page.
245
        self.wait_for(lambda: self.is_browser_on_page(), 'StudioLibraryContainerXBlockWrapper has reloaded.')
246 247 248
        # Wait longer than the default 60 seconds, because this was intermittently failing on jenkins
        # with the screenshot showing that the Loading indicator was still visible. See TE-745.
        self.wait_for(lambda: self.is_finished_loading(), 'Loading indicator is not visible.', timeout=120)
249 250 251 252

        # And wait to make sure the ajax post has finished.
        self.wait_for_ajax()
        self.wait_for_element_absence(btn_selector, 'Wait for the XBlock to finish reloading')