diff --git a/common/test/acceptance/pages/lms/annotation_component.py b/common/test/acceptance/pages/lms/annotation_component.py new file mode 100644 index 0000000000..ad8ac3fd20 --- /dev/null +++ b/common/test/acceptance/pages/lms/annotation_component.py @@ -0,0 +1,91 @@ +""" +Annotation Component Page. +""" +from bok_choy.page_object import PageObject +from selenium.webdriver import ActionChains + + +class AnnotationComponentPage(PageObject): + """ + View of annotation component page. + """ + + url = None + active_problem = 0 + + def is_browser_on_page(self): + return self.q(css='.annotatable-title').present + + @property + def component_name(self): + """ + Return the current problem name. + """ + return self.q(css='.annotatable-title').text[0] + + def click_reply_annotation(self, problem): + """ + Mouse over on annotation selector and click on "Reply to Annotation". + """ + annotation_span_selector = '.annotatable-span[data-problem-id="{}"]'.format(problem) + self.mouse_hover(self.browser.find_element_by_css_selector(annotation_span_selector)) + self.wait_for_element_visibility(annotation_span_selector, "Reply to Annotation link is visible") + + annotation_reply_selector = '.annotatable-reply[data-problem-id="{}"]'.format(problem) + self.q(css=annotation_reply_selector).click() + + self.active_problem = problem + + def active_problem_selector(self, sub_selector): + """ + Return css selector for current active problem with sub_selector. + """ + return 'div[data-problem-id="{}"] {}'.format( + self.q(css='.vert-{}'.format(self.active_problem+1)).map( + lambda el: el.get_attribute('data-id')).results[0], + sub_selector, + ) + + def mouse_hover(self, element): + """ + Mouse over on given element. + """ + mouse_hover_action = ActionChains(self.browser).move_to_element(element) + mouse_hover_action.perform() + + def check_scroll_to_problem(self): + """ + Return visibility of active problem's input selector. + """ + annotation_input_selector = self.active_problem_selector('.annotation-input') + return self.q(css=annotation_input_selector).visible + + def answer_problem(self): + """ + Submit correct answer for active problem. + """ + self.q(css=self.active_problem_selector('.comment')).fill('Test Response') + self.q(css=self.active_problem_selector('.tag[data-id="{}"]'.format(self.active_problem))).click() + self.q(css=self.active_problem_selector('.check')).click() + + def check_feedback(self): + """ + Return visibility of active problem's feedback. + """ + self.wait_for_element_visibility( + self.active_problem_selector('.tag-status.correct'), "Correct is visible" + ) + return self.q(css=self.active_problem_selector('.tag-status.correct')).visible + + def click_return_to_annotation(self): + """ + Click on active problem's "Return to Annotation" link. + """ + self.q(css=self.active_problem_selector('.annotation-return')).click() + + def check_scroll_to_annotation(self): + """ + Return visibility of active annotation component header. + """ + annotation_header_selector = '.annotation-header' + return self.q(css=annotation_header_selector).visible diff --git a/common/test/acceptance/tests/test_annotatable.py b/common/test/acceptance/tests/test_annotatable.py new file mode 100644 index 0000000000..24780369cc --- /dev/null +++ b/common/test/acceptance/tests/test_annotatable.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +""" +E2E tests for the LMS. +""" +import time + +from .helpers import UniqueCourseTest +from ..pages.studio.auto_auth import AutoAuthPage +from ..pages.lms.courseware import CoursewarePage +from ..pages.lms.annotation_component import AnnotationComponentPage +from ..fixtures.course import CourseFixture, XBlockFixtureDesc +from ..fixtures.xqueue import XQueueResponseFixture +from textwrap import dedent + + +def _correctness(choice, target): + if choice == target: + return "correct" + elif abs(choice - target) == 1: + return "partially-correct" + else: + return "incorrect" + + +class AnnotatableProblemTest(UniqueCourseTest): + """ + Tests for annotation components. + """ + USERNAME = "STAFF_TESTER" + EMAIL = "johndoe@example.com" + + + DATA_TEMPLATE = dedent("""\ + + Instruction text +

{}

+
+ """) + + ANNOTATION_TEMPLATE = dedent("""\ + Before {0}. + + Region Contents {0} + + After {0}. + """) + + PROBLEM_TEMPLATE = dedent("""\ + + + + Question {number} + Region Contents {number} + What number is this region? + Type your response below: + What number is this region? + + {options} + + + + + This problem is checking region {number} + + + """) + + OPTION_TEMPLATE = """""" + + def setUp(self): + super(AnnotatableProblemTest, self).setUp() + + self.courseware_page = CoursewarePage(self.browser, self.course_id) + + # Install a course with two annotations and two annotations problems. + course_fix = CourseFixture( + self.course_info['org'], self.course_info['number'], + self.course_info['run'], self.course_info['display_name'] + ) + + self.annotation_count = 2 + course_fix.add_children( + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Annotation Vertical').add_children( + XBlockFixtureDesc('annotatable', 'Test Annotation Module', + data=self.DATA_TEMPLATE.format("\n".join( + self.ANNOTATION_TEMPLATE.format(i) for i in xrange(self.annotation_count) + ))), + XBlockFixtureDesc('problem', 'Test Annotation Problem 0', + data=self.PROBLEM_TEMPLATE.format(number=0, options="\n".join( + self.OPTION_TEMPLATE.format( + number=k, + correctness=_correctness(k, 0)) + for k in xrange(self.annotation_count) + ))), + XBlockFixtureDesc('problem', 'Test Annotation Problem 1', + data=self.PROBLEM_TEMPLATE.format(number=1, options="\n".join( + self.OPTION_TEMPLATE.format( + number=k, + correctness=_correctness(k, 1)) + for k in xrange(self.annotation_count) + ))) + ) + ) + ) + ).install() + + # Auto-auth register for the course. + AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, + course_id=self.course_id, staff=False).visit() + + def _goto_annotation_component_page(self): + """ + Open annotation component page with assertion. + """ + self.courseware_page.visit() + annotation_component_page = AnnotationComponentPage(self.browser) + self.assertEqual( + annotation_component_page.component_name, 'TEST ANNOTATION MODULE'.format() + ) + return annotation_component_page + + def test_annotation_component(self): + """ + Test annotation components links to annotation problems. + """ + + annotation_component_page = self._goto_annotation_component_page() + + for i in xrange(self.annotation_count): + annotation_component_page.click_reply_annotation(i) + self.assertTrue(annotation_component_page.check_scroll_to_problem()) + + annotation_component_page.answer_problem() + self.assertTrue(annotation_component_page.check_feedback()) + + annotation_component_page.click_return_to_annotation() + self.assertTrue(annotation_component_page.check_scroll_to_annotation()) diff --git a/lms/djangoapps/courseware/features/annotatable.feature b/lms/djangoapps/courseware/features/annotatable.feature index 57091b1d2d..619ad7d369 100644 --- a/lms/djangoapps/courseware/features/annotatable.feature +++ b/lms/djangoapps/courseware/features/annotatable.feature @@ -7,22 +7,3 @@ Feature: LMS.Annotatable Component When I view the annotatable component Then the annotatable component has rendered And the annotatable component has 2 highlighted passages - -# Disabling due to frequent intermittent failures. TNL-353 -# This features's acceptance tests should be rewritten in bok-choy, rather than fixed here. -# -# 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 -# Then I am scrolled to that annotation problem -# When I answer that annotation problem -# Then I receive 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 | diff --git a/lms/djangoapps/courseware/features/annotatable.py b/lms/djangoapps/courseware/features/annotatable.py index 829e939efa..3040a23a7f 100644 --- a/lms/djangoapps/courseware/features/annotatable.py +++ b/lms/djangoapps/courseware/features/annotatable.py @@ -1,7 +1,7 @@ import textwrap from lettuce import world, steps -from nose.tools import assert_in, assert_equals, assert_true +from nose.tools import assert_in, assert_equals from common import i_am_registered_for_the_course, visit_scenario_item @@ -100,74 +100,6 @@ class AnnotatableSteps(object): 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\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\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 'div[data-problem-id="{}"] {}'.format( - world.scenario_dict['PROBLEMS'][self.active_problem].location.to_deprecated_string(), - 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 receive 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()