Commit a0b8577a by Calen Pennington

Merge pull request #1988 from cpennington/annotation-module-acceptance-tests

Add acceptance tests of Annotatable module
parents 6d6bcb79 77942cec
......@@ -9,68 +9,11 @@ import course_modes.tests.factories as cmf
from lettuce import world
@world.absorb
class UserFactory(sf.UserFactory):
"""
User account for lms / cms
"""
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
pass
@world.absorb
class UserProfileFactory(sf.UserProfileFactory):
"""
Demographics etc for the User
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass
@world.absorb
class RegistrationFactory(sf.RegistrationFactory):
"""
Activation key for registering the user account
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass
@world.absorb
class GroupFactory(sf.GroupFactory):
"""
Groups for user permissions for courses
"""
pass
@world.absorb
class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory):
"""
Users allowed to enroll in the course outside of the usual window
"""
pass
@world.absorb
class CourseModeFactory(cmf.CourseModeFactory):
"""
Course modes
"""
pass
@world.absorb
class CourseFactory(xf.CourseFactory):
"""
Courseware courses
"""
pass
@world.absorb
class ItemFactory(xf.ItemFactory):
"""
Everything included inside a course
"""
pass
world.absorb(sf.UserFactory)
world.absorb(sf.UserProfileFactory)
world.absorb(sf.RegistrationFactory)
world.absorb(sf.GroupFactory)
world.absorb(sf.CourseEnrollmentAllowedFactory)
world.absorb(cmf.CourseModeFactory)
world.absorb(xf.CourseFactory)
world.absorb(xf.ItemFactory)
......@@ -11,10 +11,14 @@
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613
# django_url is assigned late in the process of loading lettuce,
# so we import this as a module, and then read django_url from
# it to get the correct value
import lettuce.django
from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
from nose.tools import assert_equals # pylint: disable=E0611
from logging import getLogger
......@@ -135,7 +139,7 @@ def should_have_link_with_id_and_text(step, link_id, text):
def should_have_link_with_path_and_text(step, path, text):
link = world.browser.find_link_by_text(text)
assert len(link) > 0
assert_equals(link.first["href"], django_url(path))
assert_equals(link.first["href"], lettuce.django.django_url(path))
@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
......@@ -154,7 +158,7 @@ def should_see_in_the_page(step, doesnt_appear, text):
def i_am_logged_in(step):
world.create_user('robot', 'test')
world.log_in(username='robot', password='test')
world.browser.visit(django_url('/'))
world.browser.visit(lettuce.django.django_url('/'))
dash_css = 'section.container.dashboard'
assert world.is_css_present(dash_css)
......@@ -176,7 +180,7 @@ def dialogs_are_closed(step):
@step(u'visit the url "([^"]*)"')
def visit_url(step, url):
world.browser.visit(django_url(url))
world.browser.visit(lettuce.django.django_url(url))
@step(u'wait for AJAX to (?:finish|complete)')
......
......@@ -2,10 +2,18 @@
#pylint: disable=W0621
from lettuce import world
import time
import json
import re
import platform
# django_url is assigned late in the process of loading lettuce,
# so we import this as a module, and then read django_url from
# it to get the correct value
import lettuce.django
from textwrap import dedent
from urllib import quote_plus
from selenium.common.exceptions import (
......@@ -14,7 +22,6 @@ from selenium.common.exceptions import (
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
from nose.tools import assert_true # pylint: disable=E0611
......@@ -247,13 +254,13 @@ def wait_for_ajax_complete():
@world.absorb
def visit(url):
world.browser.visit(django_url(url))
world.browser.visit(lettuce.django.django_url(url))
wait_for_js_to_load()
@world.absorb
def url_equals(url):
return world.browser.url == django_url(url)
return world.browser.url == lettuce.django.django_url(url)
@world.absorb
......
......@@ -18,6 +18,8 @@
block.element = element
block.name = $element.data("name")
$element.trigger("xblock-initialized")
$element.data("initialized", true)
block
initializeBlocks: (element) ->
......
@shard_2
Feature: LMS.Annotatable Component
As a student, I want to view an Annotatable component in the LMS
Scenario: An Annotatable component can be rendered in the LMS
Given that a course has an annotatable component with 2 annotations
When I view the annotatable component
Then the annotatable component has rendered
And the annotatable component has 2 highlighted passages
Scenario: An Annotatable component links to annonation problems in the LMS
Given that a course has an annotatable component with 2 annotations
And the course has 2 annotatation problems
When I view the annotatable component
And I click "Reply to annotation" on passage <problem>
Then I am scrolled to that annotation problem
When I answer that annotation problem
Then I recieve feedback on that annotation problem
When I click "Return to annotation" on that problem
Then I am scrolled to the annotatable component
Examples:
| problem |
| 0 |
| 1 |
import textwrap
from lettuce import world, steps
from nose.tools import assert_in, assert_equals, assert_true
from common import i_am_registered_for_the_course, visit_scenario_item
DATA_TEMPLATE = textwrap.dedent("""\
<annotatable>
<instructions>Instruction text</instructions>
<p>{}</p>
</annotatable>
""")
ANNOTATION_TEMPLATE = textwrap.dedent("""\
Before {0}.
<annotation title="region {0}" body="Comment {0}" highlight="yellow" problem="{0}">
Region Contents {0}
</annotation>
After {0}.
""")
PROBLEM_TEMPLATE = textwrap.dedent("""\
<problem max_attempts="1" weight="">
<annotationresponse>
<annotationinput>
<title>Question {number}</title>
<text>Region Contents {number}</text>
<comment>What number is this region?</comment>
<comment_prompt>Type your response below:</comment_prompt>
<tag_prompt>What number is this region?</tag_prompt>
<options>
{options}
</options>
</annotationinput>
</annotationresponse>
<solution>
This problem is checking region {number}
</solution>
</problem>
""")
OPTION_TEMPLATE = """<option choice="{correctness}">{number}</option>"""
def _correctness(choice, target):
if choice == target:
return "correct"
elif abs(choice - target) == 1:
return "partially-correct"
else:
return "incorrect"
@steps
class AnnotatableSteps(object):
def __init__(self):
self.annotations_count = None
self.active_problem = None
def define_component(self, step, count):
r"""that a course has an annotatable component with (?P<count>\d+) annotations$"""
count = int(count)
coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum)
world.scenario_dict['ANNOTATION_VERTICAL'] = world.ItemFactory(
parent_location=world.scenario_dict['SECTION'].location,
category='vertical',
display_name="Test Annotation Vertical"
)
world.scenario_dict['ANNOTATABLE'] = world.ItemFactory(
parent_location=world.scenario_dict['ANNOTATION_VERTICAL'].location,
category='annotatable',
display_name="Test Annotation Module",
data=DATA_TEMPLATE.format("\n".join(ANNOTATION_TEMPLATE.format(i) for i in xrange(count)))
)
self.annotations_count = count
def view_component(self, step):
r"""I view the annotatable component$"""
visit_scenario_item('ANNOTATABLE')
def check_rendered(self, step):
r"""the annotatable component has rendered$"""
world.wait_for_js_variable_truthy('$(".xblock-student_view[data-type=Annotatable]").data("initialized")')
annotatable_text = world.css_find('.xblock-student_view[data-type=Annotatable]').first.text
assert_in("Instruction text", annotatable_text)
for i in xrange(self.annotations_count):
assert_in("Region Contents {}".format(i), annotatable_text)
def count_passages(self, step, count):
r"""the annotatable component has (?P<count>\d+) highlighted passages$"""
count = int(count)
assert_equals(len(world.css_find('.annotatable-span')), count)
assert_equals(len(world.css_find('.annotatable-span.highlight')), count)
assert_equals(len(world.css_find('.annotatable-span.highlight-yellow')), count)
def add_problems(self, step, count):
r"""the course has (?P<count>\d+) annotatation problems$"""
count = int(count)
for i in xrange(count):
world.scenario_dict.setdefault('PROBLEMS', []).append(
world.ItemFactory(
parent_location=world.scenario_dict['ANNOTATION_VERTICAL'].location,
category='problem',
display_name="Test Annotation Problem {}".format(i),
data=PROBLEM_TEMPLATE.format(
number=i,
options="\n".join(
OPTION_TEMPLATE.format(
number=k,
correctness=_correctness(k, i)
)
for k in xrange(count)
)
)
)
)
def click_reply(self, step, problem):
r"""I click "Reply to annotation" on passage (?P<problem>\d+)$"""
problem = int(problem)
annotation_span_selector = '.annotatable-span[data-problem-id="{}"]'.format(problem)
world.css_find(annotation_span_selector).first.mouse_over()
annotation_reply_selector = '.annotatable-reply[data-problem-id="{}"]'.format(problem)
assert_equals(len(world.css_find(annotation_reply_selector)), 1)
world.css_click(annotation_reply_selector)
self.active_problem = problem
def active_problem_selector(self, subselector):
return 'section[data-problem-id="{}"] {}'.format(
world.scenario_dict['PROBLEMS'][self.active_problem].location.url(),
subselector,
)
def check_scroll_to_problem(self, step):
r"""I am scrolled to that annotation problem$"""
annotation_input_selector = self.active_problem_selector('.annotation-input')
assert_true(world.css_visible(annotation_input_selector))
def answer_problem(self, step):
r"""I answer that annotation problem$"""
world.css_fill(self.active_problem_selector('.comment'), 'Test Response')
world.css_click(self.active_problem_selector('.tag[data-id="{}"]'.format(self.active_problem)))
world.css_click(self.active_problem_selector('.check'))
def check_feedback(self, step):
r"""I recieve feedback on that annotation problem$"""
world.wait_for_visible(self.active_problem_selector('.tag-status.correct'))
assert_equals(len(world.css_find(self.active_problem_selector('.tag-status.correct'))), 1)
assert_equals(len(world.css_find(self.active_problem_selector('.show'))), 1)
def click_return_to(self, step):
r"""I click "Return to annotation" on that problem$"""
world.css_click(self.active_problem_selector('.annotation-return'))
def check_scroll_to_annotatable(self, step):
r"""I am scrolled to the annotatable component$"""
assert_true(world.css_visible('.annotation-header'))
# This line is required by @steps in order to actually bind the step
# regexes
AnnotatableSteps()
......@@ -4,7 +4,9 @@
from __future__ import absolute_import
from lettuce import world, step
from lettuce.django import django_url
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from student.models import CourseEnrollment
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
......@@ -27,18 +29,24 @@ 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)
world.scenario_dict['COURSE'] = world.CourseFactory.create(org='edx',
number=course,
display_name='Test Course')
# Add a section to the course to contain problems
world.scenario_dict['SECTION'] = world.ItemFactory.create(parent_location=world.scenario_dict['COURSE'].location,
display_name='Test Section')
world.ItemFactory.create(
parent_location=world.scenario_dict['SECTION'].location,
world.scenario_dict['COURSE'] = world.CourseFactory.create(
org='edx',
number=course,
display_name='Test Course'
)
# Add a chapter to the course to contain problems
world.scenario_dict['CHAPTER'] = world.ItemFactory.create(
parent_location=world.scenario_dict['COURSE'].location,
category='chapter',
display_name='Test Chapter',
)
world.scenario_dict['SECTION'] = world.ItemFactory.create(
parent_location=world.scenario_dict['CHAPTER'].location,
category='sequential',
display_name='Test Section')
display_name='Test Section',
)
@step(u'I am registered for the course "([^"]*)"$')
......@@ -74,23 +82,32 @@ def go_into_course(step):
def course_id(course_num):
return "%s/%s/%s" % (world.scenario_dict['COURSE'].org, course_num,
world.scenario_dict['COURSE'].display_name.replace(" ", "_"))
world.scenario_dict['COURSE'].url_name)
def course_location(course_num):
return Location(loc_or_tag="i4x",
org=world.scenario_dict['COURSE'].org,
course=course_num,
category='course',
name=world.scenario_dict['COURSE'].display_name.replace(" ", "_"))
return world.scenario_dict['COURSE'].location._replace(course=course_num)
def section_location(course_num):
return Location(loc_or_tag="i4x",
org=world.scenario_dict['COURSE'].org,
course=course_num,
category='sequential',
name=world.scenario_dict['SECTION'].display_name.replace(" ", "_"))
return world.scenario_dict['SECTION'].location._replace(course=course_num)
def visit_scenario_item(item_key):
"""
Go to the courseware page containing the item stored in `world.scenario_dict`
under the key `item_key`
"""
url = django_url(reverse(
'jump_to',
kwargs={
'course_id': world.scenario_dict['COURSE'].id,
'location': str(world.scenario_dict[item_key].location),
}
))
world.browser.visit(url)
def get_courses():
......
......@@ -3,9 +3,10 @@
import os
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from lettuce import world, step
from lettuce.django import django_url
from common import course_id
from common import course_id, visit_scenario_item
from courseware.tests.factories import InstructorFactory
......@@ -111,7 +112,7 @@ def add_correct_lti_to_course(_step, fields):
metadata.update(_step.hashes[0])
world.scenario_dict['LTI'] = world.ItemFactory.create(
parent_location=world.scenario_dict['SEQUENTIAL'].location,
parent_location=world.scenario_dict['SECTION'].location,
category=category,
display_name='LTI',
metadata=metadata,
......@@ -122,19 +123,7 @@ def add_correct_lti_to_course(_step, fields):
port=world.browser.port,
))
course = world.scenario_dict["COURSE"]
chapter_name = world.scenario_dict['SECTION'].display_name.replace(
" ", "_")
section_name = chapter_name
path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format(
org=course.org,
num=course.number,
name=course.display_name.replace(' ', '_'),
chapter=chapter_name,
section=section_name)
url = django_url(path)
world.browser.visit(url)
visit_scenario_item('LTI')
def create_course(course, metadata):
......@@ -180,12 +169,13 @@ def create_course(course, metadata):
)
# Add a section to the course to contain problems
world.scenario_dict['SECTION'] = world.ItemFactory.create(
world.scenario_dict['CHAPTER'] = world.ItemFactory.create(
parent_location=world.scenario_dict['COURSE'].location,
display_name='Test Section',
category='chapter',
display_name='Test Chapter',
)
world.scenario_dict['SEQUENTIAL'] = world.ItemFactory.create(
parent_location=world.scenario_dict['SECTION'].location,
world.scenario_dict['SECTION'] = world.ItemFactory.create(
parent_location=world.scenario_dict['CHAPTER'].location,
category='sequential',
display_name='Test Section',
metadata={'graded': True, 'format': 'Homework'})
......
......@@ -7,60 +7,35 @@ Steps for problem.feature lettuce tests
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course
from common import i_am_registered_for_the_course, visit_scenario_item
from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course
from nose.tools import assert_equal
@step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt')
def view_problem_with_attempts(step, problem_type, attempts):
def _view_problem(step, problem_type, problem_settings=None):
i_am_registered_for_the_course(step, 'model_course')
# Ensure that the course has this problem type
add_problem_to_course(world.scenario_dict['COURSE'].number, problem_type, {'max_attempts': attempts})
add_problem_to_course(world.scenario_dict['COURSE'].number, problem_type, problem_settings)
# Go to the one section in the factory-created course
# which should be loaded with the correct problem
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
(world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,))
world.browser.visit(url)
visit_scenario_item('SECTION')
@step(u'I am viewing a "([^"]*)" that shows the answer "([^"]*)"')
def view_problem_with_show_answer(step, problem_type, answer):
i_am_registered_for_the_course(step, 'model_course')
@step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt')
def view_problem_with_attempts(step, problem_type, attempts):
_view_problem(step, problem_type, {'max_attempts': attempts})
# Ensure that the course has this problem type
add_problem_to_course('model_course', problem_type, {'showanswer': answer})
# Go to the one section in the factory-created course
# which should be loaded with the correct problem
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
(world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,))
world.browser.visit(url)
@step(u'I am viewing a "([^"]*)" that shows the answer "([^"]*)"')
def view_problem_with_show_answer(step, problem_type, answer):
_view_problem(step, problem_type, {'showanswer': answer})
@step(u'I am viewing a "([^"]*)" problem')
def view_problem(step, 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 = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
(world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,))
world.browser.visit(url)
_view_problem(step, problem_type)
@step(u'External graders respond "([^"]*)"')
......
......@@ -307,7 +307,7 @@ def problem_has_answer(problem_type, answer_class):
pass
def add_problem_to_course(course, problem_type, extraMeta=None):
def add_problem_to_course(course, problem_type, extra_meta=None):
'''
Add a problem to the course we have created using factories.
'''
......@@ -318,8 +318,8 @@ def add_problem_to_course(course, problem_type, extraMeta=None):
factory_dict = PROBLEM_DICT[problem_type]
problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs'])
metadata = {'rerandomize': 'always'} if not 'metadata' in factory_dict else factory_dict['metadata']
if extraMeta:
metadata = dict(metadata, **extraMeta)
if extra_meta:
metadata = dict(metadata, **extra_meta)
# Create a problem item using our generated XML
# We set rerandomize=always in the metadata so that the "Reset" button
......
......@@ -7,8 +7,7 @@ from lettuce.django import django_url
@step('I register for the course "([^"]*)"$')
def i_register_for_the_course(_step, course):
cleaned_name = world.scenario_dict['COURSE'].display_name.replace(' ', '_')
url = django_url('courses/%s/%s/%s/about' % (world.scenario_dict['COURSE'].org, course, cleaned_name))
url = django_url('courses/%s/about' % world.scenario_dict['COURSE'].id)
world.browser.visit(url)
world.css_click('section.intro a.register')
......
......@@ -2,7 +2,7 @@
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course, section_location
from common import i_am_registered_for_the_course, section_location, visit_scenario_item
from django.utils.translation import ugettext as _
############### ACTIONS ####################
......@@ -28,12 +28,7 @@ def view_video(_step, player_mode):
# Make sure we have a video
add_video_to_course(coursenum, player_mode.lower())
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
(world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,))
world.browser.visit(url)
visit_scenario_item('SECTION')
def add_video_to_course(course, player_mode):
......
......@@ -4,7 +4,7 @@ from time import sleep
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course, section_location
from common import i_am_registered_for_the_course, section_location, visit_scenario_item
@step('I view the word cloud and it has rendered')
......@@ -18,16 +18,7 @@ def view_word_cloud(_step):
i_am_registered_for_the_course(_step, coursenum)
add_word_cloud_to_course(coursenum)
chapter_name = world.scenario_dict['SECTION'].display_name.replace(
" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (
world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,)
)
world.browser.visit(url)
visit_scenario_item('SECTION')
@step('I press the Save button')
......
......@@ -10,7 +10,7 @@
-e git+https://github.com/edx/django-staticfiles.git@d89aae2a82f2b#egg=django-staticfiles
-e git+https://github.com/edx/django-pipeline.git@88ec8a011e481918fdc9d2682d4017c835acd8be#egg=django-pipeline
-e git+https://github.com/edx/django-wiki.git@41815e2ef1b0323f92900f8e60711b0f0c37766b#egg=django-wiki
-e git+https://github.com/edx/lettuce.git@503fe2d2599290c45b021d6c424ab5ea899e42be#egg=lettuce
-e git+https://github.com/gabrielfalcao/lettuce.git@cccc3978ad2df82a78b6f9648fe2e9baddd22f88#egg=lettuce
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment