From 90213d483cb0af36ca5fd33aa2c3864c6ec8d0e0 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:25:36 -0400 Subject: [PATCH 01/13] Wrote lettuce tests for drop-down, multiple choice, and checkbox problems. --- common/djangoapps/terrain/steps.py | 15 +++- lms/djangoapps/courseware/features/common.py | 10 ++- .../courseware/features/courseware.feature | 2 +- .../features/high-level-tabs.feature | 2 +- .../courseware/features/problems.feature | 53 +++++++++++++ .../courseware/features/problems.py | 79 +++++++++++++++++++ .../courseware/features/registration.feature | 4 +- .../courseware/features/registration.py | 11 +-- 8 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 lms/djangoapps/courseware/features/problems.feature create mode 100644 lms/djangoapps/courseware/features/problems.py diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 3dcef9b1ed..330740b6b3 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -116,6 +116,11 @@ def scroll_to_bottom(): @world.absorb def create_user(uname): + + # If the user already exists, don't try to create it again + if len(User.objects.filter(username=uname)) > 0: + return + portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') portal_user.set_password('test') portal_user.save() @@ -133,13 +138,17 @@ def log_in(email, password): world.browser.visit(django_url('/')) world.browser.is_element_present_by_css('header.global', 10) world.browser.click_link_by_href('#login-modal') + + # wait for the login dialog to load + assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) + login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').fill(email) - login_form.find_by_name('password').fill(password) + login_form.find_by_name('email').type(email) + login_form.find_by_name('password').type(password) login_form.find_by_name('submit').click() # wait for the page to redraw - assert world.browser.is_element_present_by_css('.content-wrapper', 10) + assert world.browser.is_element_present_by_css('.content-wrapper', wait_time=10) @world.absorb diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 2e19696ad4..145a56e183 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -81,11 +81,15 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() -@step(u'I am registered for a course$') -def i_am_registered_for_a_course(step): +@step(u'I am registered for the course "([^"]*)"$') +def i_am_registered_for_the_course(step, course_id): world.create_user('robot') u = User.objects.get(username='robot') - CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall') + + # If the user is not already enrolled, enroll the user. + if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0: + CourseEnrollment.objects.create(user=u, course_id=course_id) + world.log_in('robot@edx.org', 'test') diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature index 279e5732c9..14e7786fc9 100644 --- a/lms/djangoapps/courseware/features/courseware.feature +++ b/lms/djangoapps/courseware/features/courseware.feature @@ -4,7 +4,7 @@ Feature: View the Courseware Tab I want to view the info on the courseware tab Scenario: I can get to the courseware tab when logged in - Given I am registered for a course + Given I am registered for the course "MITx/6.002x/2013_Spring" And I log in And I click on View Courseware When I click on the "Courseware" tab diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 2e9c4f1886..354376b154 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -8,7 +8,7 @@ Feature: All the high level tabs should work # TODO: break this apart so that if one fails the others # will still run Scenario: A student can see all tabs of the course - Given I am registered for a course + Given I am registered for the course "MITx/6.002x/2013_Spring" And I log in And I click on View Courseware When I click on the "Courseware" tab diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature new file mode 100644 index 0000000000..cc459fa35f --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.feature @@ -0,0 +1,53 @@ +Feature: Answer choice problems + As a student in an edX course + In order to test my understanding of the material + I want to answer choice based problems + + Scenario: I can answer a problem correctly + Given I am viewing a "" problem + When I answer a "" problem "correctly" + Then My "" answer is marked "correct" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + Scenario: I can answer a problem incorrectly + Given I am viewing a "" problem + When I answer a "" problem "incorrectly" + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + Scenario: I can submit a blank answer + Given I am viewing a "" problem + When I check a problem + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + + Scenario: I can reset a problem + Given I am viewing a "" problem + And I answer a "" problem "ly" + When I reset the problem + Then My "" answer is marked "unanswered" + + Examples: + | ProblemType | Correctness | + | drop down | correct | + | drop down | incorrect | + | multiple choice | correct | + | multiple choice | incorrect | + | checkbox | correct | + | checkbox | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py new file mode 100644 index 0000000000..4758e16b8d --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.py @@ -0,0 +1,79 @@ +from lettuce import world, step +from lettuce.django import django_url +from selenium.webdriver.support.ui import Select +from common import i_am_registered_for_the_course + +problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', + 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', + 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', } + +@step(u'I am viewing a "([^"]*)" problem') +def view_problem(step, problem_type): + i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') + url = django_url(problem_urls[problem_type]) + world.browser.visit(url) + +@step(u'I answer a "([^"]*)" problem "([^"]*)ly"') +def answer_problem(step, problem_type, correctness): + assert(correctness in ['correct', 'incorrect']) + + if problem_type == "drop down": + select_name = "input_i4x-edX-model_course-problem-Drop_Down_Problem_2_1" + option_text = 'Option 2' if correctness == 'correct' else 'Option 3' + world.browser.select(select_name, option_text) + + elif problem_type == "multiple choice": + if correctness == 'correct': + world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + else: + world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + + elif problem_type == "checkbox": + if correctness == 'correct': + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + else: + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + + check_problem(step) + +@step(u'I check a problem') +def check_problem(step): + world.browser.find_by_css("input.check").click() + +@step(u'I reset the problem') +def reset_problem(step): + world.browser.find_by_css('input.reset').click() + +@step(u'My "([^"]*)" answer is marked "([^"]*)"') +def assert_answer_mark(step, problem_type, correctness): + assert(correctness in ['correct', 'incorrect', 'unanswered']) + + if problem_type == "multiple choice": + if correctness == 'unanswered': + mark_classes = ['.choicegroup_correct', '.choicegroup_incorrect', + '.correct', '.incorrect'] + for css in mark_classes: + assert(world.browser.is_element_not_present_by_css(css)) + + else: + if correctness == 'correct': + mark_class = '.choicegroup_correct' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + + else: + # Two ways to be marked incorrect: either applying a + # class to the label (marking a particular option) + # or applying a class to a span (marking the whole problem incorrect) + mark_classes = ['.choicegroup_incorrect', '.incorrect'] + assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or + world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + + else: + if correctness == 'unanswered': + assert(world.browser.is_element_not_present_by_css('.correct')) + assert(world.browser.is_element_not_present_by_css('.incorrect')) + + else: + mark_class = '.correct' if correctness == 'correct' else '.incorrect' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature index d9b588534b..890beec1d8 100644 --- a/lms/djangoapps/courseware/features/registration.feature +++ b/lms/djangoapps/courseware/features/registration.feature @@ -6,11 +6,11 @@ Feature: Register for a course Scenario: I can register for a course Given I am logged in And I visit the courses page - When I register for the course numbered "6.002x" + When I register for the course "MITx/6.002x/2013_Spring" Then I should see the course numbered "6.002x" in my dashboard Scenario: I can unregister for a course - Given I am registered for a course + Given I am registered for the course "MITx/6.002x/2013_Spring" And I visit the dashboard When I click the link with the text "Unregister" And I press the "Unregister" button in the Unenroll dialog diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index f585136412..5535319f15 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,12 +1,9 @@ from lettuce import world, step +from lettuce.django import django_url - -@step('I register for the course numbered "([^"]*)"$') -def i_register_for_the_course(step, course): - courses_section = world.browser.find_by_css('section.courses') - course_link_css = 'article[id*="%s"] > div' % course - course_link = courses_section.find_by_css(course_link_css).first - course_link.click() +@step('I register for the course "([^"]*)"$') +def i_register_for_the_course(step, course_id): + world.browser.visit(django_url('courses/%s/about' % course_id)) intro_section = world.browser.find_by_css('section.intro') register_link = intro_section.find_by_css('a.register') From 56a6363d7ae95367ea09a985624eb09ce3178f70 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:40:37 -0400 Subject: [PATCH 02/13] CSS selectors in lettuce tests for problems now include the element tag. --- lms/djangoapps/courseware/features/problems.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 4758e16b8d..55467378f4 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -51,8 +51,8 @@ def assert_answer_mark(step, problem_type, correctness): if problem_type == "multiple choice": if correctness == 'unanswered': - mark_classes = ['.choicegroup_correct', '.choicegroup_incorrect', - '.correct', '.incorrect'] + mark_classes = ['label.choicegroup_correct', 'label.choicegroup_incorrect', + 'span.correct', 'span.incorrect'] for css in mark_classes: assert(world.browser.is_element_not_present_by_css(css)) @@ -65,15 +65,15 @@ def assert_answer_mark(step, problem_type, correctness): # Two ways to be marked incorrect: either applying a # class to the label (marking a particular option) # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['.choicegroup_incorrect', '.incorrect'] + mark_classes = ['label.choicegroup_incorrect', 'label.incorrect'] assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) else: if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('.correct')) - assert(world.browser.is_element_not_present_by_css('.incorrect')) + assert(world.browser.is_element_not_present_by_css('span.correct')) + assert(world.browser.is_element_not_present_by_css('span.incorrect')) else: - mark_class = '.correct' if correctness == 'correct' else '.incorrect' + mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) From e6466fdddc15c1e4fd9fc4993140deaddf001c8e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:44:27 -0400 Subject: [PATCH 03/13] Changed user creation so that it creates the user only if it doesn't already exist Updated login dialog handling to workaround multiple login dialogs that sometimes appear on a page (where the first one is hidden) --- common/djangoapps/terrain/steps.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 330740b6b3..5917d171b9 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -142,9 +142,14 @@ def log_in(email, password): # wait for the login dialog to load assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) - login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').type(email) - login_form.find_by_name('password').type(password) + # For some reason, the page sometimes includes two #login_form + # elements, the first of which is not visible. + # To avoid this, we always select the last of the two #login_form + # dialogs + login_form = world.browser.find_by_css('form#login_form').last + + login_form.find_by_name('email').fill(email) + login_form.find_by_name('password').fill(password) login_form.find_by_name('submit').click() # wait for the page to redraw From b3946828c034d567b8751c7aa50319edad485cc4 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 16:18:27 -0400 Subject: [PATCH 04/13] Added lettuce tests for string problems --- .../courseware/features/problems.feature | 5 +++++ lms/djangoapps/courseware/features/problems.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index cc459fa35f..f50d8329b6 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -13,6 +13,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -24,6 +25,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -35,6 +37,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can reset a problem @@ -51,3 +54,5 @@ Feature: Answer choice problems | multiple choice | incorrect | | checkbox | correct | | checkbox | incorrect | + | string | correct | + | string | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 55467378f4..19c0195b15 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -5,7 +5,8 @@ from common import i_am_registered_for_the_course problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', - 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', } + 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', + 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems' } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -35,6 +36,11 @@ def answer_problem(step, problem_type, correctness): else: world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + elif problem_type == 'string': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") + textvalue = 'correct string' if correctness == 'correct' else 'incorrect' + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -65,10 +71,18 @@ def assert_answer_mark(step, problem_type, correctness): # Two ways to be marked incorrect: either applying a # class to the label (marking a particular option) # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['label.choicegroup_incorrect', 'label.incorrect'] + mark_classes = ['label.choicegroup_incorrect', 'span.incorrect'] assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + elif problem_type == "string": + if correctness == 'unanswered': + assert(world.browser.is_element_not_present_by_css('div.correct')) + assert(world.browser.is_element_not_present_by_css('div.incorrect')) + else: + mark_class = 'div.correct' if correctness == 'correct' else 'div.incorrect' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + else: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('span.correct')) From 4d5a8e757c883918b32974e66e7958fb434caff2 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 17:01:34 -0400 Subject: [PATCH 05/13] Added lettuce tests for numerical problem --- .../courseware/features/problems.feature | 5 +++++ .../courseware/features/problems.py | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index f50d8329b6..a1e4712f6a 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -14,6 +14,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -26,6 +27,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -38,6 +40,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can reset a problem @@ -56,3 +59,5 @@ Feature: Answer choice problems | checkbox | incorrect | | string | correct | | string | incorrect | + | numerical | correct | + | numerical | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 19c0195b15..666182684c 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -1,12 +1,14 @@ from lettuce import world, step from lettuce.django import django_url from selenium.webdriver.support.ui import Select +import random from common import i_am_registered_for_the_course problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', - 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems' } + 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', + 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -25,22 +27,27 @@ def answer_problem(step, problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() else: - world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() elif problem_type == "checkbox": if correctness == 'correct': - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() else: - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() elif problem_type == 'string': textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") textvalue = 'correct string' if correctness == 'correct' else 'incorrect' textfield.fill(textvalue) + elif problem_type == 'numerical': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Numerical_Problem_2_1") + textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -75,7 +82,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) - elif problem_type == "string": + elif problem_type in ["string", "numerical"]: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('div.correct')) assert(world.browser.is_element_not_present_by_css('div.incorrect')) From 5a2a4055f8bb84fff9bf53888e8b23423f7d6703 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 17:18:02 -0400 Subject: [PATCH 06/13] Added lettuce test for formula problem --- lms/djangoapps/courseware/features/problems.feature | 5 +++++ lms/djangoapps/courseware/features/problems.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index a1e4712f6a..12458537d0 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -15,6 +15,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -28,6 +29,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -41,6 +43,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can reset a problem @@ -61,3 +64,5 @@ Feature: Answer choice problems | string | incorrect | | numerical | correct | | numerical | incorrect | + | formula | correct | + | formula | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 666182684c..c1b634783d 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -8,7 +8,8 @@ problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/ 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', - 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', } + 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', + 'formula': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Formula_Problems', } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -48,6 +49,11 @@ def answer_problem(step, problem_type, correctness): textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) textfield.fill(textvalue) + elif problem_type == 'formula': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Formula_Problem_2_1") + textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -82,7 +88,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) - elif problem_type in ["string", "numerical"]: + elif problem_type in ["string", "numerical", "formula"]: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('div.correct')) assert(world.browser.is_element_not_present_by_css('div.incorrect')) From 3d8625da9ceed06dcc8057edd6b4b6bf363f5e7e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 14 Mar 2013 08:42:35 -0400 Subject: [PATCH 07/13] Refactored problem lettuce test implementation --- .../courseware/features/problems.py | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index c1b634783d..1985847bd3 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -4,17 +4,10 @@ from selenium.webdriver.support.ui import Select import random from common import i_am_registered_for_the_course -problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', - 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', - 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', - 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', - 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', - 'formula': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Formula_Problems', } - @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') - url = django_url(problem_urls[problem_type]) + url = django_url(problem_url(problem_type)) world.browser.visit(url) @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') @@ -28,31 +21,28 @@ def answer_problem(step, problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + inputfield('multiple choice', choice='choice_3').check() else: - world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + inputfield('multiple choice', choice='choice_2').check() elif problem_type == "checkbox": if correctness == 'correct': - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + inputfield('checkbox', choice='choice_0').check() + inputfield('checkbox', choice='choice_2').check() else: - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + inputfield('checkbox', choice='choice_3').check() elif problem_type == 'string': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - textfield.fill(textvalue) + inputfield('string').fill(textvalue) elif problem_type == 'numerical': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Numerical_Problem_2_1") textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) - textfield.fill(textvalue) + inputfield('numerical').fill(textvalue) elif problem_type == 'formula': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Formula_Problem_2_1") textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - textfield.fill(textvalue) + inputfield('formula').fill(textvalue) check_problem(step) @@ -104,3 +94,35 @@ def assert_answer_mark(step, problem_type, correctness): else: mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + +def problem_url(problem_type): + base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' + url_extensions = { 'drop down': 'Drop_Down_Problems', + 'multiple choice': 'Multiple_Choice_Problems', + 'checkbox': 'Checkbox_Problems', + 'string': 'String_Problems', + 'numerical': 'Numerical_Problems', + 'formula': 'Formula_Problems', } + + assert(problem_type in url_extensions) + return base + url_extensions[problem_type] + + + +def inputfield(problem_type, choice=None): + field_extensions = { 'drop down': 'Drop_Down_Problem', + 'multiple choice': 'Multiple_Choice_Problem', + 'checkbox': 'Checkbox_Problem', + 'string': 'String_Problem', + 'numerical': 'Numerical_Problem', + 'formula': 'Formula_Problem', } + + assert(problem_type in field_extensions) + extension = field_extensions[problem_type] + sel = "input#input_i4x-edX-model_course-problem-%s_2_1" % extension + + if choice is not None: + base = "_choice_" if problem_type == "multiple choice" else "_" + sel = sel + base + str(choice) + + return world.browser.find_by_css(sel) From a20a3a02bb4b46ce7df6f6dd27637a8c430efaaa Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 14 Mar 2013 09:15:24 -0400 Subject: [PATCH 08/13] Refactored lettuce problem test assertion that a problem is correct/incorrect/unanswered --- .../courseware/features/problems.py | 100 ++++++++++++------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 1985847bd3..686fc8c7a1 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -12,6 +12,12 @@ def view_problem(step, problem_type): @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') def answer_problem(step, problem_type, correctness): + """ Mark a given problem type correct or incorrect, then submit it. + + *problem_type* is a string representing the type of problem (e.g. 'drop down') + *correctness* is in ['correct', 'incorrect'] + """ + assert(correctness in ['correct', 'incorrect']) if problem_type == "drop down": @@ -44,6 +50,7 @@ def answer_problem(step, problem_type, correctness): textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' inputfield('formula').fill(textvalue) + # Submit the problem check_problem(step) @step(u'I check a problem') @@ -56,46 +63,70 @@ def reset_problem(step): @step(u'My "([^"]*)" answer is marked "([^"]*)"') def assert_answer_mark(step, problem_type, correctness): + """ Assert that the expected answer mark is visible for a given problem type. + + *problem_type* is a string identifying the type of problem (e.g. 'drop down') + *correctness* is in ['correct', 'incorrect', 'unanswered'] + + Asserting that a problem is marked 'unanswered' means that + the problem is NOT marked correct and NOT marked incorrect. + This can occur, for example, if the user has reset the problem. """ + + # Dictionaries that map problem types to the css selectors + # for correct/incorrect marks. + # The elements are lists of selectors because a particular problem type + # might be marked in multiple ways. + # For example, multiple choice is marked incorrect differently + # depending on whether the user selects an incorrect + # item or submits without selecting any item) + correct_selectors = { 'drop down': ['span.correct'], + 'multiple choice': ['label.choicegroup_correct'], + 'checkbox': ['span.correct'], + 'string': ['div.correct'], + 'numerical': ['div.correct'], + 'formula': ['div.correct'], } + + incorrect_selectors = { 'drop down': ['span.incorrect'], + 'multiple choice': ['label.choicegroup_incorrect', + 'span.incorrect'], + 'checkbox': ['span.incorrect'], + 'string': ['div.incorrect'], + 'numerical': ['div.incorrect'], + 'formula': ['div.incorrect'], } + assert(correctness in ['correct', 'incorrect', 'unanswered']) + assert(problem_type in correct_selectors and problem_type in incorrect_selectors) - if problem_type == "multiple choice": - if correctness == 'unanswered': - mark_classes = ['label.choicegroup_correct', 'label.choicegroup_incorrect', - 'span.correct', 'span.incorrect'] - for css in mark_classes: - assert(world.browser.is_element_not_present_by_css(css)) - - else: - if correctness == 'correct': - mark_class = '.choicegroup_correct' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + # Assert that the question has the expected mark + # (either correct or incorrect) + if correctness in ["correct", "incorrect"]: - else: - # Two ways to be marked incorrect: either applying a - # class to the label (marking a particular option) - # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['label.choicegroup_incorrect', 'span.incorrect'] - assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or - world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + selector_dict = correct_selectors if correctness == "correct" else incorrect_selectors - elif problem_type in ["string", "numerical", "formula"]: - if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('div.correct')) - assert(world.browser.is_element_not_present_by_css('div.incorrect')) - else: - mark_class = 'div.correct' if correctness == 'correct' else 'div.incorrect' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + # At least one of the correct selectors should be present + for sel in selector_dict[problem_type]: + has_expected_mark = world.browser.is_element_present_by_css(sel, wait_time=4) + # As soon as we find the selector, break out of the loop + if has_expected_mark: + break + + # Expect that we found the right mark (correct or incorrect) + assert(has_expected_mark) + + # Assert that the question has neither correct nor incorrect + # because it is unanswered (possibly reset) else: - if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('span.correct')) - assert(world.browser.is_element_not_present_by_css('span.incorrect')) + # Get all the correct/incorrect selectors for this problem type + selector_list = correct_selectors[problem_type] + incorrect_selectors[problem_type] + + # Assert that none of the correct/incorrect selectors are present + for sel in selector_list: + assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) - else: - mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) def problem_url(problem_type): + """ Construct a url to a page with the given problem type """ base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' url_extensions = { 'drop down': 'Drop_Down_Problems', 'multiple choice': 'Multiple_Choice_Problems', @@ -110,6 +141,13 @@ def problem_url(problem_type): def inputfield(problem_type, choice=None): + """ Return the element for *problem_type*. + For example, if problem_type is 'string', return + the text field for the string problem in the test course. + + *choice* is the name of the checkbox input in a group + of checkboxes. """ + field_extensions = { 'drop down': 'Drop_Down_Problem', 'multiple choice': 'Multiple_Choice_Problem', 'checkbox': 'Checkbox_Problem', From 8423816076f48539a5035f3b4a011bbecd3c52ff Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 11:33:04 -0400 Subject: [PATCH 09/13] LMS contentstore lettuce tests now dynamically create courses in mongo using terrain.factories.py and capa.tests.response_xml_factory --- common/djangoapps/terrain/factories.py | 33 +++- common/djangoapps/terrain/steps.py | 28 ++-- .../capa/capa/tests/response_xml_factory.py | 8 +- lms/djangoapps/courseware/features/common.py | 77 +++++++++- .../courseware/features/courseware.feature | 11 -- .../features/high-level-tabs.feature | 33 ++-- .../courseware/features/problems.py | 142 ++++++++++++++---- .../courseware/features/registration.feature | 7 +- .../courseware/features/registration.py | 7 +- lms/envs/acceptance.py | 24 ++- 10 files changed, 272 insertions(+), 98 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/courseware.feature diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index a531f4fd26..5fa88e4b1d 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -121,21 +121,41 @@ class XModuleItemFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): """ - kwargs must include parent_location, template. Can contain display_name - target_class is ignored + Uses *kwargs*: + + *parent_location* (required): the location of the parent module + (e.g. the parent course or section) + + *template* (required): the template to create the item from + (e.g. i4x://templates/section/Empty) + + *data* (optional): the data for the item + (e.g. XML problem definition for a problem item) + + *display_name* (optional): the display name of the item + + *metadata* (optional): dictionary of metadata attributes + + *target_class* is ignored """ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] parent_location = Location(kwargs.get('parent_location')) template = Location(kwargs.get('template')) + data = kwargs.get('data') display_name = kwargs.get('display_name') + metadata = kwargs.get('metadata', {}) store = modulestore('direct') # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) - dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + # If a display name is set, use that + dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex + dest_location = parent_location._replace(category=template.category, + name=dest_name) new_item = store.clone_item(template, dest_location) @@ -143,8 +163,15 @@ class XModuleItemFactory(Factory): if display_name is not None: new_item.display_name = display_name + # Add additional metadata or override current metadata + new_item.metadata.update(metadata) + store.update_metadata(new_item.location.url(), own_metadata(new_item)) + # replace the data with the optional *data* parameter + if data is not None: + store.update_item(new_item.location, data) + if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 5917d171b9..1f90113f46 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -69,6 +69,11 @@ def the_page_title_should_be(step, title): assert_equals(world.browser.title, title) +@step(u'the page title should contain "([^"]*)"$') +def the_page_title_should_contain(step, title): + assert(title in world.browser.title) + + @step('I am a logged in user$') def i_am_logged_in_user(step): create_user('robot') @@ -80,18 +85,6 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() -@step('I am registered for a course$') -def i_am_registered_for_a_course(step): - create_user('robot') - u = User.objects.get(username='robot') - CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall') - - -@step('I am registered for course "([^"]*)"$') -def i_am_registered_for_course_by_id(step, course_id): - register_by_course_id(course_id) - - @step('I am staff for course "([^"]*)"$') def i_am_staff_for_course_by_id(step, course_id): register_by_course_id(course_id, True) @@ -139,13 +132,16 @@ def log_in(email, password): world.browser.is_element_present_by_css('header.global', 10) world.browser.click_link_by_href('#login-modal') - # wait for the login dialog to load - assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) + # Wait for the login dialog to load + # This is complicated by the fact that sometimes a second #login_form + # dialog loads, while the first one remains hidden. + # We give them both time to load, starting with the second one. + world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=2) + world.browser.is_element_present_by_css('form#login_form', wait_time=2) # For some reason, the page sometimes includes two #login_form # elements, the first of which is not visible. - # To avoid this, we always select the last of the two #login_form - # dialogs + # To avoid this, we always select the last of the two #login_form dialogs login_form = world.browser.find_by_css('form#login_form').last login_form.find_by_name('email').fill(email) diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 7aa299d20d..08ed1ca668 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -151,13 +151,11 @@ class ResponseXMLFactory(object): choice_element = etree.SubElement(group_element, "choice") choice_element.set("correct", "true" if correct_val else "false") - # Add some text describing the choice - etree.SubElement(choice_element, "startouttext") - etree.text = "Choice description" - etree.SubElement(choice_element, "endouttext") - # Add a name identifying the choice, if one exists + # For simplicity, we use the same string as both the + # name attribute and the text of the element if name: + choice_element.text = str(name) choice_element.set("name", str(name)) return group_element diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 145a56e183..4f307511df 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -5,6 +5,10 @@ from lettuce.django import django_url from django.conf import settings from django.contrib.auth.models import User from student.models import CourseEnrollment +from terrain.factories import CourseFactory, ItemFactory +from xmodule.modulestore import Location +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates import time from logging import getLogger @@ -81,17 +85,53 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_SECTION_NAME = "Problem" + +@step(u'The course "([^"]*)" exists$') +def create_course(step, course): + + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + flush_xmodule_store() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = CourseFactory.create(org=TEST_COURSE_ORG, + number=course, + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section = ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME) + + problem_section = ItemFactory.create(parent_location=section.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME) + @step(u'I am registered for the course "([^"]*)"$') -def i_am_registered_for_the_course(step, course_id): +def i_am_registered_for_the_course(step, course): + # Create the course + create_course(step, course) + + # Create the user world.create_user('robot') u = User.objects.get(username='robot') # If the user is not already enrolled, enroll the user. - if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0: - CourseEnrollment.objects.create(user=u, course_id=course_id) + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course)) world.log_in('robot@edx.org', 'test') +@step(u'The course "([^"]*)" has extra tab "([^"]*)"$') +def add_tab_to_course(step, course, extra_tab_name): + section_item = ItemFactory.create(parent_location=course_location(course), + template="i4x://edx/templates/static_tab/Empty", + display_name=str(extra_tab_name)) + @step(u'I am an edX user$') def i_am_an_edx_user(step): @@ -101,3 +141,34 @@ def i_am_an_edx_user(step): @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): world.create_user(uname) + + +def flush_xmodule_store(): + # Flush and initialize the module store + # It needs the templates because it creates new records + # by cloning from the template. + # Note that if your test module gets in some weird state + # (though it shouldn't), do this manually + # from the bash shell to drop it: + # $ mongo test_xmodule --eval "db.dropDatabase()" + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() + +def course_id(course_num): + return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, + TEST_COURSE_NAME.replace(" ", "_")) + +def course_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='course', + name=TEST_COURSE_NAME.replace(" ", "_")) + +def section_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='sequential', + name=TEST_SECTION_NAME.replace(" ", "_")) diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature deleted file mode 100644 index 14e7786fc9..0000000000 --- a/lms/djangoapps/courseware/features/courseware.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: View the Courseware Tab - As a student in an edX course - In order to work on the course - I want to view the info on the courseware tab - - Scenario: I can get to the courseware tab when logged in - Given I am registered for the course "MITx/6.002x/2013_Spring" - And I log in - And I click on View Courseware - When I click on the "Courseware" tab - Then the "Courseware" tab is active diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 354376b154..102f752e1f 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,21 +3,18 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -# Note this didn't work as a scenario outline because -# before each scenario was not flushing the database -# TODO: break this apart so that if one fails the others -# will still run - Scenario: A student can see all tabs of the course - Given I am registered for the course "MITx/6.002x/2013_Spring" - And I log in - And I click on View Courseware - When I click on the "Courseware" tab - Then the page title should be "6.002x Courseware" - When I click on the "Course Info" tab - Then the page title should be "6.002x Course Info" - When I click on the "Textbook" tab - Then the page title should be "6.002x Textbook" - When I click on the "Wiki" tab - Then the page title should be "6.002x | edX Wiki" - When I click on the "Progress" tab - Then the page title should be "6.002x Progress" +Scenario: I can navigate to all high-level tabs in a course + Given: I am registered for the course "6.002x" + And The course "6.002x" has extra tab "Custom Tab" + And I log in + And I click on View Courseware + When I click on the "" tab + Then the page title should contain "" + + Examples: + | TabName | PageTitle | + | Courseware | 6.002x Courseware | + | Course Info | 6.002x Course Info | + | Custom Tab | 6.002x Custom Tab | + | Wiki | edX Wiki | + | Progress | 6.002x Progress | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 686fc8c7a1..3af4843c3c 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,14 +2,118 @@ from lettuce import world, step from lettuce.django import django_url from selenium.webdriver.support.ui import Select import random -from common import i_am_registered_for_the_course +import textwrap +from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location +from terrain.factories import ItemFactory +from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ + ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ + StringResponseXMLFactory, NumericalResponseXMLFactory, \ + FormulaResponseXMLFactory, CustomResponseXMLFactory + +# Factories from capa.tests.response_xml_factory that we will use +# to generate the problem XML, with the keyword args used to configure +# the output. +problem_factory_dict = { + 'drop down': { + 'factory': OptionResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Option 2', + 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'], + 'correct_option': 'Option 2'}}, + + 'multiple choice': { + 'factory': MultipleChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choice 3', + 'choices': [False, False, True, False], + 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}}, + + 'checkbox': { + 'factory': ChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choices 1 and 3', + 'choice_type':'checkbox', + 'choices':[True, False, True, False, False], + 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, + + 'string': { + 'factory': StringResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is "correct string"', + 'case_sensitive': False, + 'answer': 'correct string' }}, + + 'numerical': { + 'factory': NumericalResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is pi + 1', + 'answer': '4.14159', + 'tolerance': '0.00001', + 'math_display': True }}, + + 'formula': { + 'factory': FormulaResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', + 'sample_dict': {'x': (-100, 100), 'y': (-100, 100) }, + 'num_samples': 10, + 'tolerance': 0.00001, + 'math_display': True, + 'answer': 'x^2+2*x+y'}}, + + 'script': { + 'factory': CustomResponseXMLFactory(), + 'kwargs': { + 'question_text': 'Enter two integers that sum to 10.', + 'cfn': 'test_add_to_ten', + 'expect': '10', + 'num_inputs': 2, + 'script': textwrap.dedent(""" + def test_add_to_ten(expect,ans): + try: + a1=int(ans[0]) + a2=int(ans[1]) + except ValueError: + a1=0 + a2=0 + return (a1+a2)==int(expect) + """) }}, + } + +def add_problem_to_course(course, problem_type): + + assert(problem_type in problem_factory_dict) + + # Generate the problem XML using capa.tests.response_xml_factory + factory_dict = problem_factory_dict[problem_type] + problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) + + # Create a problem item using our generated XML + # We set rerandomize=always in the metadata so that the "Reset" button + # will appear. + problem_item = ItemFactory.create(parent_location=section_location(course), + template="i4x://edx/templates/problem/Blank_Common_Problem", + display_name=str(problem_type), + data=problem_xml, + metadata={'rerandomize':'always'}) @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): - i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') - url = django_url(problem_url(problem_type)) + i_am_registered_for_the_course(step, 'model_course') + + # Ensure that the course has this problem type + add_problem_to_course('model_course', problem_type) + + # Go to the one section in the factory-created course + # which should be loaded with the correct problem + chapter_name = TEST_SECTION_NAME.replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + world.browser.visit(url) + @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') def answer_problem(step, problem_type, correctness): """ Mark a given problem type correct or incorrect, then submit it. @@ -21,7 +125,7 @@ def answer_problem(step, problem_type, correctness): assert(correctness in ['correct', 'incorrect']) if problem_type == "drop down": - select_name = "input_i4x-edX-model_course-problem-Drop_Down_Problem_2_1" + select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" option_text = 'Option 2' if correctness == 'correct' else 'Option 3' world.browser.select(select_name, option_text) @@ -125,21 +229,6 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) -def problem_url(problem_type): - """ Construct a url to a page with the given problem type """ - base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' - url_extensions = { 'drop down': 'Drop_Down_Problems', - 'multiple choice': 'Multiple_Choice_Problems', - 'checkbox': 'Checkbox_Problems', - 'string': 'String_Problems', - 'numerical': 'Numerical_Problems', - 'formula': 'Formula_Problems', } - - assert(problem_type in url_extensions) - return base + url_extensions[problem_type] - - - def inputfield(problem_type, choice=None): """ Return the element for *problem_type*. For example, if problem_type is 'string', return @@ -148,19 +237,14 @@ def inputfield(problem_type, choice=None): *choice* is the name of the checkbox input in a group of checkboxes. """ - field_extensions = { 'drop down': 'Drop_Down_Problem', - 'multiple choice': 'Multiple_Choice_Problem', - 'checkbox': 'Checkbox_Problem', - 'string': 'String_Problem', - 'numerical': 'Numerical_Problem', - 'formula': 'Formula_Problem', } - - assert(problem_type in field_extensions) - extension = field_extensions[problem_type] - sel = "input#input_i4x-edX-model_course-problem-%s_2_1" % extension + sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_") if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" sel = sel + base + str(choice) + # If the input element doesn't exist, fail immediately + assert(world.browser.is_element_present_by_css(sel, wait_time=4)) + + # Retrieve the input element return world.browser.find_by_css(sel) diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature index 890beec1d8..5933f860bb 100644 --- a/lms/djangoapps/courseware/features/registration.feature +++ b/lms/djangoapps/courseware/features/registration.feature @@ -4,13 +4,14 @@ Feature: Register for a course I want to register for a class on the edX website Scenario: I can register for a course - Given I am logged in + Given The course "6.002x" exists + And I am logged in And I visit the courses page - When I register for the course "MITx/6.002x/2013_Spring" + When I register for the course "6.002x" Then I should see the course numbered "6.002x" in my dashboard Scenario: I can unregister for a course - Given I am registered for the course "MITx/6.002x/2013_Spring" + Given I am registered for the course "6.002x" And I visit the dashboard When I click the link with the text "Unregister" And I press the "Unregister" button in the Unenroll dialog diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 5535319f15..9587842dd6 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,9 +1,12 @@ from lettuce import world, step from lettuce.django import django_url +from common import TEST_COURSE_ORG, TEST_COURSE_NAME @step('I register for the course "([^"]*)"$') -def i_register_for_the_course(step, course_id): - world.browser.visit(django_url('courses/%s/about' % course_id)) +def i_register_for_the_course(step, course): + cleaned_name = TEST_COURSE_NAME.replace(' ', '_') + url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name)) + world.browser.visit(url) intro_section = world.browser.find_by_css('section.intro') register_link = intro_section.find_by_css('a.register') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index b6941f4a70..3dac545367 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -8,16 +8,24 @@ from .test import * # otherwise the browser will not render the pages correctly DEBUG = True -# Show the courses that are in the data directory -COURSES_ROOT = ENV_ROOT / "data" -DATA_DIR = COURSES_ROOT +# Use the mongo store for acceptance tests +modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore', + 'fs_root': GITHUB_REPO_ROOT, + 'render_template': 'mitxmako.shortcuts.render_to_string', +} + MODULESTORE = { 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': DATA_DIR, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options } } From e41bb8462cc1796ad07eb5fbc24381665b9701f4 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 12:13:07 -0400 Subject: [PATCH 10/13] Added lettuce tests for script (customresponse) problems. Increased wait time for login screen to reduce false positives. --- common/djangoapps/terrain/steps.py | 2 +- .../courseware/features/problems.feature | 5 ++++ .../courseware/features/problems.py | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 1f90113f46..50fe0faf39 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -136,7 +136,7 @@ def log_in(email, password): # This is complicated by the fact that sometimes a second #login_form # dialog loads, while the first one remains hidden. # We give them both time to load, starting with the second one. - world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=2) + world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=4) world.browser.is_element_present_by_css('form#login_form', wait_time=2) # For some reason, the page sometimes includes two #login_form diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 12458537d0..8828ebc699 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -16,6 +16,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -30,6 +31,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -44,6 +46,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can reset a problem @@ -66,3 +69,5 @@ Feature: Answer choice problems | numerical | incorrect | | formula | correct | | formula | incorrect | + | script | correct | + | script | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 3af4843c3c..0b5ecbe20a 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -154,6 +154,19 @@ def answer_problem(step, problem_type, correctness): textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' inputfield('formula').fill(textvalue) + elif problem_type == 'script': + # Correct answer is any two integers that sum to 10 + first_addend = random.randint(-100, 100) + second_addend = 10 - first_addend + + # If we want an incorrect answer, then change + # the second addend so they no longer sum to 10 + if correctness == 'incorrect': + second_addend += random.randint(1, 10) + + inputfield('script', input_num=1).fill(str(first_addend)) + inputfield('script', input_num=2).fill(str(second_addend)) + # Submit the problem check_problem(step) @@ -188,7 +201,8 @@ def assert_answer_mark(step, problem_type, correctness): 'checkbox': ['span.correct'], 'string': ['div.correct'], 'numerical': ['div.correct'], - 'formula': ['div.correct'], } + 'formula': ['div.correct'], + 'script': ['div.correct'], } incorrect_selectors = { 'drop down': ['span.incorrect'], 'multiple choice': ['label.choicegroup_incorrect', @@ -196,7 +210,8 @@ def assert_answer_mark(step, problem_type, correctness): 'checkbox': ['span.incorrect'], 'string': ['div.incorrect'], 'numerical': ['div.incorrect'], - 'formula': ['div.incorrect'], } + 'formula': ['div.incorrect'], + 'script': ['div.incorrect'] } assert(correctness in ['correct', 'incorrect', 'unanswered']) assert(problem_type in correct_selectors and problem_type in incorrect_selectors) @@ -229,7 +244,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) -def inputfield(problem_type, choice=None): +def inputfield(problem_type, choice=None, input_num=1): """ Return the element for *problem_type*. For example, if problem_type is 'string', return the text field for the string problem in the test course. @@ -237,7 +252,8 @@ def inputfield(problem_type, choice=None): *choice* is the name of the checkbox input in a group of checkboxes. """ - sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_") + sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % + (problem_type.replace(" ", "_"), str(input_num))) if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" From 3a13cd7b34b35a50feba2dd4d5c55944b687263c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 15:09:39 -0400 Subject: [PATCH 11/13] Merged changes in factories.py with version in master --- common/djangoapps/terrain/factories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 5fa88e4b1d..5ea34d1190 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -164,9 +164,9 @@ class XModuleItemFactory(Factory): new_item.display_name = display_name # Add additional metadata or override current metadata - new_item.metadata.update(metadata) - - store.update_metadata(new_item.location.url(), own_metadata(new_item)) + item_metadata = own_metadata(new_item) + item_metadata.update(metadata) + store.update_metadata(new_item.location.url(), item_metadata) # replace the data with the optional *data* parameter if data is not None: From 5fd1e7426d9fdd46bbbb209d5e04d9ffafc2def6 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 16:17:45 -0400 Subject: [PATCH 12/13] Lettuce tests now import one_time_startup.py to ensure that mongo caches are initialized for the test database. This avoids a warning from the mongo modulestore. --- common/djangoapps/terrain/browser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0881d86124..6a60802d07 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -3,6 +3,11 @@ from splinter.browser import Browser from logging import getLogger import time +# Let the LMS and CMS do their one-time setup +# For example, setting up mongo caches +from lms import one_time_startup +from cms import one_time_startup + logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") @@ -16,7 +21,6 @@ def initial_setup(server): # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') - @before.each_scenario def reset_data(scenario): # Clean out the django test database defined in the From 568f557dfc5663dd0a33cd80bae2b0f6abecf81c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 18 Mar 2013 13:50:25 -0400 Subject: [PATCH 13/13] Pep8 fixes Changed constant to uppercase --- common/djangoapps/terrain/browser.py | 3 +- common/djangoapps/terrain/factories.py | 2 +- common/djangoapps/terrain/steps.py | 1 + .../capa/capa/tests/response_xml_factory.py | 66 +++++++++++-------- lms/djangoapps/courseware/features/common.py | 10 ++- lms/djangoapps/courseware/features/courses.py | 1 + .../features/high-level-tabs.feature | 2 +- .../courseware/features/homepage.feature | 4 +- lms/djangoapps/courseware/features/login.py | 1 + .../courseware/features/openended.feature | 4 +- .../courseware/features/problems.feature | 2 +- .../courseware/features/problems.py | 53 ++++++++------- .../courseware/features/registration.py | 1 + .../features/smart-accordion.feature | 2 +- 14 files changed, 88 insertions(+), 64 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 6a60802d07..6394959532 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -6,7 +6,7 @@ import time # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches from lms import one_time_startup -from cms import one_time_startup +from cms import one_time_startup logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") @@ -21,6 +21,7 @@ def initial_setup(server): # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') + @before.each_scenario def reset_data(scenario): # Clean out the django test database defined in the diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 5ea34d1190..c36bf935f1 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -154,7 +154,7 @@ class XModuleItemFactory(Factory): # If a display name is set, use that dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex - dest_location = parent_location._replace(category=template.category, + dest_location = parent_location._replace(category=template.category, name=dest_name) new_item = store.clone_item(template, dest_location) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 50fe0faf39..52eeb23c4a 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -101,6 +101,7 @@ def i_am_an_edx_user(step): #### helper functions + @world.absorb def scroll_to_bottom(): # Maximize the browser diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 08ed1ca668..aa401b70cd 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -1,6 +1,7 @@ from lxml import etree from abc import ABCMeta, abstractmethod + class ResponseXMLFactory(object): """ Abstract base class for capa response XML factories. Subclasses override create_response_element and @@ -13,7 +14,7 @@ class ResponseXMLFactory(object): """ Subclasses override to return an etree element representing the capa response XML (e.g. ). - + The tree should NOT contain any input elements (such as ) as these will be added later.""" return None @@ -25,7 +26,7 @@ class ResponseXMLFactory(object): return None def build_xml(self, **kwargs): - """ Construct an XML string for a capa response + """ Construct an XML string for a capa response based on **kwargs. **kwargs is a dictionary that will be passed @@ -37,7 +38,7 @@ class ResponseXMLFactory(object): *question_text*: The text of the question to display, wrapped in

