From f31475a2fda7f8dac5ed6a32a295808ed42960e4 Mon Sep 17 00:00:00 2001 From: Ben McMorran Date: Mon, 21 Jul 2014 14:00:23 -0400 Subject: [PATCH] Add bokchoy tests for outline page --- cms/static/js/views/course_outline.js | 1 + cms/static/js/views/pages/container.js | 2 +- cms/static/js/views/xblock_outline.js | 2 +- .../test/acceptance/pages/studio/container.py | 24 +- .../test/acceptance/pages/studio/overview.py | 198 +++++- common/test/acceptance/pages/studio/utils.py | 11 + .../acceptance/tests/test_studio_container.py | 46 +- .../acceptance/tests/test_studio_general.py | 51 -- .../acceptance/tests/test_studio_outline.py | 574 ++++++++++++++++++ .../tests/test_studio_split_test.py | 34 +- 10 files changed, 813 insertions(+), 130 deletions(-) create mode 100644 common/test/acceptance/tests/test_studio_outline.py diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index caa0d845bc..72f61aae8d 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -125,6 +125,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ // as it cannot visually effect the other sections. if (childCategory === 'chapter' && children && children.length > 1) { childView.$el.remove(); + children.splice(children.indexOf(childView.model), 1); } else { this.refresh(); } diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 25806b1fb1..cb2c33bc57 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -22,7 +22,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views }); this.nameEditor.render(); if (this.options.action === 'new') { - this.nameEditor.$('.xblock-field-value').click(); + this.nameEditor.$('.xblock-field-value-edit').click(); } this.xblockView = new ContainerView({ el: this.$('.wrapper-xblock'), diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index a342743270..31c0755216 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -194,7 +194,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ } ViewUtils.setScrollOffset(locatorElement, scrollOffset); if (editDisplayName) { - locatorElement.find('> .wrapper-xblock-header .xblock-field-value').click(); + locatorElement.find('> .wrapper-xblock-header .xblock-field-value-edit').click(); } } this.initialState = null; diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 311c59ea1a..5850bb2d7d 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -8,7 +8,7 @@ from . import BASE_URL from selenium.webdriver.common.action_chains import ActionChains -from utils import click_css, wait_for_notification +from utils import click_css, wait_for_notification, confirm_prompt class ContainerPage(PageObject): @@ -16,6 +16,8 @@ class ContainerPage(PageObject): Container page in Studio """ NAME_SELECTOR = '.page-header-title' + NAME_INPUT_SELECTOR = '.page-header .xblock-field-input' + NAME_FIELD_WRAPPER_SELECTOR = '.page-header .wrapper-xblock-field' def __init__(self, browser, locator): super(ContainerPage, self).__init__(browser) @@ -134,7 +136,7 @@ class ContainerPage(PageObject): Discards draft changes (which will then re-render the page). """ click_css(self, 'a.action-discard', 0, require_notification=False) - self.q(css='a.button.action-primary').first.click() + confirm_prompt(self) self.wait_for_ajax() def toggle_staff_lock(self): @@ -149,7 +151,7 @@ class ContainerPage(PageObject): self.q(css='a.action-staff-lock').first.click() else: click_css(self, 'a.action-staff-lock', 0, require_notification=False) - self.q(css='a.button.action-primary').first.click() + confirm_prompt(self) self.wait_for_ajax() return not was_locked_initially @@ -218,16 +220,8 @@ class ContainerPage(PageObject): """ # Click the delete button click_css(self, 'a.delete-button', source_index, require_notification=False) - - # Wait for the warning prompt to appear - self.wait_for_element_visibility('#prompt-warning', 'Deletion warning prompt is visible') - - # Make sure the delete button is there - confirmation_button_css = '#prompt-warning a.button.action-primary' - self.wait_for_element_visibility(confirmation_button_css, 'Confirmation dialog button is visible') - # Click the confirmation dialog button - click_css(self, confirmation_button_css, 0) + confirm_prompt(self) def edit(self): """ @@ -255,6 +249,12 @@ class ContainerPage(PageObject): """ return self.q(css=".xblock-message.information").first.text[0] + def is_inline_editing_display_name(self): + """ + Return whether this container's display name is in its editable form. + """ + return "is-editing" in self.q(css=self.NAME_FIELD_WRAPPER_SELECTOR).first.attrs("class")[0] + class XBlockWrapper(PageObject): """ diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index ff3147bea4..5e4edfedda 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -6,7 +6,7 @@ from bok_choy.promise import EmptyPromise from .course_page import CoursePage from .container import ContainerPage -from .utils import set_input_value_and_save +from .utils import set_input_value_and_save, click_css, confirm_prompt class CourseOutlineItem(object): @@ -17,9 +17,13 @@ class CourseOutlineItem(object): EDIT_BUTTON_SELECTOR = '.xblock-title .xblock-field-value-edit' NAME_SELECTOR = '.xblock-title .xblock-field-value' NAME_INPUT_SELECTOR = '.xblock-title .xblock-field-input' + NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field' def __repr__(self): - return "{}(, {!r})".format(self.__class__.__name__, self.locator) + # CourseOutlineItem is also used as a mixin for CourseOutlinePage, which doesn't have a locator + # Check for the existence of a locator so that errors when navigating to the course outline page don't show up + # as errors in the repr method instead. + return "{}(, {!r})".format(self.__class__.__name__, self.locator if hasattr(self, 'locator') else None) def _bounded_selector(self, selector): """ @@ -50,6 +54,14 @@ class CourseOutlineItem(object): set_input_value_and_save(self, self._bounded_selector(self.NAME_INPUT_SELECTOR), new_name) self.wait_for_ajax() + def in_editable_form(self): + """ + Return whether this outline item's display name is in its editable form. + """ + return "is-editing" in self.q( + css=self._bounded_selector(self.NAME_FIELD_WRAPPER_SELECTOR) + )[0].get_attribute("class") + class CourseOutlineContainer(CourseOutlineItem): """ @@ -76,6 +88,15 @@ class CourseOutlineContainer(CourseOutlineItem): ).attrs('data-locator')[0] ) + def children(self, child_class=None): + """ + Returns all the children page objects of class child_class. + """ + if not child_class: + child_class = self.CHILD_CLASS + return self.q(css=child_class.BODY_SELECTOR).map( + lambda el: child_class(self.browser, el.get_attribute('data-locator'))).results + def child_at(self, index, child_class=None): """ Returns the child at the specified index. @@ -84,11 +105,47 @@ class CourseOutlineContainer(CourseOutlineItem): if not child_class: child_class = self.CHILD_CLASS - return child_class( - self.browser, - self.q(css=child_class.BODY_SELECTOR).attrs('data-locator')[index] + return self.children(child_class)[index] + + def add_child(self, require_notification=True): + """ + Adds a child to this xblock, waiting for notifications. + """ + click_css( + self, + self._bounded_selector(".add-xblock-component a.add-button"), + require_notification=require_notification, ) + def toggle_expand(self): + """ + Toggle the expansion of this subsection. + """ + + self.browser.execute_script("jQuery.fx.off = true;") + + def subsection_expanded(): + add_button = self.q(css=self._bounded_selector('> .add-xblock-component a.add-button')).first.results + return add_button and add_button[0].is_displayed() + + currently_expanded = subsection_expanded() + + self.q(css=self._bounded_selector('.ui-toggle-expansion')).first.click() + + EmptyPromise( + lambda: subsection_expanded() != currently_expanded, + "Check that the container {} has been toggled".format(self.locator) + ).fulfill() + + return self + + @property + def is_collapsed(self): + """ + Return whether this outline item is currently collapsed. + """ + return "collapsed" in self.q(css=self._bounded_selector('')).first.attrs("class")[0] + class CourseOutlineChild(PageObject, CourseOutlineItem): """ @@ -101,10 +158,17 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): def is_browser_on_page(self): return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present + def delete(self, cancel=False): + """ + Clicks the delete button, then cancels at the confirmation prompt if cancel is True. + """ + click_css(self, self._bounded_selector('.delete-button'), require_notification=False) + confirm_prompt(self, cancel) + class CourseOutlineUnit(CourseOutlineChild): """ - PageObject that wraps a unit link on the Studio Course Overview page. + PageObject that wraps a unit link on the Studio Course Outline page. """ url = None BODY_SELECTOR = '.outline-item-unit' @@ -123,7 +187,7 @@ class CourseOutlineUnit(CourseOutlineChild): class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): """ - :class`.PageObject` that wraps a subsection block on the Studio Course Overview page. + :class`.PageObject` that wraps a subsection block on the Studio Course Outline page. """ url = None @@ -136,31 +200,28 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): """ return self.child(title) - def toggle_expand(self): + def units(self): """ - Toggle the expansion of this subsection. + Returns the units in this subsection. """ - self.browser.execute_script("jQuery.fx.off = true;") + return self.children() - def subsection_expanded(): - add_button = self.q(css=self._bounded_selector('.add-button')).first.results - return add_button and add_button[0].is_displayed() + def unit_at(self, index): + """ + Returns the CourseOutlineUnit at the specified index. + """ + return self.child_at(index) - currently_expanded = subsection_expanded() - - self.q(css=self._bounded_selector('.ui-toggle-expansion')).first.click() - - EmptyPromise( - lambda: subsection_expanded() != currently_expanded, - "Check that the subsection {} has been toggled".format(self.locator) - ).fulfill() - - return self + def add_unit(self): + """ + Adds a unit to this subsection + """ + self.add_child(require_notification=False) class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): """ - :class`.PageObject` that wraps a section block on the Studio Course Overview page. + :class`.PageObject` that wraps a section block on the Studio Course Outline page. """ url = None BODY_SELECTOR = '.outline-item-section' @@ -172,6 +233,33 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): """ return self.child(title) + def subsections(self): + """ + Returns a list of the CourseOutlineSubsections of this section + """ + return self.children() + + def subsection_at(self, index): + """ + Returns the CourseOutlineSubsection at the specified index. + """ + return self.child_at(index) + + def add_subsection(self): + """ + Adds a subsection to this section + """ + self.add_child() + + +class ExpandCollapseLinkState: + """ + Represents the three states that the expand/collapse link can be in + """ + MISSING = 0 + COLLAPSE = 1 + EXPAND = 2 + class CourseOutlinePage(CoursePage, CourseOutlineContainer): """ @@ -179,10 +267,19 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): """ url_path = "course" CHILD_CLASS = CourseOutlineSection + EXPAND_COLLAPSE_CSS = '.toggle-button-expand-collapse' + BOTTOM_ADD_SECTION_BUTTON = '.course-outline > .add-xblock-component .add-button' def is_browser_on_page(self): return self.q(css='body.view-outline').present + def view_live(self): + """ + Clicks the "View Live" link and switches to the new tab + """ + click_css(self, '.view-live-button', require_notification=False) + self.browser.switch_to_window(self.browser.window_handles[-1]) + def section(self, title): """ Return the :class:`.CourseOutlineSection` with the title `title`. @@ -194,7 +291,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): Returns the :class:`.CourseOutlineSection` at the specified index. """ return self.child_at(index) - + def click_section_name(self, parent_css=''): """ Find and click on first section name in course outline @@ -229,3 +326,54 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): Open release date edit modal of first section in course outline """ self.q(css='div.section-published-date a.edit-release-date').first.click() + + def sections(self): + """ + Returns the sections of this course outline page. + """ + return self.children() + + def add_section_from_top_button(self): + """ + Clicks the button for adding a section which resides at the top of the screen. + """ + click_css(self, '.wrapper-mast nav.nav-actions .add-button') + + def add_section_from_bottom_button(self): + """ + Clicks the button for adding a section which resides at the bottom of the screen. + """ + click_css(self, self.BOTTOM_ADD_SECTION_BUTTON) + + def toggle_expand_collapse(self): + """ + Toggles whether all sections are expanded or collapsed + """ + self.q(css=self.EXPAND_COLLAPSE_CSS).click() + + @property + def bottom_add_section_button(self): + """ + Returns the query representing the bottom add section button. + """ + return self.q(css=self.BOTTOM_ADD_SECTION_BUTTON).first + + @property + def has_no_content_message(self): + """ + Returns true if a message informing the user that the course has no content is visible + """ + return self.q(css='.course-outline .no-content').is_present() + + @property + def expand_collapse_link_state(self): + """ + Returns the current state of the expand/collapse link + """ + link = self.q(css=self.EXPAND_COLLAPSE_CSS)[0] + if not link.is_displayed(): + return ExpandCollapseLinkState.MISSING + elif "collapse-all" in link.get_attribute("class"): + return ExpandCollapseLinkState.COLLAPSE + else: + return ExpandCollapseLinkState.EXPAND diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index 7bb43b2b10..92db97bfd6 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -139,3 +139,14 @@ def set_input_value_and_save(page, css, value): action = action.send_keys(Keys.BACKSPACE) # Send the new text, then hit the enter key so that the change event is triggered). action.send_keys(value).send_keys(Keys.ENTER).perform() + + +def confirm_prompt(page, cancel=False): + """ + Ensures that a modal prompt and confirmation button are visible, then clicks the button. The prompt is canceled iff + cancel is True. + """ + page.wait_for_element_visibility('.prompt', 'Prompt is visible') + confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary') + page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible') + click_css(page, confirmation_button_css, require_notification=(not cancel)) diff --git a/common/test/acceptance/tests/test_studio_container.py b/common/test/acceptance/tests/test_studio_container.py index 8a080f4799..90f45703a8 100644 --- a/common/test/acceptance/tests/test_studio_container.py +++ b/common/test/acceptance/tests/test_studio_container.py @@ -6,18 +6,17 @@ displaying containers within units. from nose.plugins.attrib import attr from ..pages.studio.overview import CourseOutlinePage -from ..fixtures.course import XBlockFixtureDesc +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.lms.courseware import CoursewarePage from ..pages.lms.staff_view import StaffPage -from unittest import skip -from acceptance.tests.base_studio_test import StudioCourseTest import datetime from bok_choy.promise import Promise, EmptyPromise +from acceptance.tests.base_studio_test import StudioCourseTest @attr('shard_1') @@ -388,23 +387,17 @@ class UnitPublishingTest(ContainerBase): LAST_PUBLISHED = 'Last published' LAST_SAVED = 'Draft saved on' - def setup_fixtures(self): + def populate_course_fixture(self, course_fixture): """ Sets up a course structure with a unit and a single HTML child. """ + self.html_content = '

