Merge pull request #4941 from edx/zub/story/tnl56-dragdropintocollapsedunittests
add tests for drag and drop unit into collapsed subsection on course out...
This commit is contained in:
@@ -11,7 +11,8 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
|
||||
handleClass: '.unit-drag-handle',
|
||||
droppableClass: 'ol.sortable-unit-list',
|
||||
parentLocationSelector: 'li.courseware-subsection',
|
||||
refresh: jasmine.createSpy('Spy on Unit')
|
||||
refresh: jasmine.createSpy('Spy on Unit'),
|
||||
ensureChildrenRendered: jasmine.createSpy('Spy on Unit')
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -23,7 +24,8 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
|
||||
handleClass: '.subsection-drag-handle',
|
||||
droppableClass: '.sortable-subsection-list',
|
||||
parentLocationSelector: 'section',
|
||||
refresh: jasmine.createSpy('Spy on Subsection')
|
||||
refresh: jasmine.createSpy('Spy on Subsection'),
|
||||
ensureChildrenRendered: jasmine.createSpy('Spy on Subsection')
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -277,6 +279,10 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
|
||||
expect($('#subsection-1')).not.toHaveClass('expand-on-drop');
|
||||
});
|
||||
it("expands a collapsed element when something is dropped in it", function () {
|
||||
expandElementSpy = spyOn(ContentDragger, 'expandElement').andCallThrough();
|
||||
expect(expandElementSpy).not.toHaveBeenCalled();
|
||||
expect($('#subsection-2').data('ensureChildrenRendered')).not.toHaveBeenCalled();
|
||||
|
||||
$('#subsection-2').addClass('is-collapsed');
|
||||
ContentDragger.dragState.dropDestination = $('#list-2');
|
||||
ContentDragger.dragState.attachMethod = "prepend";
|
||||
@@ -286,6 +292,10 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
|
||||
}, null, {
|
||||
clientX: $('#unit-1').offset().left
|
||||
});
|
||||
|
||||
// verify collapsed element expands while ensuring its children are properly rendered
|
||||
expect(expandElementSpy).toHaveBeenCalled();
|
||||
expect($('#subsection-2').data('ensureChildrenRendered')).toHaveBeenCalled();
|
||||
expect($('#subsection-2')).not.toHaveClass('is-collapsed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -8,6 +8,7 @@ from bok_choy.promise import EmptyPromise
|
||||
|
||||
from selenium.webdriver.support.ui import Select
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
from .course_page import CoursePage
|
||||
from .container import ContainerPage
|
||||
@@ -255,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
|
||||
@@ -269,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):
|
||||
"""
|
||||
@@ -288,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.
|
||||
"""
|
||||
@@ -325,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.
|
||||
"""
|
||||
@@ -510,6 +550,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
if subsection.is_collapsed:
|
||||
subsection.toggle_expand()
|
||||
|
||||
@property
|
||||
def xblocks(self):
|
||||
"""
|
||||
Return a list of xblocks loaded on the outline page.
|
||||
"""
|
||||
return self.children(CourseOutlineChild)
|
||||
|
||||
|
||||
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
|
||||
@@ -53,6 +53,76 @@ class CourseOutlineTest(StudioCourseTest):
|
||||
)
|
||||
)
|
||||
|
||||
def do_action_and_verify(self, outline_page, action, expected_ordering):
|
||||
"""
|
||||
Perform the supplied action and then verify the resulting ordering.
|
||||
"""
|
||||
if outline_page is None:
|
||||
outline_page = self.course_outline_page.visit()
|
||||
|
||||
action(outline_page)
|
||||
verify_ordering(self, outline_page, expected_ordering)
|
||||
|
||||
# Reload the page and expand all subsections to see that the change was persisted.
|
||||
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')
|
||||
class CourseOutlineDragAndDropTest(CourseOutlineTest):
|
||||
"""
|
||||
Tests of drag and drop within the outline page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
def populate_course_fixture(self, course_fixture):
|
||||
"""
|
||||
Create a course with one section, two subsections, and four units
|
||||
"""
|
||||
# with collapsed outline
|
||||
self.chap_1_handle = 0
|
||||
self.chap_1_seq_1_handle = 1
|
||||
|
||||
# with first sequential expanded
|
||||
self.seq_1_vert_1_handle = 2
|
||||
self.seq_1_vert_2_handle = 3
|
||||
self.chap_1_seq_2_handle = 4
|
||||
|
||||
course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', "1").add_children(
|
||||
XBlockFixtureDesc('sequential', '1.1').add_children(
|
||||
XBlockFixtureDesc('vertical', '1.1.1'),
|
||||
XBlockFixtureDesc('vertical', '1.1.2')
|
||||
),
|
||||
XBlockFixtureDesc('sequential', '1.2').add_children(
|
||||
XBlockFixtureDesc('vertical', '1.2.1'),
|
||||
XBlockFixtureDesc('vertical', '1.2.2')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def drag_and_verify(self, source, target, expected_ordering, outline_page=None):
|
||||
self.do_action_and_verify(
|
||||
outline_page,
|
||||
lambda (outline): drag(outline, source, target),
|
||||
expected_ordering
|
||||
)
|
||||
|
||||
def test_drop_unit_in_collapsed_subsection(self):
|
||||
"""
|
||||
Drag vertical "1.1.2" from subsection "1.1" into collapsed subsection "1.2" which already
|
||||
have its own verticals.
|
||||
"""
|
||||
course_outline_page = self.course_outline_page.visit()
|
||||
# expand first subsection
|
||||
course_outline_page.q(css='.outline-item.outline-subsection.is-collapsed .ui-toggle-expansion').first.click()
|
||||
|
||||
expected_ordering = [{"1": ["1.1", "1.2"]},
|
||||
{"1.1": ["1.1.1"]},
|
||||
{"1.2": ["1.1.2", "1.2.1", "1.2.2"]}]
|
||||
self.drag_and_verify(self.seq_1_vert_2_handle, self.chap_1_seq_2_handle, expected_ordering, course_outline_page)
|
||||
|
||||
|
||||
@attr('shard_2')
|
||||
class WarningMessagesTest(CourseOutlineTest):
|
||||
|
||||
Reference in New Issue
Block a user