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()