diff --git a/cms/static/js/views/move_xblock_breadcrumb.js b/cms/static/js/views/move_xblock_breadcrumb.js
index 2d891b3c5c34..d4f5b90d00db 100644
--- a/cms/static/js/views/move_xblock_breadcrumb.js
+++ b/cms/static/js/views/move_xblock_breadcrumb.js
@@ -13,10 +13,6 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, MoveXBlockBreadcrumbVi
var MoveXBlockBreadcrumb = Backbone.View.extend({
el: '.breadcrumb-container',
- defaultRenderOptions: {
- breadcrumbs: ['Course Outline']
- },
-
events: {
'click .parent-nav-button': 'handleBreadcrumbButtonPress'
},
@@ -29,7 +25,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, MoveXBlockBreadcrumbVi
render: function(options) {
HtmlUtils.setHtml(
this.$el,
- this.template(_.extend({}, this.defaultRenderOptions, options))
+ this.template(options)
);
Backbone.trigger('move:breadcrumbRendered');
return this;
diff --git a/cms/templates/js/move-xblock-list.underscore b/cms/templates/js/move-xblock-list.underscore
index 5dcc22267449..eb51eae41ddc 100644
--- a/cms/templates/js/move-xblock-list.underscore
+++ b/cms/templates/js/move-xblock-list.underscore
@@ -13,7 +13,7 @@
<%- categoryText %>:
-
+
<% for (var i = 0; i < xblocks.length; i++) {
var xblock = xblocks[i];
%>
diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py
index 644f926ef8f3..85a6f83a77cb 100644
--- a/common/test/acceptance/pages/studio/container.py
+++ b/common/test/acceptance/pages/studio/container.py
@@ -63,11 +63,16 @@ def _is_finished_loading():
is_done = num_wrappers == (num_initialized_xblocks + num_failed_xblocks)
return (is_done, is_done)
+ def _loading_spinner_hidden():
+ """ promise function to check loading spinner state """
+ is_spinner_hidden = self.q(css='div.ui-loading.is-hidden').present
+ return is_spinner_hidden, is_spinner_hidden
+
# First make sure that an element with the view-container class is present on the page,
# and then wait for the loading spinner to go away and all the xblocks to be initialized.
return (
self.q(css='body.view-container').present and
- self.q(css='div.ui-loading.is-hidden').present and
+ Promise(_loading_spinner_hidden, 'loading spinner is hidden.').fulfill() and
Promise(_is_finished_loading, 'Finished rendering the xblock wrappers.').fulfill()
)
@@ -101,6 +106,13 @@ def active_xblocks(self):
"""
return self._get_xblocks(".is-active ")
+ @property
+ def displayed_children(self):
+ """
+ Return a list of displayed xblocks loaded on the container page.
+ """
+ return self._get_xblocks()[0].children
+
@property
def publish_title(self):
"""
@@ -262,6 +274,29 @@ def edit(self):
"""
return _click_edit(self, '.edit-button', '.xblock-studio_view')
+ def verify_confirmation_message(self, message):
+ """
+ Verify for confirmation message.
+ """
+ def _verify_message():
+ """ promise function to check confirmation message state """
+ text = self.q(css='#page-alert .alert.confirmation #alert-confirmation-title').text
+ return text and message in text[0]
+
+ self.wait_for(_verify_message, description='confirmation message present')
+
+ def click_undo_move_link(self):
+ """
+ Click undo move link.
+ """
+ click_css(self, '#page-alert .alert.confirmation .nav-actions .action-primary')
+
+ def click_take_me_there_link(self):
+ """
+ Click take me there link.
+ """
+ click_css(self, '#page-alert .alert.confirmation .nav-actions .action-secondary', require_notification=False)
+
def add_missing_groups(self):
"""
Click the "add missing groups" link.
@@ -382,7 +417,7 @@ def children(self):
"""
Will return any first-generation descendant xblocks of this xblock.
"""
- descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map(
+ descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).filter(lambda el: el.is_displayed()).map(
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results
# Now remove any non-direct descendants.
@@ -468,6 +503,13 @@ def has_edit_visibility_button(self):
"""
return self.q(css=self._bounded_selector('.visibility-button')).is_present()
+ @property
+ def has_move_modal_button(self):
+ """
+ Returns True if this xblock has move modal button else False
+ """
+ return self.q(css=self._bounded_selector('.move-button')).is_present()
+
def go_to_container(self):
"""
Open the container page linked to by this xblock, and return
@@ -505,6 +547,15 @@ def open_settings_tab(self):
"""
self._click_button('settings_tab')
+ def open_move_modal(self):
+ """
+ Opens the move modal.
+ """
+ click_css(self, '.move-button', require_notification=False)
+ self.wait_for(
+ lambda: self.q(css='.modal-window.move-modal').visible, description='move modal is visible'
+ )
+
def set_field_val(self, field_display_name, field_value):
"""
If editing, set the value of a field.
diff --git a/common/test/acceptance/pages/studio/move_xblock.py b/common/test/acceptance/pages/studio/move_xblock.py
new file mode 100644
index 000000000000..2b89b4235970
--- /dev/null
+++ b/common/test/acceptance/pages/studio/move_xblock.py
@@ -0,0 +1,78 @@
+"""
+Move XBlock Modal Page Object
+"""
+from bok_choy.page_object import PageObject
+from common.test.acceptance.pages.common.utils import click_css
+
+
+class MoveModalView(PageObject):
+ """
+ A base class for move xblock
+ """
+
+ def __init__(self, browser):
+ """
+ Arguments:
+ browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
+ """
+ super(MoveModalView, self).__init__(browser)
+
+ def is_browser_on_page(self):
+ return self.q(css='.modal-window.move-modal').present
+
+ def url(self):
+ """
+ Returns None because this is not directly accessible via URL.
+ """
+ return None
+
+ def save(self):
+ """
+ Clicks save button.
+ """
+ click_css(self, 'a.action-save')
+
+ def cancel(self):
+ """
+ Clicks cancel button.
+ """
+ click_css(self, 'a.action-cancel', require_notification=False)
+
+ def click_forward_button(self, source_index):
+ """
+ Click forward button at specified `source_index`.
+ """
+ css = '.move-modal .xblock-items-container .xblock-item'
+ self.q(css='.button-forward').nth(source_index).click()
+ self.wait_for(
+ lambda: len(self.q(css=css).results) > 0, description='children are visible'
+ )
+
+ def click_move_button(self):
+ """
+ Click move button.
+ """
+ self.q(css='.modal-actions .action-move').first.click()
+
+ @property
+ def is_move_button_enabled(self):
+ """
+ Returns True if move button on modal is enabled else False.
+ """
+ return not self.q(css='.modal-actions .action-move.is-disabled').present
+
+ @property
+ def children_category(self):
+ """
+ Get displayed children category.
+ """
+ return self.q(css='.xblock-items-container').attrs('data-items-category')[0]
+
+ def navigate_to_category(self, category, navigation_options):
+ """
+ Navigates to specifec `category` for a specified `source_index`.
+ """
+ child_category = self.children_category
+ while child_category != category:
+ self.click_forward_button(navigation_options[child_category])
+ child_category = self.children_category
diff --git a/common/test/acceptance/tests/studio/test_studio_container.py b/common/test/acceptance/tests/studio/test_studio_container.py
index 4efa7ed849a1..e496a14a0921 100644
--- a/common/test/acceptance/tests/studio/test_studio_container.py
+++ b/common/test/acceptance/tests/studio/test_studio_container.py
@@ -10,6 +10,7 @@
from common.test.acceptance.pages.studio.component_editor import ComponentEditorView, ComponentVisibilityEditorView
from common.test.acceptance.pages.studio.container import ContainerPage
from common.test.acceptance.pages.studio.html_component_editor import HtmlComponentEditorView
+from common.test.acceptance.pages.studio.move_xblock import MoveModalView
from common.test.acceptance.pages.studio.utils import add_discussion, drag
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.staff_view import StaffPage
@@ -1136,3 +1137,231 @@ def test_common_problem_types_tab(self):
"Text Input with Hints and Feedback",
]
self.assertEqual(page.get_category_tab_components('problem', 1), expected_components)
+
+
+@attr(shard=1)
+class MoveComponentTest(ContainerBase):
+ """
+ Tests of moving an XBlock to another XBlock.
+ """
+ def setUp(self, is_staff=True):
+ super(MoveComponentTest, self).setUp(is_staff=is_staff)
+ self.container = ContainerPage(self.browser, None)
+ self.move_modal_view = MoveModalView(self.browser)
+
+ self.navigation_options = {
+ 'section': 0,
+ 'subsection': 0,
+ 'unit': 1,
+ }
+ self.source_component_display_name = 'HTML 11'
+ self.source_xblock_category = 'component'
+ self.message_move = 'Success! "{display_name}" has been moved.'
+ self.message_undo = 'Move cancelled. "{display_name}" has been moved back to its original location.'
+
+ def populate_course_fixture(self, course_fixture):
+ """
+ Sets up a course structure.
+ """
+ # pylint: disable=attribute-defined-outside-init
+ self.unit_page1 = XBlockFixtureDesc('vertical', 'Test Unit 1').add_children(
+ XBlockFixtureDesc('html', 'HTML 11'),
+ XBlockFixtureDesc('html', 'HTML 12')
+ )
+ self.unit_page2 = XBlockFixtureDesc('vertical', 'Test Unit 2').add_children(
+ XBlockFixtureDesc('html', 'HTML 21'),
+ XBlockFixtureDesc('html', 'HTML 22')
+ )
+ course_fixture.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ self.unit_page1,
+ self.unit_page2
+ )
+ )
+ )
+
+ def verify_move_opertions(self, unit_page, source_component, operation, component_display_names_after_operation):
+ """
+ Verify move operations.
+
+ Arguments:
+ unit_page (Object) Unit container page.
+ source_component (Object) source XBlock object to be moved.
+ operation (str), `move` or `undo move` operation.
+ component_display_names_after_operation (dict) display names of components after operation in source/dest
+ """
+ source_component.open_move_modal()
+ self.move_modal_view.navigate_to_category(self.source_xblock_category, self.navigation_options)
+ self.assertEqual(self.move_modal_view.is_move_button_enabled, True)
+
+ self.move_modal_view.click_move_button()
+ self.container.verify_confirmation_message(
+ self.message_move.format(display_name=self.source_component_display_name)
+ )
+ self.assertEqual(len(unit_page.displayed_children), 1)
+
+ if operation == 'move':
+ self.container.click_take_me_there_link()
+ elif operation == 'undo_move':
+ self.container.click_undo_move_link()
+ self.container.verify_confirmation_message(
+ self.message_undo.format(display_name=self.source_component_display_name)
+ )
+
+ unit_page = ContainerPage(self.browser, None)
+ components = unit_page.displayed_children
+ self.assertEqual(
+ [component.name for component in components],
+ component_display_names_after_operation
+ )
+
+ def test_move_component_successfully(self):
+ """
+ Test if we can move a component successfully.
+
+ Given I am a staff user
+ And I go to unit page in first section
+ And I open the move modal
+ And I navigate to unit in second section
+ And I see move button is enabled
+ When I click on the move button
+ Then I see move operation success message
+ And When I click on take me there link
+ Then I see moved component there.
+ """
+ unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
+ components = unit_page.displayed_children
+ self.assertEqual(len(components), 2)
+
+ self.verify_move_opertions(
+ unit_page=unit_page,
+ source_component=components[0],
+ operation='move',
+ component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 11']
+ )
+
+ def test_undo_move_component_successfully(self):
+ """
+ Test if we can undo move a component successfully.
+
+ Given I am a staff user
+ And I go to unit page in first section
+ And I open the move modal
+ When I click on the move button
+ Then I see move operation successful message
+ And When I clicked on undo move link
+ Then I see that undo move operation is successful
+ """
+ unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
+ components = unit_page.displayed_children
+ self.assertEqual(len(components), 2)
+
+ self.verify_move_opertions(
+ unit_page=unit_page,
+ source_component=components[0],
+ operation='undo_move',
+ component_display_names_after_operation=['HTML 11', 'HTML 12']
+ )
+
+ def test_content_experiment(self):
+ """
+ Test if we can move a component of content experiment successfully.
+
+ Given that I am a staff user
+ And I go to content experiment page
+ And I open the move dialogue modal
+ When I navigate to the unit in second section
+ Then I see move button is enabled
+ And when I click on the move button
+ Then I see move operation success message
+ And when I click on take me there link
+ Then I see moved component there
+ And when I undo move a component
+ Then I see that undo move operation success message
+ """
+ # Add content experiment support to course.
+ self.course_fixture.add_advanced_settings({
+ u'advanced_modules': {'value': ['split_test']},
+ })
+
+ # Create group configurations
+ # pylint: disable=protected-access
+ self.course_fixture._update_xblock(self.course_fixture._course_location, {
+ 'metadata': {
+ u'user_partitions': [
+ create_user_partition_json(
+ 0,
+ 'Test Group Configuration',
+ 'Description of the group configuration.',
+ [Group('0', 'Group A'), Group('1', 'Group B')]
+ ),
+ ],
+ },
+ })
+
+ # Add split test to unit_page1 and assign newly created group configuration to it
+ split_test = XBlockFixtureDesc('split_test', 'Test Content Experiment', metadata={'user_partition_id': 0})
+ self.course_fixture.create_xblock(self.unit_page1.locator, split_test)
+
+ # Visit content experiment container page.
+ unit_page = ContainerPage(self.browser, split_test.locator)
+ unit_page.visit()
+
+ group_a_locator = unit_page.displayed_children[0].locator
+
+ # Add some components to Group A.
+ self.course_fixture.create_xblock(
+ group_a_locator, XBlockFixtureDesc('html', 'HTML 311')
+ )
+ self.course_fixture.create_xblock(
+ group_a_locator, XBlockFixtureDesc('html', 'HTML 312')
+ )
+
+ # Go to group page to move it's component.
+ group_container_page = ContainerPage(self.browser, group_a_locator)
+ group_container_page.visit()
+
+ # Verify content experiment block has correct groups and components.
+ components = group_container_page.displayed_children
+ self.assertEqual(len(components), 2)
+
+ self.source_component_display_name = 'HTML 311'
+
+ # Verify undo move operation for content experiment.
+ self.verify_move_opertions(
+ unit_page=group_container_page,
+ source_component=components[0],
+ operation='undo_move',
+ component_display_names_after_operation=['HTML 311', 'HTML 312']
+ )
+
+ # Verify move operation for content experiment.
+ self.verify_move_opertions(
+ unit_page=group_container_page,
+ source_component=components[0],
+ operation='move',
+ component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 311']
+ )
+
+ def test_a11y(self):
+ """
+ Verify move modal a11y.
+ """
+ unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
+
+ unit_page.a11y_audit.config.set_scope(
+ include=[".modal-window.move-modal"]
+ )
+ unit_page.a11y_audit.config.set_rules({
+ 'ignore': [
+ 'color-contrast', # TODO: AC-716
+ 'link-href', # TODO: AC-716
+ ]
+ })
+
+ unit_page.displayed_children[0].open_move_modal()
+
+ for category in ['section', 'subsection', 'component']:
+ self.move_modal_view.navigate_to_category(category, self.navigation_options)
+ unit_page.a11y_audit.check_for_accessibility_errors()