""" Utility methods useful for Studio page tests. """ from bok_choy.javascript import js_defined from bok_choy.promise import EmptyPromise from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from common.test.acceptance.pages.common.utils import click_css, sync_on_notification NAV_HELP_NOT_SIGNED_IN_CSS = '.nav-item.nav-not-signedin-help a' NAV_HELP_CSS = '.nav-item.nav-account-help a' SIDE_BAR_HELP_AS_LIST_ITEM = '.bit li.action-item a' SIDE_BAR_HELP_CSS = '.external-help a, .external-help-button' @js_defined('window.jQuery') def press_the_notification_button(page, name): # Because the notification uses a CSS transition, # Selenium will always report it as being visible. # This makes it very difficult to successfully click # the "Save" button at the UI level. # Instead, we use JavaScript to reliably click # the button. btn_css = 'div#page-notification button.action-%s' % name.lower() page.browser.execute_script("$('{}').focus().click()".format(btn_css)) page.wait_for_ajax() def add_discussion(page, menu_index=0): """ Add a new instance of the discussion category. menu_index specifies which instance of the menus should be used (based on vertical placement within the page). """ page.wait_for_component_menu() click_css(page, 'button>span.large-discussion-icon', menu_index) def add_advanced_component(page, menu_index, name): """ Adds an instance of the advanced component with the specified name. menu_index specifies which instance of the menus should be used (based on vertical placement within the page). """ # Click on the Advanced icon. page.wait_for_component_menu() click_css(page, 'button>span.large-advanced-icon', menu_index, require_notification=False) # This does an animation to hide the first level of buttons # and instead show the Advanced buttons that are available. # We should be OK though because click_css turns off jQuery animations # Make sure that the menu of advanced components is visible before clicking (the HTML is always on the # page, but will have display none until the large-advanced-icon is clicked). page.wait_for_element_visibility('.new-component-advanced', 'Advanced component menu is visible') # Now click on the component to add it. component_css = 'button[data-category={}]'.format(name) page.wait_for_element_visibility(component_css, 'Advanced component {} is visible'.format(name)) # Adding some components, e.g. the Discussion component, will make an ajax call # but we should be OK because the click_css method is written to handle that. click_css(page, component_css, 0) def add_component(page, item_type, specific_type, is_advanced_problem=False): """ 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' ) # "Common Problem Types" are shown by default. # For advanced problem types you must first select the "Advanced" tab. if is_advanced_problem: advanced_tab = page.q(css='.problem-type-tabs a').filter(text='Advanced').first advanced_tab.click() # Wait for the advanced tab to be active css = '.problem-type-tabs li.ui-tabs-active a' page.wait_for( lambda: len(page.q(css=css).filter(text='Advanced').execute()) > 0, 'Waiting for the Advanced problem tab to be active' ) all_options = page.q(css='.new-component-{} ul.new-component-template li button span'.format(item_type)) chosen_option = all_options.filter(text=specific_type).first chosen_option.click() sync_on_notification(page) page.wait_for_ajax() def add_components(page, item_type, items, is_advanced_problem=False): """ Adds multiple components of a specific type. item_type should be "advanced", "html", "problem", or "video" items is a list of components of specific type to be added. Please note that if you want to create an advanced problem then all other items must be of advanced problem type. """ for item in items: add_component(page, item_type, item, is_advanced_problem) def add_html_component(page, menu_index, boilerplate=None): """ Adds an instance of the HTML component with the specified name. menu_index specifies which instance of the menus should be used (based on vertical placement within the page). """ # Click on the HTML icon. page.wait_for_component_menu() click_css(page, 'button>span.large-html-icon', menu_index, require_notification=False) # Make sure that the menu of HTML components is visible before clicking page.wait_for_element_visibility('.new-component-html', 'HTML component menu is visible') # Now click on the component to add it. component_css = 'button[data-category=html]' if boilerplate: component_css += '[data-boilerplate={}]'.format(boilerplate) else: component_css += ':not([data-boilerplate])' page.wait_for_element_visibility(component_css, 'HTML component {} is visible'.format(boilerplate)) # Adding some components will make an ajax call but we should be OK because # the click_css method is written to handle that. click_css(page, component_css, 0) @js_defined('window.jQuery') def type_in_codemirror(page, index, text, find_prefix="$"): script = """ var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror; CodeMirror.signal(cm, "focus", cm); cm.setValue(arguments[0]); CodeMirror.signal(cm, "blur", cm);""".format(index=index, find_prefix=find_prefix) page.browser.execute_script(script, str(text)) @js_defined('window.jQuery') def get_codemirror_value(page, index=0, find_prefix="$"): return page.browser.execute_script( """ return {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror.getValue(); """.format(index=index, find_prefix=find_prefix) ) def get_input_value(page, css_selector): """ Returns the value of the field matching the css selector. """ page.wait_for_element_presence( css_selector, 'Elements matching "{}" selector are present'.format(css_selector) ) return page.q(css=css_selector).attrs('value')[0] def set_input_value(page, css, value): """ Sets the text field with the given label (display name) to the specified value. """ input_element = page.q(css=css).results[0] # Click in the input to give it the focus input_element.click() # Select all, then input the value input_element.send_keys(Keys.CONTROL + 'a') input_element.send_keys(value) # Return the input_element for chaining return input_element def set_input_value_and_save(page, css, value): """ Sets the text field with given label (display name) to the specified value, and presses Save. """ set_input_value(page, css, value).send_keys(Keys.ENTER) page.wait_for_ajax() def drag(page, source_index, target_index, placeholder_height=0): """ Gets the drag handle with index source_index (relative to the vertical layout of the page) and drags it to the location of the drag handle with target_index. This should drag the element with the source_index drag handle BEFORE the one with the target_index drag handle. """ draggables = page.q(css='.drag-handle') source = draggables[source_index] target = draggables[target_index] action = ActionChains(page.browser) action.click_and_hold(source).move_to_element_with_offset( target, 0, placeholder_height ) if placeholder_height == 0: action.release(target).perform() else: action.release().perform() sync_on_notification(page) def verify_ordering(test_class, page, expected_orderings): """ Verifies the expected ordering of xblocks on the page. """ xblocks = page.xblocks blocks_checked = set() for expected_ordering in expected_orderings: for xblock in xblocks: parent = expected_ordering.keys()[0] if xblock.name == parent: blocks_checked.add(parent) children = xblock.children expected_length = len(expected_ordering.get(parent)) test_class.assertEqual( expected_length, len(children), "Number of children incorrect for group {0}. Expected {1} but got {2}.".format(parent, expected_length, len(children))) for idx, expected in enumerate(expected_ordering.get(parent)): test_class.assertEqual(expected, children[idx].name) blocks_checked.add(expected) break test_class.assertEqual(len(blocks_checked), len(xblocks)) def click_studio_help(page): """ Click the Studio help link in the page footer. """ help_link_selector = '.cta-show-sock' # check if help link is visible EmptyPromise(lambda: page.q(css=help_link_selector).visible, "Help link visible").fulfill() page.q(css=help_link_selector).click() # check if extended support section is visible. EmptyPromise( lambda: page.q(css='.support .list-actions a').results[0].text != '', 'Support section opened' ).fulfill() def studio_help_links(page): """Return the list of Studio help links in the page footer.""" return page.q(css='.support .list-actions a').results class HelpMixin(object): """ Mixin for testing Help links. """ def get_nav_help_element_and_click_help(self, signed_in=True): """ Click on the help, and also get the DOM help element. It operates on the help elements in the navigation bar. Arguments: signed_in (bool): Indicates whether user is signed in or not. Returns: WebElement: Help DOM element in the navigation bar. """ element_css = None if signed_in: element_css = NAV_HELP_CSS else: element_css = NAV_HELP_NOT_SIGNED_IN_CSS self.q(css=element_css).first.click() return self.q(css=element_css).results[0] def get_side_bar_help_element_and_click_help(self, as_list_item=False, index=-1): """ Click on the help, and also get the DOM help element. It operates on the help elements in the side bar. Arguments: as_list_item (bool): Indicates whether help element is enclosed in a 'li' DOM element. index (int): The index of element in case there are more than one matching elements. Returns: WebElement: Help DOM element in the side bar. """ element_css = None if as_list_item: element_css = SIDE_BAR_HELP_AS_LIST_ITEM else: element_css = SIDE_BAR_HELP_CSS help_element = self.q(css=element_css).results[index] help_element.click() return help_element