303 lines
10 KiB
Python
303 lines
10 KiB
Python
"""
|
|
Library edit page in Studio
|
|
"""
|
|
from bok_choy.javascript import js_defined, wait_for_js
|
|
from bok_choy.page_object import PageObject
|
|
from bok_choy.promise import EmptyPromise
|
|
from selenium.webdriver.common.keys import Keys
|
|
from selenium.webdriver.support.select import Select
|
|
from .overview import CourseOutlineModal
|
|
from .container import XBlockWrapper
|
|
from ...pages.studio.pagination import PaginatedMixin
|
|
from ...tests.helpers import disable_animations
|
|
from .utils import confirm_prompt, wait_for_notification
|
|
from . import BASE_URL
|
|
|
|
|
|
class LibraryPage(PageObject, PaginatedMixin):
|
|
"""
|
|
Library page in Studio
|
|
"""
|
|
|
|
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
|
|
|
|
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()
|
|
self.wait_for_element_invisibility(
|
|
'.ui-loading',
|
|
'Wait for the page to complete its initial loading of XBlocks via AJAX'
|
|
)
|
|
disable_animations(self)
|
|
|
|
@property
|
|
def xblocks(self):
|
|
"""
|
|
Return a list of xblocks loaded on the container page.
|
|
"""
|
|
return self._get_xblocks()
|
|
|
|
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()
|
|
wait_for_notification(self)
|
|
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:
|
|
confirm_prompt(self) # this will also wait_for_notification()
|
|
self.wait_for_ajax()
|
|
|
|
def _get_xblocks(self):
|
|
"""
|
|
Create an XBlockWrapper for each XBlock div found on the page.
|
|
"""
|
|
prefix = '.wrapper-xblock.level-page '
|
|
return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map(
|
|
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))
|
|
).results
|
|
|
|
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.
|
|
"""
|
|
return self.q(css='.wrapper-xblock.level-page .studio-xblock-wrapper').filter(
|
|
lambda el: el.get_attribute('data-locator') == xblock_id
|
|
)
|
|
|
|
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'
|
|
"""
|
|
return self._div_for_xblock_id(xblock_id)[0].find_element_by_css_selector(
|
|
'.header-actions .{action}-button.action-button'.format(action=action)
|
|
)
|
|
|
|
|
|
class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
|
|
"""
|
|
Library Content XBlock Modal edit window
|
|
"""
|
|
url = None
|
|
MODAL_SELECTOR = ".wrapper-modal-window-edit-xblock"
|
|
|
|
# Labels used to identify the fields on the edit modal:
|
|
LIBRARY_LABEL = "Libraries"
|
|
COUNT_LABEL = "Count"
|
|
SCORED_LABEL = "Scored"
|
|
PROBLEM_TYPE_LABEL = "Problem Type"
|
|
|
|
def is_browser_on_page(self):
|
|
"""
|
|
Check that we are on the right page in the browser.
|
|
"""
|
|
return self.is_shown()
|
|
|
|
@property
|
|
def library_key(self):
|
|
"""
|
|
Gets value of first library key input
|
|
"""
|
|
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
|
|
if library_key_input is not None:
|
|
return library_key_input.get_attribute('value').strip(',')
|
|
return None
|
|
|
|
@library_key.setter
|
|
def library_key(self, library_key):
|
|
"""
|
|
Sets value of first library key input, creating it if necessary
|
|
"""
|
|
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
|
|
if library_key_input is None:
|
|
library_key_input = self._add_library_key()
|
|
if library_key is not None:
|
|
# can't use lib_text.clear() here as input get deleted by client side script
|
|
library_key_input.send_keys(Keys.HOME)
|
|
library_key_input.send_keys(Keys.SHIFT, Keys.END)
|
|
library_key_input.send_keys(library_key)
|
|
else:
|
|
library_key_input.clear()
|
|
EmptyPromise(lambda: self.library_key == library_key, "library_key is updated in modal.").fulfill()
|
|
|
|
@property
|
|
def count(self):
|
|
"""
|
|
Gets value of children count input
|
|
"""
|
|
return int(self.get_metadata_input(self.COUNT_LABEL).get_attribute('value'))
|
|
|
|
@count.setter
|
|
def count(self, count):
|
|
"""
|
|
Sets value of children count input
|
|
"""
|
|
count_text = self.get_metadata_input(self.COUNT_LABEL)
|
|
count_text.clear()
|
|
count_text.send_keys(count)
|
|
EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill()
|
|
|
|
@property
|
|
def scored(self):
|
|
"""
|
|
Gets value of scored select
|
|
"""
|
|
value = self.get_metadata_input(self.SCORED_LABEL).get_attribute('value')
|
|
if value == 'True':
|
|
return True
|
|
elif value == 'False':
|
|
return False
|
|
raise ValueError("Unknown value {value} set for {label}".format(value=value, label=self.SCORED_LABEL))
|
|
|
|
@scored.setter
|
|
def scored(self, scored):
|
|
"""
|
|
Sets value of scored select
|
|
"""
|
|
select_element = self.get_metadata_input(self.SCORED_LABEL)
|
|
select_element.click()
|
|
scored_select = Select(select_element)
|
|
scored_select.select_by_value(str(scored))
|
|
EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill()
|
|
|
|
@property
|
|
def capa_type(self):
|
|
"""
|
|
Gets value of CAPA type select
|
|
"""
|
|
return self.get_metadata_input(self.PROBLEM_TYPE_LABEL).get_attribute('value')
|
|
|
|
@capa_type.setter
|
|
def capa_type(self, value):
|
|
"""
|
|
Sets value of CAPA type select
|
|
"""
|
|
select_element = self.get_metadata_input(self.PROBLEM_TYPE_LABEL)
|
|
select_element.click()
|
|
problem_type_select = Select(select_element)
|
|
problem_type_select.select_by_value(value)
|
|
EmptyPromise(lambda: self.capa_type == value, "problem type is updated in modal.").fulfill()
|
|
|
|
def _add_library_key(self):
|
|
"""
|
|
Adds library key input
|
|
"""
|
|
wrapper = self._get_metadata_element(self.LIBRARY_LABEL)
|
|
add_button = wrapper.find_element_by_xpath(".//a[contains(@class, 'create-action')]")
|
|
add_button.click()
|
|
return self._get_list_inputs(wrapper)[0]
|
|
|
|
def _get_list_inputs(self, list_wrapper):
|
|
"""
|
|
Finds nested input elements (useful for List and Dict fields)
|
|
"""
|
|
return list_wrapper.find_elements_by_xpath(".//input[@type='text']")
|
|
|
|
def _get_metadata_element(self, metadata_key):
|
|
"""
|
|
Gets metadata input element (a wrapper div for List and Dict fields)
|
|
"""
|
|
metadata_inputs = self.find_css(".metadata_entry .wrapper-comp-setting label.setting-label")
|
|
target_label = [elem for elem in metadata_inputs if elem.text == metadata_key][0]
|
|
label_for = target_label.get_attribute('for')
|
|
return self.find_css("#" + label_for)[0]
|
|
|
|
def get_metadata_input(self, metadata_key):
|
|
"""
|
|
Gets input/select element for given field
|
|
"""
|
|
element = self._get_metadata_element(metadata_key)
|
|
if element.tag_name == 'div':
|
|
# List or Dict field - return first input
|
|
# TODO support multiple values
|
|
inputs = self._get_list_inputs(element)
|
|
element = inputs[0] if inputs else None
|
|
return element
|
|
|
|
|
|
@js_defined('window.LibraryContentAuthorView')
|
|
class StudioLibraryContainerXBlockWrapper(XBlockWrapper):
|
|
"""
|
|
Wraps :class:`.container.XBlockWrapper` for use with LibraryContent blocks
|
|
"""
|
|
url = None
|
|
|
|
def is_browser_on_page(self):
|
|
"""
|
|
Returns true iff the library content area has been loaded
|
|
"""
|
|
return self.q(css='article.content-primary').visible
|
|
|
|
def is_finished_loading(self):
|
|
"""
|
|
Returns true iff the Loading indicator is not visible
|
|
"""
|
|
return not self.q(css='div.ui-loading').visible
|
|
|
|
@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"))
|
|
|
|
@wait_for_js # Wait for the fragment.initialize_js('LibraryContentAuthorView') call to finish
|
|
def refresh_children(self):
|
|
"""
|
|
Click "Update now..." button
|
|
"""
|
|
btn_selector = self._bounded_selector(".library-update-btn")
|
|
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)
|
|
self.wait_for(lambda: self.is_browser_on_page(), 'StudioLibraryContainerXBlockWrapper has reloaded.')
|
|
self.wait_for(lambda: self.is_finished_loading(), 'Loading indicator is not visible.')
|
|
|
|
# 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')
|