diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index f9b66da633..9d89cc3d13 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -39,12 +39,18 @@ class ContainerPage(PageObject): def is_browser_on_page(self): def _is_finished_loading(): - # Wait until all components have been loaded. - # See common/static/coffee/src/xblock/core.coffee which adds the - # class "xblock-initialized" at the end of initializeBlock - num_wrappers = len(self.q(css=XBlockWrapper.BODY_SELECTOR).results) - num_xblocks_init = len(self.q(css='{} .xblock.xblock-initialized'.format(XBlockWrapper.BODY_SELECTOR)).results) - is_done = num_wrappers == num_xblocks_init + is_done = False + # Get the request token of the first xblock rendered on the page and assume it is correct. + data_request_elements = self.q(css='[data-request-token]') + if len(data_request_elements) > 0: + request_token = data_request_elements.first.attrs('data-request-token')[0] + # Then find the number of Studio xblock wrappers on the page with that request token. + num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results) + # Wait until all components have been loaded. + # See common/static/coffee/src/xblock/core.coffee which adds the + # class "xblock-initialized" at the end of initializeBlock + num_xblocks_init = len(self.q(css='{} .xblock.xblock-initialized[data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results) + is_done = num_wrappers == num_xblocks_init return (is_done, is_done) # First make sure that an element with the view-container class is present on the page, diff --git a/common/test/acceptance/tests/base_studio_test.py b/common/test/acceptance/tests/base_studio_test.py index f41ba7ecb2..824ca6480e 100644 --- a/common/test/acceptance/tests/base_studio_test.py +++ b/common/test/acceptance/tests/base_studio_test.py @@ -1,7 +1,7 @@ from ..pages.studio.auto_auth import AutoAuthPage from ..fixtures.course import CourseFixture from .helpers import UniqueCourseTest - +from ..pages.studio.overview import CourseOutlinePage class StudioCourseTest(UniqueCourseTest): @@ -45,3 +45,77 @@ class StudioCourseTest(UniqueCourseTest): password=user.get('password') ) self.auth_page.visit() + + +class ContainerBase(StudioCourseTest): + """ + Base class for tests that do operations on the container page. + """ + + def setUp(self): + """ + Create a unique identifier for the course used in this test. + """ + # Ensure that the superclass sets up + super(ContainerBase, self).setUp() + + self.outline = CourseOutlinePage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + + def go_to_nested_container_page(self): + """ + Go to the nested container page. + """ + unit = self.go_to_unit_page() + # The 0th entry is the unit page itself. + container = unit.xblocks[1].go_to_container() + return container + + def go_to_unit_page(self, section_name='Test Section', subsection_name='Test Subsection', unit_name='Test Unit'): + """ + Go to the test unit page. + + If make_draft is true, the unit page will be put into draft mode. + """ + self.outline.visit() + 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. + """ + container = self.go_to_nested_container_page() + action(container) + + self.verify_ordering(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) diff --git a/common/test/acceptance/tests/test_studio_bad_data.py b/common/test/acceptance/tests/test_studio_bad_data.py new file mode 100644 index 0000000000..a42a45ebd0 --- /dev/null +++ b/common/test/acceptance/tests/test_studio_bad_data.py @@ -0,0 +1,104 @@ +from nose.plugins.attrib import attr +from .base_studio_test import ContainerBase +from ..fixtures.course import XBlockFixtureDesc + + +@attr('shard_1') +class BadComponentTest(ContainerBase): + """ + Tests that components with bad content do not break the Unit page. + """ + __test__ = False + + def get_bad_html_content(self): + """ + Return the "bad" HTML content that has been problematic for Studio. + """ + pass + + def populate_course_fixture(self, course_fixture): + """ + Sets up a course structure with a unit and a HTML component with bad data and a properly constructed problem. + """ + + course_fixture.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc('html', 'Unit HTML', data=self.get_bad_html_content()), + XBlockFixtureDesc('problem', 'Unit Problem', data='') + ) + ) + ) + ) + + def test_html_comp_visible(self): + """ + Tests that bad HTML data within an HTML component doesn't prevent Studio from + displaying the components on the unit page. + """ + unit = self.go_to_unit_page() + self.verify_ordering(unit, [{"": ["Unit HTML", "Unit Problem"]}]) + + +@attr('shard_1') +class CopiedFromLmsBadContentTest(BadComponentTest): + """ + Tests that components with HTML copied from the LMS (LmsRuntime) do not break the Unit page. + """ + __test__ = True + + def get_bad_html_content(self): + """ + Return the "bad" HTML content that has been problematic for Studio. + """ + return """ +
+

