diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index f98153e180..ad000078de 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -102,11 +102,17 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): result = self.client.login(username=self.USERNAME, password=self.PASSWORD) self.assertTrue(result, msg="Could not log in") - @ddt.data("verified", "professional") - def test_start_flow_not_verified(self, course_mode): + @ddt.data( + ("verified", "verify_student_start_flow"), + ("professional", "verify_student_start_flow"), + ("verified", "verify_student_begin_flow"), + ("professional", "verify_student_begin_flow") + ) + @ddt.unpack + def test_start_flow_not_verified(self, course_mode, payment_flow): course = self._create_course(course_mode) self._enroll(course.id) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_displayed_mode(response, course_mode) self._assert_steps_displayed( response, @@ -120,11 +126,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ]) self._assert_upgrade_session_flag(False) - @ddt.data("no-id-professional") - def test_start_flow_with_no_id_professional(self, course_mode): + @ddt.data( + ("no-id-professional", "verify_student_start_flow"), + ("no-id-professional", "verify_student_begin_flow") + ) + @ddt.unpack + def test_start_flow_with_no_id_professional(self, course_mode, payment_flow): course = self._create_course(course_mode) self._enroll(course.id) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_displayed_mode(response, course_mode) self._assert_steps_displayed( response, @@ -134,12 +144,26 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG) self._assert_requirements_displayed(response, []) - @ddt.data("expired", "denied") - def test_start_flow_expired_or_denied_verification(self, verification_status): + def test_ab_testing_page(self): + course = self._create_course("verified") + self._enroll(course.id, "verified") + response = self._get_page("verify_student_begin_flow", course.id) + self._assert_displayed_mode(response, "verified") + self.assertContains(response, "Upgrade to a Verified Certificate") + self.assertContains(response, "Before you upgrade to a certificate track,") + self.assertContains(response, "To receive a certificate, you must also verify your identity") + self.assertContains(response, "You will use your webcam to take a picture of") + + @ddt.data( + ("expired", "verify_student_start_flow"), + ("denied", "verify_student_begin_flow") + ) + @ddt.unpack + def test_start_flow_expired_or_denied_verification(self, verification_status, payment_flow): course = self._create_course("verified") self._enroll(course.id, "verified") self._set_verification_status(verification_status) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) # Expect the same content as when the user has not verified self._assert_steps_displayed( @@ -154,18 +178,24 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ]) @ddt.data( - ("verified", "submitted"), - ("verified", "approved"), - ("verified", "error"), - ("professional", "submitted"), - ("no-id-professional", None), + ("verified", "submitted", "verify_student_start_flow"), + ("verified", "approved", "verify_student_start_flow"), + ("verified", "error", "verify_student_start_flow"), + ("professional", "submitted", "verify_student_start_flow"), + ("no-id-professional", None, "verify_student_start_flow"), + ("verified", "submitted", "verify_student_begin_flow"), + ("verified", "approved", "verify_student_begin_flow"), + ("verified", "error", "verify_student_begin_flow"), + ("professional", "submitted", "verify_student_begin_flow"), + ("no-id-professional", None, "verify_student_begin_flow"), + ) @ddt.unpack - def test_start_flow_already_verified(self, course_mode, verification_status): + def test_start_flow_already_verified(self, course_mode, verification_status, payment_flow): course = self._create_course(course_mode) self._enroll(course.id) self._set_verification_status(verification_status) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_displayed_mode(response, course_mode) self._assert_steps_displayed( response, @@ -175,11 +205,17 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG) self._assert_requirements_displayed(response, []) - @ddt.data("verified", "professional") - def test_start_flow_already_paid(self, course_mode): + @ddt.data( + ("verified", "verify_student_start_flow"), + ("professional", "verify_student_start_flow"), + ("verified", "verify_student_begin_flow"), + ("professional", "verify_student_begin_flow") + ) + @ddt.unpack + def test_start_flow_already_paid(self, course_mode, payment_flow): course = self._create_course(course_mode) self._enroll(course.id, course_mode) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_displayed_mode(response, course_mode) self._assert_steps_displayed( response, @@ -192,15 +228,16 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): PayAndVerifyView.WEBCAM_REQ, ]) - def test_start_flow_not_enrolled(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_start_flow_not_enrolled(self, payment_flow): course = self._create_course("verified") self._set_verification_status("submitted") - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) # This shouldn't happen if the student has been auto-enrolled, # but if they somehow end up on this page without enrolling, # treat them as if they need to pay - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_steps_displayed( response, PayAndVerifyView.PAYMENT_STEPS, @@ -208,7 +245,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ) self._assert_requirements_displayed(response, []) - def test_start_flow_unenrolled(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_start_flow_unenrolled(self, payment_flow): course = self._create_course("verified") self._set_verification_status("submitted") self._enroll(course.id, "verified") @@ -216,7 +254,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): # If unenrolled, treat them like they haven't paid at all # (we assume that they've gotten a refund or didn't pay initially) - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_steps_displayed( response, PayAndVerifyView.PAYMENT_STEPS, @@ -225,27 +263,31 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self._assert_requirements_displayed(response, []) @ddt.data( - ("verified", "submitted"), - ("verified", "approved"), - ("professional", "submitted") + ("verified", "submitted", "verify_student_start_flow"), + ("verified", "approved", "verify_student_start_flow"), + ("professional", "submitted", "verify_student_start_flow"), + ("verified", "submitted", "verify_student_begin_flow"), + ("verified", "approved", "verify_student_begin_flow"), + ("professional", "submitted", "verify_student_begin_flow") ) @ddt.unpack - def test_start_flow_already_verified_and_paid(self, course_mode, verification_status): + def test_start_flow_already_verified_and_paid(self, course_mode, verification_status, payment_flow): course = self._create_course(course_mode) self._enroll(course.id, course_mode) self._set_verification_status(verification_status) response = self._get_page( - 'verify_student_start_flow', + payment_flow, course.id, expected_status_code=302 ) self._assert_redirects_to_dashboard(response) @patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True}) - def test_pay_and_verify_hides_header_nav(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_pay_and_verify_hides_header_nav(self, payment_flow): course = self._create_course("verified") self._enroll(course.id, "verified") - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) # Verify that the header navigation links are hidden for the edx.org version self.assertNotContains(response, "How it Works") @@ -351,7 +393,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): PayAndVerifyView.WEBCAM_REQ, ]) - def test_payment_cannot_skip(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_payment_cannot_skip(self, payment_flow): """ Simple test to verify that certain steps cannot be skipped. This test sets up a scenario where the user should be on the MAKE_PAYMENT_STEP, but is trying to @@ -360,7 +403,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): """ course = self._create_course("verified") response = self._get_page( - 'verify_student_start_flow', + payment_flow, course.id, skip_first_step=True ) @@ -523,6 +566,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): pages = [ 'verify_student_start_flow', + 'verify_student_begin_flow', 'verify_student_verify_now', 'verify_student_upgrade_and_verify', ] @@ -534,16 +578,25 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): expected_status_code=404 ) - @ddt.data([], ["no-id-professional", "professional"], ["honor", "audit"]) - def test_no_id_professional_entry_point(self, modes_available): + @ddt.data( + ([], "verify_student_start_flow"), + (["no-id-professional", "professional"], "verify_student_start_flow"), + (["honor", "audit"], "verify_student_start_flow"), + ([], "verify_student_begin_flow"), + (["no-id-professional", "professional"], "verify_student_begin_flow"), + (["honor", "audit"], "verify_student_begin_flow"), + ) + @ddt.unpack + def test_no_id_professional_entry_point(self, modes_available, payment_flow): course = self._create_course(*modes_available) if "no-id-professional" in modes_available or "professional" in modes_available: - self._get_page("verify_student_start_flow", course.id, expected_status_code=200) + self._get_page(payment_flow, course.id, expected_status_code=200) else: - self._get_page("verify_student_start_flow", course.id, expected_status_code=404) + self._get_page(payment_flow, course.id, expected_status_code=404) @ddt.data( "verify_student_start_flow", + "verify_student_begin_flow", "verify_student_verify_now", "verify_student_upgrade_and_verify", ) @@ -561,6 +614,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): @ddt.data( "verify_student_start_flow", + "verify_student_begin_flow", "verify_student_verify_now", "verify_student_upgrade_and_verify", ) @@ -572,11 +626,12 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): expected_status_code=404 ) - def test_account_not_active(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_account_not_active(self, payment_flow): self.user.is_active = False self.user.save() course = self._create_course("verified") - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self._assert_steps_displayed( response, PayAndVerifyView.PAYMENT_STEPS + PayAndVerifyView.VERIFICATION_STEPS, @@ -588,32 +643,36 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): PayAndVerifyView.WEBCAM_REQ, ]) - def test_no_contribution(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_no_contribution(self, payment_flow): # Do NOT specify a contribution for the course in a session var. course = self._create_course("verified") - response = self._get_page("verify_student_start_flow", course.id) + response = self._get_page(payment_flow, course.id) self._assert_contribution_amount(response, "") - def test_contribution_other_course(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_contribution_other_course(self, payment_flow): # Specify a contribution amount for another course in the session course = self._create_course("verified") other_course_id = CourseLocator(org="other", run="test", course="test") self._set_contribution("12.34", other_course_id) # Expect that the contribution amount is NOT pre-filled, - response = self._get_page("verify_student_start_flow", course.id) + response = self._get_page(payment_flow, course.id) self._assert_contribution_amount(response, "") - def test_contribution(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_contribution(self, payment_flow): # Specify a contribution amount for this course in the session course = self._create_course("verified") self._set_contribution("12.34", course.id) # Expect that the contribution amount is pre-filled, - response = self._get_page("verify_student_start_flow", course.id) + response = self._get_page(payment_flow, course.id) self._assert_contribution_amount(response, "12.34") - def test_verification_deadline(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_verification_deadline(self, payment_flow): deadline = datetime(2999, 1, 2, tzinfo=pytz.UTC) course = self._create_course("verified") @@ -625,7 +684,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self._set_deadlines(course.id, upgrade_deadline=deadline, verification_deadline=deadline) # Expect that the expiration date is set - response = self._get_page("verify_student_start_flow", course.id) + response = self._get_page(payment_flow, course.id) data = self._get_page_data(response) self.assertEqual(data['verification_deadline'], "Jan 02, 2999 at 00:00 UTC") @@ -661,7 +720,9 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): # Try to pay or upgrade. # We should get an error message since the deadline has passed. - for page_name in ["verify_student_start_flow", "verify_student_upgrade_and_verify"]: + for page_name in ["verify_student_start_flow", + "verify_student_begin_flow", + "verify_student_upgrade_and_verify"]: response = self._get_page(page_name, course.id) self.assertContains(response, "Upgrade Deadline Has Passed") @@ -708,18 +769,20 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self.assertContains(response, "Jan 02, 1999 at 00:00 UTC") @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) - def test_embargo_restrict(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_embargo_restrict(self, payment_flow): course = self._create_course("verified") with restrict_course(course.id) as redirect_url: # Simulate that we're embargoed from accessing this # course based on our IP address. - response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302) + response = self._get_page(payment_flow, course.id, expected_status_code=302) self.assertRedirects(response, redirect_url) @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) - def test_embargo_allow(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_embargo_allow(self, payment_flow): course = self._create_course("verified") - self._get_page('verify_student_start_flow', course.id) + self._get_page(payment_flow, course.id) def _create_course(self, *course_modes, **kwargs): """Create a new course with the specified course modes. """ @@ -918,7 +981,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): url = reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_id)}) self.assertRedirects(response, url) - def test_course_upgrade_page_with_unicode_and_special_values_in_display_name(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_course_upgrade_page_with_unicode_and_special_values_in_display_name(self, payment_flow): """Check the course information on the page. """ mode_display_name = u"Introduction à l'astrophysique" course = CourseFactory.create(display_name=mode_display_name) @@ -932,13 +996,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ) self._enroll(course.id) - response_dict = self._get_page_data(self._get_page('verify_student_start_flow', course.id)) + response_dict = self._get_page_data(self._get_page(payment_flow, course.id)) self.assertEqual(response_dict['course_name'], mode_display_name) @httpretty.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY) - def test_processors_api(self): + @ddt.data("verify_student_start_flow", "verify_student_begin_flow") + def test_processors_api(self, payment_flow): """ Check that when working with a product being processed by the ecommerce api, we correctly call to that api for the list of @@ -957,7 +1022,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): content_type="application/json", ) # make the server request - response = self._get_page('verify_student_start_flow', course.id) + response = self._get_page(payment_flow, course.id) self.assertEqual(response.status_code, 200) # ensure the mock api call was made. NOTE: the following line diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index ec28a5fd85..e93417dc26 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -23,6 +23,16 @@ urlpatterns = patterns( } ), + # This is for A/B testing. + url( + r'^begin-flow/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + views.PayAndVerifyView.as_view(), + name="verify_student_begin_flow", + kwargs={ + 'message': views.PayAndVerifyView.FIRST_TIME_VERIFY_MSG + } + ), + # The user is enrolled in a non-paid mode and wants to upgrade. # This is the same as the "start verification" flow, # except with slight messaging changes. diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 0870a4e8fa..b1d124f975 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -423,7 +423,9 @@ class PayAndVerifyView(View): 'verification_good_until': verification_good_until, 'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"), 'nav_hidden': True, + 'is_ab_testing': 'begin-flow' in request.path, } + return render_to_response("verify_student/pay_and_verify.html", context) def _redirect_if_necessary( diff --git a/lms/static/images/icon-sm-professional.png b/lms/static/images/icon-sm-professional.png new file mode 100644 index 0000000000..e6d0ac9386 Binary files /dev/null and b/lms/static/images/icon-sm-professional.png differ diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 86634d607f..d6fe7bd31f 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -674,6 +674,7 @@ 'lms/include/js/spec/verify_student/image_input_spec.js', 'lms/include/js/spec/verify_student/review_photos_step_view_spec.js', 'lms/include/js/spec/verify_student/make_payment_step_view_spec.js', + 'lms/include/js/spec/verify_student/make_payment_step_view_ab_testing_spec.js', 'lms/include/js/spec/edxnotes/utils/logger_spec.js', 'lms/include/js/spec/edxnotes/views/notes_factory_spec.js', 'lms/include/js/spec/edxnotes/views/shim_spec.js', diff --git a/lms/static/js/spec/verify_student/make_payment_step_view_ab_testing_spec.js b/lms/static/js/spec/verify_student/make_payment_step_view_ab_testing_spec.js new file mode 100644 index 0000000000..27fa12573f --- /dev/null +++ b/lms/static/js/spec/verify_student/make_payment_step_view_ab_testing_spec.js @@ -0,0 +1,225 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common/js/spec_helpers/ajax_helpers', + 'common/js/spec_helpers/template_helpers', + 'js/verify_student/views/make_payment_step_view' + ], + function( $, _, Backbone, AjaxHelpers, TemplateHelpers, MakePaymentStepView ) { + 'use strict'; + + var checkPaymentButtons, + expectPaymentSubmitted, + goToPayment, + expectPaymentDisabledBecauseInactive, + expectPaymentButtonEnabled, + expectPriceSelected, + createView, + SERVER_ERROR_MSG = 'An error occurred!'; + + describe( 'edx.verify_student.MakePaymentStepView', function() { + + var STEP_DATA = { + minPrice: '12', + currency: 'usd', + processors: ['test-payment-processor'], + courseKey: 'edx/test/test', + courseModeSlug: 'verified', + isABTesting: true + }; + + createView = function( stepDataOverrides ) { + var view = new MakePaymentStepView({ + el: $( '#current-step-container' ), + stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ), + errorModel: new ( Backbone.Model.extend({}) )() + }).render(); + + // Stub the payment form submission + spyOn( view, 'submitForm' ).andCallFake( function() {} ); + return view; + }; + + expectPriceSelected = function( price ) { + var sel = $( 'input[name="contribution"]' ); + + // check that contribution value is same as price given + expect( sel.length ).toEqual(1); + expect( sel.val() ).toEqual(price); + }; + + expectPaymentButtonEnabled = function( isEnabled ) { + var el = $( '.payment-button'), + appearsDisabled = el.hasClass( 'is-disabled' ), + isDisabled = el.prop( 'disabled' ); + + expect( appearsDisabled ).not.toEqual( isEnabled ); + expect( isDisabled ).not.toEqual( isEnabled ); + }; + + expectPaymentDisabledBecauseInactive = function() { + var payButton = $( '.payment-button' ); + + // Payment button should be hidden + expect( payButton.length ).toEqual(0); + }; + + + goToPayment = function( requests, kwargs ) { + var params = { + contribution: kwargs.amount || '', + course_id: kwargs.courseId || '', + processor: kwargs.processor || '', + sku: kwargs.sku || '' + }; + + // Click the "go to payment" button + $( '.payment-button' ).click(); + + // Verify that the request was made to the server + AjaxHelpers.expectPostRequest( + requests, '/verify_student/create_order/', $.param( params ) + ); + + // Simulate the server response + if ( kwargs.succeeds ) { + // TODO put fixture responses in the right place + AjaxHelpers.respondWithJson( + requests, {payment_page_url: 'http://payment-page-url/', payment_form_data: {foo: 'bar'}} + ); + } else { + AjaxHelpers.respondWithTextError( requests, 400, SERVER_ERROR_MSG); + } + }; + + expectPaymentSubmitted = function( view, params ) { + var form; + + expect(view.submitForm).toHaveBeenCalled(); + form = view.submitForm.mostRecentCall.args[0]; + + expect(form.serialize()).toEqual($.param(params)); + expect(form.attr('method')).toEqual('POST'); + expect(form.attr('action')).toEqual('http://payment-page-url/'); + }; + + checkPaymentButtons = function( requests, buttons ) { + var $el = $( '.payment-button' ); + expect($el.length).toEqual(_.size(buttons)); + _.each(buttons, function( expectedText, expectedId ) { + var buttonEl = $( '#' + expectedId), + request; + + buttonEl.removeAttr('disabled'); + expect( buttonEl.length ).toEqual( 1 ); + expect( buttonEl[0] ).toHaveClass( 'payment-button' ); + expect( buttonEl[0] ).toHaveText( expectedText ); + expect( buttonEl[0] ).toHaveClass( 'action-primary-blue' ); + + buttonEl[0].click(); + expect( buttonEl[0] ).toHaveClass( 'is-selected' ); + expectPaymentButtonEnabled( false ); + request = AjaxHelpers.currentRequest(requests); + expect(request.requestBody.split('&')).toContain('processor=' + expectedId); + AjaxHelpers.respondWithJson(requests, {}); + }); + }; + + beforeEach(function() { + window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']); + + setFixtures( '
' ); + TemplateHelpers.installTemplate( 'templates/verify_student/make_payment_step_ab_testing' ); + }); + + it( 'A/B Testing: check Initialize method with AB testing enable ', function() { + var view = createView(); + expect( view.templateName ).toEqual('make_payment_step_ab_testing'); + expect( view.btnClass ).toEqual('action-primary-blue'); + + }); + + it( 'shows users only minimum price', function() { + var view = createView(), + requests = AjaxHelpers.requests(this); + + expectPriceSelected( STEP_DATA.minPrice ); + expectPaymentButtonEnabled( true ); + goToPayment( requests, { + amount: STEP_DATA.minPrice, + courseId: STEP_DATA.courseKey, + processor: STEP_DATA.processors[0], + succeeds: true + }); + expectPaymentSubmitted( view, {foo: 'bar'} ); + }); + + it( 'A/B Testing: provides working payment buttons for a single processor', function() { + createView({processors: ['cybersource']}); + checkPaymentButtons( AjaxHelpers.requests(this), {cybersource: 'Checkout'}); + }); + + it( 'A/B Testing: provides working payment buttons for multiple processors', function() { + createView({processors: ['cybersource', 'paypal', 'other']}); + checkPaymentButtons( AjaxHelpers.requests(this), { + cybersource: 'Checkout', + paypal: 'Checkout with PayPal', + other: 'Checkout with other' + }); + }); + + it( 'A/B Testing: by default minimum price is selected if no suggested prices are given', function() { + var view = createView(), + requests = AjaxHelpers.requests( this ); + + expectPriceSelected( STEP_DATA.minPrice); + expectPaymentButtonEnabled( true ); + + goToPayment( requests, { + amount: STEP_DATA.minPrice, + courseId: STEP_DATA.courseKey, + processor: STEP_DATA.processors[0], + succeeds: true + }); + expectPaymentSubmitted( view, {foo: 'bar'} ); + }); + + it( 'A/B Testing: min price is always selected even if contribution amount is provided', function() { + // Pre-select a price NOT in the suggestions + createView({ + contributionAmount: '99.99' + }); + + // Expect that the price is filled in + expectPriceSelected( STEP_DATA.minPrice ); + }); + + it( 'A/B Testing: disables payment for inactive users', function() { + createView({ isActive: false }); + expectPaymentDisabledBecauseInactive(); + }); + + it( 'A/B Testing: displays an error if the order could not be created', function() { + var requests = AjaxHelpers.requests( this ), + view = createView(); + + goToPayment( requests, { + amount: STEP_DATA.minPrice, + courseId: STEP_DATA.courseKey, + processor: STEP_DATA.processors[0], + succeeds: false + }); + + // Expect that an error is displayed + expect( view.errorModel.get('shown') ).toBe( true ); + expect( view.errorModel.get('errorTitle') ).toEqual( 'Could not submit order' ); + expect( view.errorModel.get('errorMsg') ).toEqual( SERVER_ERROR_MSG ); + + // Expect that the payment button is re-enabled + expectPaymentButtonEnabled( true ); + }); + + }); + } +); diff --git a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js index 034c1f3b24..cda80071fe 100644 --- a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js +++ b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js @@ -108,6 +108,7 @@ define([ buttonEl.removeAttr('disabled'); expect( buttonEl.length ).toEqual( 1 ); expect( buttonEl[0] ).toHaveClass( 'payment-button' ); + expect( buttonEl[0] ).toHaveClass( 'action-primary' ); expect( buttonEl[0] ).toHaveText( expectedText ); buttonEl[0].click(); @@ -216,6 +217,12 @@ define([ 'Try the transaction again in a few minutes.' ); }); + it( 'check Initialize method without AB testing ', function() { + var view = createView(); + expect( view.templateName ).toEqual('make_payment_step'); + expect( view.btnClass ).toEqual('action-primary'); + }); + }); } ); diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index 3e989f80f9..93f271400b 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -66,7 +66,8 @@ var edx = edx || {}; verificationDeadline: el.data('verification-deadline'), courseModeSlug: el.data('course-mode-slug'), alreadyVerified: el.data('already-verified'), - verificationGoodUntil: el.data('verification-good-until') + verificationGoodUntil: el.data('verification-good-until'), + isABTesting: el.data('is-ab-testing') }, 'payment-confirmation-step': { courseKey: el.data('course-key'), diff --git a/lms/static/js/verify_student/views/make_payment_step_view.js b/lms/static/js/verify_student/views/make_payment_step_view.js index f1313d5d87..fc809e6da9 100644 --- a/lms/static/js/verify_student/views/make_payment_step_view.js +++ b/lms/static/js/verify_student/views/make_payment_step_view.js @@ -11,6 +11,15 @@ var edx = edx || {}; edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({ templateName: "make_payment_step", + btnClass: 'action-primary', + + initialize: function( obj ) { + _.extend( this, obj ); + if (this.templateContext().isABTesting) { + this.templateName = 'make_payment_step_ab_testing'; + this.btnClass = 'action-primary-blue'; + } + }, defaultContext: function() { return { @@ -27,7 +36,8 @@ var edx = edx || {}; platformName: '', alreadyVerified: false, courseModeSlug: 'audit', - verificationGoodUntil: '' + verificationGoodUntil: '', + isABTesting: false }; }, @@ -61,8 +71,8 @@ var edx = edx || {}; _getPaymentButtonHtml: function(processorName) { var self = this; return _.template( - ' ' - )({name: processorName, text: self._getPaymentButtonText(processorName)}); + ' ' + )({name: processorName, text: self._getPaymentButtonText(processorName), btnClass: this.btnClass}); }, postRender: function() { diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 1df2975e2c..052ccc8859 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -175,6 +175,14 @@ color: $white !important; } + // elements - controls + .action-primary-blue { + @extend %btn-primary-blue; + // needed for override due to .register a:link styling + border: 0 !important; + color: $white !important; + } + .action-confirm { @extend %btn-verify-primary; // needed for override due to .register a:link styling @@ -821,6 +829,109 @@ // indiv slides - review #wrapper-review { + color: $black; + + .page-title { + @extend %t-strong; + border-bottom: 2px solid $m-gray-d3; + padding-bottom: ($baseline*0.75); + margin-bottom: $baseline; + text-transform: inherit; + } + + .review { + + .certificate { + @include font-size(18); + background-repeat: no-repeat; + padding-left: ($baseline*2.5); + overflow: hidden; + min-height: 32px; + + p { + @include line-height(22); + @extend %t-strong; + margin-top: 0; + color: $black; + } + + .purchase { + @include float(right); + @include margin-left($baseline*0.75); + text-align: right; + + .product-info { + @include font-size(22); + @extend %t-strong; + color: $blue; + } + } + + &.verified_icon { + background-image: url('#{$static-path}/images/icon-sm-verified.png'); + } + + &.no-id-professional_icon, + &.professional_icon { + background-image: url('#{$static-path}/images/icon-sm-professional.png'); + } + } + + .payment-buttons { + overflow: auto; + padding-bottom: ($baseline/4); + margin: { + top: ($baseline / 2); + bottom: ($baseline * 0.75); + }; + + .payment-button { + padding: ($baseline*0.4) $baseline; + min-width: 200px; + } + .action-primary-blue { + &.is-selected { + background: $blue !important; + } + } + } + + .border-gray { + border-bottom: 2px solid $gray; + margin: ($baseline*1.12) 0; + } + } + + .container { + padding: ($baseline*0.75) 0; + + p { + @include line-height(22); + color: $black; + } + .photo-requirement { + @include font-size(12); + position: relative; + padding-left: ($baseline*2); + margin-top: ($baseline*0.75); + background-repeat: no-repeat; + background-position: left top; + + .fa { + position: absolute; + left:0; + color: $mediumGrey; + } + + h6 { + font-weight: bold; + color: $extraDarkGrey; + } + } + } + + + .review-task { margin-bottom: ($baseline*1.5); diff --git a/lms/templates/verify_student/make_payment_step_ab_testing.underscore b/lms/templates/verify_student/make_payment_step_ab_testing.underscore new file mode 100644 index 0000000000..7afa00adeb --- /dev/null +++ b/lms/templates/verify_student/make_payment_step_ab_testing.underscore @@ -0,0 +1,105 @@ ++ <%- gettext("Before you upgrade to a certificate track, you must activate your account.") %> + <%- gettext("Check your email for an activation message.") %> +
+ <% } else { %> + +<%- gettext( "Total" ) %>: $<%- minPrice %> USD
++ <% if ( courseModeSlug === 'no-id-professional' || courseModeSlug === 'professional') { %> + <%= _.sprintf( + gettext( "Professional Certificate for %(courseName)s"),{ courseName: courseName } + )%> + <% } else { %> + <%= _.sprintf( + gettext( "Verified Certificate for %(courseName)s"),{ courseName: courseName } + )%> + <% } %> +
++ <% if ( verificationDeadline ) { %> + <%- _.sprintf( + gettext( "To receive a certificate, you must also verify your identity before %(date)s." ), + { date: verificationDeadline } + ) %> + <% } else { %> + <%- gettext( "To receive a certificate, you must also verify your identity." ) %> + <% } %> + <%- gettext("To verify your identity, you need a webcam and a government-issued photo ID.") %> +
+ <% if ( requirements['photo-id-required'] ) { %> ++ <%- gettext("Your ID must be a government-issued photo ID that clearly shows your face.") %> +
++ <%- gettext("You will use your webcam to take a picture of your face and of your government-issued photo ID.") %> +
+