From 97b45cc2a74a91676436c8f2fdc7d716162b68dc Mon Sep 17 00:00:00 2001 From: Muhammad Shoaib Date: Thu, 6 Nov 2014 18:22:51 +0500 Subject: [PATCH] WL-124 added the jasmine tests for the autoenrollment csv Added bokchoy tests and assets (csv files) for CSV auto reg and enrollment. Set the env flag "ALLOW_AUTOMATED_SIGNUPS": true in bok_choy.env.json Resolved quality issues. resolved cherry pick conflicts Improved bokchoy tests as per code review suggestions. added the BDD in the docstrings for all the test scenarios changed the bok choy test string Improved bokchoy tests as per further code review suggestions. Made a MembershipPageAutoEnrollSection a separate PageObject. --- .../pages/lms/instructor_dashboard.py | 109 +++++++++++++++++- .../lms/test_lms_instructor_dashboard.py | 86 ++++++++++++++ .../test/data/uploads/auto_reg_enrollment.csv | 3 + .../auto_reg_enrollment_errors_warnings.csv | 4 + lms/envs/bok_choy.env.json | 3 +- .../coffee/fixtures/autoenrollment.html | 20 ++++ .../membership_spec.coffee | 73 ++++++++++++ .../instructor_dashboard/membership.coffee | 49 ++++---- 8 files changed, 316 insertions(+), 31 deletions(-) create mode 100644 common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py create mode 100644 common/test/data/uploads/auto_reg_enrollment.csv create mode 100644 common/test/data/uploads/auto_reg_enrollment_errors_warnings.csv create mode 100644 lms/static/coffee/fixtures/autoenrollment.html create mode 100644 lms/static/coffee/spec/instructor_dashboard/membership_spec.coffee diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index abb4715858..6f703495d5 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -5,6 +5,7 @@ Instructor (2) dashboard page. from bok_choy.page_object import PageObject from .course_page import CoursePage +import os class InstructorDashboardPage(CoursePage): @@ -12,7 +13,6 @@ class InstructorDashboardPage(CoursePage): Instructor dashboard, where course staff can manage a course. """ url_path = "instructor" - def is_browser_on_page(self): return self.q(css='div.instructor-dashboard-wrapper-2').present @@ -31,10 +31,15 @@ class MembershipPage(PageObject): Membership section of the Instructor dashboard. """ url = None - def is_browser_on_page(self): return self.q(css='a[data-section=membership].active-section').present + def select_auto_enroll_section(self): + """ + returns the MembershipPageAutoEnrollSection + """ + return MembershipPageAutoEnrollSection(self.browser) + def _get_cohort_options(self): """ Returns the available options in the cohort dropdown, including the initial "Select a cohort group". @@ -154,6 +159,106 @@ class MembershipPage(PageObject): self.q(css="a.link-cross-reference[data-section=data_download]").first.click() +class MembershipPageAutoEnrollSection(PageObject): + """ + CSV Auto Enroll section of the Membership tab of the Instructor dashboard. + """ + url = None + + auto_enroll_browse_button_selector = '.auto_enroll_csv .file-browse input.file_field#browseBtn' + auto_enroll_upload_button_selector = '.auto_enroll_csv button[name="enrollment_signup_button"]' + NOTIFICATION_ERROR = 'error' + NOTIFICATION_WARNING = 'warning' + NOTIFICATION_SUCCESS = 'confirmation' + + def is_browser_on_page(self): + return self.q(css=self.auto_enroll_browse_button_selector).present + + def is_file_attachment_browse_button_visible(self): + """ + Returns True if the Auto-Enroll Browse button is present. + """ + return self.q(css=self.auto_enroll_browse_button_selector).is_present() + + def is_upload_button_visible(self): + """ + Returns True if the Auto-Enroll Upload button is present. + """ + return self.q(css=self.auto_enroll_upload_button_selector).is_present() + + def click_upload_file_button(self): + """ + Clicks the Auto-Enroll Upload Button. + """ + self.q(css=self.auto_enroll_upload_button_selector).click() + + def is_notification_displayed(self, section_type): + """ + Valid inputs for section_type: MembershipPageAutoEnrollSection.NOTIFICATION_SUCCESS / + MembershipPageAutoEnrollSection.NOTIFICATION_WARNING / + MembershipPageAutoEnrollSection.NOTIFICATION_ERROR + Returns True if a {section_type} notification is displayed. + """ + notification_selector = '.auto_enroll_csv .results .message-%s' % section_type + self.wait_for_element_presence(notification_selector, "%s Notification" % section_type.title()) + return self.q(css=notification_selector).is_present() + + def first_notification_message(self, section_type): + """ + Valid inputs for section_type: MembershipPageAutoEnrollSection.NOTIFICATION_WARNING / + MembershipPageAutoEnrollSection.NOTIFICATION_ERROR + Returns the first message from the list of messages in the {section_type} section. + """ + error_message_selector = '.auto_enroll_csv .results .message-%s li.summary-item' % section_type + self.wait_for_element_presence(error_message_selector, "%s message" % section_type.title()) + return self.q(css=error_message_selector).text[0] + + def get_asset_path(self, file_name): + """ + Returns the full path of the file to upload. + These files have been placed in edx-platform/common/test/data/uploads/ + """ + + # Separate the list of folders in the path reaching to the current file, + # e.g. '... common/test/acceptance/pages/lms/instructor_dashboard.py' will result in + # [..., 'common', 'test', 'acceptance', 'pages', 'lms', 'instructor_dashboard.py'] + folders_list_in_path = __file__.split(os.sep) + + # Get rid of the last 4 elements: 'acceptance', 'pages', 'lms', and 'instructor_dashboard.py' + # to point to the 'test' folder, a shared point in the path's tree. + folders_list_in_path = folders_list_in_path[:-4] + + # Append the folders in the asset's path + folders_list_in_path.extend(['data', 'uploads', file_name]) + + # Return the joined path of the required asset. + return os.sep.join(folders_list_in_path) + + def upload_correct_csv_file(self): + """ + Selects the correct file and clicks the upload button. + """ + correct_files_path = self.get_asset_path('auto_reg_enrollment.csv') + self.q(css=self.auto_enroll_browse_button_selector).results[0].send_keys(correct_files_path) + self.click_upload_file_button() + + def upload_csv_file_with_errors_warnings(self): + """ + Selects the file which will generate errors and warnings and clicks the upload button. + """ + errors_warnings_files_path = self.get_asset_path('auto_reg_enrollment_errors_warnings.csv') + self.q(css=self.auto_enroll_browse_button_selector).results[0].send_keys(errors_warnings_files_path) + self.click_upload_file_button() + + def upload_non_csv_file(self): + """ + Selects an image file and clicks the upload button. + """ + errors_warnings_files_path = self.get_asset_path('image.jpg') + self.q(css=self.auto_enroll_browse_button_selector).results[0].send_keys(errors_warnings_files_path) + self.click_upload_file_button() + + class DataDownloadPage(PageObject): """ Data Download section of the Instructor dashboard. diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py new file mode 100644 index 0000000000..a486d88f7b --- /dev/null +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +End-to-end tests for the LMS Instructor Dashboard. +""" + +from ..helpers import UniqueCourseTest +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.instructor_dashboard import InstructorDashboardPage +from ...fixtures.course import CourseFixture + + +class AutoEnrollmentWithCSVTest(UniqueCourseTest): + """ + End-to-end tests for Auto-Registration and enrollment functionality via CSV file. + """ + + def setUp(self): + super(AutoEnrollmentWithCSVTest, self).setUp() + self.course_fixture = CourseFixture(**self.course_info).install() + + # login as an instructor + AutoAuthPage(self.browser, course_id=self.course_id, staff=True).visit() + + # go to the membership page on the instructor dashboard + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) + instructor_dashboard_page.visit() + self.auto_enroll_section = instructor_dashboard_page.select_membership().select_auto_enroll_section() + + def test_browse_and_upload_buttons_are_visible(self): + """ + Scenario: On the Membership tab of the Instructor Dashboard, Auto-Enroll Browse and Upload buttons are visible. + Given that I am on the Membership tab on the Instructor Dashboard + Then I see the 'REGISTER/ENROLL STUDENTS' section on the page with the 'Browse' and 'Upload' buttons + """ + self.assertTrue(self.auto_enroll_section.is_file_attachment_browse_button_visible()) + self.assertTrue(self.auto_enroll_section.is_upload_button_visible()) + + def test_clicking_file_upload_button_without_file_shows_error(self): + """ + Scenario: Clicking on the upload button without specifying a CSV file results in error. + Given that I am on the Membership tab on the Instructor Dashboard + When I click the Upload Button without specifying a CSV file + Then I should be shown an Error Notification + And The Notification message should read 'File is not attached.' + """ + self.auto_enroll_section.click_upload_file_button() + self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) + self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "File is not attached.") + + def test_uploading_correct_csv_file_results_in_success(self): + """ + Scenario: Uploading a CSV with correct data results in Success. + Given that I am on the Membership tab on the Instructor Dashboard + When I select a csv file with correct data and click the Upload Button + Then I should be shown a Success Notification. + """ + self.auto_enroll_section.upload_correct_csv_file() + self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_SUCCESS)) + + def test_uploading_csv_file_with_bad_data_results_in_errors_and_warnings(self): + """ + Scenario: Uploading a CSV with incorrect data results in error and warnings. + Given that I am on the Membership tab on the Instructor Dashboard + When I select a csv file with incorrect data and click the Upload Button + Then I should be shown an Error Notification + And a corresponding Error Message. + And I should be shown a Warning Notification + And a corresponding Warning Message. + """ + self.auto_enroll_section.upload_csv_file_with_errors_warnings() + self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) + self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Data in row #2 must have exactly four columns: email, username, full name, and country") + self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_WARNING)) + self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_WARNING), "ename (d@a.com): (An account with email d@a.com exists but the provided username ename is different. Enrolling anyway with d@a.com.)") + + def test_uploading_non_csv_file_results_in_error(self): + """ + Scenario: Uploading an image file for auto-enrollment results in error. + Given that I am on the Membership tab on the Instructor Dashboard + When I select an image file (a non-csv file) and click the Upload Button + Then I should be shown an Error Notification + And The Notification message should read 'Could not read uploaded file.' + """ + self.auto_enroll_section.upload_non_csv_file() + self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) + self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Could not read uploaded file.") diff --git a/common/test/data/uploads/auto_reg_enrollment.csv b/common/test/data/uploads/auto_reg_enrollment.csv new file mode 100644 index 0000000000..1fcf16aa52 --- /dev/null +++ b/common/test/data/uploads/auto_reg_enrollment.csv @@ -0,0 +1,3 @@ +a@a.com,aname,aname,PK +b@a.com,bname,bname,PK +c@a.com,cname,cname,PK diff --git a/common/test/data/uploads/auto_reg_enrollment_errors_warnings.csv b/common/test/data/uploads/auto_reg_enrollment_errors_warnings.csv new file mode 100644 index 0000000000..49d35536f4 --- /dev/null +++ b/common/test/data/uploads/auto_reg_enrollment_errors_warnings.csv @@ -0,0 +1,4 @@ +d@a.com,dname,dname,PK +a@a.com,missing_data +d@a.com,ename,ename,PK +e@a.com,dname,dname,PK diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index fe5d509386..bfaff9e7f5 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -71,7 +71,8 @@ "ENABLE_THIRD_PARTY_AUTH": true, "PREVIEW_LMS_BASE": "localhost:8003", "SUBDOMAIN_BRANDING": false, - "SUBDOMAIN_COURSE_LISTINGS": false + "SUBDOMAIN_COURSE_LISTINGS": false, + "ALLOW_AUTOMATED_SIGNUPS": true }, "FEEDBACK_SUBMISSION_EMAIL": "", "GITHUB_REPO_ROOT": "** OVERRIDDEN **", diff --git a/lms/static/coffee/fixtures/autoenrollment.html b/lms/static/coffee/fixtures/autoenrollment.html new file mode 100644 index 0000000000..2e010a1533 --- /dev/null +++ b/lms/static/coffee/fixtures/autoenrollment.html @@ -0,0 +1,20 @@ +
+

${_("Register/Enroll Students")}

+ +

+ ${_("To register and enroll a list of users in this course, choose a CSV file that contains the following columns in this exact order: email, username, name, and country. Please include one student per row and do not include any headers, footers, or blank lines.")} +

+ +
+
+ + +
+ Browse + +
+
+ +
+
+
\ No newline at end of file diff --git a/lms/static/coffee/spec/instructor_dashboard/membership_spec.coffee b/lms/static/coffee/spec/instructor_dashboard/membership_spec.coffee new file mode 100644 index 0000000000..28faf3e2e6 --- /dev/null +++ b/lms/static/coffee/spec/instructor_dashboard/membership_spec.coffee @@ -0,0 +1,73 @@ +describe 'AutoEnrollment', -> + beforeEach -> + loadFixtures 'coffee/fixtures/autoenrollment.html' + @autoenrollment = new AutoEnrollmentViaCsv $('.auto_enroll_csv') + + it 'binds to the enrollment_signup_button on click event', -> + expect(@autoenrollment.$enrollment_signup_button).toHandle 'click' + + it 'binds to the browse button on change event', -> + expect(@autoenrollment.$browse_button).toHandle 'change' + + it 'binds the ajax call and the result will be success', -> + spyOn($, "ajax").andCallFake((params) => + params.success({row_errors: [], general_errors: [], warnings: []}) + {always: ->} + ) + # mock the render_notification_view which returns the html (since we are only using the existing notification model) + @autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").andCallFake => + return '

Success

All accounts were created successfully.

' + + submitCallback = jasmine.createSpy().andReturn() + @autoenrollment.$student_enrollment_form.submit(submitCallback) + @autoenrollment.$enrollment_signup_button.click() + expect($('.results .message-copy').text()).toEqual('All accounts were created successfully.') + expect(submitCallback).toHaveBeenCalled() + + it 'binds the ajax call and the result will be error', -> + spyOn($, "ajax").andCallFake((params) => + params.success({ + row_errors: [{ + 'username': 'testuser1', + 'email': 'testemail1@email.com', + 'response': 'Username already exists' + }], + general_errors: [{ + 'response': 'cannot read the line 2' + }], + warnings: [] + }) + {always: ->} + ) + # mock the render_notification_view which returns the html (since we are only using the existing notification model) + @autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").andCallFake => + return '

Errors

The following errors were generated:

  • cannot read the line 2
  • testuser1 (testemail1@email.com): (Username already exists)
' + + submitCallback = jasmine.createSpy().andReturn() + @autoenrollment.$student_enrollment_form.submit(submitCallback) + @autoenrollment.$enrollment_signup_button.click() + expect($('.results .list-summary').text()).toEqual('cannot read the line 2testuser1 (testemail1@email.com): (Username already exists)'); + expect(submitCallback).toHaveBeenCalled() + + it 'binds the ajax call and the result will be warnings', -> + spyOn($, "ajax").andCallFake((params) => + params.success({ + row_errors: [], + general_errors: [], + warnings: [{ + 'username': 'user1', + 'email': 'user1email', + 'response': 'email is in valid' + }] + }) + {always: ->} + ) + # mock the render_notification_view which returns the html (since we are only using the existing notification model) + @autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").andCallFake => + return '

Warnings

The following warnings were generated:

  • user1 (user1email): (email is in valid)
' + + submitCallback = jasmine.createSpy().andReturn() + @autoenrollment.$student_enrollment_form.submit(submitCallback) + @autoenrollment.$enrollment_signup_button.click() + expect($('.results .list-summary').text()).toEqual('user1 (user1email): (email is in valid)') + expect(submitCallback).toHaveBeenCalled() \ No newline at end of file diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee index 132ae7e1d0..7aabdbc996 100644 --- a/lms/static/coffee/src/instructor_dashboard/membership.coffee +++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee @@ -174,7 +174,7 @@ class AuthListWidget extends MemberListWidget else @reload_list() -class AutoEnrollmentViaCsv +class @AutoEnrollmentViaCsv constructor: (@$container) -> # Wrapper for the AutoEnrollmentViaCsv subsection. # This object handles buttons, success and failure reporting, @@ -220,7 +220,6 @@ class AutoEnrollmentViaCsv @$results.empty() errors = [] warnings = [] - result_from_server_is_success = true if data_from_server.general_errors.length @@ -241,41 +240,35 @@ class AutoEnrollmentViaCsv warning['is_general_error'] = false warnings.push warning - render_response = (label, type, student_results) => - if type is 'success' - task_res_section = $ '
', class: 'message message-confirmation' - message_title = $ '

', class: 'message-title', text: label - task_res_section.append message_title - @$results.append task_res_section - return - - if type is 'error' - task_res_section = $ '
', class: 'message message-error' - if type is 'warning' - task_res_section = $ '
', class: 'message message-warning' - - message_title = $ '

', class: 'message-title', text: label - task_res_section. append message_title - messages_copy = $ '
', class: 'message-copy' - task_res_section. append messages_copy - messages_summary = $ '
    ', class: 'list-summary summary-items' - messages_copy.append messages_summary - + render_response = (title, message, type, student_results) => + details = [] for student_result in student_results if student_result.is_general_error - response_message = student_result.response + details.push student_result.response else response_message = student_result.username + ' ('+ student_result.email + '): ' + ' (' + student_result.response + ')' - messages_summary.append $ '
  • ', class: 'summary-item', text: response_message + details.push response_message - @$results.append task_res_section + @$results.append @render_notification_view type, title, message, details if errors.length - render_response gettext("The following errors were generated:"), 'error', errors + render_response gettext('Errors'), gettext("The following errors were generated:"), 'error', errors if warnings.length - render_response gettext("The following warnings were generated:"), 'warning', warnings + render_response gettext('Warnings'), gettext("The following warnings were generated:"), 'warning', warnings if result_from_server_is_success - render_response gettext("All accounts were created successfully."), 'success', [] + render_response gettext('Success'), gettext("All accounts were created successfully."), 'confirmation', [] + + render_notification_view: (type, title, message, details) -> + notification_model = new NotificationModel() + notification_model.set({ + 'type': type, + 'title': title, + 'message': message, + 'details': details, + }); + view = new NotificationView(model:notification_model); + view.render() + return view.$el.html() class BetaTesterBulkAddition constructor: (@$container) ->