Merge pull request #6459 from edx/content-libraries

Content libraries MVP
This commit is contained in:
Braden MacDonald
2015-01-13 11:20:20 -08:00
132 changed files with 9358 additions and 1409 deletions

View File

@@ -0,0 +1,48 @@
"""
Library Content XBlock Wrapper
"""
from bok_choy.page_object import PageObject
class LibraryContentXBlockWrapper(PageObject):
"""
A PageObject representing a wrapper around a LibraryContent block seen in the LMS
"""
url = None
BODY_SELECTOR = '.xblock-student_view div'
def __init__(self, browser, locator):
super(LibraryContentXBlockWrapper, self).__init__(browser)
self.locator = locator
def is_browser_on_page(self):
"""
Checks if page is opened
"""
return self.q(css='{}[data-id="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to this particular block's context
"""
return '{}[data-id="{}"] {}'.format(
self.BODY_SELECTOR,
self.locator,
selector
)
@property
def children_contents(self):
"""
Gets contents of all child XBlocks as list of strings
"""
child_blocks = self.q(css=self._bounded_selector("div[data-id]"))
return frozenset(child.text for child in child_blocks)
@property
def children_headers(self):
"""
Gets headers of all child XBlocks as list of strings
"""
child_blocks_headers = self.q(css=self._bounded_selector("div[data-id] h2.problem-header"))
return frozenset(child.text for child in child_blocks_headers)

View File

@@ -246,6 +246,7 @@ class CombinedLoginAndRegisterPage(PageObject):
def wait_for_errors(self):
"""Wait for errors to be visible, then return them. """
def _check_func():
"""Return success status and any errors that occurred."""
errors = self.errors
return (bool(errors), errors)
return Promise(_check_func, "Errors are visible").fulfill()
@@ -259,6 +260,7 @@ class CombinedLoginAndRegisterPage(PageObject):
def wait_for_success(self):
"""Wait for a success message to be visible, then return it."""
def _check_func():
"""Return success status and any errors that occurred."""
success = self.success
return (bool(success), success)
return Promise(_check_func, "Success message is visible").fulfill()

View File

@@ -15,7 +15,8 @@ class AutoAuthPage(PageObject):
this url will create a user and log them in.
"""
def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None):
def __init__(self, browser, username=None, email=None, password=None,
staff=None, course_id=None, roles=None, no_login=None):
"""
Auto-auth is an end-point for HTTP GET requests.
By default, it will create accounts with random user credentials,
@@ -51,6 +52,9 @@ class AutoAuthPage(PageObject):
if roles is not None:
self._params['roles'] = roles
if no_login:
self._params['no_login'] = True
@property
def url(self):
"""
@@ -66,7 +70,7 @@ class AutoAuthPage(PageObject):
def is_browser_on_page(self):
message = self.q(css='BODY').text[0]
match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
match = re.search(r'(Logged in|Created) user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
return True if match else False
def get_user_id(self):

View File

@@ -6,7 +6,7 @@ from bok_choy.page_object import PageObject
from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL
from utils import click_css, confirm_prompt
from .utils import click_css, confirm_prompt, type_in_codemirror
class ContainerPage(PageObject):
@@ -285,6 +285,7 @@ class XBlockWrapper(PageObject):
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',
'settings_tab': '.editor-modes .settings-button',
'save_settings': '.action-save',
}
@@ -312,6 +313,14 @@ class XBlockWrapper(PageObject):
"""
return self.q(css=self._bounded_selector('.xblock-student_view'))[0].text
@property
def author_content(self):
"""
Returns the text content of the xblock as displayed on the container page.
(For blocks which implement a distinct author_view).
"""
return self.q(css=self._bounded_selector('.xblock-author_view'))[0].text
@property
def name(self):
titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
@@ -336,6 +345,47 @@ class XBlockWrapper(PageObject):
grand_locators = [grandkid.locator for grandkid in grandkids]
return [descendant for descendant in descendants if descendant.locator not in grand_locators]
@property
def has_validation_message(self):
""" Is a validation warning/error/message shown? """
return self.q(css=self._bounded_selector('.xblock-message.validation')).present
def _validation_paragraph(self, css_class):
""" Helper method to return the <p> element of a validation warning """
return self.q(css=self._bounded_selector('.xblock-message.validation p.{}'.format(css_class)))
@property
def has_validation_warning(self):
""" Is a validation warning shown? """
return self._validation_paragraph('warning').present
@property
def has_validation_error(self):
""" Is a validation error shown? """
return self._validation_paragraph('error').present
@property
# pylint: disable=invalid-name
def has_validation_not_configured_warning(self):
""" Is a validation "not configured" message shown? """
return self._validation_paragraph('not-configured').present
@property
def validation_warning_text(self):
""" Get the text of the validation warning. """
return self._validation_paragraph('warning').text[0]
@property
def validation_error_text(self):
""" Get the text of the validation error. """
return self._validation_paragraph('error').text[0]
@property
# pylint: disable=invalid-name
def validation_not_configured_warning_text(self):
""" Get the text of the validation "not configured" message. """
return self._validation_paragraph('not-configured').text[0]
@property
def preview_selector(self):
return self._bounded_selector('.xblock-student_view,.xblock-author_view')
@@ -365,6 +415,34 @@ class XBlockWrapper(PageObject):
"""
self._click_button('basic_tab')
def open_settings_tab(self):
"""
If editing, click on the "Settings" tab
"""
self._click_button('settings_tab')
def set_field_val(self, field_display_name, field_value):
"""
If editing, set the value of a field.
"""
selector = '{} li.field label:contains("{}") + input'.format(self.editor_selector, field_display_name)
script = "$(arguments[0]).val(arguments[1]).change();"
self.browser.execute_script(script, selector, field_value)
def reset_field_val(self, field_display_name):
"""
If editing, reset the value of a field to its default.
"""
scope = '{} li.field label:contains("{}")'.format(self.editor_selector, field_display_name)
script = "$(arguments[0]).siblings('.setting-clear').click();"
self.browser.execute_script(script, scope)
def set_codemirror_text(self, text, index=0):
"""
Set the text of a CodeMirror editor that is part of this xblock's settings.
"""
type_in_codemirror(self, index, text, find_prefix='$("{}").find'.format(self.editor_selector))
def save_settings(self):
"""
Click on settings Save button.

View File

@@ -1,5 +1,5 @@
"""
My Courses page in Studio
Studio Home page
"""
from bok_choy.page_object import PageObject
@@ -8,7 +8,7 @@ from . import BASE_URL
class DashboardPage(PageObject):
"""
My Courses page in Studio
Studio Home page
"""
url = BASE_URL + "/course/"
@@ -40,3 +40,69 @@ class DashboardPage(PageObject):
Clicks on the course with run given by run.
"""
self.q(css='.course-run .value').filter(lambda el: el.text == run)[0].click()
def has_new_library_button(self):
"""
(bool) is the "New Library" button present?
"""
return self.q(css='.new-library-button').present
def click_new_library(self):
"""
Click on the "New Library" button
"""
self.q(css='.new-library-button').click()
def is_new_library_form_visible(self):
"""
Is the new library form visisble?
"""
return self.q(css='.wrapper-create-library').visible
def fill_new_library_form(self, display_name, org, number):
"""
Fill out the form to create a new library.
Must have called click_new_library() first.
"""
field = lambda fn: self.q(css='.wrapper-create-library #new-library-{}'.format(fn))
field('name').fill(display_name)
field('org').fill(org)
field('number').fill(number)
def is_new_library_form_valid(self):
"""
IS the new library form ready to submit?
"""
return (
self.q(css='.wrapper-create-library .new-library-save:not(.is-disabled)').present and
not self.q(css='.wrapper-create-library .wrap-error.is-shown').present
)
def submit_new_library_form(self):
"""
Submit the new library form.
"""
self.q(css='.wrapper-create-library .new-library-save').click()
def list_libraries(self):
"""
List all the libraries found on the page's list of libraries.
"""
# Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements
self.q(css='#course-index-tabs .libraries-tab a').click()
div2info = lambda element: {
'name': element.find_element_by_css_selector('.course-title').text,
'org': element.find_element_by_css_selector('.course-org .value').text,
'number': element.find_element_by_css_selector('.course-num .value').text,
'url': element.find_element_by_css_selector('a.library-link').get_attribute('href'),
}
return self.q(css='.libraries li.course-item').map(div2info).results
def has_library(self, **kwargs):
"""
Does the page's list of libraries include a library matching kwargs?
"""
for lib in self.list_libraries():
if all([lib[key] == kwargs[key] for key in kwargs]):
return True
return False

View File

@@ -0,0 +1,281 @@
"""
Library edit page in Studio
"""
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
class StudioLibraryContainerXBlockWrapper(XBlockWrapper):
"""
Wraps :class:`.container.XBlockWrapper` for use with LibraryContent blocks
"""
url = None
@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"))
def refresh_children(self):
"""
Click "Update now..." button
"""
btn_selector = self._bounded_selector(".library-update-btn")
refresh_button = self.q(css=btn_selector)
refresh_button.click()
self.wait_for_element_absence(btn_selector, 'Wait for the XBlock to reload')

View File

@@ -0,0 +1,62 @@
"""
Mixin to include for Paginated container pages
"""
from selenium.webdriver.common.keys import Keys
class PaginatedMixin(object):
"""
Mixin class used for paginated page tests.
"""
def nav_disabled(self, position, arrows=('next', 'previous')):
"""
Verifies that pagination nav is disabled. Position can be 'top' or 'bottom'.
`top` is the header, `bottom` is the footer.
To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'.
"""
return all([
self.q(css='nav.%s * a.%s-page-link.is-disabled' % (position, arrow))
for arrow in arrows
])
def move_back(self, position):
"""
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
"""
self.q(css='nav.%s * a.previous-page-link' % position)[0].click()
self.wait_until_ready()
def move_forward(self, position):
"""
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
"""
self.q(css='nav.%s * a.next-page-link' % position)[0].click()
self.wait_until_ready()
def go_to_page(self, number):
"""
Enter a number into the page number input field, and then try to navigate to it.
"""
page_input = self.q(css="#page-number-input")[0]
page_input.click()
page_input.send_keys(str(number))
page_input.send_keys(Keys.RETURN)
self.wait_until_ready()
def get_page_number(self):
"""
Returns the page number as the page represents it, in string form.
"""
return self.q(css="span.current-page")[0].get_attribute('innerHTML')
def check_page_unchanged(self, first_block_name):
"""
Used to make sure that a page has not transitioned after a bogus number is given.
"""
if not self.xblocks[0].name == first_block_name:
return False
if not self.q(css='#page-number-input')[0].get_attribute('value') == '':
return False
return True

View File

@@ -0,0 +1,191 @@
"""
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 . 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 UsersPage(PageObject):
"""
Base class for either the Course Team page or the Library Team page
"""
def __init__(self, browser, locator):
super(UsersPage, self).__init__(browser)
self.locator = locator
@property
def url(self):
"""
URL to this page - override in subclass
"""
raise NotImplementedError
def is_browser_on_page(self):
"""
Returns True iff the browser has loaded the page.
"""
return self.q(css='body.view-team').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 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').click()
@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)
class LibraryUsersPage(UsersPage):
"""
Library Team page in Studio
"""
@property
def url(self):
"""
URL to the "User Access" page for the given library.
"""
return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
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()
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]

View File

@@ -103,6 +103,33 @@ def add_advanced_component(page, menu_index, name):
click_css(page, component_css, 0)
def add_component(page, item_type, specific_type):
"""
Click one of the "Add New Component" buttons.
item_type should be "advanced", "html", "problem", or "video"
specific_type is required for some types and should be something like
"Blank Common Problem".
"""
btn = page.q(css='.add-xblock-component .add-xblock-component-button[data-type={}]'.format(item_type))
multiple_templates = btn.filter(lambda el: 'multiple-templates' in el.get_attribute('class')).present
btn.click()
if multiple_templates:
sub_template_menu_div_selector = '.new-component-{}'.format(item_type)
page.wait_for_element_visibility(sub_template_menu_div_selector, 'Wait for the templates sub-menu to appear')
page.wait_for_element_invisibility(
'.add-xblock-component .new-component',
'Wait for the add component menu to disappear'
)
all_options = page.q(css='.new-component-{} ul.new-component-template li a span'.format(item_type))
chosen_option = all_options.filter(lambda el: el.text == specific_type).first
chosen_option.click()
wait_for_notification(page)
page.wait_for_ajax()
@js_defined('window.jQuery')
def type_in_codemirror(page, index, text, find_prefix="$"):
script = """