Commit d00bf940 by Andy Armstrong

Merge pull request #4941 from edx/zub/story/tnl56-dragdropintocollapsedunittests

add tests for drag and drop unit into collapsed subsection on course out...
parents 890e25f4 2ce1540a
...@@ -11,7 +11,8 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel ...@@ -11,7 +11,8 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
handleClass: '.unit-drag-handle', handleClass: '.unit-drag-handle',
droppableClass: 'ol.sortable-unit-list', droppableClass: 'ol.sortable-unit-list',
parentLocationSelector: 'li.courseware-subsection', 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 ...@@ -23,7 +24,8 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
handleClass: '.subsection-drag-handle', handleClass: '.subsection-drag-handle',
droppableClass: '.sortable-subsection-list', droppableClass: '.sortable-subsection-list',
parentLocationSelector: 'section', 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 ...@@ -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'); expect($('#subsection-1')).not.toHaveClass('expand-on-drop');
}); });
it("expands a collapsed element when something is dropped in it", function () { 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'); $('#subsection-2').addClass('is-collapsed');
ContentDragger.dragState.dropDestination = $('#list-2'); ContentDragger.dragState.dropDestination = $('#list-2');
ContentDragger.dragState.attachMethod = "prepend"; ContentDragger.dragState.attachMethod = "prepend";
...@@ -286,6 +292,10 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel ...@@ -286,6 +292,10 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
}, null, { }, null, {
clientX: $('#unit-1').offset().left 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'); expect($('#subsection-2')).not.toHaveClass('is-collapsed');
}); });
}); });
......
...@@ -6,9 +6,7 @@ from bok_choy.page_object import PageObject ...@@ -6,9 +6,7 @@ from bok_choy.page_object import PageObject
from bok_choy.promise import Promise, EmptyPromise from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL from . import BASE_URL
from selenium.webdriver.common.action_chains import ActionChains from utils import click_css, confirm_prompt
from utils import click_css, wait_for_notification, confirm_prompt
class ContainerPage(PageObject): class ContainerPage(PageObject):
...@@ -220,26 +218,6 @@ class ContainerPage(PageObject): ...@@ -220,26 +218,6 @@ class ContainerPage(PageObject):
return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map( return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map(
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results 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): def duplicate(self, source_index):
""" """
Duplicate the item with index source_index (based on vertical placement in page). Duplicate the item with index source_index (based on vertical placement in page).
......
...@@ -8,6 +8,7 @@ from bok_choy.promise import EmptyPromise ...@@ -8,6 +8,7 @@ from bok_choy.promise import EmptyPromise
from selenium.webdriver.support.ui import Select from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from .course_page import CoursePage from .course_page import CoursePage
from .container import ContainerPage from .container import ContainerPage
...@@ -255,6 +256,9 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): ...@@ -255,6 +256,9 @@ class CourseOutlineChild(PageObject, CourseOutlineItem):
""" """
A page object that will be used as a child of :class:`CourseOutlineContainer`. A page object that will be used as a child of :class:`CourseOutlineContainer`.
""" """
url = None
BODY_SELECTOR = '.outline-item'
def __init__(self, browser, locator): def __init__(self, browser, locator):
super(CourseOutlineChild, self).__init__(browser) super(CourseOutlineChild, self).__init__(browser)
self.locator = locator self.locator = locator
...@@ -269,6 +273,39 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): ...@@ -269,6 +273,39 @@ class CourseOutlineChild(PageObject, CourseOutlineItem):
click_css(self, self._bounded_selector('.delete-button'), require_notification=False) click_css(self, self._bounded_selector('.delete-button'), require_notification=False)
confirm_prompt(self, cancel) 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): class CourseOutlineUnit(CourseOutlineChild):
""" """
...@@ -288,8 +325,11 @@ class CourseOutlineUnit(CourseOutlineChild): ...@@ -288,8 +325,11 @@ class CourseOutlineUnit(CourseOutlineChild):
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css=self.BODY_SELECTOR).present 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. :class`.PageObject` that wraps a subsection block on the Studio Course Outline page.
""" """
...@@ -325,7 +365,7 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): ...@@ -325,7 +365,7 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
self.q(css=self._bounded_selector(self.ADD_BUTTON_SELECTOR)).click() 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. :class`.PageObject` that wraps a section block on the Studio Course Outline page.
""" """
...@@ -510,6 +550,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -510,6 +550,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
if subsection.is_collapsed: if subsection.is_collapsed:
subsection.toggle_expand() subsection.toggle_expand()
@property
def xblocks(self):
"""
Return a list of xblocks loaded on the outline page.
"""
return self.children(CourseOutlineChild)
class CourseOutlineModal(object): class CourseOutlineModal(object):
MODAL_SELECTOR = ".wrapper-modal-window" MODAL_SELECTOR = ".wrapper-modal-window"
......
...@@ -148,3 +148,48 @@ def set_input_value_and_save(page, css, value): ...@@ -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. 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) 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 ...@@ -2,6 +2,7 @@ from ..pages.studio.auto_auth import AutoAuthPage
from ..fixtures.course import CourseFixture from ..fixtures.course import CourseFixture
from .helpers import UniqueCourseTest from .helpers import UniqueCourseTest
from ..pages.studio.overview import CourseOutlinePage from ..pages.studio.overview import CourseOutlinePage
from ..pages.studio.utils import verify_ordering
class StudioCourseTest(UniqueCourseTest): class StudioCourseTest(UniqueCourseTest):
""" """
...@@ -84,28 +85,6 @@ class ContainerBase(StudioCourseTest): ...@@ -84,28 +85,6 @@ class ContainerBase(StudioCourseTest):
subsection = self.outline.section(section_name).subsection(subsection_name) subsection = self.outline.section(section_name).subsection(subsection_name)
return subsection.toggle_expand().unit(unit_name).go_to() 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): def do_action_and_verify(self, action, expected_ordering):
""" """
Perform the supplied action and then verify the resulting ordering. Perform the supplied action and then verify the resulting ordering.
...@@ -113,8 +92,8 @@ class ContainerBase(StudioCourseTest): ...@@ -113,8 +92,8 @@ class ContainerBase(StudioCourseTest):
container = self.go_to_nested_container_page() container = self.go_to_nested_container_page()
action(container) action(container)
self.verify_ordering(container, expected_ordering) verify_ordering(self, container, expected_ordering)
# Reload the page to see that the change was persisted. # Reload the page to see that the change was persisted.
container = self.go_to_nested_container_page() container = self.go_to_nested_container_page()
self.verify_ordering(container, expected_ordering) verify_ordering(self, container, expected_ordering)
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from .base_studio_test import ContainerBase from .base_studio_test import ContainerBase
from ..fixtures.course import XBlockFixtureDesc from ..fixtures.course import XBlockFixtureDesc
from ..pages.studio.utils import verify_ordering
@attr('shard_1') @attr('shard_1')
...@@ -38,7 +39,7 @@ class BadComponentTest(ContainerBase): ...@@ -38,7 +39,7 @@ class BadComponentTest(ContainerBase):
displaying the components on the unit page. displaying the components on the unit page.
""" """
unit = self.go_to_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') @attr('shard_1')
......
...@@ -8,7 +8,7 @@ from nose.plugins.attrib import attr ...@@ -8,7 +8,7 @@ from nose.plugins.attrib import attr
from ..fixtures.course import XBlockFixtureDesc from ..fixtures.course import XBlockFixtureDesc
from ..pages.studio.component_editor import ComponentEditorView from ..pages.studio.component_editor import ComponentEditorView
from ..pages.studio.html_component_editor import HtmlComponentEditorView 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.courseware import CoursewarePage
from ..pages.lms.staff_view import StaffPage from ..pages.lms.staff_view import StaffPage
...@@ -75,7 +75,7 @@ class DragAndDropTest(NestedVerticalTest): ...@@ -75,7 +75,7 @@ class DragAndDropTest(NestedVerticalTest):
def drag_and_verify(self, source, target, expected_ordering): def drag_and_verify(self, source, target, expected_ordering):
self.do_action_and_verify( self.do_action_and_verify(
lambda (container): container.drag(source, target), lambda (container): drag(container, source, target, 40),
expected_ordering expected_ordering
) )
...@@ -133,9 +133,9 @@ class DragAndDropTest(NestedVerticalTest): ...@@ -133,9 +133,9 @@ class DragAndDropTest(NestedVerticalTest):
first_handle = self.group_a_item_1_handle first_handle = self.group_a_item_1_handle
# Drag newly added video component to top. # 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. # 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) duplicate_label = self.duplicate_label.format(self.group_a_item_1)
......
...@@ -9,7 +9,7 @@ from pytz import UTC ...@@ -9,7 +9,7 @@ from pytz import UTC
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState 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.courseware import CoursewarePage
from ..pages.lms.course_nav import CourseNavPage from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.staff_view import StaffPage from ..pages.lms.staff_view import StaffPage
...@@ -53,6 +53,76 @@ class CourseOutlineTest(StudioCourseTest): ...@@ -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') @attr('shard_2')
class WarningMessagesTest(CourseOutlineTest): class WarningMessagesTest(CourseOutlineTest):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment