Commit 8be8937b by Tim Krones

Merge pull request #44 from open-craft/keyboard-a11y

Make DnDv2 keyboard-accessible
parents 9a0f8652 dc0a76ba
...@@ -14,8 +14,9 @@ TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer ...@@ -14,8 +14,9 @@ TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer
MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.") MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.")
BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.") BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.") ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.")
START_FEEDBACK = _("Drag the items onto the image above.") START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK = _("Good work! You have completed this drag and drop exercise.") FINISH_FEEDBACK = _("Good work! You have completed this drag and drop exercise.")
...@@ -88,7 +89,7 @@ DEFAULT_DATA = { ...@@ -88,7 +89,7 @@ DEFAULT_DATA = {
{ {
"displayName": _("I don't belong anywhere"), "displayName": _("I don't belong anywhere"),
"feedback": { "feedback": {
"incorrect": _("You silly, there are no zones for this one."), "incorrect": ITEM_NO_ZONE_FEEDBACK,
"correct": "" "correct": ""
}, },
"zone": "none", "zone": "none",
......
...@@ -220,7 +220,7 @@ class DragAndDropBlock(XBlock): ...@@ -220,7 +220,7 @@ class DragAndDropBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def do_attempt(self, attempt, suffix=''): def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val']) item = self._get_item_definition(attempt['val'])
state = None state = None
feedback = item['feedback']['incorrect'] feedback = item['feedback']['incorrect']
...@@ -228,7 +228,7 @@ class DragAndDropBlock(XBlock): ...@@ -228,7 +228,7 @@ class DragAndDropBlock(XBlock):
is_correct = False is_correct = False
is_correct_location = False is_correct_location = False
if 'input' in attempt: if 'input' in attempt: # Student submitted numerical value for item
state = self._get_item_state().get(str(item['id'])) state = self._get_item_state().get(str(item['id']))
if state: if state:
state['input'] = attempt['input'] state['input'] = attempt['input']
...@@ -238,7 +238,7 @@ class DragAndDropBlock(XBlock): ...@@ -238,7 +238,7 @@ class DragAndDropBlock(XBlock):
feedback = item['feedback']['correct'] feedback = item['feedback']['correct']
else: else:
is_correct = False is_correct = False
elif item['zone'] == attempt['zone']: elif item['zone'] == attempt['zone']: # Student placed item in zone
is_correct_location = True is_correct_location = True
if 'inputOptions' in item: if 'inputOptions' in item:
# Input value will have to be provided for the item. # Input value will have to be provided for the item.
...@@ -250,6 +250,7 @@ class DragAndDropBlock(XBlock): ...@@ -250,6 +250,7 @@ class DragAndDropBlock(XBlock):
is_correct = True is_correct = True
feedback = item['feedback']['correct'] feedback = item['feedback']['correct']
state = { state = {
'zone': attempt['zone'],
'x_percent': attempt['x_percent'], 'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'], 'y_percent': attempt['y_percent'],
} }
...@@ -349,8 +350,13 @@ class DragAndDropBlock(XBlock): ...@@ -349,8 +350,13 @@ class DragAndDropBlock(XBlock):
""" Get all user-specific data, and any applicable feedback """ """ Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state() item_state = self._get_item_state()
for item_id, item in item_state.iteritems(): for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id) definition = self._get_item_definition(int(item_id))
item['correct_input'] = self._is_correct_input(definition, item.get('input')) item['correct_input'] = self._is_correct_input(definition, item.get('input'))
# If information about zone is missing
# (because exercise was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
if item.get('zone') is None:
item['zone'] = definition.get('zone', 'unknown')
is_finished = self._is_finished() is_finished = self._is_finished()
return { return {
...@@ -374,6 +380,12 @@ class DragAndDropBlock(XBlock): ...@@ -374,6 +380,12 @@ class DragAndDropBlock(XBlock):
return state return state
def _get_item_definition(self, item_id):
"""
Returns definition (settings) for item identified by `item_id`.
"""
return next(i for i in self.data['items'] if i['id'] == item_id)
def _get_grade(self): def _get_grade(self):
""" """
Returns the student's grade for this block. Returns the student's grade for this block.
......
...@@ -116,7 +116,8 @@ ...@@ -116,7 +116,8 @@
/* Focused option */ /* Focused option */
.xblock--drag-and-drop .drag-container .item-bank .option:focus, .xblock--drag-and-drop .drag-container .item-bank .option:focus,
.xblock--drag-and-drop .drag-container .item-bank .option:hover { .xblock--drag-and-drop .drag-container .item-bank .option:hover,
.xblock--drag-and-drop .drag-container .item-bank .option[aria-grabbed='true'] {
outline-width: 2px; outline-width: 2px;
outline-style: solid; outline-style: solid;
outline-offset: -4px; outline-offset: -4px;
...@@ -181,7 +182,6 @@ ...@@ -181,7 +182,6 @@
opacity: 0.5; opacity: 0.5;
} }
/*** Drop Target ***/ /*** Drop Target ***/
.xblock--drag-and-drop .target { .xblock--drag-and-drop .target {
display: table; display: table;
...@@ -231,7 +231,6 @@ ...@@ -231,7 +231,6 @@
/* W3C */ /* W3C */
box-pack:center; box-pack:center;
box-align:center; box-align:center;
} }
/* Focused zone */ /* Focused zone */
...@@ -263,7 +262,6 @@ ...@@ -263,7 +262,6 @@
/*** FEEDBACK ***/ /*** FEEDBACK ***/
.xblock--drag-and-drop .feedback { .xblock--drag-and-drop .feedback {
margin-top: 20px;
border-top: solid 1px #bdbdbd; border-top: solid 1px #bdbdbd;
} }
...@@ -301,10 +299,68 @@ ...@@ -301,10 +299,68 @@
font-size: 18pt; font-size: 18pt;
} }
.xblock--drag-and-drop .keyboard-help {
margin-top: 3px;
margin-bottom: 6px;
}
.xblock--drag-and-drop .keyboard-help-dialog {
position: fixed;
left: 50%;
top: 50%;
width: 1px;
height: 1px;
z-index: 1500;
}
.xblock--drag-and-drop .modal-window-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0.5;
z-index: 1500;
}
.xblock--drag-and-drop .modal-window {
display: none;
position: absolute;
width: 600px;
max-width: 90vw;
height: auto;
transform: translate(-50%, -50%);
box-sizing: border-box;
box-shadow: 0 0 7px rgba(0, 0, 0, 0.4);
border-radius: 4px;
padding: 7px;
background-color: #e5e5e5;
text-align: left;
direction: ltr;
z-index: 1500;
}
.xblock--drag-and-drop .modal-content {
border-radius: 5px;
background: white;
margin-bottom: 5px;
padding: 5px;
}
.xblock--drag-and-drop .modal-content li {
margin-left: 2%;
}
.xblock--drag-and-drop .keyboard-help-button,
.xblock--drag-and-drop .reset-button { .xblock--drag-and-drop .reset-button {
cursor: pointer; cursor: pointer;
float: right;
color: #2d74b3; color: #2d74b3;
}
.xblock--drag-and-drop .reset-button {
float: right;
margin-top: 3px; margin-top: 3px;
} }
......
...@@ -53,9 +53,18 @@ ...@@ -53,9 +53,18 @@
}; };
var itemTemplate = function(item) { var itemTemplate = function(item) {
var style = {}; // Define properties
var className = (item.class_name) ? item.class_name : ""; var className = (item.class_name) ? item.class_name : "";
var tabindex = 0; if (item.has_image) {
className += " " + "option-with-image";
}
var attributes = {
'draggable': !item.drag_disabled,
'aria-grabbed': item.grabbed,
'data-value': item.value,
'data-drag-disabled': item.drag_disabled
};
var style = {};
if (item.background_color) { if (item.background_color) {
style['background-color'] = item.background_color; style['background-color'] = item.background_color;
} }
...@@ -68,34 +77,42 @@ ...@@ -68,34 +77,42 @@
if (item.is_placed) { if (item.is_placed) {
style.left = item.x_percent + "%"; style.left = item.x_percent + "%";
style.top = item.y_percent + "%"; style.top = item.y_percent + "%";
tabindex = -1; // If an item has been placed it can no longer be interacted with, } else {
// so remove the ability to move focus to it using the keyboard // If an item has not been placed it must be possible to move focus to it using the keyboard:
} attributes.tabindex = 0;
if (item.has_image) {
className += " " + "option-with-image";
} }
var content_html = item.displayName; // Define children
var children = [
itemSpinnerTemplate(item.xhr_active),
itemInputTemplate(item.input)
];
var item_content_html = item.displayName;
if (item.imageURL) { if (item.imageURL) {
content_html = '<img src="' + item.imageURL + '" alt="' + item.imageDescription + '" />'; item_content_html = '<img src="' + item.imageURL + '" alt="' + item.imageDescription + '" />';
}
var item_content = h('div', { innerHTML: item_content_html, className: "item-content" });
if (item.is_placed) {
// Insert information about zone in which this item has been placed
var item_description_id = 'item-' + item.value + '-description';
item_content.properties.attributes = { 'aria-describedby': item_description_id };
var item_description = h(
'div',
{ id: item_description_id, className: 'sr' },
gettext('Correctly placed in: ') + item.zone
);
children.splice(1, 0, item_description);
} }
children.splice(1, 0, item_content);
return ( return (
h('div.option', h(
'div.option',
{ {
key: item.value, key: item.value,
className: className, className: className,
attributes: { attributes: attributes,
'tabindex': tabindex,
'draggable': !item.drag_disabled,
'aria-grabbed': item.grabbed,
'data-value': item.value,
'data-drag-disabled': item.drag_disabled
},
style: style style: style
}, [ },
itemSpinnerTemplate(item.xhr_active), children
h('div', {innerHTML: content_html, className: "item-content"}),
itemInputTemplate(item.input)
]
) )
); );
}; };
...@@ -132,10 +149,44 @@ ...@@ -132,10 +149,44 @@
var properties = { attributes: { 'aria-live': 'polite' } }; var properties = { attributes: { 'aria-live': 'polite' } };
return ( return (
h('section.feedback', properties, [ h('section.feedback', properties, [
h('div.reset-button', {style: {display: reset_button_display}}, gettext('Reset exercise')), h(
h('h3.title1', {style: {display: feedback_display}}, gettext('Feedback')), 'a.reset-button',
h('p.message', {style: {display: feedback_display}, { style: { display: reset_button_display }, attributes: { tabindex: 0 }},
innerHTML: ctx.feedback_html}) gettext('Reset exercise')
),
h('h3.title1', { style: { display: feedback_display } }, gettext('Feedback')),
h('p.message', { style: { display: feedback_display }, innerHTML: ctx.feedback_html })
])
);
};
var keyboardHelpTemplate = function(ctx) {
var dialog_attributes = { role: 'dialog', 'aria-labelledby': 'modal-window-title' };
var dialog_style = {};
return (
h('section.keyboard-help', [
h('a.keyboard-help-button', { attributes: { tabindex: 0 } }, gettext('Keyboard Help')),
h('div.keyboard-help-dialog', [
h('div.modal-window-overlay'),
h('div.modal-window', { attributes: dialog_attributes, style: dialog_style }, [
h('div.modal-header', [
h('h2.modal-window-title', gettext('Keyboard Help'))
]),
h('div.modal-content', [
h('p', gettext('You can complete this exercise using only your keyboard.')),
h('ul', [
h('li', gettext('Use "Tab" and "Shift-Tab" to navigate between items and zones.')),
h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, then navigate to the zone you want to drop it on.')),
h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" to drop the item on the current zone.')),
h('li', gettext('Press "Escape" if you want to cancel the drop operation (e.g. because you would like to select a different item).')),
h('li', gettext('Press "?" at any time to bring up this dialog.')),
])
]),
h('div.modal-actions', [
h('button.modal-dismiss-button', gettext("OK"))
])
])
])
]) ])
); );
}; };
...@@ -156,10 +207,17 @@ ...@@ -156,10 +207,17 @@
h('section.drag-container', [ h('section.drag-container', [
h('div.item-bank', renderCollection(itemTemplate, items_in_bank, ctx)), h('div.item-bank', renderCollection(itemTemplate, items_in_bank, ctx)),
h('div.target', [ h('div.target', [
h('div.popup', {style: {display: ctx.popup_html ? 'block' : 'none'}}, [ h(
'div.popup',
{
style: {display: ctx.popup_html ? 'block' : 'none'},
attributes: {'aria-live': 'polite'}
},
[
h('div.close.icon-remove-sign.fa-times-circle'), h('div.close.icon-remove-sign.fa-times-circle'),
h('p.popup-content', {innerHTML: ctx.popup_html}), h('p.popup-content', {innerHTML: ctx.popup_html}),
]), ]
),
h('div.target-img-wrapper', [ h('div.target-img-wrapper', [
h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}), h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
]), ]),
...@@ -167,6 +225,7 @@ ...@@ -167,6 +225,7 @@
renderCollection(itemTemplate, items_placed, ctx), renderCollection(itemTemplate, items_placed, ctx),
]), ]),
]), ]),
keyboardHelpTemplate(ctx),
feedbackTemplate(ctx), feedbackTemplate(ctx),
]) ])
); );
......
...@@ -57,6 +57,24 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -57,6 +57,24 @@ class BaseIntegrationTest(SeleniumBaseTest):
def _get_zones(self): def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone") return self._page.find_elements_by_css_selector(".drag-container .zone")
def _get_popup(self):
return self._page.find_element_by_css_selector(".popup")
def _get_popup_content(self):
return self._page.find_element_by_css_selector(".popup .popup-content")
def _get_keyboard_help(self):
return self._page.find_element_by_css_selector(".keyboard-help")
def _get_keyboard_help_button(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-button")
def _get_keyboard_help_dialog(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-dialog")
def _get_reset_button(self):
return self._page.find_element_by_css_selector('.reset-button')
def _get_feedback(self): def _get_feedback(self):
return self._page.find_element_by_css_selector(".feedback") return self._page.find_element_by_css_selector(".feedback")
......
...@@ -179,6 +179,30 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -179,6 +179,30 @@ class TestDragAndDropRender(BaseIntegrationTest):
# Zone description should only be visible to screen readers: # Zone description should only be visible to screen readers:
self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr') self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr')
def test_popup(self):
self.load_scenario()
popup = self._get_popup()
popup_content = self._get_popup_content()
self.assertFalse(popup.is_displayed())
self.assertEqual(popup.get_attribute('aria-live'), 'polite')
self.assertEqual(popup_content.text, "")
def test_keyboard_help(self):
self.load_scenario()
self._get_keyboard_help()
keyboard_help_button = self._get_keyboard_help_button()
keyboard_help_dialog = self._get_keyboard_help_dialog()
dialog_modal_overlay = keyboard_help_dialog.find_element_by_css_selector('.modal-window-overlay')
dialog_modal = keyboard_help_dialog.find_element_by_css_selector('.modal-window')
self.assertEqual(keyboard_help_button.get_attribute('tabindex'), '0')
self.assertFalse(dialog_modal_overlay.is_displayed())
self.assertFalse(dialog_modal.is_displayed())
self.assertEqual(dialog_modal.get_attribute('role'), 'dialog')
self.assertEqual(dialog_modal.get_attribute('aria-labelledby'), 'modal-window-title')
def test_feedback(self): def test_feedback(self):
self.load_scenario() self.load_scenario()
......
...@@ -87,6 +87,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -87,6 +87,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
}) })
def test_do_attempt_with_input(self): def test_do_attempt_with_input(self):
# Drop item that requires numerical input
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "0%", "y_percent": "85%"} data = {"val": 1, "zone": self.ZONE_2, "x_percent": "0%", "y_percent": "85%"}
res = self.call_handler('do_attempt', data) res = self.call_handler('do_attempt', data)
self.assertEqual(res, { self.assertEqual(res, {
...@@ -99,13 +100,16 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -99,13 +100,16 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
'items': { 'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "correct_input": False}, "1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
},
}, },
'finished': False, 'finished': False,
'overall_feedback': self.initial_feedback(), 'overall_feedback': self.initial_feedback(),
} }
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET")) self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit incorrect value
data = {"val": 1, "input": "250"} data = {"val": 1, "input": "250"}
res = self.call_handler('do_attempt', data) res = self.call_handler('do_attempt', data)
self.assertEqual(res, { self.assertEqual(res, {
...@@ -118,13 +122,17 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -118,13 +122,17 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
'items': { 'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "input": "250", "correct_input": False}, "1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
"input": "250",
},
}, },
'finished': False, 'finished': False,
'overall_feedback': self.initial_feedback(), 'overall_feedback': self.initial_feedback(),
} }
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET")) self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit correct value
data = {"val": 1, "input": "103"} data = {"val": 1, "input": "103"}
res = self.call_handler('do_attempt', data) res = self.call_handler('do_attempt', data)
self.assertEqual(res, { self.assertEqual(res, {
...@@ -137,7 +145,10 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -137,7 +145,10 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
'items': { 'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "input": "103", "correct_input": True}, "1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": True, "zone": self.ZONE_2,
"input": "103",
},
}, },
'finished': False, 'finished': False,
'overall_feedback': self.initial_feedback(), 'overall_feedback': self.initial_feedback(),
...@@ -177,7 +188,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -177,7 +188,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
"items": { "items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True} "0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1}
}, },
"finished": False, "finished": False,
'overall_feedback': self.initial_feedback(), 'overall_feedback': self.initial_feedback(),
...@@ -198,8 +209,13 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -198,8 +209,13 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
"items": { "items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True}, "0": {
"1": {"x_percent": "22%", "y_percent": "22%", "input": "99", "correct_input": True} "x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1,
},
"1": {
"x_percent": "22%", "y_percent": "22%", "correct_input": True, "zone": self.ZONE_2,
"input": "99",
}
}, },
"finished": True, "finished": True,
'overall_feedback': self.FINAL_FEEDBACK, 'overall_feedback': self.FINAL_FEEDBACK,
......
import unittest import unittest
from drag_and_drop_v2.default_data import ( from drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA TARGET_IMG_DESCRIPTION, TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
) )
from ..utils import make_block, TestCaseMixin from ..utils import make_block, TestCaseMixin
...@@ -62,25 +63,25 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -62,25 +63,25 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
assert_user_state_empty() assert_user_state_empty()
# Drag three items into the correct spot: # Drag three items into the correct spot:
data = {"val": 0, "zone": "The Top Zone", "x_percent": "33%", "y_percent": "11%"} data = {"val": 0, "zone": TOP_ZONE_TITLE, "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data) self.call_handler('do_attempt', data)
data = {"val": 1, "zone": "The Middle Zone", "x_percent": "67%", "y_percent": "80%"} data = {"val": 1, "zone": MIDDLE_ZONE_TITLE, "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data) self.call_handler('do_attempt', data)
data = {"val": 2, "zone": "The Bottom Zone", "x_percent": "99%", "y_percent": "95%"} data = {"val": 2, "zone": BOTTOM_ZONE_TITLE, "x_percent": "99%", "y_percent": "95%"}
self.call_handler('do_attempt', data) self.call_handler('do_attempt', data)
# Check the result: # Check the result:
self.assertTrue(self.block.completed) self.assertTrue(self.block.completed)
self.assertEqual(self.block.item_state, { self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%'}, '0': {'x_percent': '33%', 'y_percent': '11%', 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%'}, '1': {'x_percent': '67%', 'y_percent': '80%', 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%'}, '2': {'x_percent': '99%', 'y_percent': '95%', 'zone': BOTTOM_ZONE_TITLE},
}) })
self.assertEqual(self.call_handler('get_user_state'), { self.assertEqual(self.call_handler('get_user_state'), {
'items': { 'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True}, '0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True, 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True}, '1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True, 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True}, '2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True, 'zone': BOTTOM_ZONE_TITLE},
}, },
'finished': True, 'finished': True,
'overall_feedback': FINISH_FEEDBACK, 'overall_feedback': FINISH_FEEDBACK,
......
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