update/cleanup code + move verify_ordering and drag method to studio utils
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user