Copied from LMS HTML component

+ """ + + +@attr('shard_1') +class CopiedFromStudioBadContentTest(BadComponentTest): + """ + Tests that components with HTML copied from the Studio (containing "ui-sortable" class) do not break the Unit page. + """ + __test__ = True + + def get_bad_html_content(self): + """ + Return the "bad" HTML content that has been problematic for Studio. + """ + return """ +
    +
  1. +
    +

    VOICE COMPARISON

    +

    You can access the experimental Voice Comparison tool at the link below.

    +
    +
  2. +
+ """ + + +@attr('shard_1') +class JSErrorBadContentTest(BadComponentTest): + """ + Tests that components that throw JS errors do not break the Unit page. + """ + __test__ = True + + def get_bad_html_content(self): + """ + Return the "bad" HTML content that has been problematic for Studio. + """ + return "" diff --git a/common/test/acceptance/tests/test_studio_container.py b/common/test/acceptance/tests/test_studio_container.py index 0d5c48ae4a..1af162e419 100644 --- a/common/test/acceptance/tests/test_studio_container.py +++ b/common/test/acceptance/tests/test_studio_container.py @@ -5,8 +5,6 @@ displaying containers within units. """ from nose.plugins.attrib import attr -from ..pages.studio.overview import CourseOutlinePage - from ..fixtures.course import XBlockFixtureDesc from ..pages.studio.component_editor import ComponentEditorView from ..pages.studio.html_component_editor import HtmlComponentEditorView @@ -16,87 +14,10 @@ from ..pages.lms.staff_view import StaffPage import datetime from bok_choy.promise import Promise, EmptyPromise -from .base_studio_test import StudioCourseTest - - -@attr('shard_1') -class ContainerBase(StudioCourseTest): - """ - Base class for tests that do operations on the container page. - """ - __test__ = False - - def setUp(self): - """ - Create a unique identifier for the course used in this test. - """ - # Ensure that the superclass sets up - super(ContainerBase, self).setUp() - - self.outline = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def go_to_nested_container_page(self): - """ - Go to the nested container page. - """ - unit = self.go_to_unit_page() - # The 0th entry is the unit page itself. - container = unit.xblocks[1].go_to_container() - return container - - def go_to_unit_page(self, section_name='Test Section', subsection_name='Test Subsection', unit_name='Test Unit'): - """ - Go to the test unit page. - - If make_draft is true, the unit page will be put into draft mode. - """ - self.outline.visit() - 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. - """ - container = self.go_to_nested_container_page() - action(container) - - self.verify_ordering(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) +from .base_studio_test import ContainerBase class NestedVerticalTest(ContainerBase): - __test__ = False def populate_course_fixture(self, course_fixture): """ @@ -151,7 +72,6 @@ class DragAndDropTest(NestedVerticalTest): """ Tests of reordering within the container page. """ - __test__ = True def drag_and_verify(self, source, target, expected_ordering): self.do_action_and_verify( @@ -232,7 +152,6 @@ class AddComponentTest(NestedVerticalTest): """ Tests of adding a component to the container page. """ - __test__ = True def add_and_verify(self, menu_index, expected_ordering): self.do_action_and_verify( @@ -273,7 +192,6 @@ class DuplicateComponentTest(NestedVerticalTest): """ Tests of duplicating a component on the container page. """ - __test__ = True def duplicate_and_verify(self, source_index, expected_ordering): self.do_action_and_verify( @@ -320,7 +238,6 @@ class DeleteComponentTest(NestedVerticalTest): """ Tests of deleting a component from the container page. """ - __test__ = True def delete_and_verify(self, source_index, expected_ordering): self.do_action_and_verify( @@ -344,7 +261,6 @@ class EditContainerTest(NestedVerticalTest): """ Tests of editing a container. """ - __test__ = True def modify_display_name_and_verify(self, component): """ @@ -377,7 +293,6 @@ class UnitPublishingTest(ContainerBase): """ Tests of the publishing control and related widgets on the Unit page. """ - __test__ = True PUBLISHED_STATUS = "Publishing Status\nPublished (not yet released)" PUBLISHED_LIVE_STATUS = "Publishing Status\nPublished and Live"