diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 92ec076f1a..95db6fbff0 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -6,9 +6,7 @@ from bok_choy.page_object import PageObject from bok_choy.promise import Promise, EmptyPromise from . import BASE_URL -from selenium.webdriver.common.action_chains import ActionChains - -from utils import click_css, wait_for_notification, confirm_prompt +from utils import click_css, confirm_prompt class ContainerPage(PageObject): @@ -220,26 +218,6 @@ class ContainerPage(PageObject): return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map( lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results - def drag(self, source_index, target_index): - """ - 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 = self.q(css='.drag-handle') - source = draggables[source_index] - target = draggables[target_index] - action = ActionChains(self.browser) - # When dragging before the target element, must take into account that the placeholder - # will appear in the place where the target used to be. - placeholder_height = 40 - action.click_and_hold(source).move_to_element_with_offset( - target, 0, placeholder_height - ).release().perform() - wait_for_notification(self) - def duplicate(self, source_index): """ Duplicate the item with index source_index (based on vertical placement in page). diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index bc99be3823..d814854264 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -12,7 +12,7 @@ from selenium.webdriver.common.action_chains import ActionChains from .course_page import CoursePage from .container import ContainerPage -from .utils import set_input_value_and_save, set_input_value, click_css, confirm_prompt, wait_for_notification +from .utils import set_input_value_and_save, set_input_value, click_css, confirm_prompt class CourseOutlineItem(object): @@ -256,6 +256,9 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): """ A page object that will be used as a child of :class:`CourseOutlineContainer`. """ + url = None + BODY_SELECTOR = '.outline-item' + def __init__(self, browser, locator): super(CourseOutlineChild, self).__init__(browser) self.locator = locator @@ -270,6 +273,39 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): click_css(self, self._bounded_selector('.delete-button'), require_notification=False) confirm_prompt(self, cancel) + def _bounded_selector(self, selector): + """ + Return `selector`, but limited to this particular `CourseOutlineChild` context + """ + return '{}[data-locator="{}"] {}'.format( + self.BODY_SELECTOR, + self.locator, + selector + ) + + @property + def name(self): + titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text + if titles: + return titles[0] + else: + return None + + @property + def children(self): + """ + Will return any first-generation descendant items of this item. + """ + descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( + lambda el: CourseOutlineChild(self.browser, el.get_attribute('data-locator'))).results + + # Now remove any non-direct descendants. + grandkids = [] + for descendant in descendants: + grandkids.extend(descendant.children) + + grand_locators = [grandkid.locator for grandkid in grandkids] + return [descendant for descendant in descendants if not descendant.locator in grand_locators] class CourseOutlineUnit(CourseOutlineChild): """ @@ -289,8 +325,11 @@ class CourseOutlineUnit(CourseOutlineChild): def is_browser_on_page(self): return self.q(css=self.BODY_SELECTOR).present + def children(self): + return self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( + lambda el: CourseOutlineUnit(self.browser, el.get_attribute('data-locator'))).results -class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): +class CourseOutlineSubsection(CourseOutlineContainer, CourseOutlineChild): """ :class`.PageObject` that wraps a subsection block on the Studio Course Outline page. """ @@ -326,7 +365,7 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): self.q(css=self._bounded_selector(self.ADD_BUTTON_SELECTOR)).click() -class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): +class CourseOutlineSection(CourseOutlineContainer, CourseOutlineChild): """ :class`.PageObject` that wraps a section block on the Studio Course Outline page. """ @@ -512,85 +551,12 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): subsection.toggle_expand() @property - def outline_items(self): + def xblocks(self): """ Return a list of xblocks loaded on the outline page. """ - return self._get_outline_items() + return self.children(CourseOutlineChild) - def _get_outline_items(self, prefix=""): - return self.q(css=prefix + OutlineWrapper.BODY_SELECTOR).map( - lambda el: OutlineWrapper(self.browser, el.get_attribute('data-locator'))).results - - def drag(self, source_index, target_index): - """ - 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 = self.q(css='.drag-handle') - source = draggables[source_index] - target = draggables[target_index] - action = ActionChains(self.browser) - # When dragging before the target element, must take into account that the placeholder - # will appear in the place where the target used to be. - placeholder_height = 40 - action.click_and_hold(source).move_to_element_with_offset( - target, 0, placeholder_height - ).release().perform() - wait_for_notification(self) - - -class OutlineWrapper(PageObject): - """ - A PageObject representing a wrapper around course outline items shown on the Course Outline page. - """ - url = None - BODY_SELECTOR = '.outline-item' - NAME_SELECTOR = '.item-title' - - def __init__(self, browser, locator): - super(OutlineWrapper, self).__init__(browser) - self.locator = locator - - def is_browser_on_page(self): - return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular `CourseOutlineChild` context - """ - return '{}[data-locator="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - - @property - def name(self): - titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text - if titles: - return titles[0] - else: - return None - - @property - def children(self): - """ - Will return any first-generation descendant items of this item. - """ - descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( - lambda el: OutlineWrapper(self.browser, el.get_attribute('data-locator'))).results - - # Now remove any non-direct descendants. - grandkids = [] - for descendant in descendants: - grandkids.extend(descendant.children) - - grand_locators = [grandkid.locator for grandkid in grandkids] - return [descendant for descendant in descendants if not descendant.locator in grand_locators] class CourseOutlineModal(object): MODAL_SELECTOR = ".wrapper-modal-window" diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index b9e0c499ea..4c3f17d2f0 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -148,3 +148,48 @@ 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) + + +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() + wait_for_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)) diff --git a/common/test/acceptance/tests/base_studio_test.py b/common/test/acceptance/tests/base_studio_test.py index deea40c5ff..3164972c32 100644 --- a/common/test/acceptance/tests/base_studio_test.py +++ b/common/test/acceptance/tests/base_studio_test.py @@ -2,6 +2,7 @@ from ..pages.studio.auto_auth import AutoAuthPage from ..fixtures.course import CourseFixture from .helpers import UniqueCourseTest from ..pages.studio.overview import CourseOutlinePage +from ..pages.studio.utils import verify_ordering class StudioCourseTest(UniqueCourseTest): """ @@ -84,28 +85,6 @@ class ContainerBase(StudioCourseTest): subsection = self.outline.section(section_name).subsection(subsection_name) return subsection.toggle_expand().unit(unit_name).go_to() - def verify_ordering(self, container, expected_orderings): - """ - Verifies the expected ordering of xblocks on the page. - """ - xblocks = container.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)) - self.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)): - self.assertEqual(expected, children[idx].name) - blocks_checked.add(expected) - break - self.assertEqual(len(blocks_checked), len(xblocks)) - def do_action_and_verify(self, action, expected_ordering): """ Perform the supplied action and then verify the resulting ordering. @@ -113,8 +92,8 @@ class ContainerBase(StudioCourseTest): container = self.go_to_nested_container_page() action(container) - self.verify_ordering(container, expected_ordering) + verify_ordering(self, container, expected_ordering) # Reload the page to see that the change was persisted. container = self.go_to_nested_container_page() - self.verify_ordering(container, expected_ordering) + verify_ordering(self, container, expected_ordering) diff --git a/common/test/acceptance/tests/test_studio_bad_data.py b/common/test/acceptance/tests/test_studio_bad_data.py index a42a45ebd0..19026e4c50 100644 --- a/common/test/acceptance/tests/test_studio_bad_data.py +++ b/common/test/acceptance/tests/test_studio_bad_data.py @@ -1,6 +1,7 @@ from nose.plugins.attrib import attr from .base_studio_test import ContainerBase from ..fixtures.course import XBlockFixtureDesc +from ..pages.studio.utils import verify_ordering @attr('shard_1') @@ -38,7 +39,7 @@ class BadComponentTest(ContainerBase): displaying the components on the unit page. """ unit = self.go_to_unit_page() - self.verify_ordering(unit, [{"": ["Unit HTML", "Unit Problem"]}]) + verify_ordering(self, unit, [{"": ["Unit HTML", "Unit Problem"]}]) @attr('shard_1') diff --git a/common/test/acceptance/tests/test_studio_container.py b/common/test/acceptance/tests/test_studio_container.py index 1af162e419..c84313de03 100644 --- a/common/test/acceptance/tests/test_studio_container.py +++ b/common/test/acceptance/tests/test_studio_container.py @@ -8,7 +8,7 @@ from nose.plugins.attrib import attr from ..fixtures.course import XBlockFixtureDesc from ..pages.studio.component_editor import ComponentEditorView from ..pages.studio.html_component_editor import HtmlComponentEditorView -from ..pages.studio.utils import add_discussion +from ..pages.studio.utils import add_discussion, drag from ..pages.lms.courseware import CoursewarePage from ..pages.lms.staff_view import StaffPage @@ -75,7 +75,7 @@ class DragAndDropTest(NestedVerticalTest): def drag_and_verify(self, source, target, expected_ordering): self.do_action_and_verify( - lambda (container): container.drag(source, target), + lambda (container): drag(container, source, target, 40), expected_ordering ) @@ -133,9 +133,9 @@ class DragAndDropTest(NestedVerticalTest): first_handle = self.group_a_item_1_handle # Drag newly added video component to top. - container.drag(first_handle + 3, first_handle) + drag(container, first_handle + 3, first_handle, 40) # Drag duplicated component to top. - container.drag(first_handle + 2, first_handle) + drag(container, first_handle + 2, first_handle, 40) duplicate_label = self.duplicate_label.format(self.group_a_item_1) diff --git a/common/test/acceptance/tests/test_studio_outline.py b/common/test/acceptance/tests/test_studio_outline.py index 5d42378364..c3d1bbf2da 100644 --- a/common/test/acceptance/tests/test_studio_outline.py +++ b/common/test/acceptance/tests/test_studio_outline.py @@ -9,7 +9,7 @@ from pytz import UTC from bok_choy.promise import EmptyPromise from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState -from ..pages.studio.utils import add_discussion +from ..pages.studio.utils import add_discussion, drag, verify_ordering from ..pages.lms.courseware import CoursewarePage from ..pages.lms.course_nav import CourseNavPage from ..pages.lms.staff_view import StaffPage @@ -49,37 +49,10 @@ class CourseOutlineTest(StudioCourseTest): XBlockFixtureDesc('html', 'Test HTML Component'), XBlockFixtureDesc('discussion', 'Test Discussion Component') ) - ), - XBlockFixtureDesc('sequential', "DropS").add_children( - XBlockFixtureDesc('vertical', "DropV").add_children( - XBlockFixtureDesc('problem', 'Drop Problem 1', data=load_data_str('multiple_choice.xml')), - ) ) ) ) - def verify_ordering(self, outline_page, expected_orderings): - """ - Verifies the expected ordering of xblocks on the page. - """ - xblocks = outline_page.outline_items - 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)) - self.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)): - self.assertEqual(expected, children[idx].name) - blocks_checked.add(expected) - break - self.assertEqual(len(blocks_checked), len(xblocks)) - def do_action_and_verify(self, outline_page, action, expected_ordering): """ Perform the supplied action and then verify the resulting ordering. @@ -88,12 +61,12 @@ class CourseOutlineTest(StudioCourseTest): outline_page = self.course_outline_page.visit() action(outline_page) - self.verify_ordering(outline_page, expected_ordering) + verify_ordering(self, outline_page, expected_ordering) # Reload the page and expand all subsections to see that the change was persisted. - course_outline_page = self.course_outline_page.visit() - course_outline_page.q(css='.outline-item.outline-subsection.is-collapsed .ui-toggle-expansion').click() - self.verify_ordering(course_outline_page, expected_ordering) + outline_page = self.course_outline_page.visit() + outline_page.q(css='.outline-item.outline-subsection.is-collapsed .ui-toggle-expansion').click() + verify_ordering(self, outline_page, expected_ordering) @attr('shard_2') @@ -132,7 +105,7 @@ class CourseOutlineDragAndDropTest(CourseOutlineTest): def drag_and_verify(self, source, target, expected_ordering, outline_page=None): self.do_action_and_verify( outline_page, - lambda (outline): outline.drag(source, target), + lambda (outline): drag(outline, source, target), expected_ordering )