tags. - + *explanation_text*: The detailed explanation that will be shown if the user answers incorrectly. @@ -75,7 +76,7 @@ class ResponseXMLFactory(object): for i in range(0, int(num_responses)): response_element = self.create_response_element(**kwargs) root.append(response_element) - + # Add input elements for j in range(0, int(num_inputs)): input_element = self.create_input_element(**kwargs) @@ -135,7 +136,7 @@ class ResponseXMLFactory(object): # Names of group elements group_element_names = {'checkbox': 'checkboxgroup', 'radio': 'radiogroup', - 'multiple': 'choicegroup' } + 'multiple': 'choicegroup'} # Retrieve **kwargs choices = kwargs.get('choices', [True]) @@ -215,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): *answer*: Inline script that calculates the answer """ - + # Retrieve **kwargs cfn = kwargs.get('cfn', None) expect = kwargs.get('expect', None) @@ -245,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create the XML element. - + Uses *kwargs*: *answer*: The Python script used to evaluate the answer. @@ -272,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): For testing, we create a bare-bones version of .""" return etree.Element("schematic") + class CodeResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -284,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element: - + Uses **kwargs: - + *initial_display*: The code that initially appears in the textbox [DEFAULT: "Enter code here"] *answer_display*: The answer to display to the student @@ -326,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory): # return None here return None + class ChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -354,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): *num_samples*: The number of times to sample the student's answer to numerically compare it to the correct answer. - + *tolerance*: The tolerance within which answers will be accepted - [DEFAULT: 0.01] + [DEFAULT: 0.01] *answer*: The answer to the problem. Can be a formula string - or a Python variable defined in a script - (e.g. "$calculated_answer" for a Python variable + or a Python variable defined in a script + (e.g. "$calculated_answer" for a Python variable called calculated_answer) [REQUIRED] @@ -385,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # Set the sample information sample_str = self._sample_str(sample_dict, num_samples, tolerance) response_element.set("samples", sample_str) - + # Set the tolerance responseparam_element = etree.SubElement(response_element, "responseparam") @@ -406,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # We could sample a different range, but for simplicity, # we use the same sample string for the hints - # that we used previously. + # that we used previously. formulahint_element.set("samples", sample_str) formulahint_element.set("answer", str(hint_prompt)) @@ -434,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): high_range_vals = [str(f[1]) for f in sample_dict.values()] sample_str = (",".join(sample_dict.keys()) + "@" + ",".join(low_range_vals) + ":" + - ",".join(high_range_vals) + + ",".join(high_range_vals) + "#" + str(num_samples)) return sample_str + class ImageResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -448,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): """ Create the element. - + Uses **kwargs: - + *src*: URL for the image file [DEFAULT: "/static/image.jpg"] *width*: Width of the image [DEFAULT: 100] @@ -488,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): input_element.set("src", str(src)) input_element.set("width", str(width)) input_element.set("height", str(height)) - + if rectangle: input_element.set("rectangle", rectangle) @@ -497,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): return input_element + class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -520,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): # Both display_src and display_class given, # or neither given - assert((display_src and display_class) or + assert((display_src and display_class) or (not display_src and not display_class)) # Create the element @@ -550,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Create the element """ return etree.Element("javascriptinput") + class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -562,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class TrueFalseResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -574,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class OptionResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML""" @@ -618,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element. - + Uses **kwargs: *answer*: The correct answer (a string) [REQUIRED] @@ -640,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): # Create the element response_element = etree.Element("stringresponse") - # Set the answer attribute + # Set the answer attribute response_element.set("answer", str(answer)) # Set the case sensitivity @@ -665,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): return ResponseXMLFactory.textline_input_xml(**kwargs) + class AnnotationResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ def create_response_element(self, **kwargs): @@ -677,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): input_element = etree.Element("annotationinput") text_children = [ - {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') }, - {'tag': 'text', 'text': kwargs.get('text', 'texty text') }, - {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') }, - {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') }, - {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') } + {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')}, + {'tag': 'text', 'text': kwargs.get('text', 'texty text')}, + {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah')}, + {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')}, + {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')} ] for child in text_children: etree.SubElement(input_element, child['tag']).text = child['text'] - default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')] + default_options = [('green', 'correct'),('eggs', 'incorrect'), ('ham', 'partially-correct')] options = kwargs.get('options', default_options) options_element = etree.SubElement(input_element, 'options') @@ -696,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): option_element.text = description return input_element - diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 4f307511df..8fb2843656 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -89,6 +89,7 @@ TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = "Problem" + @step(u'The course "([^"]*)" exists$') def create_course(step, course): @@ -100,7 +101,7 @@ def create_course(step, course): # Create the course # We always use the same org and display name, # but vary the course identifier (e.g. 600x or 191x) - course = CourseFactory.create(org=TEST_COURSE_ORG, + course = CourseFactory.create(org=TEST_COURSE_ORG, number=course, display_name=TEST_COURSE_NAME) @@ -112,6 +113,7 @@ def create_course(step, course): template='i4x://edx/templates/sequential/Empty', display_name=TEST_SECTION_NAME) + @step(u'I am registered for the course "([^"]*)"$') def i_am_registered_for_the_course(step, course): # Create the course @@ -126,6 +128,7 @@ def i_am_registered_for_the_course(step, course): world.log_in('robot@edx.org', 'test') + @step(u'The course "([^"]*)" has extra tab "([^"]*)"$') def add_tab_to_course(step, course, extra_tab_name): section_item = ItemFactory.create(parent_location=course_location(course), @@ -155,10 +158,12 @@ def flush_xmodule_store(): modulestore().collection.drop() update_templates() + def course_id(course_num): - return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, + return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, TEST_COURSE_NAME.replace(" ", "_")) + def course_location(course_num): return Location(loc_or_tag="i4x", org=TEST_COURSE_ORG, @@ -166,6 +171,7 @@ def course_location(course_num): category='course', name=TEST_COURSE_NAME.replace(" ", "_")) + def section_location(course_num): return Location(loc_or_tag="i4x", org=TEST_COURSE_ORG, diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py index eb5143b782..4fbbfd24f2 100644 --- a/lms/djangoapps/courseware/features/courses.py +++ b/lms/djangoapps/courseware/features/courses.py @@ -9,6 +9,7 @@ logger = getLogger(__name__) ## support functions + def get_courses(): ''' Returns dict of lists of courses available, keyed by course.org (ie university). diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 102f752e1f..931281a455 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,7 +3,7 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -Scenario: I can navigate to all high-level tabs in a course +Scenario: I can navigate to all high -level tabs in a course Given: I am registered for the course "6.002x" And The course "6.002x" has extra tab "Custom Tab" And I log in diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature index 06a45c4bfa..c0c1c32f02 100644 --- a/lms/djangoapps/courseware/features/homepage.feature +++ b/lms/djangoapps/courseware/features/homepage.feature @@ -39,9 +39,9 @@ Feature: Homepage for web users | MITx | | HarvardX | | BerkeleyX | - | UTx | + | UTx | | WellesleyX | - | GeorgetownX | + | GeorgetownX | # # TODO: Add scenario that tests the courses available # # using a policy or a configuration file diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index ca7d710c61..094db078ca 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -34,6 +34,7 @@ def click_the_dropdown(step): #### helper functions + def user_is_an_unactivated_user(uname): u = User.objects.get(username=uname) u.is_active = False diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature index cc9f6e1c5f..1ab496144f 100644 --- a/lms/djangoapps/courseware/features/openended.feature +++ b/lms/djangoapps/courseware/features/openended.feature @@ -3,10 +3,10 @@ Feature: Open ended grading In order to complete the courseware questions I want the machine learning grading to be functional - # Commenting these all out right now until we can + # Commenting these all out right now until we can # make a reference implementation for a course with # an open ended grading problem that is always available - # + # # Scenario: An answer that is too short is rejected # Given I navigate to an openended question # And I enter the answer "z" diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 8828ebc699..a7fbac49c7 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -35,7 +35,7 @@ Feature: Answer choice problems Scenario: I can submit a blank answer Given I am viewing a "" problem - When I check a problem + When I check a problem Then My "" answer is marked "incorrect" Examples: diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 0b5ecbe20a..a6575c3d22 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -13,7 +13,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ # Factories from capa.tests.response_xml_factory that we will use # to generate the problem XML, with the keyword args used to configure # the output. -problem_factory_dict = { +PROBLEM_FACTORY_DICT = { 'drop down': { 'factory': OptionResponseXMLFactory(), 'kwargs': { @@ -32,8 +32,8 @@ problem_factory_dict = { 'factory': ChoiceResponseXMLFactory(), 'kwargs': { 'question_text': 'The correct answer is Choices 1 and 3', - 'choice_type':'checkbox', - 'choices':[True, False, True, False, False], + 'choice_type': 'checkbox', + 'choices': [True, False, True, False, False], 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, 'string': { @@ -41,7 +41,7 @@ problem_factory_dict = { 'kwargs': { 'question_text': 'The answer is "correct string"', 'case_sensitive': False, - 'answer': 'correct string' }}, + 'answer': 'correct string'}}, 'numerical': { 'factory': NumericalResponseXMLFactory(), @@ -49,13 +49,13 @@ problem_factory_dict = { 'question_text': 'The answer is pi + 1', 'answer': '4.14159', 'tolerance': '0.00001', - 'math_display': True }}, + 'math_display': True}}, 'formula': { 'factory': FormulaResponseXMLFactory(), 'kwargs': { 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', - 'sample_dict': {'x': (-100, 100), 'y': (-100, 100) }, + 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)}, 'num_samples': 10, 'tolerance': 0.00001, 'math_display': True, @@ -77,15 +77,16 @@ problem_factory_dict = { a1=0 a2=0 return (a1+a2)==int(expect) - """) }}, + """)}}, } + def add_problem_to_course(course, problem_type): - assert(problem_type in problem_factory_dict) + assert(problem_type in PROBLEM_FACTORY_DICT) # Generate the problem XML using capa.tests.response_xml_factory - factory_dict = problem_factory_dict[problem_type] + factory_dict = PROBLEM_FACTORY_DICT[problem_type] problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) # Create a problem item using our generated XML @@ -95,7 +96,8 @@ def add_problem_to_course(course, problem_type): template="i4x://edx/templates/problem/Blank_Common_Problem", display_name=str(problem_type), data=problem_xml, - metadata={'rerandomize':'always'}) + metadata={'rerandomize': 'always'}) + @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -108,9 +110,9 @@ def view_problem(step, problem_type): # which should be loaded with the correct problem chapter_name = TEST_SECTION_NAME.replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % (chapter_name, section_name)) - + world.browser.visit(url) @@ -147,7 +149,7 @@ def answer_problem(step, problem_type, correctness): inputfield('string').fill(textvalue) elif problem_type == 'numerical': - textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) + textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) inputfield('numerical').fill(textvalue) elif problem_type == 'formula': @@ -170,14 +172,17 @@ def answer_problem(step, problem_type, correctness): # Submit the problem check_problem(step) + @step(u'I check a problem') def check_problem(step): world.browser.find_by_css("input.check").click() + @step(u'I reset the problem') def reset_problem(step): world.browser.find_by_css('input.reset').click() + @step(u'My "([^"]*)" answer is marked "([^"]*)"') def assert_answer_mark(step, problem_type, correctness): """ Assert that the expected answer mark is visible for a given problem type. @@ -190,28 +195,28 @@ def assert_answer_mark(step, problem_type, correctness): This can occur, for example, if the user has reset the problem. """ # Dictionaries that map problem types to the css selectors - # for correct/incorrect marks. + # for correct/incorrect marks. # The elements are lists of selectors because a particular problem type - # might be marked in multiple ways. - # For example, multiple choice is marked incorrect differently - # depending on whether the user selects an incorrect + # might be marked in multiple ways. + # For example, multiple choice is marked incorrect differently + # depending on whether the user selects an incorrect # item or submits without selecting any item) - correct_selectors = { 'drop down': ['span.correct'], + correct_selectors = {'drop down': ['span.correct'], 'multiple choice': ['label.choicegroup_correct'], 'checkbox': ['span.correct'], 'string': ['div.correct'], 'numerical': ['div.correct'], - 'formula': ['div.correct'], + 'formula': ['div.correct'], 'script': ['div.correct'], } - incorrect_selectors = { 'drop down': ['span.incorrect'], - 'multiple choice': ['label.choicegroup_incorrect', + incorrect_selectors = {'drop down': ['span.incorrect'], + 'multiple choice': ['label.choicegroup_incorrect', 'span.incorrect'], 'checkbox': ['span.incorrect'], 'string': ['div.incorrect'], 'numerical': ['div.incorrect'], - 'formula': ['div.incorrect'], - 'script': ['div.incorrect'] } + 'formula': ['div.incorrect'], + 'script': ['div.incorrect']} assert(correctness in ['correct', 'incorrect', 'unanswered']) assert(problem_type in correct_selectors and problem_type in incorrect_selectors) @@ -252,7 +257,7 @@ def inputfield(problem_type, choice=None, input_num=1): *choice* is the name of the checkbox input in a group of checkboxes. """ - sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % + sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % (problem_type.replace(" ", "_"), str(input_num))) if choice is not None: diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 9587842dd6..94b9b50f6c 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -2,6 +2,7 @@ from lettuce import world, step from lettuce.django import django_url from common import TEST_COURSE_ORG, TEST_COURSE_NAME + @step('I register for the course "([^"]*)"$') def i_register_for_the_course(step, course): cleaned_name = TEST_COURSE_NAME.replace(' ', '_') diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature index ccf1d45601..fc51eca25d 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.feature +++ b/lms/djangoapps/courseware/features/smart-accordion.feature @@ -60,4 +60,4 @@ Feature: There are courses on the homepage # Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall # Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall" # And I log in - # Then I verify all the content of each course \ No newline at end of file + # Then I verify all the content of each course