Commit 13ecee01 by Matjaz Gregoric

[TNL-6018] Use SR.readTexts to read feedback.

parent c773d9fa
...@@ -868,6 +868,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -868,6 +868,7 @@ function DragAndDropBlock(runtime, element, configuration) {
var applyState = function(keepDraggableInit) { var applyState = function(keepDraggableInit) {
sendFeedbackPopupEvents(); sendFeedbackPopupEvents();
updateDOM(); updateDOM();
readScreenReaderMessages();
if (!keepDraggableInit) { if (!keepDraggableInit) {
destroyDraggable(); destroyDraggable();
if (!state.finished) { if (!state.finished) {
...@@ -904,7 +905,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -904,7 +905,7 @@ function DragAndDropBlock(runtime, element, configuration) {
return feedback_msgs_list.map(function(message) { return message.message; }).join('\n'); return feedback_msgs_list.map(function(message) { return message.message; }).join('\n');
}; };
var updateDOM = function(state) { var updateDOM = function() {
var new_vdom = render(state); var new_vdom = render(state);
var patches = virtualDom.diff(__vdom, new_vdom); var patches = virtualDom.diff(__vdom, new_vdom);
root = virtualDom.patch(root, patches); root = virtualDom.patch(root, patches);
...@@ -912,6 +913,39 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -912,6 +913,39 @@ function DragAndDropBlock(runtime, element, configuration) {
__vdom = new_vdom; __vdom = new_vdom;
}; };
// Uses edX JS accessibility tools to read feedback messages when present.
var readScreenReaderMessages = function() {
if (window.SR && window.SR.readTexts) {
var pluckMessages = function(feedback_items) {
return feedback_items.map(function(item) {
return item.message;
});
};
var messages = [];
// In standard mode, it makes more sense to read the per-item feedback before overall feedback.
if (state.feedback && configuration.mode === DragAndDropBlock.STANDARD_MODE) {
messages = messages.concat(pluckMessages(state.feedback));
}
if (state.overall_feedback) {
messages = messages.concat(pluckMessages(state.overall_feedback));
}
// In assessment mode overall feedback comes first then multiple per-item feedbacks.
if (state.feedback && configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
if (state.feedback.length > 0) {
if (!state.last_action_correct) {
messages.push(gettext("Some of your answers were not correct."))
}
messages = messages.concat(
gettext("Hints:"),
pluckMessages(state.feedback)
);
}
}
SR.readTexts(messages);
}
};
var publishEvent = function(data) { var publishEvent = function(data) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
......
...@@ -163,6 +163,27 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -163,6 +163,27 @@ class BaseIntegrationTest(SeleniumBaseTest):
focused_element = self.browser.switch_to.active_element focused_element = self.browser.switch_to.active_element
self.assertTrue(element != focused_element, 'expected element to not have focus') self.assertTrue(element != focused_element, 'expected element to not have focus')
def _patch_sr_read_texts(self):
"""
Creates a mock SR.readTexts function that stores submitted texts into a global variable
for later inspection.
Returns a getter function that returns stored SR texts.
"""
self.browser.execute_script(
"""
window.SR = {
received_texts: [],
readTexts: function(texts) {
window.SR.received_texts.push(texts);
}
};
"""
)
def get_sr_texts():
return self.browser.execute_script('return window.SR.received_texts')
return get_sr_texts
@staticmethod @staticmethod
def get_element_html(element): def get_element_html(element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
...@@ -239,22 +260,32 @@ class DefaultDataTestMixin(object): ...@@ -239,22 +260,32 @@ class DefaultDataTestMixin(object):
class InteractionTestBase(object): class InteractionTestBase(object):
POPUP_ERROR_CLASS = "popup-incorrect" POPUP_ERROR_CLASS = "popup-incorrect"
@classmethod def setUp(self):
def _get_items_with_zone(cls, items_map): super(InteractionTestBase, self).setUp()
scenario_xml = self._get_scenario_xml()
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self._page = self.go_to_page(self.PAGE_TITLE)
# Resize window so that the entire drag container is visible.
# Selenium has issues when dragging to an area that is off screen.
self.browser.set_window_size(1024, 1024)
@staticmethod
def _get_items_with_zone(items_map):
return { return {
item_key: definition for item_key, definition in items_map.items() item_key: definition for item_key, definition in items_map.items()
if definition.zone_ids != [] if definition.zone_ids != []
} }
@classmethod @staticmethod
def _get_items_without_zone(cls, items_map): def _get_items_without_zone(items_map):
return { return {
item_key: definition for item_key, definition in items_map.items() item_key: definition for item_key, definition in items_map.items()
if definition.zone_ids == [] if definition.zone_ids == []
} }
@classmethod @staticmethod
def _get_items_by_zone(cls, items_map): def _get_items_by_zone(items_map):
zone_ids = set([definition.zone_ids[0] for _, definition in items_map.items() if definition.zone_ids]) zone_ids = set([definition.zone_ids[0] for _, definition in items_map.items() if definition.zone_ids])
return { return {
zone_id: {item_key: definition for item_key, definition in items_map.items() zone_id: {item_key: definition for item_key, definition in items_map.items()
...@@ -262,15 +293,17 @@ class InteractionTestBase(object): ...@@ -262,15 +293,17 @@ class InteractionTestBase(object):
for zone_id in zone_ids for zone_id in zone_ids
} }
def setUp(self): @staticmethod
super(InteractionTestBase, self).setUp() def _get_incorrect_zone_for_item(item, zones):
"""Returns the first zone that is not correct for this item."""
scenario_xml = self._get_scenario_xml() zone_id = None
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) zone_title = None
self._page = self.go_to_page(self.PAGE_TITLE) for z_id, z_title in zones:
# Resize window so that the entire drag container is visible. if z_id not in item.zone_ids:
# Selenium has issues when dragging to an area that is off screen. zone_id = z_id
self.browser.set_window_size(1024, 800) zone_title = z_title
break
return [zone_id, zone_title]
def _get_item_by_value(self, item_value): def _get_item_by_value(self, item_value):
return self._page.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0] return self._page.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0]
......
...@@ -76,7 +76,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT ...@@ -76,7 +76,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT
@data(*enumerate(scenarios)) # pylint: disable=star-args @data(*enumerate(scenarios)) # pylint: disable=star-args
@unpack @unpack
def test_event(self, index, event): def test_event(self, index, event):
self.parameterized_item_positive_feedback_on_good_move(self.items_map) self.parameterized_item_positive_feedback_on_good_move_standard(self.items_map)
dummy, name, published_data = self.publish.call_args_list[index][0] dummy, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name']) self.assertEqual(name, event['name'])
self.assertEqual(published_data, event['data']) self.assertEqual(published_data, event['data'])
......
...@@ -81,14 +81,12 @@ class AssessmentInteractionTest( ...@@ -81,14 +81,12 @@ class AssessmentInteractionTest(
""" """
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_item_no_feedback_on_good_move(self, action_key): def test_item_no_feedback_on_good_move(self, action_key):
self.parameterized_item_positive_feedback_on_good_move( self.parameterized_item_positive_feedback_on_good_move_assessment(self.items_map, action_key=action_key)
self.items_map, action_key=action_key, assessment_mode=True
)
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_item_no_feedback_on_bad_move(self, action_key): def test_item_no_feedback_on_bad_move(self, action_key):
self.parameterized_item_negative_feedback_on_bad_move( self.parameterized_item_negative_feedback_on_bad_move_assessment(
self.items_map, self.all_zones, action_key=action_key, assessment_mode=True self.items_map, self.all_zones, action_key=action_key
) )
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
...@@ -250,6 +248,20 @@ class AssessmentInteractionTest( ...@@ -250,6 +248,20 @@ class AssessmentInteractionTest(
""" """
Test updating overall feedback after submitting solution in assessment mode Test updating overall feedback after submitting solution in assessment mode
""" """
get_sr_texts = self._patch_sr_read_texts()
def check_feedback(overall_feedback_lines, per_item_feedback_lines=None):
# Check that the feedback is correctly displayed in the overall feedback area.
expected_overall_feedback = "\n".join(["FEEDBACK"] + overall_feedback_lines)
self.assertEqual(self._get_feedback().text, expected_overall_feedback)
# Check that the SR.readTexts function was passed correct feedback messages.
sr_feedback_lines = overall_feedback_lines
if per_item_feedback_lines:
sr_feedback_lines += ["Some of your answers were not correct.", "Hints:"]
sr_feedback_lines += per_item_feedback_lines
self.assertEqual(get_sr_texts()[-1], sr_feedback_lines)
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element # used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self.place_item(0, TOP_ZONE_ID, Keys.RETURN) self.place_item(0, TOP_ZONE_ID, Keys.RETURN)
...@@ -261,29 +273,25 @@ class AssessmentInteractionTest( ...@@ -261,29 +273,25 @@ class AssessmentInteractionTest(
expected_grade = 2.0 / 5.0 expected_grade = 2.0 / 5.0
feedback_lines = [ feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1), FeedbackMessages.correctly_placed(1),
FeedbackMessages.not_placed(3), FeedbackMessages.not_placed(3),
START_FEEDBACK, START_FEEDBACK,
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade) FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade)
] ]
expected_feedback = "\n".join(feedback_lines) check_feedback(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
# Place the item into incorrect zone. The score does not change. # Place the item into incorrect zone. The score does not change.
self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN) self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN)
self.click_submit() self.click_submit()
feedback_lines = [ feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(1), FeedbackMessages.correctly_placed(1),
FeedbackMessages.misplaced_returned(1), FeedbackMessages.misplaced_returned(1),
FeedbackMessages.not_placed(2), FeedbackMessages.not_placed(2),
START_FEEDBACK, START_FEEDBACK,
FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade) FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade)
] ]
expected_feedback = "\n".join(feedback_lines) check_feedback(feedback_lines, ["No, this item does not belong here. Try again."])
self.assertEqual(self._get_feedback().text, expected_feedback)
# reach final attempt # reach final attempt
for _ in xrange(self.MAX_ATTEMPTS-3): for _ in xrange(self.MAX_ATTEMPTS-3):
...@@ -299,13 +307,11 @@ class AssessmentInteractionTest( ...@@ -299,13 +307,11 @@ class AssessmentInteractionTest(
expected_grade = 1.0 expected_grade = 1.0
feedback_lines = [ feedback_lines = [
"FEEDBACK",
FeedbackMessages.correctly_placed(4), FeedbackMessages.correctly_placed(4),
FINISH_FEEDBACK, FINISH_FEEDBACK,
FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_grade) FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_grade)
] ]
expected_feedback = "\n".join(feedback_lines) check_feedback(feedback_lines)
self.assertEqual(self._get_feedback().text, expected_feedback)
def test_per_item_feedback_multiple_misplaced(self): def test_per_item_feedback_multiple_misplaced(self):
self.place_item(0, MIDDLE_ZONE_ID, Keys.RETURN) self.place_item(0, MIDDLE_ZONE_ID, Keys.RETURN)
......
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