From a52ca3639bca6fda4a7791a9df96c33c906d5a29 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 21 Aug 2013 11:35:12 -0400 Subject: [PATCH] Initial BDD spec for certificates workflow --- .../djangoapps/course_modes/tests/__init__.py | 0 .../course_modes/tests/factories.py | 13 ++ .../{tests.py => tests/test_models.py} | 0 common/djangoapps/terrain/factories.py | 9 + .../courseware/features/certificates.feature | 59 +++++ .../courseware/features/certificates.py | 214 ++++++++++++++++++ 6 files changed, 295 insertions(+) create mode 100644 common/djangoapps/course_modes/tests/__init__.py create mode 100644 common/djangoapps/course_modes/tests/factories.py rename common/djangoapps/course_modes/{tests.py => tests/test_models.py} (100%) create mode 100644 lms/djangoapps/courseware/features/certificates.feature create mode 100644 lms/djangoapps/courseware/features/certificates.py diff --git a/common/djangoapps/course_modes/tests/__init__.py b/common/djangoapps/course_modes/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/course_modes/tests/factories.py b/common/djangoapps/course_modes/tests/factories.py new file mode 100644 index 0000000000..3e35b2f05c --- /dev/null +++ b/common/djangoapps/course_modes/tests/factories.py @@ -0,0 +1,13 @@ +from course_modes.models import CourseMode +from factory import DjangoModelFactory + +# Factories don't have __init__ methods, and are self documenting +# pylint: disable=W0232 +class CourseModeFactory(DjangoModelFactory): + FACTORY_FOR = CourseMode + + course_id = u'MITx/999/Robot_Super_Course' + mode_slug = 'audit' + mode_display_name = 'audit course' + min_price = 0 + currency = 'usd' diff --git a/common/djangoapps/course_modes/tests.py b/common/djangoapps/course_modes/tests/test_models.py similarity index 100% rename from common/djangoapps/course_modes/tests.py rename to common/djangoapps/course_modes/tests/test_models.py diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 2ed78aaa9f..9aa6b3b54d 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -5,6 +5,7 @@ and integration / BDD tests. ''' import student.tests.factories as sf import xmodule.modulestore.tests.factories as xf +import course_modes.tests.factories as cmf from lettuce import world @@ -51,6 +52,14 @@ class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory): pass +@world.absorb +class CourseModeFactory(cmf.CourseModeFactory): + """ + Course modes + """ + pass + + @world.absorb class CourseFactory(xf.CourseFactory): """ diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature new file mode 100644 index 0000000000..988968e938 --- /dev/null +++ b/lms/djangoapps/courseware/features/certificates.feature @@ -0,0 +1,59 @@ +Feature: Verified certificates + As a student, + In order to earn a verified certificate + I want to sign up for a verified certificate course. + + Scenario: I can audit a verified certificate course + Given I am logged in + When I select the audit track + Then I should see the course on my dashboard + + Scenario: I can submit photos to verify my identity + Given I am logged in + When I select the verified track + And I go to step "1" + And I capture my "face" photo + And I approve my "face" photo + And I go to step "2" + And I capture my "photo_id" photo + And I approve my "photo_id" photo + And I go to step "3" + And I select a contribution amount + And I confirm that the details match + And I go to step "4" + Then The course is added to my cart + And I view the payment page + + Scenario: I can pay for a verified certificate + Given I have submitted photos to verify my identity + When I submit valid payment information + Then I see that my payment was successful + And I receive an email confirmation + And I see that I am registered for a verified certificate course on my dashboard + + Scenario: I can re-take photos + Given I have submitted my "" photo + When I retake my "" photo + Then I see the new photo on the confirmation page. + + Examples: + | PhotoType | + | face | + | ID | + + Scenario: I can edit identity information + Given I have submitted face and ID photos + When I edit my name + Then I see the new name on the confirmation page. + + Scenario: I can return to the verify flow + Given I have submitted photos + When I leave the flow and return + I see the payment page + + + # Design not yet finalized + #Scenario: I can take a verified certificate course for free + # Given I have submitted photos to verify my identity + # When I give a reason why I cannot pay + # Then I see that I am registered for a verified certificate course on my dashboard diff --git a/lms/djangoapps/courseware/features/certificates.py b/lms/djangoapps/courseware/features/certificates.py new file mode 100644 index 0000000000..a18e01a92e --- /dev/null +++ b/lms/djangoapps/courseware/features/certificates.py @@ -0,0 +1,214 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from lettuce.django import django_url +from course_modes.models import CourseMode +from selenium.common.exceptions import WebDriverException + +def create_cert_course(): + world.clear_courses() + org = 'edx' + number = '999' + name = 'Certificates' + course_id = '{org}/{number}/{name}'.format( + org=org, number=number, name=name) + world.scenario_dict['COURSE'] = world.CourseFactory.create( + org=org, number=number, display_name=name) + + audit_mode = world.CourseModeFactory.create( + course_id=course_id, + mode_slug='audit', + mode_display_name='audit course', + min_price=0, + ) + assert isinstance(audit_mode, CourseMode) + + verfied_mode = world.CourseModeFactory.create( + course_id=course_id, + mode_slug='verified', + mode_display_name='verified cert course', + min_price=16, + suggested_prices='32,64,128', + currency='usd', + ) + assert isinstance(verfied_mode, CourseMode) + + +def register(): + url = 'courses/{org}/{number}/{name}/about'.format( + org='edx', number='999', name='Certificates') + world.browser.visit(django_url(url)) + + world.css_click('section.intro a.register') + assert world.is_css_present('section.wrapper h3.title') + + +@step(u'I select the audit track$') +def select_the_audit_track(step): + create_cert_course() + register() + btn_css = 'input[value="Select Audit"]' + world.css_click(btn_css) + + +def select_contribution(amount=32): + radio_css = 'input[value="{}"]'.format(amount) + world.css_click(radio_css) + assert world.css_find(radio_css).selected + + +@step(u'I select the verified track$') +def select_the_verified_track(step): + create_cert_course() + register() + select_contribution(32) + btn_css = 'input[value="Select Certificate"]' + world.css_click(btn_css) + assert world.is_css_present('li.current#progress-step0') + + +@step(u'I should see the course on my dashboard$') +def should_see_the_course_on_my_dashboard(step): + course_css = 'article.my-course' + assert world.is_css_present(course_css) + + +@step(u'I go to step "([^"]*)"$') +def goto_next_step(step, step_num): + btn_css = { + '1': 'p.m-btn-primary a', + '2': '#face_next_button a.next', + '3': '#photo_id_next_button a.next', + '4': '#pay_button', + } + next_css = { + '1': 'div#wrapper-facephoto.carousel-active', + '2': 'div#wrapper-idphoto.carousel-active', + '3': 'div#wrapper-review.carousel-active', + '4': 'div#wrapper-review.carousel-active', + } + world.css_click(btn_css[step_num]) + + # Pressing the button will advance the carousel to the next item + # and give the wrapper div the "carousel-active" class + assert world.css_find(next_css[step_num]) + + +@step(u'I capture my "([^"]*)" photo$') +def capture_my_photo(step, name): + + # Draw a red rectangle in the image element + snapshot_script = '"{}{}{}{}{}{}"'.format( + "var canvas = $('#{}_canvas');".format(name), + "var ctx = canvas[0].getContext('2d');", + "ctx.fillStyle = 'rgb(200,0,0)';", + "ctx.fillRect(0, 0, 640, 480);", + "var image = $('#{}_image');".format(name), + "image[0].src = canvas[0].toDataURL('image/png');" + ) + + # Mirror the javascript of the photo_verification.html page + world.browser.execute_script(snapshot_script) + world.browser.execute_script("$('#{}_capture_button').hide();".format(name)) + world.browser.execute_script("$('#{}_reset_button').show();".format(name)) + world.browser.execute_script("$('#{}_approve_button').show();".format(name)) + assert world.css_visible('#{}_approve_button'.format(name)) + + +@step(u'I approve my "([^"]*)" photo$') +def approve_my_photo(step, name): + button_css = { + 'face': 'div#wrapper-facephoto li.control-approve', + 'photo_id': 'div#wrapper-idphoto li.control-approve', + } + wrapper_css = { + 'face': 'div#wrapper-facephoto', + 'photo_id': 'div#wrapper-idphoto', + } + + # Make sure that the carousel is in the right place + assert world.css_has_class(wrapper_css[name], 'carousel-active') + assert world.css_visible(button_css[name]) + + # HACK: for now don't bother clicking the approve button for + # id_photo, because it is sending you back to Step 1. + # Come back and figure it out later. JZ Aug 29 2013 + if name=='face': + world.css_click(button_css[name]) + + # Make sure you didn't advance the carousel + assert world.css_has_class(wrapper_css[name], 'carousel-active') + + +@step(u'I select a contribution amount$') +def select_contribution_amount(step): + select_contribution(32) + + +@step(u'I confirm that the details match$') +def confirm_details_match(step): + # First you need to scroll down on the page + # to make the element visible? + # Currently chrome is failing with ElementNotVisibleException + world.browser.execute_script("window.scrollTo(0,1024)") + + cb_css = 'input#confirm_pics_good' + world.css_check(cb_css) + assert world.css_find(cb_css).checked + + +@step(u'The course is added to my cart') +def see_course_is_added_to_my_cart(step): + assert False, 'This step must be implemented' +@step(u'I view the payment page') +def view_the_payment_page(step): + assert False, 'This step must be implemented' +@step(u'I have submitted photos to verify my identity') +def submitted_photos_to_verify_my_identity(step): + assert False, 'This step must be implemented' +@step(u'I submit valid payment information') +def submit_valid_payment_information(step): + assert False, 'This step must be implemented' +@step(u'I see that my payment was successful') +def sesee_that_my_payment_was_successful(step): + assert False, 'This step must be implemented' +@step(u'I receive an email confirmation') +def receive_an_email_confirmation(step): + assert False, 'This step must be implemented' +@step(u'I see that I am registered for a verified certificate course on my dashboard') +def see_that_i_am_registered_for_a_verified_certificate_course_on_my_dashboard(step): + assert False, 'This step must be implemented' +@step(u'I have submitted my "([^"]*)" photo') +def submitted_my_group1_photo(step, group1): + assert False, 'This step must be implemented' +@step(u'I retake my "([^"]*)" photo') +def retake_my_group1_photo(step, group1): + assert False, 'This step must be implemented' +@step(u'I see the new photo on the confirmation page.') +def sesee_the_new_photo_on_the_confirmation_page(step): + assert False, 'This step must be implemented' +@step(u'I have submitted face and ID photos') +def submitted_face_and_id_photos(step): + assert False, 'This step must be implemented' +@step(u'I edit my name') +def edit_my_name(step): + assert False, 'This step must be implemented' +@step(u'I see the new name on the confirmation page.') +def sesee_the_new_name_on_the_confirmation_page(step): + assert False, 'This step must be implemented' +@step(u'I have submitted photos') +def submitted_photos(step): + assert False, 'This step must be implemented' +@step(u'I leave the flow and return') +def leave_the_flow_and_return(step): + assert False, 'This step must be implemented' +@step(u'I see the payment page') +def see_the_payment_page(step): + assert False, 'This step must be implemented' +@step(u'I am registered for the course') +def seam_registered_for_the_course(step): + assert False, 'This step must be implemented' +@step(u'I return to the student dashboard') +def return_to_the_student_dashboard(step): + assert False, 'This step must be implemented'