Body of HTML Unit.

' self.courseware = CoursewarePage(self.browser, self.course_id) - - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) past_start_date = datetime.datetime(1974, 6, 22) self.past_start_date_text = "Jun 22, 1974 at 00:00 UTC" - course_fix.add_children( + course_fixture.add_children( XBlockFixtureDesc('chapter', 'Test Section').add_children( XBlockFixtureDesc('sequential', 'Test Subsection').add_children( XBlockFixtureDesc('vertical', 'Test Unit').add_children( @@ -426,9 +419,7 @@ class UnitPublishingTest(ContainerBase): ) ) ) - ).install() - - self.user = course_fix.user + ) def test_publishing(self): """ @@ -495,7 +486,7 @@ class UnitPublishingTest(ContainerBase): Then I see the published content in LMS """ unit = self.go_to_unit_page() - unit.view_published_version() + self._view_published_version(unit) self._verify_components_visible(['html']) def test_view_live_changes(self): @@ -510,7 +501,7 @@ class UnitPublishingTest(ContainerBase): """ unit = self.go_to_unit_page() add_discussion(unit) - unit.view_published_version() + self._view_published_version(unit) self._verify_components_visible(['html']) self.assertEqual(self.html_content, self.courseware.xblock_component_html_content(0)) @@ -527,7 +518,7 @@ class UnitPublishingTest(ContainerBase): unit = self.go_to_unit_page() add_discussion(unit) unit.publish_action.click() - unit.view_published_version() + self._view_published_version(unit) self._verify_components_visible(['html', 'discussion']) def test_initially_unlocked_visible_to_students(self): @@ -547,7 +538,7 @@ class UnitPublishingTest(ContainerBase): self._verify_release_date_info( unit, self.RELEASE_TITLE_RELEASED, self.past_start_date_text + ' with Section "Unlocked Section"' ) - unit.view_published_version() + self._view_published_version(unit) self._verify_student_view_visible(['problem']) def test_locked_visible_to_staff_only(self): @@ -567,7 +558,7 @@ class UnitPublishingTest(ContainerBase): self.assertTrue(checked) self.assertFalse(unit.currently_visible_to_students) self._verify_publish_title(unit, self.LOCKED_STATUS) - unit.view_published_version() + self._view_published_version(unit) # Will initially be in staff view, locked component should be visible. self._verify_components_visible(['problem']) # Switch to student view and verify not visible @@ -591,7 +582,7 @@ class UnitPublishingTest(ContainerBase): unit, self.RELEASE_TITLE_RELEASED, self.past_start_date_text + ' with Subsection "Subsection With Locked Unit"' ) - unit.view_published_version() + self._view_published_version(unit) self._verify_student_view_locked() def test_unlocked_visible_to_all(self): @@ -611,7 +602,7 @@ class UnitPublishingTest(ContainerBase): self.assertFalse(checked) self._verify_publish_title(unit, self.PUBLISHED_STATUS) self.assertTrue(unit.currently_visible_to_students) - unit.view_published_version() + self._view_published_version(unit) # Will initially be in staff view, components always visible. self._verify_components_visible(['discussion']) # Switch to student view and verify visible. @@ -641,7 +632,7 @@ class UnitPublishingTest(ContainerBase): unit.publish_action.click() unit.wait_for_ajax() self._verify_publish_title(unit, self.PUBLISHED_STATUS) - unit.view_published_version() + self._view_published_version(unit) self.assertTrue(modified_content in self.courseware.xblock_component_html_content(0)) def test_delete_child_in_published_unit(self): @@ -662,9 +653,16 @@ class UnitPublishingTest(ContainerBase): unit.publish_action.click() unit.wait_for_ajax() self._verify_publish_title(unit, self.PUBLISHED_STATUS) - unit.view_published_version() + self._view_published_version(unit) self.assertEqual(0, self.courseware.num_xblock_components) + def _view_published_version(self, unit): + """ + Goes to the published version, then waits for the browser to load the page. + """ + unit.view_published_version() + self.courseware.wait_for_page() + def _verify_and_return_staff_page(self): """ Verifies that the browser is on the staff page and returns a StaffPage. diff --git a/common/test/acceptance/tests/test_studio_general.py b/common/test/acceptance/tests/test_studio_general.py index e888f9c4be..5b56c816f0 100644 --- a/common/test/acceptance/tests/test_studio_general.py +++ b/common/test/acceptance/tests/test_studio_general.py @@ -113,57 +113,6 @@ class CoursePagesTest(StudioCourseTest): page.visit() -@attr('shard_1') -class CourseSectionTest(StudioCourseTest): - """ - Tests that verify the sections name editable only inside headers in Studio Course Outline 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(CourseSectionTest, self).setUp() - self.course_outline_page = CourseOutlinePage( - self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] - ) - self.course_outline_page.visit() - - def populate_course_fixture(self, course_fixture): - """ Populates the course fixture with a test section """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section') - ) - - def test_section_name_editable_in_course_outline(self): - """ - Check that section name is editable on course outline page. - """ - new_name = u"Test Section New" - section = self.course_outline_page.section_at(0) - self.assertEqual(section.name, u"Test Section") - section.change_name(new_name) - self.browser.refresh() - self.assertEqual(section.name, new_name) - - # TODO: re-enable when release date support is added back - # def test_section_name_not_editable_inside_modal(self): - # """ - # Check that section name is not editable inside "Section Release Date" modal on course outline page. - # """ - # parent_css='div.modal-window' - # self.course_outline_page.click_release_date() - # section_name = self.course_outline_page.get_section_name(parent_css)[0] - # self.assertEqual(section_name, '"Test Section"') - # self.course_outline_page.click_section_name(parent_css) - # section_name_edit_form = self.course_outline_page.section_name_edit_form_present(parent_css) - # self.assertFalse(section_name_edit_form) - - -@attr('shard_1') class DiscussionPreviewTest(StudioCourseTest): """ Tests that Inline Discussions are rendered with a custom preview in Studio diff --git a/common/test/acceptance/tests/test_studio_outline.py b/common/test/acceptance/tests/test_studio_outline.py new file mode 100644 index 0000000000..ad109e042f --- /dev/null +++ b/common/test/acceptance/tests/test_studio_outline.py @@ -0,0 +1,574 @@ +""" +Acceptance tests for studio related to the outline page. +""" + +from bok_choy.promise import EmptyPromise + +from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState +from ..pages.lms.courseware import CoursewarePage +from ..fixtures.course import XBlockFixtureDesc + +from acceptance.tests.base_studio_test import StudioCourseTest + + +class CourseOutlineTest(StudioCourseTest): + """ + Base class for all course outline tests + """ + + COURSE_ID_SEPARATOR = "." + + def setUp(self): + """ + Install a course with no content using a fixture. + """ + super(CourseOutlineTest, self).setUp() + self.course_outline_page = CourseOutlinePage( + self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] + ) + + def populate_course_fixture(self, course_fixture): + """ Install a course with sections/problems, tabs, updates, and handouts """ + course_fixture.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc('html', 'Test HTML Component'), + XBlockFixtureDesc('discussion', 'Test Discussion Component') + ) + ) + ) + ) + + +class EditNamesTest(CourseOutlineTest): + """ + Feature: Click-to-edit section/subsection names + """ + + __test__ = True + + def set_name_and_verify(self, item, old_name, new_name, expected_name): + """ + Changes the display name of item from old_name to new_name, then verifies that its value is expected_name. + """ + self.assertEqual(item.name, old_name) + item.change_name(new_name) + self.assertFalse(item.in_editable_form()) + self.assertEqual(item.name, expected_name) + + def test_edit_section_name(self): + """ + Scenario: Click-to-edit section name + Given that I have created a section + When I click on the name of section + Then the section name becomes editable + And given that I have edited the section name + When I click outside of the edited section name + Then the section name saves + And becomes non-editable + """ + self.course_outline_page.visit() + self.set_name_and_verify( + self.course_outline_page.section_at(0), + 'Test Section', + 'Changed', + 'Changed' + ) + + def test_edit_subsection_name(self): + """ + Scenario: Click-to-edit subsection name + Given that I have created a subsection + When I click on the name of subsection + Then the subsection name becomes editable + And given that I have edited the subsection name + When I click outside of the edited subsection name + Then the subsection name saves + And becomes non-editable + """ + self.course_outline_page.visit() + self.set_name_and_verify( + self.course_outline_page.section_at(0).subsection_at(0), + 'Test Subsection', + 'Changed', + 'Changed' + ) + + def test_edit_empty_section_name(self): + """ + Scenario: Click-to-edit section name, enter empty name + Given that I have created a section + And I have clicked to edit the name of the section + And I have entered an empty section name + When I click outside of the edited section name + Then the section name does not change + And becomes non-editable + """ + self.course_outline_page.visit() + self.set_name_and_verify( + self.course_outline_page.section_at(0), + 'Test Section', + '', + 'Test Section' + ) + + def test_edit_empty_subsection_name(self): + """ + Scenario: Click-to-edit subsection name, enter empty name + Given that I have created a subsection + And I have clicked to edit the name of the subsection + And I have entered an empty subsection name + When I click outside of the edited subsection name + Then the subsection name does not change + And becomes non-editable + """ + self.course_outline_page.visit() + self.set_name_and_verify( + self.course_outline_page.section_at(0).subsection_at(0), + 'Test Subsection', + '', + 'Test Subsection' + ) + + +class CreateSectionsTest(CourseOutlineTest): + """ + Feature: Create new sections/subsections/units + """ + + __test__ = True + + def populate_course_fixture(self, course_fixture): + """ Start with a completely empty course to easily test adding things to it """ + pass + + def test_create_new_section_from_top_button(self): + """ + Scenario: Create new section from button at top of page + Given that I am on the course outline + When I click the "+ Add section" button at the top of the page + Then I see a new section added to the bottom of the page + And the display name is in its editable form. + """ + self.course_outline_page.visit() + self.course_outline_page.add_section_from_top_button() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) + + def test_create_new_section_from_bottom_button(self): + """ + Scenario: Create new section from button at bottom of page + Given that I am on the course outline + When I click the "+ Add section" button at the bottom of the page + Then I see a new section added to the bottom of the page + And the display name is in its editable form. + """ + self.course_outline_page.visit() + self.course_outline_page.add_section_from_bottom_button() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) + + def test_create_new_subsection(self): + """ + Scenario: Create new subsection + Given that I have created a section + When I click the "+ Add subsection" button in that section + Then I see a new subsection added to the bottom of the section + And the display name is in its editable form. + """ + self.course_outline_page.visit() + self.course_outline_page.add_section_from_top_button() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.course_outline_page.section_at(0).add_subsection() + subsections = self.course_outline_page.section_at(0).subsections() + self.assertEqual(len(subsections), 1) + self.assertTrue(subsections[0].in_editable_form()) + + def test_create_new_unit(self): + """ + Scenario: Create new unit + Given that I have created a section + And that I have created a subsection within that section + When I click the "+ Add unit" button in that subsection + Then I am redirected to a New Unit page + And the display name is in its editable form. + """ + self.course_outline_page.visit() + self.course_outline_page.add_section_from_top_button() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.course_outline_page.section_at(0).add_subsection() + self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) + self.course_outline_page.section_at(0).subsection_at(0).add_unit() + unit_page = ContainerPage(self.browser, None) + EmptyPromise(unit_page.is_browser_on_page, 'Browser is on the unit page').fulfill() + self.assertTrue(unit_page.is_inline_editing_display_name()) + + +class DeleteContentTest(CourseOutlineTest): + """ + Feature: Deleting sections/subsections/units + """ + + __test__ = True + + def test_delete_section(self): + """ + Scenario: Delete section + Given that I am on the course outline + When I click the delete button for a section on the course outline + Then I should receive a confirmation message, asking me if I really want to delete the section + When I click "Yes, I want to delete this component" + Then the confirmation message should close + And the section should immediately be deleted from the course outline + """ + self.course_outline_page.visit() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.course_outline_page.section_at(0).delete() + self.assertEqual(len(self.course_outline_page.sections()), 0) + + def test_cancel_delete_section(self): + """ + Scenario: Cancel delete of section + Given that I clicked the delte button for a section on the course outline + And I received a confirmation message, asking me if I really want to delete the component + When I click "Cancel" + Then the confirmation message should close + And the section should remain in the course outline + """ + self.course_outline_page.visit() + self.assertEqual(len(self.course_outline_page.sections()), 1) + self.course_outline_page.section_at(0).delete(cancel=True) + self.assertEqual(len(self.course_outline_page.sections()), 1) + + def test_delete_subsection(self): + """ + Scenario: Delete subsection + Given that I am on the course outline + When I click the delete button for a subsection on the course outline + Then I should receive a confirmation message, asking me if I really want to delete the subsection + When I click "Yes, I want to delete this component" + Then the confiramtion message should close + And the subsection should immediately be deleted from the course outline + """ + self.course_outline_page.visit() + self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) + self.course_outline_page.section_at(0).subsection_at(0).delete() + self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 0) + + def test_cancel_delete_subsection(self): + """ + Scenario: Cancel delete of subsection + Given that I clicked the delete button for a subsection on the course outline + And I received a confirmation message, asking me if I really want to delete the subsection + When I click "cancel" + Then the confirmation message should close + And the subsection should remain in the course outline + """ + self.course_outline_page.visit() + self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) + self.course_outline_page.section_at(0).subsection_at(0).delete(cancel=True) + self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) + + def test_delete_unit(self): + """ + Scenario: Delete unit + Given that I am on the course outline + When I click the delete button for a unit on the course outline + Then I should receive a confirmation message, asking me if I really want to delete the unit + When I click "Yes, I want to delete this unit" + Then the confirmation message should close + And the unit should immediately be deleted from the course outline + """ + self.course_outline_page.visit() + self.course_outline_page.section_at(0).subsection_at(0).toggle_expand() + self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) + self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).delete() + self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 0) + + def test_cancel_delete_unit(self): + """ + Scenario: Cancel delete of unit + Given that I clicked the delete button for a unit on the course outline + And I received a confirmation message, asking me if I really want to delete the unit + When I click "Cancel" + Then the confirmation message should close + And the unit should remain in the course outline + """ + self.course_outline_page.visit() + self.course_outline_page.section_at(0).subsection_at(0).toggle_expand() + self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) + self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).delete(cancel=True) + self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) + + def test_delete_all_no_content_message(self): + """ + Scenario: Delete all sections/subsections/units in a course, "no content" message should appear + Given that I delete all sections, subsections, and units in a course + When I visit the course outline + Then I will see a message that says, "You haven't added any content to this course yet" + Add see a + Add Section button + """ + self.course_outline_page.visit() + self.assertFalse(self.course_outline_page.has_no_content_message) + self.course_outline_page.section_at(0).delete() + self.assertEqual(len(self.course_outline_page.sections()), 0) + self.assertTrue(self.course_outline_page.has_no_content_message) + + +class ExpandCollapseMultipleSectionsTest(CourseOutlineTest): + """ + Feature: Courses with multiple sections can expand and collapse all sections. + """ + + __test__ = True + + def populate_course_fixture(self, course_fixture): + """ Start with a course with two sections """ + course_fixture.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit') + ) + ), + XBlockFixtureDesc('chapter', 'Test Section 2').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children( + XBlockFixtureDesc('vertical', 'Test Unit 2') + ) + ) + ) + + def verify_all_sections(self, collapsed): + """ + Verifies that all sections are collapsed if collapsed is True, otherwise all expanded. + """ + for section in self.course_outline_page.sections(): + self.assertEqual(collapsed, section.is_collapsed) + + def toggle_all_sections(self): + """ + Toggles the expand collapse state of all sections. + """ + for section in self.course_outline_page.sections(): + section.toggle_expand() + + def test_expanded_by_default(self): + """ + Scenario: The default layout for the outline page is to show sections in expanded view + Given I have a course with sections + When I navigate to the course outline page + Then I see the "Collapse All Sections" link + And all sections are expanded + """ + self.course_outline_page.visit() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) + self.verify_all_sections(collapsed=False) + + def test_no_expand_link_for_empty_course(self): + """ + Scenario: Collapse link is removed after last section of a course is deleted + Given I have a course with multiple sections + And I navigate to the course outline page + When I will confirm all alerts + And I press the "section" delete icon + Then I do not see the "Collapse All Sections" link + And I will see a message that says "You haven't added any content to this course yet" + """ + self.course_outline_page.visit() + for section in self.course_outline_page.sections(): + section.delete() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) + self.assertTrue(self.course_outline_page.has_no_content_message) + + def test_collapse_all_when_all_expanded(self): + """ + Scenario: Collapse all sections when all sections are expanded + Given I navigate to the outline page of a course with sections + And all sections are expanded + When I click the "Collapse All Sections" link + Then I see the "Expand All Sections" link + And all sections are collapsed + """ + self.course_outline_page.visit() + self.verify_all_sections(collapsed=False) + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) + self.verify_all_sections(collapsed=True) + + def test_collapse_all_when_some_expanded(self): + """ + Scenario: Collapsing all sections when 1 or more sections are already collapsed + Given I navigate to the outline page of a course with sections + And all sections are expanded + When I collapse the first section + And I click the "Collapse All Sections" link + Then I see the "Expand All Sections" link + And all sections are collapsed + """ + self.course_outline_page.visit() + self.verify_all_sections(collapsed=False) + self.course_outline_page.section_at(0).toggle_expand() + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) + self.verify_all_sections(collapsed=True) + + def test_expand_all_when_all_collapsed(self): + """ + Scenario: Expanding all sections when all sections are collapsed + Given I navigate to the outline page of a course with multiple sections + And I click the "Collapse All Sections" link + When I click the "Expand All Sections" link + Then I see the "Collapse All Sections" link + And all sections are expanded + """ + self.course_outline_page.visit() + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) + self.verify_all_sections(collapsed=False) + + def test_expand_all_when_some_collapsed(self): + """ + Scenario: Expanding all sections when 1 or more sections are already expanded + Given I navigate to the outline page of a course with multiple sections + And I click the "Collapse All Sections" link + When I expand the first section + And I click the "Expand All Sections" link + Then I see the "Collapse All Sections" link + And all sections are expanded + """ + self.course_outline_page.visit() + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) + self.course_outline_page.section_at(0).toggle_expand() + self.course_outline_page.toggle_expand_collapse() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) + self.verify_all_sections(collapsed=False) + + +class ExpandCollapseSingleSectionTest(CourseOutlineTest): + """ + Feature: Courses with a single section can expand and collapse all sections. + """ + + __test__ = True + + def test_no_expand_link_for_empty_course(self): + """ + Scenario: Collapse link is removed after last section of a course is deleted + Given I have a course with one section + And I navigate to the course outline page + When I will confirm all alerts + And I press the "section" delete icon + Then I do not see the "Collapse All Sections" link + And I will see a message that says "You haven't added any content to this course yet" + """ + self.course_outline_page.visit() + self.course_outline_page.section_at(0).delete() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) + self.assertTrue(self.course_outline_page.has_no_content_message) + + +class ExpandCollapseEmptyTest(CourseOutlineTest): + """ + Feature: Courses with no sections initially can expand and collapse all sections after addition. + """ + + __test__ = True + + def populate_course_fixture(self, course_fixture): + """ Start with an empty course """ + pass + + def test_no_expand_link_for_empty_course(self): + """ + Scenario: Expand/collapse for a course with no sections + Given I have a course with no sections + When I navigate to the course outline page + Then I do not see the "Collapse All Sections" link + """ + self.course_outline_page.visit() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) + + def test_link_appears_after_section_creation(self): + """ + Scenario: Collapse link appears after creating first section of a course + Given I have a course with no sections + When I navigate to the course outline page + And I add a section + Then I see the "Collapse All Sections" link + And all sections are expanded + """ + self.course_outline_page.visit() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) + self.course_outline_page.add_section_from_top_button() + self.assertEquals(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) + self.assertFalse(self.course_outline_page.section_at(0).is_collapsed) + + +class DefaultStatesEmptyTest(CourseOutlineTest): + """ + Feature: Misc course outline default states/actions when starting with an empty course + """ + + __test__ = True + + def populate_course_fixture(self, course_fixture): + """ Start with an empty course """ + pass + + def test_empty_course_message(self): + """ + Scenario: Empty course state + Given that I am in a course with no sections, subsections, nor units + When I visit the course outline + Then I will see a message that says "You haven't added any content to this course yet" + And see a + Add Section button + """ + self.course_outline_page.visit() + self.assertTrue(self.course_outline_page.has_no_content_message) + self.assertTrue(self.course_outline_page.bottom_add_section_button.is_present()) + + +class DefaultStatesContentTest(CourseOutlineTest): + """ + Feature: Misc course outline default states/actions when starting with a course with content + """ + + __test__ = True + + def test_view_live(self): + """ + Scenario: View Live version from course outline + Given that I am on the course outline + When I click the "View Live" button + Then a new tab will open to the course on the LMS + """ + self.course_outline_page.visit() + self.course_outline_page.view_live() + courseware = CoursewarePage(self.browser, self.course_id) + courseware.wait_for_page() + self.assertEqual(courseware.num_xblock_components, 2) + self.assertEqual(courseware.xblock_component_type(0), 'html') + self.assertEqual(courseware.xblock_component_type(1), 'discussion') + + +class UnitNavigationTest(CourseOutlineTest): + """ + Feature: Navigate to units + """ + + __test__ = True + + def test_navigate_to_unit(self): + """ + Scenario: Click unit name to navigate to unit page + Given that I have expanded a section/subsection so I can see unit names + When I click on a unit name + Then I will be taken to the appropriate unit page + """ + self.course_outline_page.visit() + self.course_outline_page.section_at(0).subsection_at(0).toggle_expand() + unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to() + self.assertTrue(unit.is_browser_on_page) diff --git a/common/test/acceptance/tests/test_studio_split_test.py b/common/test/acceptance/tests/test_studio_split_test.py index e646dc2327..3d65cebdda 100644 --- a/common/test/acceptance/tests/test_studio_split_test.py +++ b/common/test/acceptance/tests/test_studio_split_test.py @@ -65,30 +65,22 @@ class SplitTestMixin(object): Promise(missing_groups_button_not_present, "Add missing groups button should not be showing.").fulfill() - @attr('shard_1') -class SplitTest(ContainerBase, SplitTestMixin): +class SplitTest(ContainerBase): """ Tests for creating and editing split test instances in Studio. """ __test__ = True - def setUp(self): - super(SplitTest, self).setUp() - # This line should be called once courseFixture is installed - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ + def populate_course_fixture(self, course_fixture): + course_fixture.add_advanced_settings( + { + u"advanced_modules": {"value": ["split_test"]}, + u"user_partitions": {"value": [ UserPartition(0, 'Configuration alpha,beta', 'first', [Group("0", 'alpha'), Group("1", 'beta')]).to_json(), UserPartition(1, 'Configuration 0,1,2', 'second', [Group("0", 'Group 0'), Group("1", 'Group 1'), Group("2", 'Group 2')]).to_json() - ], - }, - }) - - def populate_course_fixture(self, course_fixture): - """ Populates the course """ - course_fixture.add_advanced_settings( - {u"advanced_modules": {"value": ["split_test"]}} + ]} + } ) course_fixture.add_children( @@ -99,6 +91,16 @@ class SplitTest(ContainerBase, SplitTestMixin): ) ) + def verify_add_missing_groups_button_not_present(self, container): + """ + Checks that the "add missing groups" button/link is not present. + """ + def missing_groups_button_not_present(): + button_present = container.missing_groups_button_present() + return (not button_present, not button_present) + + Promise(missing_groups_button_not_present, "Add missing groups button should not be showing.").fulfill() + def create_poorly_configured_split_instance(self): """ Creates a split test instance with a missing group and an inactive group.