diff --git a/.gitignore b/.gitignore index 3b7223108b..2fd1ca0181 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ lms/lib/comment_client/python nosetests.xml cover_html/ .idea/ +chromedriver.log \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py new file mode 100644 index 0000000000..1f14e29083 --- /dev/null +++ b/cms/djangoapps/contentstore/features/common.py @@ -0,0 +1,127 @@ +from lettuce import world, step +from factories import * +from django.core.management import call_command +from lettuce.django import django_url +from django.conf import settings +from django.core.management import call_command +from nose.tools import assert_true +from nose.tools import assert_equal +import xmodule.modulestore.django + +from logging import getLogger +logger = getLogger(__name__) + +########### STEP HELPERS ############## +@step('I (?:visit|access|open) the Studio homepage$') +def i_visit_the_studio_homepage(step): + # To make this go to port 8001, put + # LETTUCE_SERVER_PORT = 8001 + # in your settings.py file. + world.browser.visit(django_url('/')) + assert world.browser.is_element_present_by_css('body.no-header', 10) + +@step('I am logged into Studio$') +def i_am_logged_into_studio(step): + log_into_studio() + +@step('I confirm the alert$') +def i_confirm_with_ok(step): + world.browser.get_alert().accept() + +@step(u'I press the "([^"]*)" delete icon$') +def i_press_the_category_delete_icon(step, category): + if category == 'section': + css = 'a.delete-button.delete-section-button span.delete-icon' + elif category == 'subsection': + css='a.delete-button.delete-subsection-button span.delete-icon' + else: + assert False, 'Invalid category: %s' % category + css_click(css) + +####### HELPER FUNCTIONS ############## +def create_studio_user( + uname='robot', + em='robot+studio@edx.org', + password='test'): + studio_user = UserFactory.build( + username=uname, + email=em) + studio_user.set_password(password) + studio_user.save() + + registration = RegistrationFactory(user=studio_user) + registration.register(studio_user) + registration.activate() + + user_profile = UserProfileFactory(user=studio_user) + +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()" + xmodule.modulestore.django._MODULESTORES = {} + xmodule.modulestore.django.modulestore().collection.drop() + xmodule.templates.update_templates() + +def assert_css_with_text(css,text): + assert_true(world.browser.is_element_present_by_css(css, 5)) + assert_equal(world.browser.find_by_css(css).text, text) + +def css_click(css): + world.browser.find_by_css(css).first.click() + +def css_fill(css, value): + world.browser.find_by_css(css).first.fill(value) + +def clear_courses(): + flush_xmodule_store() + +def fill_in_course_info( + name='Robot Super Course', + org='MITx', + num='101'): + css_fill('.new-course-name',name) + css_fill('.new-course-org',org) + css_fill('.new-course-number',num) + +def log_into_studio( + uname='robot', + email='robot+studio@edx.org', + password='test'): + create_studio_user(uname, email) + world.browser.cookies.delete() + world.browser.visit(django_url('/')) + world.browser.is_element_present_by_css('body.no-header', 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('submit').click() + + assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + +def create_a_course(): + css_click('a.new-course-button') + fill_in_course_info() + css_click('input.new-course-save') + assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5)) + +def add_section(name='My Section'): + link_css = 'a.new-courseware-section-button' + css_click(link_css) + name_css = '.new-section-name' + save_css = '.new-section-name-save' + css_fill(name_css,name) + css_click(save_css) + +def add_subsection(name='Subsection One'): + css = 'a.new-subsection-item' + css_click(css) + name_css = 'input.new-subsection-name-input' + save_css = 'input.new-subsection-name-save' + css_fill(name_css, name) + css_click(save_css) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature new file mode 100644 index 0000000000..39d39b50aa --- /dev/null +++ b/cms/djangoapps/contentstore/features/courses.feature @@ -0,0 +1,13 @@ +Feature: Create Course + In order offer a course on the edX platform + As a course author + I want to create courses + + Scenario: Create a course + Given There are no courses + And I am logged into Studio + When I click the New Course button + And I fill in the new course information + And I press the "Save" button + Then the Courseware page has loaded in Studio + And I see a link for adding a new section \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py new file mode 100644 index 0000000000..2c1cf6281a --- /dev/null +++ b/cms/djangoapps/contentstore/features/courses.py @@ -0,0 +1,50 @@ +from lettuce import world, step +from common import * + +############### ACTIONS #################### +@step('There are no courses$') +def no_courses(step): + clear_courses() + +@step('I click the New Course button$') +def i_click_new_course(step): + css_click('.new-course-button') + +@step('I fill in the new course information$') +def i_fill_in_a_new_course_information(step): + fill_in_course_info() + +@step('I create a new course$') +def i_create_a_course(step): + create_a_course() + +@step('I click the course link in My Courses$') +def i_click_the_course_link_in_my_courses(step): + course_css = 'span.class-name' + css_click(course_css) + +############ ASSERTIONS ################### +@step('the Courseware page has loaded in Studio$') +def courseware_page_has_loaded_in_studio(step): + courseware_css = 'a#courseware-tab' + assert world.browser.is_element_present_by_css(courseware_css) + +@step('I see the course listed in My Courses$') +def i_see_the_course_in_my_courses(step): + course_css = 'span.class-name' + assert_css_with_text(course_css,'Robot Super Course') + +@step('the course is loaded$') +def course_is_loaded(step): + class_css = 'a.class-name' + assert_css_with_text(class_css,'Robot Super Course') + +@step('I am on the "([^"]*)" tab$') +def i_am_on_tab(step, tab_name): + header_css = 'div.inner-wrapper h1' + assert_css_with_text(header_css,tab_name) + +@step('I see a link for adding a new section$') +def i_see_new_section_link(step): + link_css = 'a.new-courseware-section-button' + assert_css_with_text(link_css,'New Section') diff --git a/cms/djangoapps/contentstore/features/factories.py b/cms/djangoapps/contentstore/features/factories.py new file mode 100644 index 0000000000..389f2bac49 --- /dev/null +++ b/cms/djangoapps/contentstore/features/factories.py @@ -0,0 +1,31 @@ +import factory +from student.models import User, UserProfile, Registration +from datetime import datetime +import uuid + +class UserProfileFactory(factory.Factory): + FACTORY_FOR = UserProfile + + user = None + name = 'Robot Studio' + courseware = 'course.xml' + +class RegistrationFactory(factory.Factory): + FACTORY_FOR = Registration + + user = None + activation_key = uuid.uuid4().hex + +class UserFactory(factory.Factory): + FACTORY_FOR = User + + username = 'robot-studio' + email = 'robot+studio@edx.org' + password = 'test' + first_name = 'Robot' + last_name = 'Studio' + is_staff = False + is_active = True + is_superuser = False + last_login = datetime.now() + date_joined = datetime.now() \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature new file mode 100644 index 0000000000..ad00ba2911 --- /dev/null +++ b/cms/djangoapps/contentstore/features/section.feature @@ -0,0 +1,26 @@ +Feature: Create Section + In order offer a course on the edX platform + As a course author + I want to create and edit sections + + Scenario: Add a new section to a course + Given I have opened a new course in Studio + When I click the New Section link + And I enter the section name and click save + Then I see my section on the Courseware page + And I see a release date for my section + And I see a link to create a new subsection + + Scenario: Edit section release date + Given I have opened a new course in Studio + And I have added a new section + When I click the Edit link for the release date + And I save a new section release date + Then the section release date is updated + + Scenario: Delete section + Given I have opened a new course in Studio + And I have added a new section + When I press the "section" delete icon + And I confirm the alert + Then the section does not exist \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py new file mode 100644 index 0000000000..8ac30e2170 --- /dev/null +++ b/cms/djangoapps/contentstore/features/section.py @@ -0,0 +1,82 @@ +from lettuce import world, step +from common import * + +############### ACTIONS #################### +@step('I have opened a new course in Studio$') +def i_have_opened_a_new_course(step): + clear_courses() + log_into_studio() + create_a_course() + +@step('I click the new section link$') +def i_click_new_section_link(step): + link_css = 'a.new-courseware-section-button' + css_click(link_css) + +@step('I enter the section name and click save$') +def i_save_section_name(step): + name_css = '.new-section-name' + save_css = '.new-section-name-save' + css_fill(name_css,'My Section') + css_click(save_css) + +@step('I have added a new section$') +def i_have_added_new_section(step): + add_section() + +@step('I click the Edit link for the release date$') +def i_click_the_edit_link_for_the_release_date(step): + button_css = 'div.section-published-date a.edit-button' + css_click(button_css) + +@step('I save a new section release date$') +def i_save_a_new_section_release_date(step): + date_css = 'input.start-date.date.hasDatepicker' + time_css = 'input.start-time.time.ui-timepicker-input' + css_fill(date_css,'12/25/2013') + # click here to make the calendar go away + css_click(time_css) + css_fill(time_css,'12:00am') + css_click('a.save-button') + +############ ASSERTIONS ################### +@step('I see my section on the Courseware page$') +def i_see_my_section_on_the_courseware_page(step): + section_css = 'span.section-name-span' + assert_css_with_text(section_css,'My Section') + +@step('the section does not exist$') +def section_does_not_exist(step): + css = 'span.section-name-span' + assert world.browser.is_element_not_present_by_css(css) + +@step('I see a release date for my section$') +def i_see_a_release_date_for_my_section(step): + import re + + css = 'span.published-status' + assert world.browser.is_element_present_by_css(css) + status_text = world.browser.find_by_css(css).text + + # e.g. 11/06/2012 at 16:25 + msg = 'Will Release:' + date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]' + time_regex = '[0-2][0-9]:[0-5][0-9]' + match_string = '%s %s at %s' % (msg, date_regex, time_regex) + assert re.match(match_string,status_text) + +@step('I see a link to create a new subsection$') +def i_see_a_link_to_create_a_new_subsection(step): + css = 'a.new-subsection-item' + assert world.browser.is_element_present_by_css(css) + +@step('the section release date picker is not visible$') +def the_section_release_date_picker_not_visible(step): + css = 'div.edit-subsection-publish-settings' + assert False, world.browser.find_by_css(css).visible + +@step('the section release date is updated$') +def the_section_release_date_is_updated(step): + css = 'span.published-status' + status_text = world.browser.find_by_css(css).text + assert status_text == 'Will Release: 12/25/2013 at 12:00am' diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature new file mode 100644 index 0000000000..8a6f93d33b --- /dev/null +++ b/cms/djangoapps/contentstore/features/signup.feature @@ -0,0 +1,12 @@ +Feature: Sign in + In order to use the edX content + As a new user + I want to signup for a student account + + Scenario: Sign up from the homepage + Given I visit the Studio homepage + When I click the link with the text "Sign up" + And I fill in the registration form + And I press the "Create My Account" button on the registration form + Then I should see be on the studio home page + And I should see the message "please click on the activation link in your email." \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py new file mode 100644 index 0000000000..7794511f94 --- /dev/null +++ b/cms/djangoapps/contentstore/features/signup.py @@ -0,0 +1,23 @@ +from lettuce import world, step + +@step('I fill in the registration form$') +def i_fill_in_the_registration_form(step): + register_form = world.browser.find_by_css('form#register_form') + register_form.find_by_name('email').fill('robot+studio@edx.org') + register_form.find_by_name('password').fill('test') + register_form.find_by_name('username').fill('robot-studio') + register_form.find_by_name('name').fill('Robot Studio') + register_form.find_by_name('terms_of_service').check() + +@step('I press the "([^"]*)" button on the registration form$') +def i_press_the_button_on_the_registration_form(step, button): + register_form = world.browser.find_by_css('form#register_form') + register_form.find_by_value(button).click() + +@step('I should see be on the studio home page$') +def i_should_see_be_on_the_studio_home_page(step): + assert world.browser.find_by_css('div.inner-wrapper') + +@step(u'I should see the message "([^"]*)"$') +def i_should_see_the_message(step, msg): + assert world.browser.is_text_present(msg, 5) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature new file mode 100644 index 0000000000..5acb5bfe44 --- /dev/null +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -0,0 +1,18 @@ +Feature: Create Subsection + In order offer a course on the edX platform + As a course author + I want to create and edit subsections + + Scenario: Add a new subsection to a section + Given I have opened a new course section in Studio + When I click the New Subsection link + And I enter the subsection name and click save + Then I see my subsection on the Courseware page + + Scenario: Delete a subsection + Given I have opened a new course section in Studio + And I have added a new subsection + And I see my subsection on the Courseware page + When I press the "subsection" delete icon + And I confirm the alert + Then the subsection does not exist diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py new file mode 100644 index 0000000000..ea614d3feb --- /dev/null +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -0,0 +1,39 @@ +from lettuce import world, step +from common import * + +############### ACTIONS #################### +@step('I have opened a new course section in Studio$') +def i_have_opened_a_new_course_section(step): + clear_courses() + log_into_studio() + create_a_course() + add_section() + +@step('I click the New Subsection link') +def i_click_the_new_subsection_link(step): + css = 'a.new-subsection-item' + css_click(css) + +@step('I enter the subsection name and click save$') +def i_save_subsection_name(step): + name_css = 'input.new-subsection-name-input' + save_css = 'input.new-subsection-name-save' + css_fill(name_css,'Subsection One') + css_click(save_css) + +@step('I have added a new subsection$') +def i_have_added_a_new_subsection(step): + add_subsection() + +############ ASSERTIONS ################### +@step('I see my subsection on the Courseware page$') +def i_see_my_subsection_on_the_courseware_page(step): + css = 'span.subsection-name' + assert world.browser.is_element_present_by_css(css) + css = 'span.subsection-name-value' + assert_css_with_text(css,'Subsection One') + +@step('the subsection does not exist$') +def the_subsection_does_not_exist(step): + css = 'span.subsection-name' + assert world.browser.is_element_not_present_by_css(css) \ No newline at end of file diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py new file mode 100644 index 0000000000..5bc9b53fc4 --- /dev/null +++ b/cms/envs/acceptance.py @@ -0,0 +1,38 @@ +""" +This config file extends the test environment configuration +so that we can run the lettuce acceptance tests. +""" +from .test import * + +# You need to start the server in debug mode, +# 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 +# MODULESTORE = { +# 'default': { +# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', +# 'OPTIONS': { +# 'data_dir': DATA_DIR, +# 'default_class': 'xmodule.hidden_module.HiddenDescriptor', +# } +# } +# } + +# Set this up so that rake lms[acceptance] and running the +# harvest command both use the same (test) database +# which they can flush without messing up your dev db +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ENV_ROOT / "db" / "test_mitx.db", + 'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db", + } +} + +# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command +INSTALLED_APPS += ('lettuce.django',) +LETTUCE_APPS = ('contentstore',) +LETTUCE_SERVER_PORT = 8001 diff --git a/lms/djangoapps/terrain/browser.py b/lms/djangoapps/terrain/browser.py index 7fe684e153..1c2d401680 100644 --- a/lms/djangoapps/terrain/browser.py +++ b/lms/djangoapps/terrain/browser.py @@ -11,7 +11,7 @@ from django.core.management import call_command @before.harvest def initial_setup(server): # Launch firefox - world.browser = Browser('firefox') + world.browser = Browser('chrome') @before.each_scenario def reset_data(scenario): @@ -24,3 +24,4 @@ def reset_data(scenario): def teardown_browser(total): # Quit firefox world.browser.quit() + pass \ No newline at end of file diff --git a/lms/djangoapps/terrain/steps.py b/lms/djangoapps/terrain/steps.py index ce82a0a044..6824fa16ce 100644 --- a/lms/djangoapps/terrain/steps.py +++ b/lms/djangoapps/terrain/steps.py @@ -44,6 +44,10 @@ def and_i_press_the_button(step, value): button_css = 'input[value="%s"]' % value world.browser.find_by_css(button_css).first.click() +@step(u'I click the link with the text "([^"]*)"$') +def click_the_link_with_the_text_group1(step, linktext): + world.browser.find_link_by_text(linktext).first.click() + @step('I should see that the path is "([^"]*)"$') def i_should_see_that_the_path_is(step, path): assert world.browser.url == django_url(path)