From ff00fbd1f1ff7acfdf30d700e482f47c9f873e15 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 9 Apr 2014 16:50:34 -0400 Subject: [PATCH] Bok choy acceptance test for drag and drop. --- .../test/acceptance/pages/studio/container.py | 36 +++++ ...t_studio.py => test_studio_acid_xblock.py} | 143 +---------------- .../acceptance/tests/test_studio_container.py | 94 +++++++++-- .../acceptance/tests/test_studio_general.py | 148 ++++++++++++++++++ 4 files changed, 264 insertions(+), 157 deletions(-) rename common/test/acceptance/tests/{test_studio.py => test_studio_acid_xblock.py} (59%) create mode 100644 common/test/acceptance/tests/test_studio_general.py diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 070f39a7ea..2f3b8a09b5 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -6,6 +6,7 @@ from bok_choy.page_object import PageObject from bok_choy.promise import Promise from . import BASE_URL +from selenium.webdriver.common.action_chains import ActionChains class ContainerPage(PageObject): """ @@ -44,6 +45,25 @@ class ContainerPage(PageObject): return self.q(css=XBlockWrapper.BODY_SELECTOR).map( lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results + def drag(self, source_index, target_index, after=True): + """ + 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 AFTER the + one with the target_index drag handle, unless 'after' is set to False. + """ + draggables = self.q(css='.drag-handle') + source = draggables[source_index] + target = draggables[target_index] + action = ActionChains(self.browser) + action.click_and_hold(source).perform() # pylint: disable=protected-access + action.move_to_element_with_offset( + target, 0, target.size['height']/2 if after else 0 + ).perform() # pylint: disable=protected-access + action.release().perform() + # TODO: should wait for "Saving" to go away so we know the operation is complete? + class XBlockWrapper(PageObject): """ @@ -78,6 +98,22 @@ class XBlockWrapper(PageObject): else: return None + @property + def children(self): + """ + Will return any first-generation descendant xblocks of this xblock. + """ + descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( + lambda el: XBlockWrapper(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] + @property def preview_selector(self): return self._bounded_selector('.xblock-student_view') diff --git a/common/test/acceptance/tests/test_studio.py b/common/test/acceptance/tests/test_studio_acid_xblock.py similarity index 59% rename from common/test/acceptance/tests/test_studio.py rename to common/test/acceptance/tests/test_studio_acid_xblock.py index 158ff929c2..0e1e2d7231 100644 --- a/common/test/acceptance/tests/test_studio.py +++ b/common/test/acceptance/tests/test_studio_acid_xblock.py @@ -1,156 +1,15 @@ """ -Acceptance tests for Studio. +Acceptance tests for Studio related to the acid xblock. """ from unittest import skip from bok_choy.web_app_test import WebAppTest -from ..pages.studio.asset_index import AssetIndexPage from ..pages.studio.auto_auth import AutoAuthPage -from ..pages.studio.checklists import ChecklistsPage -from ..pages.studio.course_import import ImportPage -from ..pages.studio.course_info import CourseUpdatesPage -from ..pages.studio.edit_tabs import PagesPage -from ..pages.studio.export import ExportPage -from ..pages.studio.howitworks import HowitworksPage -from ..pages.studio.index import DashboardPage -from ..pages.studio.login import LoginPage -from ..pages.studio.manage_users import CourseTeamPage from ..pages.studio.overview import CourseOutlinePage -from ..pages.studio.settings import SettingsPage -from ..pages.studio.settings_advanced import AdvancedSettingsPage -from ..pages.studio.settings_graders import GradingPage -from ..pages.studio.signup import SignupPage -from ..pages.studio.textbooks import TextbooksPage from ..pages.xblock.acid import AcidView from ..fixtures.course import CourseFixture, XBlockFixtureDesc -from .helpers import UniqueCourseTest - - -class LoggedOutTest(WebAppTest): - """ - Smoke test for pages in Studio that are visible when logged out. - """ - - def setUp(self): - super(LoggedOutTest, self).setUp() - self.pages = [LoginPage(self.browser), HowitworksPage(self.browser), SignupPage(self.browser)] - - def test_page_existence(self): - """ - Make sure that all the pages are accessible. - Rather than fire up the browser just to check each url, - do them all sequentially in this testcase. - """ - for page in self.pages: - page.visit() - - -class LoggedInPagesTest(WebAppTest): - """ - Tests that verify the pages in Studio that you can get to when logged - in and do not have a course yet. - """ - - def setUp(self): - super(LoggedInPagesTest, self).setUp() - self.auth_page = AutoAuthPage(self.browser, staff=True) - self.dashboard_page = DashboardPage(self.browser) - - def test_dashboard_no_courses(self): - """ - Make sure that you can get to the dashboard page without a course. - """ - self.auth_page.visit() - self.dashboard_page.visit() - - -class CoursePagesTest(UniqueCourseTest): - """ - Tests that verify the pages in Studio that you can get to when logged - in and have a course. - """ - - COURSE_ID_SEPARATOR = "." - - def setUp(self): - """ - Install a course with no content using a fixture. - """ - super(UniqueCourseTest, self).setUp() - - CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ).install() - - self.auth_page = AutoAuthPage(self.browser, staff=True) - - self.pages = [ - clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']) - for clz in [ - AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage, - PagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage, - AdvancedSettingsPage, GradingPage, TextbooksPage - ] - ] - - def test_page_existence(self): - """ - Make sure that all these pages are accessible once you have a course. - Rather than fire up the browser just to check each url, - do them all sequentially in this testcase. - """ - # Log in - self.auth_page.visit() - - # Verify that each page is available - for page in self.pages: - page.visit() - - -class DiscussionPreviewTest(UniqueCourseTest): - """ - Tests that Inline Discussions are rendered with a custom preview in Studio - """ - - def setUp(self): - super(DiscussionPreviewTest, self).setUp() - CourseFixture(**self.course_info).add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - ) - ) - ) - ) - ).install() - - AutoAuthPage(self.browser, staff=True).visit() - cop = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - cop.visit() - self.unit = cop.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit') - self.unit.go_to() - - def test_is_preview(self): - """ - Ensure that the preview version of the discussion is rendered. - """ - self.assertTrue(self.unit.q(css=".discussion-preview").present) - self.assertFalse(self.unit.q(css=".discussion-show").present) - - class XBlockAcidBase(WebAppTest): """ Base class for tests that verify that XBlock integration is working correctly diff --git a/common/test/acceptance/tests/test_studio_container.py b/common/test/acceptance/tests/test_studio_container.py index 9f1a9d17a8..80d51635d4 100644 --- a/common/test/acceptance/tests/test_studio_container.py +++ b/common/test/acceptance/tests/test_studio_container.py @@ -29,6 +29,15 @@ class ContainerBase(UniqueCourseTest): self.course_info['run'] ) + self.container_title = "" + self.group_a = "Expand or Collapse\nGroup A" + self.group_b = "Expand or Collapse\nGroup B" + self.group_empty = "Expand or Collapse\nGroup Empty" + self.group_a_item_1 = "Group A Item 1" + self.group_a_item_2 = "Group A Item 2" + self.group_b_item_1 = "Group B Item 1" + self.group_b_item_2 = "Group B Item 2" + self.setup_fixtures() self.auth_page.visit() @@ -47,12 +56,13 @@ class ContainerBase(UniqueCourseTest): XBlockFixtureDesc('vertical', 'Test Unit').add_children( XBlockFixtureDesc('vertical', 'Test Container').add_children( XBlockFixtureDesc('vertical', 'Group A').add_children( - XBlockFixtureDesc('html', 'Group A Item 1'), - XBlockFixtureDesc('html', 'Group A Item 2') + XBlockFixtureDesc('html', self.group_a_item_1), + XBlockFixtureDesc('html', self.group_a_item_2) ), + XBlockFixtureDesc('vertical', 'Group Empty'), XBlockFixtureDesc('vertical', 'Group B').add_children( - XBlockFixtureDesc('html', 'Group B Item 1'), - XBlockFixtureDesc('html', 'Group B Item 2') + XBlockFixtureDesc('html', self.group_b_item_1), + XBlockFixtureDesc('html', self.group_b_item_2) ) ) ) @@ -76,23 +86,77 @@ class DragAndDropTest(ContainerBase): """ __test__ = True - def verify_ordering(self, container, expected_ordering): + def verify_ordering(self, container, expected_orderings): xblocks = container.xblocks - for xblock in xblocks: - print xblock.name - # TODO: need to verify parenting structure on page. Just checking - # the order of the xblocks is not sufficient. + for expected_ordering in expected_orderings: + for xblock in xblocks: + parent = expected_ordering.keys()[0] + if xblock.name == 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) - - def test_reorder_in_group(self): + def drag_and_verify(self, source, target, expected_ordering, after=True): container = self.go_to_container_page(make_draft=True) - # Swap Group A Item 1 and Group A Item 2. - container.drag(1, 2) + container.drag(source, target, after) - expected_ordering = [{"Group A": ["Group A Item 2", "Group A Item 1"]}, - {"Group B": ["Group B Item 1", "Group B Item 2"]}] self.verify_ordering(container, expected_ordering) # Reload the page to see that the reordering was saved persisted. container = self.go_to_container_page() self.verify_ordering(container, expected_ordering) + + def test_reorder_in_group(self): + """ + Drag Group B Item 2 before Group B Item 1. + """ + expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, + {self.group_a: [self.group_a_item_1, self.group_a_item_2]}, + {self.group_b: [self.group_b_item_2, self.group_b_item_1]}, + {self.group_empty: []}] + self.drag_and_verify(6, 4, expected_ordering) + + def test_drag_to_top(self): + """ + Drag Group A Item 1 to top level (outside of Group A). + """ + expected_ordering = [{self.container_title: [self.group_a_item_1, self.group_a, self.group_empty, self.group_b]}, + {self.group_a: [self.group_a_item_2]}, + {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, + {self.group_empty: []}] + self.drag_and_verify(1, 0, expected_ordering, False) + + def test_drag_into_different_group(self): + """ + Drag Group A Item 1 into Group B (last element). + """ + expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, + {self.group_a: [self.group_a_item_2]}, + {self.group_b: [self.group_b_item_1, self.group_b_item_2, self.group_a_item_1]}, + {self.group_empty: []}] + self.drag_and_verify(1, 6, expected_ordering) + + def test_drag_group_into_group(self): + """ + Drag Group B into Group A (last element). + """ + expected_ordering = [{self.container_title: [self.group_a, self.group_empty]}, + {self.group_a: [self.group_a_item_1, self.group_a_item_2, self.group_b]}, + {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, + {self.group_empty: []}] + self.drag_and_verify(4, 2, expected_ordering) + + # Not able to drag into the empty group with automation (difficult even outside of automation). + # def test_drag_into_empty(self): + # """ + # Drag Group B Item 1 to Group Empty. + # """ + # expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, + # {self.group_a: [self.group_a_item_1, self.group_a_item_2]}, + # {self.group_b: [self.group_b_item_2]}, + # {self.group_empty: [self.group_b_item_1]}] + # self.drag_and_verify(6, 4, expected_ordering, False) diff --git a/common/test/acceptance/tests/test_studio_general.py b/common/test/acceptance/tests/test_studio_general.py new file mode 100644 index 0000000000..4b58d0e57b --- /dev/null +++ b/common/test/acceptance/tests/test_studio_general.py @@ -0,0 +1,148 @@ +""" +Acceptance tests for Studio. +""" +from bok_choy.web_app_test import WebAppTest + +from ..pages.studio.asset_index import AssetIndexPage +from ..pages.studio.auto_auth import AutoAuthPage +from ..pages.studio.checklists import ChecklistsPage +from ..pages.studio.course_import import ImportPage +from ..pages.studio.course_info import CourseUpdatesPage +from ..pages.studio.edit_tabs import PagesPage +from ..pages.studio.export import ExportPage +from ..pages.studio.howitworks import HowitworksPage +from ..pages.studio.index import DashboardPage +from ..pages.studio.login import LoginPage +from ..pages.studio.manage_users import CourseTeamPage +from ..pages.studio.overview import CourseOutlinePage +from ..pages.studio.settings import SettingsPage +from ..pages.studio.settings_advanced import AdvancedSettingsPage +from ..pages.studio.settings_graders import GradingPage +from ..pages.studio.signup import SignupPage +from ..pages.studio.textbooks import TextbooksPage +from ..fixtures.course import CourseFixture, XBlockFixtureDesc + +from .helpers import UniqueCourseTest + + +class LoggedOutTest(WebAppTest): + """ + Smoke test for pages in Studio that are visible when logged out. + """ + + def setUp(self): + super(LoggedOutTest, self).setUp() + self.pages = [LoginPage(self.browser), HowitworksPage(self.browser), SignupPage(self.browser)] + + def test_page_existence(self): + """ + Make sure that all the pages are accessible. + Rather than fire up the browser just to check each url, + do them all sequentially in this testcase. + """ + for page in self.pages: + page.visit() + + +class LoggedInPagesTest(WebAppTest): + """ + Tests that verify the pages in Studio that you can get to when logged + in and do not have a course yet. + """ + + def setUp(self): + super(LoggedInPagesTest, self).setUp() + self.auth_page = AutoAuthPage(self.browser, staff=True) + self.dashboard_page = DashboardPage(self.browser) + + def test_dashboard_no_courses(self): + """ + Make sure that you can get to the dashboard page without a course. + """ + self.auth_page.visit() + self.dashboard_page.visit() + + +class CoursePagesTest(UniqueCourseTest): + """ + Tests that verify the pages in Studio that you can get to when logged + in and have a course. + """ + + COURSE_ID_SEPARATOR = "." + + def setUp(self): + """ + Install a course with no content using a fixture. + """ + super(UniqueCourseTest, self).setUp() + + CourseFixture( + self.course_info['org'], + self.course_info['number'], + self.course_info['run'], + self.course_info['display_name'] + ).install() + + self.auth_page = AutoAuthPage(self.browser, staff=True) + + self.pages = [ + clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']) + for clz in [ + AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage, + PagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage, + AdvancedSettingsPage, GradingPage, TextbooksPage + ] + ] + + def test_page_existence(self): + """ + Make sure that all these pages are accessible once you have a course. + Rather than fire up the browser just to check each url, + do them all sequentially in this testcase. + """ + # Log in + self.auth_page.visit() + + # Verify that each page is available + for page in self.pages: + page.visit() + + +class DiscussionPreviewTest(UniqueCourseTest): + """ + Tests that Inline Discussions are rendered with a custom preview in Studio + """ + + def setUp(self): + super(DiscussionPreviewTest, self).setUp() + CourseFixture(**self.course_info).add_children( + XBlockFixtureDesc("chapter", "Test Section").add_children( + XBlockFixtureDesc("sequential", "Test Subsection").add_children( + XBlockFixtureDesc("vertical", "Test Unit").add_children( + XBlockFixtureDesc( + "discussion", + "Test Discussion", + ) + ) + ) + ) + ).install() + + AutoAuthPage(self.browser, staff=True).visit() + cop = CourseOutlinePage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + cop.visit() + self.unit = cop.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit') + self.unit.go_to() + + def test_is_preview(self): + """ + Ensure that the preview version of the discussion is rendered. + """ + self.assertTrue(self.unit.q(css=".discussion-preview").present) + self.assertFalse(self.unit.q(css=".discussion-show").present)