Commit f435c5b7 by Eugeny Kolpakov Committed by GitHub

Merge pull request #93 from edx-solutions/ekolpakov/assessment-display-item-feedback

Assessment mode: display item feedback
parents 4d0d5001 5f7d26cb
......@@ -138,8 +138,8 @@ image. You can define custom success and error feedback for each item. In
standard mode, the feedback text is displayed in a popup after the learner drops
the item on a zone - the success feedback is shown if the item is dropped on a
correct zone, while the error feedback is shown when dropping the item on an
incorrect drop zone. In assessment mode, the success and error feedback texts
are not used.
incorrect drop zone. In assessment mode, the success feedback texts
are not used, while error feedback texts are shown when learner submits a solution.
You can select any number of zones for an item to belong to using
the checkboxes; all zones defined in the previous step are available.
......
......@@ -246,7 +246,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"target_img_description": self.target_img_description,
"item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None,
"initial_feedback": self.data['feedback']['start'],
# final feedback (data.feedback.finish) is not included - it may give away answers.
}
......@@ -392,17 +391,29 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
self._validate_do_attempt()
self.attempts += 1
self._mark_complete_and_publish_grade() # must happen before _get_feedback
# pylint: disable=fixme
# TODO: Refactor this method to "freeze" item_state and pass it to methods that need access to it.
# These implicit dependencies between methods exist because most of them use `item_state` or other
# fields, either as an "input" (i.e. read value) or as output (i.e. set value) or both. As a result,
# incorrect order of invocation causes issues:
self._mark_complete_and_publish_grade() # must happen before _get_feedback - sets grade
correct = self._is_answer_correct() # must happen before manipulating item_state - reads item_state
overall_feedback_msgs, misplaced_ids = self._get_feedback()
overall_feedback_msgs, misplaced_ids = self._get_feedback(include_item_feedback=True)
misplaced_items = []
for item_id in misplaced_ids:
del self.item_state[item_id]
misplaced_items.append(self._get_item_definition(int(item_id)))
feedback_msgs = [FeedbackMessage(item['feedback']['incorrect'], None) for item in misplaced_items]
return {
'correct': correct,
'attempts': self.attempts,
'misplaced_items': list(misplaced_ids),
'overall_feedback': self._present_overall_feedback(overall_feedback_msgs)
'feedback': self._present_feedback(feedback_msgs),
'overall_feedback': self._present_feedback(overall_feedback_msgs)
}
@XBlock.json_handler
......@@ -487,7 +498,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
self.i18n_service.gettext("Max number of attempts reached")
)
def _get_feedback(self):
def _get_feedback(self, include_item_feedback=False):
"""
Builds overall feedback for both standard and assessment modes
"""
......@@ -510,16 +521,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
message = message_template(len(ids_list), self.i18n_service.ngettext)
feedback_msgs.append(FeedbackMessage(message, message_class))
_add_msg_if_exists(
items.correctly_placed, FeedbackMessages.correctly_placed, FeedbackMessages.MessageClasses.CORRECTLY_PLACED
)
_add_msg_if_exists(misplaced_ids, FeedbackMessages.misplaced, FeedbackMessages.MessageClasses.MISPLACED)
_add_msg_if_exists(missing_ids, FeedbackMessages.not_placed, FeedbackMessages.MessageClasses.NOT_PLACED)
if misplaced_ids and self.attempts_remain:
feedback_msgs.append(
FeedbackMessage(FeedbackMessages.MISPLACED_ITEMS_RETURNED, None)
if self.item_state or include_item_feedback:
_add_msg_if_exists(
items.correctly_placed,
FeedbackMessages.correctly_placed,
FeedbackMessages.MessageClasses.CORRECTLY_PLACED
)
_add_msg_if_exists(misplaced_ids, FeedbackMessages.misplaced, FeedbackMessages.MessageClasses.MISPLACED)
_add_msg_if_exists(missing_ids, FeedbackMessages.not_placed, FeedbackMessages.MessageClasses.NOT_PLACED)
if self.attempts_remain and (misplaced_ids or missing_ids):
problem_feedback_message = self.data['feedback']['start']
......@@ -539,7 +548,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return feedback_msgs, misplaced_ids
@staticmethod
def _present_overall_feedback(feedback_messages):
def _present_feedback(feedback_messages):
"""
Transforms feedback messages into format expected by frontend code
"""
......@@ -563,14 +572,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
self._publish_item_dropped_event(item_attempt, is_correct)
item_feedback_key = 'correct' if is_correct else 'incorrect'
item_feedback = item['feedback'][item_feedback_key]
item_feedback = FeedbackMessage(item['feedback'][item_feedback_key], None)
overall_feedback, __ = self._get_feedback()
return {
'correct': is_correct,
'finished': self._is_answer_correct(),
'overall_feedback': self._present_overall_feedback(overall_feedback),
'feedback': item_feedback
'overall_feedback': self._present_feedback(overall_feedback),
'feedback': self._present_feedback([item_feedback])
}
def _drop_item_assessment(self, item_attempt):
......@@ -612,6 +621,18 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
Helper method to update `self.completed` and submit grade event if appropriate conditions met.
"""
# pylint: disable=fixme
# TODO: (arguable) split this method into "clean" functions (with no side effects and implicit state)
# This method implicitly depends on self.item_state (via _is_answer_correct and _get_grade)
# and also updates self.grade if some conditions are met. As a result this method implies some order of
# invocation:
# * it should be called after learner-caused updates to self.item_state is applied
# * it should be called before self.item_state cleanup is applied (i.e. returning misplaced items to item bank)
# * it should be called before any method that depends on self.grade (i.e. self._get_feedback)
# Splitting it into a "clean" functions will allow to capture this implicit invocation order in caller method
# and help avoid bugs caused by invocation order violation in future.
# There's no going back from "completed" status to "incomplete"
self.completed = self.completed or self._is_answer_correct() or not self.attempts_remain
grade = self._get_grade()
......@@ -694,7 +715,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'items': item_state,
'finished': is_finished,
'attempts': self.attempts,
'overall_feedback': self._present_overall_feedback(overall_feedback_msgs)
'overall_feedback': self._present_feedback(overall_feedback_msgs)
}
def _get_item_state(self):
......@@ -794,8 +815,8 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
Returns the student's grade for this block.
"""
correct_count, required_count = self._get_item_stats()
return correct_count / float(required_count) * self.weight
correct_count, total_count = self._get_item_stats()
return correct_count / float(total_count) * self.weight
def _answer_correctness(self):
"""
......@@ -807,8 +828,8 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
* Partial: Some items are at their correct place.
* Incorrect: None items are at their correct place.
"""
correct_count, required_count = self._get_item_stats()
if correct_count == required_count:
correct_count, total_count = self._get_item_stats()
if correct_count == total_count:
return self.SOLUTION_CORRECT
elif correct_count == 0:
return self.SOLUTION_INCORRECT
......
......@@ -356,13 +356,11 @@
.xblock--drag-and-drop .popup .popup-content {
color: #ffffff;
margin-left: 15px;
margin-top: 35px;
margin-bottom: 15px;
margin: 35px 15px 15px 15px;
font-size: 14px;
}
.xblock--drag-and-drop .popup .close {
.xblock--drag-and-drop .popup .close-feedback-popup-button {
cursor: pointer;
float: right;
margin-right: 8px;
......@@ -373,7 +371,7 @@
font-size: 18pt;
}
.xblock--drag-and-drop .popup .close:focus {
.xblock--drag-and-drop .popup .close-feedback-popup-button:focus {
outline: 2px solid white;
}
......
......@@ -499,6 +499,10 @@ msgstr ""
msgid "You have used {used} of {total} attempts."
msgstr ""
#: public/js/drag_and_drop.js
msgid "Some of your answers were not correct."
msgstr ""
#: public/js/drag_and_drop_edit.js
msgid "There was an error with your form."
msgstr ""
......@@ -511,12 +515,12 @@ msgstr ""
msgid "None"
msgstr ""
#: utils.py:18
msgid "Final attempt was used, highest score is {score}"
#: public/js/drag_and_drop_edit.js
msgid "Close item feedback popup"
msgstr ""
#: utils.py:19
msgid "Misplaced items were returned to item bank."
#: utils.py:18
msgid "Final attempt was used, highest score is {score}"
msgstr ""
#: utils.py:24
......@@ -526,8 +530,8 @@ msgstr[0] ""
msgstr[1] ""
#: utils.py:32
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] ""
msgstr[1] ""
......
......@@ -585,6 +585,10 @@ msgstr ""
msgid "You have used {used} of {total} attempts."
msgstr "Ýöü hävé üséd {used} öf {total} ättémpts. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
#: public/js/drag_and_drop.js
msgid "Some of your answers were not correct."
msgstr "Sömé öf ýöür änswérs wéré nöt cörréct. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
#: public/js/drag_and_drop_edit.js
msgid "There was an error with your form."
msgstr ""
......@@ -599,28 +603,28 @@ msgstr ""
msgid "None"
msgstr "Nöné Ⱡ'σяєм ι#"
#: utils.py:18
msgid "Fïnäl ättémpt wäs üséd, hïghést sçöré ïs {score} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
msgstr ""
#: public/js/drag_and_drop_edit.js
msgid "Close item feedback popup"
msgstr "Çlösé ïtém féédßäçk pöpüp Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: utils.py:19
msgid "Mïspläçéd ïtéms wéré rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
msgstr ""
#: utils.py:18
msgid "Final attempt was used, highest score is {score}"
msgstr "Fïnäl ättémpt wäs üséd, hïghést sçöré ïs {score} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: utils.py:24
msgid "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgid_plural "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
msgstr[0] ""
msgstr[1] ""
msgid "Correctly placed {correct_count} item."
msgid_plural "Correctly placed {correct_count} items."
msgstr[0] "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgstr[1] "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: utils.py:32
msgid "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgid_plural "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
msgstr[0] ""
msgstr[1] ""
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: utils.py:40
msgid "Dïd nöt pläçé {missing_count} réqüïréd ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
msgid_plural "Dïd nöt pläçé {missing_count} réqüïréd ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
msgstr[0] ""
msgstr[1] ""
msgid "Did not place {missing_count} required item."
msgid_plural "Did not place {missing_count} required items."
msgstr[0] "Dïd nöt pläçé {missing_count} réqüïréd ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
msgstr[1] "Dïd nöt pläçé {missing_count} réqüïréd ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
......@@ -42,7 +42,6 @@ class FeedbackMessages(object):
NOT_PLACED = INCORRECT_SOLUTION
FINAL_ATTEMPT_TPL = _('Final attempt was used, highest score is {score}')
MISPLACED_ITEMS_RETURNED = _('Misplaced item(s) were returned to item bank.')
@staticmethod
def correctly_placed(number, ngettext=ngettext_fallback):
......
......@@ -214,6 +214,8 @@ class DefaultDataTestMixin(object):
class InteractionTestBase(object):
POPUP_ERROR_CLASS = "popup-incorrect"
@classmethod
def _get_items_with_zone(cls, items_map):
return {
......@@ -309,7 +311,7 @@ class InteractionTestBase(object):
u"Spinner should not be in {}".format(elem.get_attribute('innerHTML'))
)
def place_item(self, item_value, zone_id, action_key=None):
def place_item(self, item_value, zone_id, action_key=None, wait=True):
"""
Place item with ID of item_value into zone with ID of zone_id.
zone_id=None means place item back to the item bank.
......@@ -319,7 +321,8 @@ class InteractionTestBase(object):
self.drag_item_to_zone(item_value, zone_id)
else:
self.move_item_to_zone(item_value, zone_id, action_key)
self.wait_for_ajax()
if wait:
self.wait_for_ajax()
def drag_item_to_zone(self, item_value, zone_id):
"""
......@@ -429,3 +432,12 @@ class InteractionTestBase(object):
""" Only needed if there are multiple blocks on the page. """
self._page = self.browser.find_elements_by_css_selector(self.default_css_selector)[idx]
self.scroll_down(0)
def assert_popup_correct(self, popup):
self.assertNotIn(self.POPUP_ERROR_CLASS, popup.get_attribute('class'))
def assert_popup_incorrect(self, popup):
self.assertIn(self.POPUP_ERROR_CLASS, popup.get_attribute('class'))
def assert_button_enabled(self, submit_button, enabled=True):
self.assertEqual(submit_button.is_enabled(), enabled)
from ddt import ddt, data, unpack
from ddt import data, ddt, unpack
from mock import Mock, patch
from workbench.runtime import WorkbenchRuntime
from drag_and_drop_v2.default_data import TOP_ZONE_TITLE, TOP_ZONE_ID, ITEM_CORRECT_FEEDBACK
from drag_and_drop_v2.default_data import (
TOP_ZONE_TITLE, TOP_ZONE_ID, MIDDLE_ZONE_TITLE, MIDDLE_ZONE_ID, ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK
)
from tests.integration.test_base import BaseIntegrationTest, DefaultDataTestMixin, InteractionTestBase
from tests.integration.test_interaction import DefaultDataTestMixin, ParameterizedTestsMixin
from tests.integration.test_interaction_assessment import DefaultAssessmentDataTestMixin, AssessmentTestMixin
from .test_base import BaseIntegrationTest, DefaultDataTestMixin
from .test_interaction import ParameterizedTestsMixin
from tests.integration.test_base import InteractionTestBase
class BaseEventsTests(InteractionTestBase, BaseIntegrationTest):
def setUp(self):
mock = Mock()
context = patch.object(WorkbenchRuntime, 'publish', mock)
context.start()
self.addCleanup(context.stop)
self.publish = mock
super(BaseEventsTests, self).setUp()
@ddt
class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, InteractionTestBase, BaseIntegrationTest):
class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsTests):
"""
Tests that the analytics events are fired and in the proper order.
"""
......@@ -54,14 +66,6 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, Interaction
},
)
def setUp(self):
mock = Mock()
context = patch.object(WorkbenchRuntime, 'publish', mock)
context.start()
self.addCleanup(context.stop)
self.publish = mock
super(EventsFiredTest, self).setUp()
def _get_scenario_xml(self): # pylint: disable=no-self-use
return "<vertical_demo><drag-and-drop-v2/></vertical_demo>"
......@@ -71,6 +75,68 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, Interaction
self.parameterized_item_positive_feedback_on_good_move(self.items_map)
dummy, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(
published_data, event['data']
)
self.assertEqual(published_data, event['data'])
@ddt
class AssessmentEventsFiredTest(
DefaultAssessmentDataTestMixin, AssessmentTestMixin, BaseEventsTests
):
scenarios = (
{
'name': 'edx.drag_and_drop_v2.loaded',
'data': {},
},
{
'name': 'edx.drag_and_drop_v2.item.picked_up',
'data': {'item_id': 0},
},
{
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'is_correct': False,
'item_id': 0,
'location': MIDDLE_ZONE_TITLE,
'location_id': MIDDLE_ZONE_ID,
},
},
{
'name': 'edx.drag_and_drop_v2.item.picked_up',
'data': {'item_id': 1},
},
{
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'is_correct': False,
'item_id': 1,
'location': TOP_ZONE_TITLE,
'location_id': TOP_ZONE_ID,
},
},
{
'name': 'grade',
'data': {'max_value': 1, 'value': (1.0 / 5)},
},
{
'name': 'edx.drag_and_drop_v2.feedback.opened',
'data': {
'content': "\n".join([ITEM_INCORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK]),
'truncated': False,
},
},
)
def test_event(self):
self.scroll_down(pixels=100)
self.place_item(0, MIDDLE_ZONE_ID)
self.wait_until_ondrop_xhr_finished(self._get_item_by_value(0))
self.place_item(1, TOP_ZONE_ID)
self.wait_until_ondrop_xhr_finished(self._get_item_by_value(0))
self.click_submit()
self.wait_for_ajax()
for index, event in enumerate(self.scenarios):
dummy, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(published_data, event['data'])
......@@ -42,8 +42,8 @@ class ParameterizedTestsMixin(object):
self.assertEqual(feedback_popup_html, '')
self.assertFalse(popup.is_displayed())
else:
self.assertEqual(feedback_popup_html, definition.feedback_positive)
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assertEqual(feedback_popup_html, "<p>{}</p>".format(definition.feedback_positive))
self.assert_popup_correct(popup)
self.assertTrue(popup.is_displayed())
def parameterized_item_negative_feedback_on_bad_move(
......@@ -74,7 +74,7 @@ class ParameterizedTestsMixin(object):
self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True)
else:
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
self.assert_popup_incorrect(popup)
self.assertTrue(popup.is_displayed())
self.assert_reverted_item(definition.item_id)
......@@ -288,7 +288,7 @@ class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestB
for i, zone in enumerate(item.zone_ids):
self.place_item(item.item_id, zone, None)
self.wait_until_html_in(item.feedback_positive[i], feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assert_popup_correct(popup)
self.assert_placed_item(item.item_id, item.zone_title[i])
reset.click()
self.wait_until_disabled(reset)
......@@ -534,12 +534,15 @@ class TestMaxItemsPerZone(InteractionTestBase, BaseIntegrationTest):
self.assertTrue(feedback_popup.is_displayed())
feedback_popup_content = self._get_popup_content()
self.assertEqual(
feedback_popup_content.get_attribute('innerHTML'),
"You cannot add any more items to this zone."
self.assertIn(
"You cannot add any more items to this zone.",
feedback_popup_content.get_attribute('innerHTML')
)
def test_item_returned_to_bank_after_refresh(self):
"""
Tests that an item returned to the bank stays there after page refresh
"""
zone_id = "Zone Left Align"
self.place_item(6, zone_id)
self.place_item(7, zone_id)
......
......@@ -2,6 +2,7 @@
from ddt import ddt, data
from mock import Mock, patch
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
......@@ -175,7 +176,6 @@ class AssessmentInteractionTest(
FeedbackMessages.correctly_placed(1),
FeedbackMessages.misplaced(1),
FeedbackMessages.not_placed(2),
FeedbackMessages.MISPLACED_ITEMS_RETURNED,
START_FEEDBACK
]
expected_feedback = "\n".join(feedback_lines)
......@@ -219,6 +219,44 @@ class AssessmentInteractionTest(
expected_grade = {'max_value': 1, 'value': (1.0 / 5.0)}
self.assertEqual(published_grade, expected_grade)
def test_per_item_feedback_multiple_misplaced(self):
self.place_item(0, MIDDLE_ZONE_ID, Keys.RETURN)
self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN)
self.place_item(2, TOP_ZONE_ID, Keys.RETURN)
self.click_submit()
placed_item_definitions = [self.items_map[item_id] for item_id in (1, 2, 3)]
expected_message_elements = [
"<li>{msg}</li>".format(msg=definition.feedback_negative)
for definition in placed_item_definitions
]
for message_element in expected_message_elements:
self.assertIn(message_element, self._get_popup_content().get_attribute('innerHTML'))
def test_submit_disabled_during_drop_item(self):
def delayed_drop_item(item_attempt, suffix=''): # pylint: disable=unused-argument
# some delay to allow selenium check submit button disabled status while "drop_item"
# XHR is still executing
time.sleep(0.1)
return {}
self.place_item(0, TOP_ZONE_ID)
self.assert_placed_item(0, TOP_ZONE_TITLE, assessment_mode=True)
submit_button = self._get_submit_button()
self.assert_button_enabled(submit_button) # precondition check
with patch('drag_and_drop_v2.DragAndDropBlock._drop_item_assessment', Mock(side_effect=delayed_drop_item)):
item_id = 1
self.place_item(item_id, MIDDLE_ZONE_ID, wait=False)
# do not wait for XHR to complete
self.assert_button_enabled(submit_button, enabled=False)
self.wait_until_ondrop_xhr_finished(self._get_placed_item_by_value(item_id))
self.assert_button_enabled(submit_button, enabled=True)
class TestMaxItemsPerZoneAssessment(TestMaxItemsPerZone):
assessment_mode = True
......@@ -228,6 +266,9 @@ class TestMaxItemsPerZoneAssessment(TestMaxItemsPerZone):
return self._make_scenario_xml(data=scenario_data, max_items_per_zone=2, mode=Constants.ASSESSMENT_MODE)
def test_drop_item_to_same_zone_does_not_show_popup(self):
"""
Tests that picking item from saturated zone and dropping it back again does not trigger error popup
"""
zone_id = "Zone Left Align"
self.place_item(6, zone_id)
self.place_item(7, zone_id)
......
......@@ -209,7 +209,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
popup_wrapper = self._get_popup_wrapper()
popup_content = self._get_popup_content()
self.assertFalse(popup.is_displayed())
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assertIn('popup', popup.get_attribute('class'))
self.assertEqual(popup_content.text, "")
self.assertEqual(popup_wrapper.get_attribute('aria-live'), 'polite')
......
......@@ -26,7 +26,7 @@ class TestDragAndDropTitleAndProblem(BaseIntegrationTest):
self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name)
is_problem_header_visible = len(page.find_elements_by_css_selector('section.problem > h3')) > 0
is_problem_header_visible = len(page.find_elements_by_css_selector('section.problem > h4')) > 0
self.assertEqual(is_problem_header_visible, show_problem_header)
problem = page.find_element_by_css_selector('section.problem > p')
......@@ -52,8 +52,8 @@ class TestDragAndDropTitleAndProblem(BaseIntegrationTest):
page = self.go_to_page(const_page_name)
if show_title:
problem_header = page.find_element_by_css_selector('h2.problem-title')
problem_header = page.find_element_by_css_selector('h3.problem-title')
self.assertEqual(self.get_element_html(problem_header), display_name)
else:
with self.assertRaises(NoSuchElementException):
page.find_element_by_css_selector('h2.problem-title')
page.find_element_by_css_selector('h3.problem-title')
......@@ -9,7 +9,6 @@
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "test",
......
......@@ -9,7 +9,6 @@
"target_img_description": "This describes the target image",
"item_background_color": "white",
"item_text_color": "#000080",
"initial_feedback": "HTML <strong>Intro</strong> Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "unique_name",
......
......@@ -9,7 +9,6 @@
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "Intro Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "",
......
......@@ -9,7 +9,6 @@
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "test",
......
......@@ -69,7 +69,6 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
"target_img_description": TARGET_IMG_DESCRIPTION,
"item_background_color": None,
"item_text_color": None,
"initial_feedback": START_FEEDBACK,
"url_name": "",
})
self.assertEqual(zones, DEFAULT_DATA["zones"])
......
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