From 39acbdcb2b43628401c842861f53af0bf732657b Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 9 Jan 2015 16:42:05 -0500 Subject: [PATCH] End-to-end bok choy test for cohorted courseware. --- .../test/acceptance/pages/lms/courseware.py | 9 +- .../pages/lms/instructor_dashboard.py | 9 +- .../tests/lms/test_lms_user_preview.py | 49 ++-- .../tests/studio/base_studio_test.py | 4 +- .../tests/studio/test_studio_split_test.py | 1 - .../tests/test_cohorted_courseware.py | 227 ++++++++++++++++++ 6 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 common/test/acceptance/tests/test_cohorted_courseware.py diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py index 510c19b9be..b5c835a398 100644 --- a/common/test/acceptance/pages/lms/courseware.py +++ b/common/test/acceptance/pages/lms/courseware.py @@ -33,12 +33,19 @@ class CoursewarePage(CoursePage): """ return len(self.q(css=self.subsection_selector)) + @property + def xblock_components(self): + """ + Return the xblock components within the unit on the page. + """ + return self.q(css=self.xblock_component_selector) + @property def num_xblock_components(self): """ Return the number of rendered xblocks within the unit on the page """ - return len(self.q(css=self.xblock_component_selector)) + return len(self.xblock_components) def xblock_component_type(self, index=0): """ diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index b120ba4305..5543359f53 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -133,6 +133,10 @@ class MembershipPageCohortManagementSection(PageObject): """ Returns the name of the selected cohort. """ + EmptyPromise( + lambda: len(self._get_cohort_options().results) > 0, + "Waiting for cohort selector to populate" + ).fulfill() return self._cohort_name( self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] ) @@ -163,7 +167,10 @@ class MembershipPageCohortManagementSection(PageObject): Adds a new manual cohort with the specified name. If a content group should also be associated, the name of the content group should be specified. """ - self.q(css=self._bounded_selector("div.cohort-management-nav .action-create")).first.click() + create_buttons = self.q(css=self._bounded_selector(".action-create")) + # There are 2 create buttons on the page. The second one is only present when no cohort yet exists + # (in which case the first is not visible). Click on the last present create button. + create_buttons.results[len(create_buttons.results) - 1].click() textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0] textinput.send_keys(cohort_name) if content_group: diff --git a/common/test/acceptance/tests/lms/test_lms_user_preview.py b/common/test/acceptance/tests/lms/test_lms_user_preview.py index b17e74250c..ff23534ee3 100644 --- a/common/test/acceptance/tests/lms/test_lms_user_preview.py +++ b/common/test/acceptance/tests/lms/test_lms_user_preview.py @@ -266,28 +266,26 @@ class CourseWithContentGroupsTest(StaffViewTest): """) + self.alpha_text = "VISIBLE TO ALPHA" + self.beta_text = "VISIBLE TO BETA" + self.everyone_text = "VISIBLE TO EVERYONE" + course_fixture.add_children( XBlockFixtureDesc('chapter', 'Test Section').add_children( XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc( - 'problem', 'Visible to alpha', data=problem_data, metadata={"group_access": {0: [0]}} - ), - XBlockFixtureDesc( - 'problem', 'Visible to beta', data=problem_data, metadata={"group_access": {0: [1]}} - ), - XBlockFixtureDesc('problem', 'Visible to everyone', data=problem_data) + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc( + 'problem', self.alpha_text, data=problem_data, metadata={"group_access": {0: [0]}} + ), + XBlockFixtureDesc( + 'problem', self.beta_text, data=problem_data, metadata={"group_access": {0: [1]}} + ), + XBlockFixtureDesc('problem', self.everyone_text, data=problem_data) + ) ) ) ) - def _verify_visible_problems(self, expected_items): - """ - Verify that the expected problems are visible. - """ - course_nav = CourseNavPage(self.browser) - actual_items = course_nav.sequence_items - self.assertItemsEqual(expected_items, actual_items) - def test_staff_sees_all_problems(self): """ Scenario: Staff see all problems @@ -296,8 +294,8 @@ class CourseWithContentGroupsTest(StaffViewTest): When I view the courseware in the LMS with staff access Then I see all the problems, regardless of their group_access property """ - self._goto_staff_page() - self._verify_visible_problems(['Visible to alpha', 'Visible to beta', 'Visible to everyone']) + course_page = self._goto_staff_page() + verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.beta_text, self.everyone_text]) def test_student_not_in_content_group(self): """ @@ -310,7 +308,7 @@ class CourseWithContentGroupsTest(StaffViewTest): """ course_page = self._goto_staff_page() course_page.set_staff_view_mode('Student') - self._verify_visible_problems(['Visible to everyone']) + verify_expected_problem_visibility(self, course_page, [self.everyone_text]) def test_as_student_in_alpha(self): """ @@ -323,7 +321,7 @@ class CourseWithContentGroupsTest(StaffViewTest): """ course_page = self._goto_staff_page() course_page.set_staff_view_mode('Student in alpha') - self._verify_visible_problems(['Visible to alpha', 'Visible to everyone']) + verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text]) def test_as_student_in_beta(self): """ @@ -336,4 +334,15 @@ class CourseWithContentGroupsTest(StaffViewTest): """ course_page = self._goto_staff_page() course_page.set_staff_view_mode('Student in beta') - self._verify_visible_problems(['Visible to beta', 'Visible to everyone']) + verify_expected_problem_visibility(self, course_page, [self.beta_text, self.everyone_text]) + + +def verify_expected_problem_visibility(test, courseware_page, expected_problems): + """ + Helper method that checks that the expected problems are visible on the current page. + """ + test.assertEqual( + len(expected_problems), courseware_page.num_xblock_components, "Incorrect number of visible problems" + ) + for index, expected_problem in enumerate(expected_problems): + test.assertIn(expected_problem, courseware_page.xblock_components[index].text) diff --git a/common/test/acceptance/tests/studio/base_studio_test.py b/common/test/acceptance/tests/studio/base_studio_test.py index 02fdcbe998..066117a303 100644 --- a/common/test/acceptance/tests/studio/base_studio_test.py +++ b/common/test/acceptance/tests/studio/base_studio_test.py @@ -58,12 +58,12 @@ class ContainerBase(StudioCourseTest): Base class for tests that do operations on the container page. """ - def setUp(self): + def setUp(self, is_staff=False): """ Create a unique identifier for the course used in this test. """ # Ensure that the superclass sets up - super(ContainerBase, self).setUp() + super(ContainerBase, self).setUp(is_staff=is_staff) self.outline = CourseOutlinePage( self.browser, diff --git a/common/test/acceptance/tests/studio/test_studio_split_test.py b/common/test/acceptance/tests/studio/test_studio_split_test.py index 395cb1f48b..3d8ef20bae 100644 --- a/common/test/acceptance/tests/studio/test_studio_split_test.py +++ b/common/test/acceptance/tests/studio/test_studio_split_test.py @@ -14,7 +14,6 @@ from bok_choy.promise import Promise, EmptyPromise from ...fixtures.course import XBlockFixtureDesc from ...pages.studio.component_editor import ComponentEditorView from ...pages.studio.overview import CourseOutlinePage, CourseOutlineUnit -from ...pages.studio.settings_advanced import AdvancedSettingsPage from ...pages.studio.container import ContainerPage from ...pages.studio.settings_group_configurations import GroupConfigurationsPage from ...pages.studio.utils import add_advanced_component diff --git a/common/test/acceptance/tests/test_cohorted_courseware.py b/common/test/acceptance/tests/test_cohorted_courseware.py new file mode 100644 index 0000000000..b702352a6d --- /dev/null +++ b/common/test/acceptance/tests/test_cohorted_courseware.py @@ -0,0 +1,227 @@ +""" +End-to-end test for cohorted courseware. This uses both Studio and LMS. +""" + +from nose.plugins.attrib import attr +import json + +from studio.base_studio_test import ContainerBase + +from ..pages.studio.settings_group_configurations import GroupConfigurationsPage +from ..pages.studio.settings_advanced import AdvancedSettingsPage +from ..pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage +from ..fixtures.course import XBlockFixtureDesc +from ..pages.studio.component_editor import ComponentVisibilityEditorView +from ..pages.lms.instructor_dashboard import InstructorDashboardPage +from ..pages.lms.course_nav import CourseNavPage +from ..pages.lms.courseware import CoursewarePage +from ..pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage +from ..tests.lms.test_lms_user_preview import verify_expected_problem_visibility + +from bok_choy.promise import EmptyPromise + + +@attr('shard_1') +class EndToEndCohortedCoursewareTest(ContainerBase): + + def setUp(self, is_staff=True): + + super(EndToEndCohortedCoursewareTest, self).setUp(is_staff=is_staff) + self.staff_user = self.user + + self.content_group_a = "Content Group A" + self.content_group_b = "Content Group B" + + # Create a student who will be in "Cohort A" + self.cohort_a_student_username = "cohort_a_student" + self.cohort_a_student_email = "cohort_a_student@example.com" + StudioAutoAuthPage( + self.browser, username=self.cohort_a_student_username, email=self.cohort_a_student_email, no_login=True + ).visit() + + # Create a student who will be in "Cohort B" + self.cohort_b_student_username = "cohort_b_student" + self.cohort_b_student_email = "cohort_b_student@example.com" + StudioAutoAuthPage( + self.browser, username=self.cohort_b_student_username, email=self.cohort_b_student_email, no_login=True + ).visit() + + # Create a student who will end up in the default cohort group + self.cohort_default_student_username = "cohort default student" + self.cohort_default_student_email = "cohort_default_student@example.com" + StudioAutoAuthPage( + self.browser, username=self.cohort_default_student_username, + email=self.cohort_default_student_email, no_login=True + ).visit() + + # Start logged in as the staff user. + StudioAutoAuthPage( + self.browser, username=self.staff_user["username"], email=self.staff_user["email"] + ).visit() + + def populate_course_fixture(self, course_fixture): + """ + Populate the children of the test course fixture. + """ + self.group_a_problem = 'GROUP A CONTENT' + self.group_b_problem = 'GROUP B CONTENT' + self.group_a_and_b_problem = 'GROUP A AND B CONTENT' + self.visible_to_all_problem = 'VISIBLE TO ALL CONTENT' + course_fixture.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit').add_children( + XBlockFixtureDesc('problem', self.group_a_problem, data=''), + XBlockFixtureDesc('problem', self.group_b_problem, data=''), + XBlockFixtureDesc('problem', self.group_a_and_b_problem, data=''), + XBlockFixtureDesc('problem', self.visible_to_all_problem, data='') + ) + ) + ) + ) + + def enable_cohorts_in_course(self): + """ + This turns on cohorts for the course. Currently this is still done through Advanced + Settings. Eventually it will be done in the LMS Instructor Dashboard. + """ + advanced_settings = AdvancedSettingsPage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + + advanced_settings.visit() + cohort_config = '{"cohorted": true}' + advanced_settings.set('Cohort Configuration', cohort_config) + advanced_settings.refresh_and_wait_for_load() + + self.assertEquals( + json.loads(cohort_config), + json.loads(advanced_settings.get('Cohort Configuration')), + 'Wrong input for Cohort Configuration' + ) + + def create_content_groups(self): + """ + Creates two content groups in Studio Group Configurations Settings. + """ + group_configurations_page = GroupConfigurationsPage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + group_configurations_page.visit() + + group_configurations_page.create_first_content_group() + config = group_configurations_page.content_groups[0] + config.name = self.content_group_a + config.save() + + group_configurations_page.add_content_group() + config = group_configurations_page.content_groups[1] + config.name = self.content_group_b + config.save() + + def link_problems_to_content_groups_and_publish(self): + """ + Updates 3 of the 4 existing problems to limit their visibility by content group. + Publishes the modified units. + """ + container_page = self.go_to_unit_page() + + def set_visibility(problem_index, content_group, second_content_group=None): + problem = container_page.xblocks[problem_index] + problem.edit_visibility() + if second_content_group: + ComponentVisibilityEditorView(self.browser, problem.locator).select_option( + second_content_group, save=False + ) + ComponentVisibilityEditorView(self.browser, problem.locator).select_option(content_group) + + set_visibility(1, self.content_group_a) + set_visibility(2, self.content_group_b) + set_visibility(3, self.content_group_a, self.content_group_b) + + container_page.publish_action.click() + + def create_cohorts_and_assign_students(self): + """ + Adds 2 manual cohorts, linked to content groups, to the course. + Each cohort is assigned one student. + """ + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) + instructor_dashboard_page.visit() + membership_page = instructor_dashboard_page.select_membership() + cohort_management_page = membership_page.select_cohort_management_section() + + def add_cohort_with_student(cohort_name, content_group, student): + cohort_management_page.add_cohort(cohort_name, content_group=content_group) + # After adding the cohort, it should automatically be selected + EmptyPromise( + lambda: cohort_name == cohort_management_page.get_selected_cohort(), "Waiting for new cohort" + ).fulfill() + cohort_management_page.add_students_to_selected_cohort([student]) + + add_cohort_with_student("Cohort A", self.content_group_a, self.cohort_a_student_username) + add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username) + + def view_cohorted_content_as_different_users(self): + """ + View content as staff, student in Cohort A, student in Cohort B, and student in Default Cohort. + """ + courseware_page = CoursewarePage(self.browser, self.course_id) + + def login_and_verify_visible_problems(username, email, expected_problems): + LmsAutoAuthPage( + self.browser, username=username, email=email, course_id=self.course_id + ).visit() + courseware_page.visit() + verify_expected_problem_visibility(self, courseware_page, expected_problems) + + login_and_verify_visible_problems( + self.staff_user["username"], self.staff_user["email"], + [self.group_a_problem, self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem] + ) + + login_and_verify_visible_problems( + self.cohort_a_student_username, self.cohort_a_student_email, + [self.group_a_problem, self.group_a_and_b_problem, self.visible_to_all_problem] + ) + + login_and_verify_visible_problems( + self.cohort_b_student_username, self.cohort_b_student_email, + [self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem] + ) + + login_and_verify_visible_problems( + self.cohort_default_student_username, self.cohort_default_student_email, + [self.visible_to_all_problem] + ) + + def test_cohorted_courseware(self): + """ + Scenario: Can create content that is only visible to students in particular cohorts + Given that I have course with 4 problems, 1 staff member, and 3 students + When I enable cohorts in the course + And I create two content groups, Content Group A, and Content Group B, in the course + And I link one problem to Content Group A + And I link one problem to Content Group B + And I link one problem to both Content Group A and Content Group B + And one problem remains unlinked to any Content Group + And I create two manual cohorts, Cohort A and Cohort B, + linked to Content Group A and Content Group B, respectively + And I assign one student to each manual cohort + And one student remains in the default cohort + Then the staff member can see all 4 problems + And the student in Cohort A can see all the problems except the one linked to Content Group B + And the student in Cohort B can see all the problems except the one linked to Content Group A + And the student in the default cohort can ony see the problem that is unlinked to any Content Group + """ + self.enable_cohorts_in_course() + self.create_content_groups() + self.link_problems_to_content_groups_and_publish() + self.create_cohorts_and_assign_students() + self.view_cohorted_content_as_different_users()