Commit 69ee413d by Matjaz Gregoric Committed by GitHub

Merge pull request #112 from open-craft/mtyaka/a11y-interaction-issues

a11y Interaction Improvements
parents c773d9fa 85262c32
...@@ -12,7 +12,7 @@ install: ...@@ -12,7 +12,7 @@ install:
- "pip install selenium==2.53.0" - "pip install selenium==2.53.0"
- "pip uninstall -y xblock-drag-and-drop-v2" - "pip uninstall -y xblock-drag-and-drop-v2"
- "python setup.py sdist" - "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-2.0.13.tar.gz" - "pip install dist/xblock-drag-and-drop-v2-2.0.14.tar.gz"
script: script:
- pep8 drag_and_drop_v2 tests --max-line-length=120 - pep8 drag_and_drop_v2 tests --max-line-length=120
- pylint drag_and_drop_v2 - pylint drag_and_drop_v2
......
Version 2.0.14 (2017-01-17)
---------------------------
* Various accessibility improvements (PRs #110, #111, #112)
Version 2.0.13 (2017-01-02)
---------------------------
* i18n improvements (PR #113)
Version 2.0.12 (2016-11-08) Version 2.0.12 (2016-11-08)
--------------------------- ---------------------------
......
...@@ -8,7 +8,8 @@ function DragAndDropTemplates(configuration) { ...@@ -8,7 +8,8 @@ function DragAndDropTemplates(configuration) {
} }
return ( return (
h("div.spinner-wrapper", {key: item.value + '-spinner'}, [ h("div.spinner-wrapper", {key: item.value + '-spinner'}, [
h("i.fa.fa-spin.fa-spinner") h("span.fa.fa-spin.fa-spinner", {attributes: {'aria-hidden': true}}),
h("span.sr", gettext('Submitting'))
]) ])
); );
}; };
...@@ -53,7 +54,6 @@ function DragAndDropTemplates(configuration) { ...@@ -53,7 +54,6 @@ function DragAndDropTemplates(configuration) {
var itemTemplate = function(item, ctx) { var itemTemplate = function(item, ctx) {
// Define properties // Define properties
var descriptionClassName = "sr description";
var className = (item.class_name) ? item.class_name : ""; var className = (item.class_name) ? item.class_name : "";
var zone = getZone(item.zone, ctx) || {}; var zone = getZone(item.zone, ctx) || {};
if (item.has_image) { if (item.has_image) {
...@@ -119,8 +119,8 @@ function DragAndDropTemplates(configuration) { ...@@ -119,8 +119,8 @@ function DragAndDropTemplates(configuration) {
var item_description_id = configuration.url_name + '-item-' + item.value + '-description'; var item_description_id = configuration.url_name + '-item-' + item.value + '-description';
item_content.properties.attributes = { 'aria-describedby': item_description_id }; item_content.properties.attributes = { 'aria-describedby': item_description_id };
item_description = h( item_description = h(
'div', 'div.sr.description',
{ key: item.value + '-description', id: item_description_id, className: descriptionClassName }, { key: item_description_id, id: item_description_id},
description_content description_content
); );
} }
...@@ -185,7 +185,7 @@ function DragAndDropTemplates(configuration) { ...@@ -185,7 +185,7 @@ function DragAndDropTemplates(configuration) {
var item_wrapper = 'div.item-wrapper.item-align.item-align-' + zone.align; var item_wrapper = 'div.item-wrapper.item-align.item-align-' + zone.align;
var is_item_in_zone = function(i) { return i.is_placed && (i.zone === zone.uid); }; var is_item_in_zone = function(i) { return i.is_placed && (i.zone === zone.uid); };
var items_in_zone = $.grep(ctx.items, is_item_in_zone); var items_in_zone = $.grep(ctx.items, is_item_in_zone);
var zone_description_id = configuration.url_name + '-zone-' + zone.uid + '-description'; var zone_description_id = zone.prefixed_uid + '-description';
if (items_in_zone.length == 0) { if (items_in_zone.length == 0) {
var zone_description = h( var zone_description = h(
'div', 'div',
...@@ -221,8 +221,15 @@ function DragAndDropTemplates(configuration) { ...@@ -221,8 +221,15 @@ function DragAndDropTemplates(configuration) {
} }
}, },
[ [
h('p', { className: className }, zone.title), h(
h('p', { className: 'zone-description sr' }, zone.description || gettext("droppable")), 'p',
{ className: className },
[
zone.title,
h('span.sr', gettext(', dropzone'))
]
),
h('p', { className: 'zone-description sr' }, zone.description || gettext('droppable')),
h(item_wrapper, renderCollection(itemTemplate, items_in_zone, ctx)), h(item_wrapper, renderCollection(itemTemplate, items_in_zone, ctx)),
zone_description zone_description
] ]
...@@ -242,10 +249,10 @@ function DragAndDropTemplates(configuration) { ...@@ -242,10 +249,10 @@ function DragAndDropTemplates(configuration) {
}); });
return ( return (
h('section.feedback', {}, [ h('div.feedback', {attributes: {'role': 'group', 'aria-label': gettext('Feedback')}}, [
h( h(
"div.feedback-content", "div.feedback-content",
{ attributes: { 'aria-live': 'polite' } }, {},
[ [
h('h3.title1', { style: { display: feedback_display } }, gettext('Feedback')), h('h3.title1', { style: { display: feedback_display } }, gettext('Feedback')),
h('div.messages', { style: { display: feedback_display } }, feedback_messages), h('div.messages', { style: { display: feedback_display } }, feedback_messages),
...@@ -286,27 +293,42 @@ function DragAndDropTemplates(configuration) { ...@@ -286,27 +293,42 @@ function DragAndDropTemplates(configuration) {
}; };
var submitAnswerTemplate = function(ctx) { var submitAnswerTemplate = function(ctx) {
var attemptsUsedId = "attempts-used-"+configuration.url_name; var submitButtonProperties = {
var attemptsUsedDisplay = (ctx.max_attempts && ctx.max_attempts > 0) ? 'inline': 'none'; disabled: ctx.disable_submit_button || ctx.submit_spinner,
attributes: {}
};
var attemptsUsedInfo = null;
if (ctx.max_attempts && ctx.max_attempts > 0) {
var attemptsUsedId = "attempts-used-" + configuration.url_name;
submitButtonProperties.attributes["aria-describedby"] = attemptsUsedId;
var attemptsUsedTemplate = gettext("You have used {used} of {total} attempts.");
var attemptsUsedText = attemptsUsedTemplate.
replace("{used}", ctx.attempts).replace("{total}", ctx.max_attempts);
attemptsUsedInfo = h("span.attempts-used", {id: attemptsUsedId}, attemptsUsedText);
}
var submitSpinner = null;
if (ctx.submit_spinner) {
submitSpinner = h('span', [
h('span.fa.fa-spin.fa-spinner', {attributes: {'aria-hidden': true}}),
h('span.sr', gettext('Submitting'))
]);
}
return ( return (
h("section.action-toolbar-item.submit-answer", {}, [ h("div.action-toolbar-item.submit-answer", {}, [
h( h(
"button.btn-brand.submit-answer-button", "button.btn-brand.submit-answer-button",
{ submitButtonProperties,
disabled: ctx.disable_submit_button || ctx.submit_spinner, [
attributes: {"aria-describedby": attemptsUsedId}}, submitSpinner,
[ ' ', // whitespace between spinner icon and text
(ctx.submit_spinner ? h("span.fa.fa-spin.fa-spinner") : null), gettext("Submit")
gettext("Submit") ]
] ),
), attemptsUsedInfo
h( ])
"span.attempts-used#"+attemptsUsedId, {style: {display: attemptsUsedDisplay}},
gettext("You have used {used} of {total} attempts.")
.replace("{used}", ctx.attempts).replace("{total}", ctx.max_attempts)
)
])
); );
}; };
...@@ -351,7 +373,7 @@ function DragAndDropTemplates(configuration) { ...@@ -351,7 +373,7 @@ function DragAndDropTemplates(configuration) {
go_to_beginning_button_class += ' sr'; go_to_beginning_button_class += ' sr';
} }
return( return(
h("section.action-toolbar-item.sidebar-buttons", {}, [ h("div.action-toolbar-item.sidebar-buttons", {}, [
sidebarButtonTemplate( sidebarButtonTemplate(
go_to_beginning_button_class, go_to_beginning_button_class,
"fa-arrow-up", "fa-arrow-up",
...@@ -397,13 +419,7 @@ function DragAndDropTemplates(configuration) { ...@@ -397,13 +419,7 @@ function DragAndDropTemplates(configuration) {
return h( return h(
popupSelector, popupSelector,
{ {
style: {display: have_messages ? 'block' : 'none'}, style: {display: have_messages ? 'block' : 'none'}
attributes: {
"tabindex": "-1",
'aria-live': 'polite',
'aria-atomic': 'true',
'aria-relevant': 'additions',
}
}, },
[ [
h( h(
...@@ -491,18 +507,24 @@ function DragAndDropTemplates(configuration) { ...@@ -491,18 +507,24 @@ function DragAndDropTemplates(configuration) {
return h('div.problem-progress', { return h('div.problem-progress', {
id: configuration.url_name + '-problem-progress', id: configuration.url_name + '-problem-progress',
attributes: {'role': 'status', 'aria-live': 'polite'} attributes: {role: 'status'}
}, progress_text); }, progress_text);
}; };
var mainTemplate = function(ctx) { var mainTemplate = function(ctx) {
var main_element_properties = {attributes: {role: 'group'}};
var problemProgress = progressTemplate(ctx); var problemProgress = progressTemplate(ctx);
var problemTitle = null; var problemTitle = null;
if (ctx.show_title) { if (ctx.show_title) {
var problem_title_id = configuration.url_name + '-problem-title';
problemTitle = h('h3.problem-title', { problemTitle = h('h3.problem-title', {
id: problem_title_id,
innerHTML: ctx.title_html, innerHTML: ctx.title_html,
attributes: {'aria-describedby': problemProgress.properties.id} attributes: {'aria-describedby': problemProgress.properties.id}
}); });
main_element_properties.attributes['arial-labelledby'] = problem_title_id;
} else {
main_element_properties.attributes['aria-label'] = gettext('Drag and Drop Problem');
} }
var problemHeader = ctx.show_problem_header ? h('h4.title1', gettext('Problem')) : null; var problemHeader = ctx.show_problem_header ? h('h4.title1', gettext('Problem')) : null;
// Render only items in the bank here, including placeholders. Placed // Render only items in the bank here, including placeholders. Placed
...@@ -523,36 +545,37 @@ function DragAndDropTemplates(configuration) { ...@@ -523,36 +545,37 @@ function DragAndDropTemplates(configuration) {
item_bank_properties.attributes['role'] = 'button'; item_bank_properties.attributes['role'] = 'button';
} }
return ( return (
h('section.themed-xblock.xblock--drag-and-drop', [ h('div.themed-xblock.xblock--drag-and-drop', main_element_properties, [
problemTitle, problemTitle,
problemProgress, problemProgress,
h('div', [forwardKeyboardHelpButtonTemplate(ctx)]), h('div', [forwardKeyboardHelpButtonTemplate(ctx)]),
h('section.problem', [ h('div.problem', [
problemHeader, problemHeader,
h('p', {innerHTML: ctx.problem_html}), h('p', {innerHTML: ctx.problem_html}),
]), ]),
h('section.drag-container', {}, [ h('div.drag-container', {}, [
h('div.item-bank', item_bank_properties, [ h('div.item-bank', item_bank_properties, [
renderCollection(itemTemplate, items_in_bank, ctx), renderCollection(itemTemplate, items_in_bank, ctx),
renderCollection(itemPlaceholderTemplate, items_placed, ctx) renderCollection(itemPlaceholderTemplate, items_placed, ctx)
]), ]),
h('div.target', h('div.target', {attributes: {'role': 'group', 'arial-label': gettext('Drop Targets')}}, [
{}, itemFeedbackPopupTemplate(ctx),
[ h('div.target-img-wrapper', [
itemFeedbackPopupTemplate(ctx), h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
h('div.target-img-wrapper', [ ]),
h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
]
),
renderCollection(zoneTemplate, ctx.zones, ctx) renderCollection(zoneTemplate, ctx.zones, ctx)
]), ]),
]), ]),
h("section.actions-toolbar", {}, [ h("div.actions-toolbar", {attributes: {'role': 'group', 'aria-label': gettext('Actions')}}, [
sidebarTemplate(ctx), sidebarTemplate(ctx),
(ctx.show_submit_answer ? submitAnswerTemplate(ctx) : null), (ctx.show_submit_answer ? submitAnswerTemplate(ctx) : null),
]), ]),
keyboardHelpPopupTemplate(ctx), keyboardHelpPopupTemplate(ctx),
feedbackTemplate(ctx), feedbackTemplate(ctx),
h('div.sr.reader-feedback-area', {
attributes: {'aria-live': 'polite', 'aria-atomic': true},
innerHTML: ctx.screen_reader_messages
}),
]) ])
); );
}; };
...@@ -664,6 +687,9 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -664,6 +687,9 @@ function DragAndDropBlock(runtime, element, configuration) {
// to watch for load events on any child element, since load events do not bubble. // to watch for load events on any child element, since load events do not bubble.
element.addEventListener('load', webkitFix, true); element.addEventListener('load', webkitFix, true);
// Remove the spinner and create a blank slate for virtualDom to take over.
$root.empty();
applyState(); applyState();
initDroppable(); initDroppable();
...@@ -904,7 +930,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -904,7 +930,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 +938,51 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -912,6 +938,51 @@ function DragAndDropBlock(runtime, element, configuration) {
__vdom = new_vdom; __vdom = new_vdom;
}; };
var sr_clear_timeout = null;
var setScreenReaderMessages = function() {
clearTimeout(sr_clear_timeout);
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)
);
}
}
var paragraphs = messages.map(function(msg) {
return '<p>' + msg + '</p>';
});
state.screen_reader_messages = paragraphs.join('');
// Remove the text on next redraw. This will make screen readers read the message again,
// next time the user performs an action, even if next feedback message did not change from
// last attempt (for example: if user drops the same item on two wrong zones one after another,
// the negative feedback should be read out twice, not only on first drop).
sr_clear_timeout = setTimeout(function() {
state.screen_reader_messages = '';
}, 0);
};
var publishEvent = function(data) { var publishEvent = function(data) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
...@@ -981,16 +1052,18 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -981,16 +1052,18 @@ function DragAndDropBlock(runtime, element, configuration) {
return false; return false;
}; };
var placeItem = function($zone, $item) { var placeGrabbedItem = function($zone) {
var item_id;
if ($item !== undefined) {
item_id = $item.data('value');
} else {
item_id = $selectedItem.data('value');
}
var zone = String($zone.data('uid')); var zone = String($zone.data('uid'));
var zone_align = $zone.data('zone_align'); var zone_align = $zone.data('zone_align');
var items = configuration.items;
var item_id;
for (var i = 0; i < items.length; i++) {
if (items[i].grabbed) {
item_id = items[i].id;
break;
}
}
var items_in_zone_count = countItemsInZone(zone, [item_id.toString()]); var items_in_zone_count = countItemsInZone(zone, [item_id.toString()]);
if (configuration.max_items_per_zone && configuration.max_items_per_zone <= items_in_zone_count) { if (configuration.max_items_per_zone && configuration.max_items_per_zone <= items_in_zone_count) {
...@@ -1036,18 +1109,17 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1036,18 +1109,17 @@ function DragAndDropBlock(runtime, element, configuration) {
} else if (isCancelKey(evt)) { } else if (isCancelKey(evt)) {
evt.preventDefault(); evt.preventDefault();
state.keyboard_placement_mode = false; state.keyboard_placement_mode = false;
releaseItem($selectedItem); releaseGrabbedItems();
} else if (isActionKey(evt)) { } else if (isActionKey(evt)) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
state.keyboard_placement_mode = false; state.keyboard_placement_mode = false;
releaseItem($selectedItem);
if ($zone.is('.item-bank')) { if ($zone.is('.item-bank')) {
delete state.items[$selectedItem.data('value')]; delete state.items[$selectedItem.data('value')];
applyState();
} else { } else {
placeItem($zone); placeGrabbedItem($zone);
} }
releaseGrabbedItems();
} }
} else if (isTabKey(evt) && !evt.shiftKey) { } else if (isTabKey(evt) && !evt.shiftKey) {
// If the user just dropped an item to this zone, next TAB keypress // If the user just dropped an item to this zone, next TAB keypress
...@@ -1074,8 +1146,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1074,8 +1146,7 @@ function DragAndDropBlock(runtime, element, configuration) {
tolerance: 'pointer', tolerance: 'pointer',
drop: function(evt, ui) { drop: function(evt, ui) {
var $zone = $(this); var $zone = $(this);
var $item = ui.helper; placeGrabbedItem($zone);
placeItem($zone, $item);
} }
}); });
...@@ -1087,7 +1158,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1087,7 +1158,7 @@ function DragAndDropBlock(runtime, element, configuration) {
drop: function(evt, ui) { drop: function(evt, ui) {
var $item = ui.helper; var $item = ui.helper;
var item_id = $item.data('value'); var item_id = $item.data('value');
releaseItem($item); releaseGrabbedItems();
delete state.items[item_id]; delete state.items[item_id];
applyState(); applyState();
} }
...@@ -1140,7 +1211,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1140,7 +1211,7 @@ function DragAndDropBlock(runtime, element, configuration) {
stop: function(evt, ui) { stop: function(evt, ui) {
// Revert to original position. // Revert to original position.
$item.css($item.data('initial-position')); $item.css($item.data('initial-position'));
releaseItem($(this)); releaseGrabbedItems();
} }
}); });
} catch (e) { } catch (e) {
...@@ -1152,31 +1223,27 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1152,31 +1223,27 @@ function DragAndDropBlock(runtime, element, configuration) {
var grabItem = function($item, interaction_type) { var grabItem = function($item, interaction_type) {
var item_id = $item.data('value'); var item_id = $item.data('value');
setGrabbedState(item_id, true, interaction_type); configuration.items.forEach(function(item) {
if (item.id === item_id) {
item.grabbed = true;
item.grabbed_with = interaction_type;
} else {
item.grabbed = false;
delete item.grabbed_with;
}
});
closePopup(false); closePopup(false);
// applyState(true) skips destroying and initializing draggable // applyState(true) skips destroying and initializing draggable
applyState(true); applyState(true);
}; };
var releaseItem = function($item) { var releaseGrabbedItems = function() {
var item_id = $item.data('value');
setGrabbedState(item_id, false);
// applyState(true) skips destroying and initializing draggable
applyState(true);
};
var setGrabbedState = function(item_id, grabbed, interaction_type) {
configuration.items.forEach(function(item) { configuration.items.forEach(function(item) {
if (item.id === item_id) { item.grabbed = false;
if (grabbed) { delete item.grabbed_with;
item.grabbed = true;
item.grabbed_with = interaction_type;
} else {
item.grabbed = false;
delete item.grabbed_with;
}
}
}); });
// applyState(true) skips destroying and initializing draggable
applyState(true);
}; };
var destroyDraggable = function() { var destroyDraggable = function() {
...@@ -1220,6 +1287,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1220,6 +1287,7 @@ function DragAndDropBlock(runtime, element, configuration) {
state.finished = true; state.finished = true;
state.overall_feedback = data.overall_feedback; state.overall_feedback = data.overall_feedback;
} }
setScreenReaderMessages();
} }
applyState(); applyState();
if (state.feedback && state.feedback.length > 0) { if (state.feedback && state.feedback.length > 0) {
...@@ -1324,6 +1392,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1324,6 +1392,7 @@ function DragAndDropBlock(runtime, element, configuration) {
} else { } else {
state.finished = true; state.finished = true;
} }
setScreenReaderMessages();
}).always(function() { }).always(function() {
state.submit_spinner = false; state.submit_spinner = false;
applyState(); applyState();
...@@ -1451,7 +1520,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1451,7 +1520,8 @@ function DragAndDropBlock(runtime, element, configuration) {
showing_answer: state.showing_answer, showing_answer: state.showing_answer,
show_answer_spinner: state.show_answer_spinner, show_answer_spinner: state.show_answer_spinner,
disable_go_to_beginning_button: !canGoToBeginning(), disable_go_to_beginning_button: !canGoToBeginning(),
show_go_to_beginning_button: state.go_to_beginning_button_visible show_go_to_beginning_button: state.go_to_beginning_button_visible,
screen_reader_messages: (state.screen_reader_messages || '')
}; };
return renderView(context); return renderView(context);
......
{% load i18n %} {% load i18n %}
<section class="themed-xblock xblock--drag-and-drop"> <div class="themed-xblock xblock--drag-and-drop">
<i class="fa fa-spin fa-spinner initial-load-spinner"></i>{% trans "Loading drag and drop problem." %} <span class="fa fa-spin fa-spinner initial-load-spinner" aria-hidden="true"></span>
</section> {% trans "Loading drag and drop problem." %}
</div>
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
<div class="xblock--drag-and-drop--editor editor-with-buttons"> <div class="xblock--drag-and-drop--editor editor-with-buttons">
{{ js_templates|safe }} {{ js_templates|safe }}
<section class="drag-builder"> <div class="drag-builder">
<div class="tab feedback-tab"> <section class="tab feedback-tab">
<header class="tab-header">
<section class="tab-content"> <h3>{% trans "Basic Settings" %}</h3>
</header>
<div class="tab-content">
<form class="feedback-form"> <form class="feedback-form">
<label class="h4"> <label class="h4">
<span>{% trans fields.display_name.display_name %}</span> <span>{% trans fields.display_name.display_name %}</span>
...@@ -80,14 +82,14 @@ ...@@ -80,14 +82,14 @@
<textarea class="final-feedback">{{ self.data.feedback.finish }}</textarea> <textarea class="final-feedback">{{ self.data.feedback.finish }}</textarea>
</label> </label>
</form> </form>
</section> </div>
</div> </section>
<div class="tab zones-tab hidden"> <section class="tab zones-tab hidden">
<header class="tab-header"> <header class="tab-header">
<h3>{% trans "Zones" %}</h3> <h3>{% trans "Zones" %}</h3>
</header> </header>
<section class="tab-content"> <div class="tab-content">
<form class="target-image-form"> <form class="target-image-form">
<label class="h4" for="background-url-{{id_suffix}}"> <label class="h4" for="background-url-{{id_suffix}}">
<span>{% trans "Background URL" %}</span> <span>{% trans "Background URL" %}</span>
...@@ -114,8 +116,8 @@ ...@@ -114,8 +116,8 @@
{% endblocktrans %} {% endblocktrans %}
</div> </div>
</form> </form>
</section> </div>
<section class="tab-content"> <div class="tab-content">
<form class="display-labels-form"> <form class="display-labels-form">
<h4>{% trans "Zone labels" %}</h4> <h4>{% trans "Zone labels" %}</h4>
<label class="checkbox-label"> <label class="checkbox-label">
...@@ -130,8 +132,8 @@ ...@@ -130,8 +132,8 @@
<span>{% trans "Display zone borders on the image" %}</span> <span>{% trans "Display zone borders on the image" %}</span>
</label> </label>
</form> </form>
</section> </div>
<section class="tab-content"> <div class="tab-content">
<h4>{% trans "Zone definitions" %}</h4> <h4>{% trans "Zone definitions" %}</h4>
<div class="zone-editor"> <div class="zone-editor">
<div class="controls"> <div class="controls">
...@@ -147,14 +149,14 @@ ...@@ -147,14 +149,14 @@
</div> </div>
</div> </div>
</div> </div>
</section> </div>
</div> </section>
<div class="tab items-tab hidden"> <section class="tab items-tab hidden">
<header class="tab-header"> <header class="tab-header">
<h3>{% trans "Items" %}</h3> <h3>{% trans "Items" %}</h3>
</header> </header>
<section class="tab-content"> <div class="tab-content">
<form class="item-styles-form"> <form class="item-styles-form">
<label class="h4"> <label class="h4">
<span>{% trans fields.item_background_color.display_name %}</span> <span>{% trans fields.item_background_color.display_name %}</span>
...@@ -184,18 +186,18 @@ ...@@ -184,18 +186,18 @@
{% trans fields.max_items_per_zone.help %} {% trans fields.max_items_per_zone.help %}
</div> </div>
</form> </form>
</section> </div>
<section class="tab-content"> <div class="tab-content">
<h4>{% trans "Item definitions" %}</h4> <h4>{% trans "Item definitions" %}</h4>
<form class="items-form"></form> <form class="items-form"></form>
<button class="btn add-item add-element"> <button class="btn add-item add-element">
<span class="icon add" aria-hidden="true"></span> <span class="icon add" aria-hidden="true"></span>
{% trans "Add an item" %} {% trans "Add an item" %}
</button> </button>
</section> </div>
</div> </section>
</section> </div>
<div class="xblock-actions"> <div class="xblock-actions">
<ul class="action-buttons"> <ul class="action-buttons">
......
...@@ -595,6 +595,31 @@ msgid_plural "{possible} points possible (ungraded)" ...@@ -595,6 +595,31 @@ msgid_plural "{possible} points possible (ungraded)"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:374
#: drag_and_drop_v2/public/js/drag_and_drop.js:839
msgid "Hints:"
msgstr ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:224
msgid ", dropzone"
msgstr ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:479
msgid "Drag and Drop Problem"
msgstr ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:509
msgid "Drop Targets"
msgstr ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:517
msgid "Actions"
msgstr ""
#: drag_and_drop_v2/public/js/drag_and_drop.js:12
msgid "Submitting"
msgstr ""
#: utils.py:44 #: utils.py:44
msgid "Your highest score is {score}" msgid "Your highest score is {score}"
msgstr "" msgstr ""
......
...@@ -694,6 +694,31 @@ msgstr[1] "{possible} pöïnts pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυ ...@@ -694,6 +694,31 @@ msgstr[1] "{possible} pöïnts pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυ
msgid "Close" msgid "Close"
msgstr "Çlösé Ⱡ'σяєм ιρѕ#" msgstr "Çlösé Ⱡ'σяєм ιρѕ#"
#: drag_and_drop_v2/public/js/drag_and_drop.js:374
#: drag_and_drop_v2/public/js/drag_and_drop.js:839
msgid "Hints:"
msgstr "Hïnts: Ⱡ'σяєм ιρѕυ#"
#: drag_and_drop_v2/public/js/drag_and_drop.js:224
msgid ", dropzone"
msgstr ", dröpzöné Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: drag_and_drop_v2/public/js/drag_and_drop.js:479
msgid "Drag and Drop Problem"
msgstr "Dräg änd Dröp Prößlém Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: drag_and_drop_v2/public/js/drag_and_drop.js:509
msgid "Drop Targets"
msgstr "Dröp Tärgéts Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: drag_and_drop_v2/public/js/drag_and_drop.js:517
msgid "Actions"
msgstr "Àçtïöns Ⱡ'σяєм ιρѕυм #"
#: drag_and_drop_v2/public/js/drag_and_drop.js:12
msgid "Submitting"
msgstr "Süßmïttïng Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: utils.py:44 #: utils.py:44
msgid "Your highest score is {score}" msgid "Your highest score is {score}"
msgstr "Ýöür hïghést sçöré ïs {score} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" msgstr "Ýöür hïghést sçöré ïs {score} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
......
...@@ -23,7 +23,7 @@ def package_data(pkg, root_list): ...@@ -23,7 +23,7 @@ def package_data(pkg, root_list):
setup( setup(
name='xblock-drag-and-drop-v2', name='xblock-drag-and-drop-v2',
version='2.0.13', version='2.0.14',
description='XBlock - Drag-and-Drop v2', description='XBlock - Drag-and-Drop v2',
packages=['drag_and_drop_v2'], packages=['drag_and_drop_v2'],
install_requires=[ install_requires=[
......
...@@ -48,7 +48,7 @@ ItemDefinition = namedtuple( # pylint: disable=invalid-name ...@@ -48,7 +48,7 @@ ItemDefinition = namedtuple( # pylint: disable=invalid-name
class BaseIntegrationTest(SeleniumBaseTest): class BaseIntegrationTest(SeleniumBaseTest):
default_css_selector = 'section.themed-xblock.xblock--drag-and-drop' default_css_selector = '.themed-xblock.xblock--drag-and-drop'
module_name = __name__ module_name = __name__
_additional_escapes = { _additional_escapes = {
...@@ -239,22 +239,32 @@ class DefaultDataTestMixin(object): ...@@ -239,22 +239,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 +272,17 @@ class InteractionTestBase(object): ...@@ -262,15 +272,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]
...@@ -312,7 +324,7 @@ class InteractionTestBase(object): ...@@ -312,7 +324,7 @@ class InteractionTestBase(object):
both the HTML attribute and the DOM property are set to false. both the HTML attribute and the DOM property are set to false.
We work around that selenium bug by using JavaScript to get the correct value of 'draggable'. We work around that selenium bug by using JavaScript to get the correct value of 'draggable'.
""" """
script = "return $('div.option[data-value={}]').prop('draggable')".format(item_value) script = "return $('.option[data-value={}]').prop('draggable')".format(item_value)
return self.browser.execute_script(script) return self.browser.execute_script(script)
def assertDraggable(self, item_value): def assertDraggable(self, item_value):
...@@ -370,7 +382,7 @@ class InteractionTestBase(object): ...@@ -370,7 +382,7 @@ class InteractionTestBase(object):
item.send_keys("") item.send_keys("")
item.send_keys(action_key) item.send_keys(action_key)
# Focus is on first *zone* now # Focus is on first *zone* now
self.assert_grabbed_item(item) self.assert_item_grabbed(item)
# Get desired zone and figure out how many times we have to press Tab to focus the zone. # Get desired zone and figure out how many times we have to press Tab to focus the zone.
if zone_id is None: # moving back to the bank if zone_id is None: # moving back to the bank
zone = self._get_item_bank() zone = self._get_item_bank()
...@@ -387,9 +399,12 @@ class InteractionTestBase(object): ...@@ -387,9 +399,12 @@ class InteractionTestBase(object):
ActionChains(self.browser).send_keys(Keys.TAB).perform() ActionChains(self.browser).send_keys(Keys.TAB).perform()
zone.send_keys(action_key) zone.send_keys(action_key)
def assert_grabbed_item(self, item): def assert_item_grabbed(self, item):
self.assertEqual(item.get_attribute('aria-grabbed'), 'true') self.assertEqual(item.get_attribute('aria-grabbed'), 'true')
def assert_item_not_grabbed(self, item):
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
def assert_placed_item(self, item_value, zone_title, assessment_mode=False): def assert_placed_item(self, item_value, zone_title, assessment_mode=False):
item = self._get_placed_item_by_value(item_value) item = self._get_placed_item_by_value(item_value)
self.wait_until_visible(item) self.wait_until_visible(item)
...@@ -474,3 +489,10 @@ class InteractionTestBase(object): ...@@ -474,3 +489,10 @@ class InteractionTestBase(object):
def assert_button_enabled(self, submit_button, enabled=True): def assert_button_enabled(self, submit_button, enabled=True):
self.assertEqual(submit_button.is_enabled(), enabled) self.assertEqual(submit_button.is_enabled(), enabled)
def assert_reader_feedback_messages(self, expected_message_lines):
expected_paragraphs = ['<p>{}</p>'.format(l) for l in expected_message_lines]
expected_html = ''.join(expected_paragraphs)
feedback_area = self._page.find_element_by_css_selector('.reader-feedback-area')
actual_html = feedback_area.get_attribute('innerHTML')
self.assertEqual(actual_html, expected_html)
...@@ -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'])
......
...@@ -50,35 +50,92 @@ class ParameterizedTestsMixin(object): ...@@ -50,35 +50,92 @@ class ParameterizedTestsMixin(object):
ActionChains(self.browser).send_keys(Keys.TAB).perform() ActionChains(self.browser).send_keys(Keys.TAB).perform()
self.assertFocused(go_to_beginning_button) self.assertFocused(go_to_beginning_button)
def parameterized_item_positive_feedback_on_good_move( def parameterized_item_positive_feedback_on_good_move_standard(
self, items_map, scroll_down=100, action_key=None, assessment_mode=False self, items_map, scroll_down=100, action_key=None, feedback=None
): ):
if feedback is None:
feedback = self.feedback
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items # Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down) self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values(): items_with_zones = self._get_items_with_zone(items_map).values()
for i, definition in enumerate(items_with_zones):
self.place_item(definition.item_id, definition.zone_ids[0], action_key) self.place_item(definition.item_id, definition.zone_ids[0], action_key)
self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id)) self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id))
self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=assessment_mode) self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=False)
feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
if assessment_mode: self.assertEqual(feedback_popup_html, "<p>{}</p>".format(definition.feedback_positive))
self.assertEqual(feedback_popup_html, '') self.assert_popup_correct(popup)
self.assertFalse(popup.is_displayed()) self.assertTrue(popup.is_displayed())
if action_key: expected_sr_texts = [definition.feedback_positive]
# Next TAB keypress should move focus to "Go to Beginning button" if i == len(items_with_zones) - 1:
self._test_next_tab_goes_to_go_to_beginning_button() # We just dropped the last item, so the problem is done and we should see the final feedback.
overall_feedback = feedback['final']
else: else:
self.assertEqual(feedback_popup_html, "<p>{}</p>".format(definition.feedback_positive)) overall_feedback = feedback['intro']
self.assert_popup_correct(popup) expected_sr_texts.append(overall_feedback)
self.assert_reader_feedback_messages(expected_sr_texts)
self._test_popup_focus_and_close(popup, action_key)
def parameterized_item_positive_feedback_on_good_move_assessment(
self, items_map, scroll_down=100, action_key=None, feedback=None
):
if feedback is None:
feedback = self.feedback
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
items_with_zones = self._get_items_with_zone(items_map).values()
for definition in items_with_zones:
self.place_item(definition.item_id, definition.zone_ids[0], action_key)
self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id))
self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=True)
feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed())
self.assert_reader_feedback_messages([])
if action_key:
# Next TAB keypress should move focus to "Go to Beginning button"
self._test_next_tab_goes_to_go_to_beginning_button()
def parameterized_item_negative_feedback_on_bad_move_standard(
self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None
):
if feedback is None:
feedback = self.feedback
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in items_map.values():
zone_id, _ = self._get_incorrect_zone_for_item(definition, all_zones)
if zone_id is not None: # Some items may be placed in any zone, ignore those.
self.place_item(definition.item_id, zone_id, action_key)
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assert_popup_incorrect(popup)
self.assertTrue(popup.is_displayed()) self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id)
expected_sr_texts = [definition.feedback_negative, feedback['intro']]
self.assert_reader_feedback_messages(expected_sr_texts)
self._test_popup_focus_and_close(popup, action_key) self._test_popup_focus_and_close(popup, action_key)
def parameterized_item_negative_feedback_on_bad_move( def parameterized_item_negative_feedback_on_bad_move_assessment(
self, items_map, all_zones, scroll_down=100, action_key=None, assessment_mode=False self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None
): ):
if feedback is None:
feedback = self.feedback
popup = self._get_popup() popup = self._get_popup()
feedback_popup_content = self._get_popup_content() feedback_popup_content = self._get_popup_content()
...@@ -86,30 +143,17 @@ class ParameterizedTestsMixin(object): ...@@ -86,30 +143,17 @@ class ParameterizedTestsMixin(object):
self.scroll_down(pixels=scroll_down) self.scroll_down(pixels=scroll_down)
for definition in items_map.values(): for definition in items_map.values():
# Get first zone that is not correct for this item. zone_id, zone_title = self._get_incorrect_zone_for_item(definition, all_zones)
zone_id = None
zone_title = None
for z_id, z_title in all_zones:
if z_id not in definition.zone_ids:
zone_id = z_id
zone_title = z_title
break
if zone_id is not None: # Some items may be placed in any zone, ignore those. if zone_id is not None: # Some items may be placed in any zone, ignore those.
self.place_item(definition.item_id, zone_id, action_key) self.place_item(definition.item_id, zone_id, action_key)
if assessment_mode: self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id))
self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id)) feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') self.assertEqual(feedback_popup_html, '')
self.assertEqual(feedback_popup_html, '') self.assertFalse(popup.is_displayed())
self.assertFalse(popup.is_displayed()) self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True)
self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True) self.assert_reader_feedback_messages([])
if action_key: if action_key:
self._test_next_tab_goes_to_go_to_beginning_button() self._test_next_tab_goes_to_go_to_beginning_button()
else:
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assert_popup_incorrect(popup)
self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id)
self._test_popup_focus_and_close(popup, action_key)
def parameterized_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None): def parameterized_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None):
# Scroll drop zones into view to make sure Selenium can successfully drop items # Scroll drop zones into view to make sure Selenium can successfully drop items
...@@ -247,11 +291,13 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, Paramet ...@@ -247,11 +291,13 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, Paramet
""" """
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_item_positive_feedback_on_good_move(self, action_key): def test_item_positive_feedback_on_good_move(self, action_key):
self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=action_key) self.parameterized_item_positive_feedback_on_good_move_standard(self.items_map, action_key=action_key)
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_item_negative_feedback_on_bad_move(self, action_key): def test_item_negative_feedback_on_bad_move(self, action_key):
self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones, action_key=action_key) self.parameterized_item_negative_feedback_on_bad_move_standard(
self.items_map, self.all_zones, action_key=action_key
)
@data(*ITEM_DRAG_KEYBOARD_KEYS) @data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_cannot_move_items_between_zones(self, action_key): def test_cannot_move_items_between_zones(self, action_key):
...@@ -332,6 +378,23 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, Paramet ...@@ -332,6 +378,23 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, Paramet
# After placing all items, we get the full score. # After placing all items, we get the full score.
self.assertEqual(progress.text, '1/1 point (ungraded)') self.assertEqual(progress.text, '1/1 point (ungraded)')
@data(*ITEM_DRAG_KEYBOARD_KEYS)
def test_cannot_select_multiple_items(self, action_key):
if action_key:
all_item_ids = self.items_map.keys()
# Go through all items and select them all using the keyboard action key.
for item_id in all_item_ids:
item = self._get_item_by_value(item_id)
item.send_keys('')
item.send_keys(action_key)
# Item should be grabbed.
self.assert_item_grabbed(item)
# Other items should NOT be grabbed.
for other_item_id in all_item_ids:
if other_item_id != item_id:
other_item = self._get_item_by_value(other_item_id)
self.assert_item_not_grabbed(other_item)
class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
...@@ -477,16 +540,22 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase ...@@ -477,16 +540,22 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase
def test_item_positive_feedback_on_good_move(self): def test_item_positive_feedback_on_good_move(self):
self._switch_to_block(0) self._switch_to_block(0)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block1']) self.parameterized_item_positive_feedback_on_good_move_standard(
self.item_maps['block1'], feedback=self.feedback['block1']
)
self._switch_to_block(1) self._switch_to_block(1)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block2'], scroll_down=1000) self.parameterized_item_positive_feedback_on_good_move_standard(
self.item_maps['block2'], feedback=self.feedback['block2'], scroll_down=1000
)
def test_item_negative_feedback_on_bad_move(self): def test_item_negative_feedback_on_bad_move(self):
self._switch_to_block(0) self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_move(self.item_maps['block1'], self.all_zones['block1']) self.parameterized_item_negative_feedback_on_bad_move_standard(
self.item_maps['block1'], self.all_zones['block1'], feedback=self.feedback['block1']
)
self._switch_to_block(1) self._switch_to_block(1)
self.parameterized_item_negative_feedback_on_bad_move( self.parameterized_item_negative_feedback_on_bad_move_standard(
self.item_maps['block2'], self.all_zones['block2'], scroll_down=1000 self.item_maps['block2'], self.all_zones['block2'], feedback=self.feedback['block2'], scroll_down=1000
) )
def test_final_feedback_and_reset(self): def test_final_feedback_and_reset(self):
......
...@@ -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,18 @@ class AssessmentInteractionTest( ...@@ -250,6 +248,18 @@ class AssessmentInteractionTest(
""" """
Test updating overall feedback after submitting solution in assessment mode Test updating overall feedback after submitting solution in assessment mode
""" """
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.readText 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.assert_reader_feedback_messages(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 +271,25 @@ class AssessmentInteractionTest( ...@@ -261,29 +271,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 +305,11 @@ class AssessmentInteractionTest( ...@@ -299,13 +305,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)
......
...@@ -194,7 +194,7 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -194,7 +194,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
'#-Zone_{}'.format(zone_number), **zone_box_percentages '#-Zone_{}'.format(zone_number), **zone_box_percentages
) )
zone_name = zone.find_element_by_css_selector('p.zone-name') zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertEqual(zone_name.text, 'Zone {}'.format(zone_number)) self.assertEqual(zone_name.text, 'Zone {}\n, dropzone'.format(zone_number))
zone_description = zone.find_element_by_css_selector('p.zone-description') zone_description = zone.find_element_by_css_selector('p.zone-description')
self.assertEqual(zone_description.text, 'This describes zone {}'.format(zone_number)) self.assertEqual(zone_description.text, 'This describes zone {}'.format(zone_number))
# Zone description should only be visible to screen readers: # Zone description should only be visible to screen readers:
...@@ -204,12 +204,10 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -204,12 +204,10 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.load_scenario() self.load_scenario()
popup = self._get_popup() popup = self._get_popup()
popup_wrapper = self._get_popup_wrapper()
popup_content = self._get_popup_content() popup_content = self._get_popup_content()
self.assertFalse(popup.is_displayed()) self.assertFalse(popup.is_displayed())
self.assertIn('popup', popup.get_attribute('class')) self.assertIn('popup', popup.get_attribute('class'))
self.assertEqual(popup_content.text, "") self.assertEqual(popup_content.text, "")
self.assertEqual(popup_wrapper.get_attribute('aria-live'), 'polite')
@data(None, Keys.RETURN) @data(None, Keys.RETURN)
def test_go_to_beginning_button(self, action_key): def test_go_to_beginning_button(self, action_key):
...@@ -253,9 +251,7 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -253,9 +251,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
def test_feedback(self): def test_feedback(self):
self.load_scenario() self.load_scenario()
feedback = self._get_feedback()
feedback_message = self._get_feedback_message() feedback_message = self._get_feedback_message()
self.assertEqual(feedback.get_attribute('aria-live'), 'polite')
self.assertEqual(feedback_message.text, START_FEEDBACK) self.assertEqual(feedback_message.text, START_FEEDBACK)
def test_background_image(self): def test_background_image(self):
......
...@@ -26,10 +26,10 @@ class TestDragAndDropTitleAndProblem(BaseIntegrationTest): ...@@ -26,10 +26,10 @@ class TestDragAndDropTitleAndProblem(BaseIntegrationTest):
self.addCleanup(scenarios.remove_scenario, const_page_id) self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name) page = self.go_to_page(const_page_name)
is_problem_header_visible = len(page.find_elements_by_css_selector('section.problem > h4')) > 0 is_problem_header_visible = len(page.find_elements_by_css_selector('.problem > h4')) > 0
self.assertEqual(is_problem_header_visible, show_problem_header) self.assertEqual(is_problem_header_visible, show_problem_header)
problem = page.find_element_by_css_selector('section.problem > p') problem = page.find_element_by_css_selector('.problem > p')
self.assertEqual(self.get_element_html(problem), problem_text) self.assertEqual(self.get_element_html(problem), problem_text)
@unpack @unpack
......
...@@ -45,7 +45,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -45,7 +45,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
def test_template_contents(self): def test_template_contents(self):
context = {} context = {}
student_fragment = self.block.runtime.render(self.block, 'student_view', context) student_fragment = self.block.runtime.render(self.block, 'student_view', context)
self.assertIn('<section class="themed-xblock xblock--drag-and-drop">', student_fragment.content) self.assertIn('<div class="themed-xblock xblock--drag-and-drop">', student_fragment.content)
self.assertIn('Loading drag and drop problem.', student_fragment.content) self.assertIn('Loading drag and drop problem.', student_fragment.content)
def test_get_configuration(self): def test_get_configuration(self):
......
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