From 97c5c4d8c24a90d6548de2483326ac4ed6c90b50 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric Date: Wed, 20 Jul 2016 19:33:41 +0800 Subject: [PATCH] Keep items in dropped position in assessment mode. Items dropped onto wrong zones in standard mode are immediately removed from the board and sent back to the bank. In assessment mode, items should stick in the dropped position until the entire problem is explicitly submitted by clicking a button. This commit only implements the "stick" part, but does not implement the submit button and associated handler. This commit also fixes an existing bug where the template function would gett reused between blocks. The template function (which is slightly different for each block, depending on block's configuration) was getting assigned to the global DragAndDropBlock function. Each block would overwrite the previous value, which caused bugs. --- drag_and_drop_v2/drag_and_drop_v2.py | 19 +- drag_and_drop_v2/public/js/drag_and_drop.js | 52 +++-- .../translations/en/LC_MESSAGES/text.po | 4 + .../translations/eo/LC_MESSAGES/text.po | 4 + tests/integration/test_interaction.py | 206 +++++++++++++----- tests/unit/data/html/config_out.json | 1 + tests/unit/data/old/config_out.json | 1 + tests/unit/data/plain/config_out.json | 1 + tests/unit/test_advanced.py | 10 + tests/unit/test_basics.py | 1 + 10 files changed, 217 insertions(+), 82 deletions(-) diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py index 08f35cbea..a2bcf5ccb 100644 --- a/drag_and_drop_v2/drag_and_drop_v2.py +++ b/drag_and_drop_v2/drag_and_drop_v2.py @@ -190,6 +190,7 @@ def items_without_answers(): return items return { + "mode": self.mode, "zones": self._get_zones(), # SDK doesn't supply url_name. "url_name": getattr(self, 'url_name', ''), @@ -337,12 +338,18 @@ def do_attempt(self, attempt, suffix=''): 'is_correct': is_correct, }) - return { - 'correct': is_correct, - 'finished': self._is_finished(), - 'overall_feedback': overall_feedback, - 'feedback': feedback - } + if self.mode == self.ASSESSMENT_MODE: + # In assessment mode we don't send any feedback on drop. + result = {} + else: + result = { + 'correct': is_correct, + 'finished': self._is_finished(), + 'overall_feedback': overall_feedback, + 'feedback': feedback + } + + return result @XBlock.json_handler def reset(self, data, suffix=''): diff --git a/drag_and_drop_v2/public/js/drag_and_drop.js b/drag_and_drop_v2/public/js/drag_and_drop.js index 43ad31e54..85ad5f371 100644 --- a/drag_and_drop_v2/public/js/drag_and_drop.js +++ b/drag_and_drop_v2/public/js/drag_and_drop.js @@ -1,4 +1,4 @@ -function DragNDropTemplates(url_name) { +function DragAndDropTemplates(configuration) { "use strict"; var h = virtualDom.h; // Set up a mock for gettext if it isn't available in the client runtime: @@ -107,13 +107,22 @@ function DragNDropTemplates(url_name) { 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 = 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 }; var zone_title = (zone.title || "Unknown Zone"); // This "Unknown" text should never be seen, so does not need i18n + var description_content; + if (configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) { + // In assessment mode placed items will "stick" even when not in correct zone. + description_content = gettext('Placed in: {zone_title}').replace('{zone_title}', zone_title); + } else { + // In standard mode item is immediately returned back to the bank if dropped on a wrong zone, + // so all placed items are always in the correct zone. + description_content = gettext('Correctly placed in: {zone_title}').replace('{zone_title}', zone_title); + } var item_description = h( 'div', { id: item_description_id, className: 'sr' }, - gettext('Correctly placed in: ') + zone_title + description_content ); children.splice(1, 0, item_description); } @@ -283,14 +292,17 @@ function DragNDropTemplates(url_name) { ); }; - DragAndDropBlock.renderView = mainTemplate; - + return mainTemplate; } function DragAndDropBlock(runtime, element, configuration) { "use strict"; - DragNDropTemplates(configuration.url_name); + DragAndDropBlock.STANDARD_MODE = 'standard'; + DragAndDropBlock.ASSESSMENT_MODE = 'assessment'; + + var renderView = DragAndDropTemplates(configuration); + // Set up a mock for gettext if it isn't available in the client runtime: if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; } @@ -747,17 +759,21 @@ function DragAndDropBlock(runtime, element, configuration) { $.post(url, JSON.stringify(data), 'json') .done(function(data){ - state.last_action_correct = data.correct; - if (data.correct) { - state.items[item_id].correct = true; - state.items[item_id].submitting_location = false; - } else { - delete state.items[item_id]; - } - state.feedback = data.feedback; - if (data.finished) { - state.finished = true; - state.overall_feedback = data.overall_feedback; + state.items[item_id].submitting_location = false; + // In standard mode we immediately return item to the bank if dropped on wrong zone. + // In assessment mode we leave it in the chosen zone until explicit answer submission. + if (configuration.mode === DragAndDropBlock.STANDARD_MODE) { + state.last_action_correct = data.correct; + if (data.correct) { + state.items[item_id].correct = true; + } else { + delete state.items[item_id]; + } + state.feedback = data.feedback; + if (data.finished) { + state.finished = true; + state.overall_feedback = data.overall_feedback; + } } applyState(); }) @@ -866,7 +882,7 @@ function DragAndDropBlock(runtime, element, configuration) { display_reset_button: Object.keys(state.items).length > 0, }; - return DragAndDropBlock.renderView(context); + return renderView(context); }; /** diff --git a/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po b/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po index 9b9d624c1..f34ec739d 100644 --- a/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po +++ b/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po @@ -408,6 +408,10 @@ msgstr "" msgid "ok" msgstr "" +#: public/js/drag_and_drop.js +msgid "Placed in: " +msgstr "" + #: public/js/drag_and_drop.js msgid "Correctly placed in: " msgstr "" diff --git a/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po b/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po index ad3a2b7ac..51b2e56dc 100644 --- a/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po +++ b/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po @@ -493,6 +493,10 @@ msgstr "Çänçél Ⱡ'σяєм ιρѕυ#" msgid "ok" msgstr "ök Ⱡ'σя#" +#: public/js/drag_and_drop.js +msgid "Placed in: " +msgstr "Pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + #: public/js/drag_and_drop.js msgid "Correctly placed in: " msgstr "Çörréçtlý pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py index cb72a2dd5..a91b91fce 100644 --- a/tests/integration/test_interaction.py +++ b/tests/integration/test_interaction.py @@ -6,6 +6,7 @@ from selenium.common.exceptions import NoSuchElementException from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait from workbench.runtime import WorkbenchRuntime from xblockutils.resources import ResourceLoader @@ -89,6 +90,19 @@ def _get_zone_position(self, zone_id): 'return $("div[data-uid=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id) ) + @staticmethod + def wait_until_ondrop_xhr_finished(elem): + """ + Waits until the XHR request triggered by dropping the item finishes loading. + """ + wait = WebDriverWait(elem, 2) + # While the XHR is in progress, a spinner icon is shown inside the item. + # When the spinner disappears, we can assume that the XHR request has finished. + wait.until( + lambda e: 'fa-spinner' not in e.get_attribute('innerHTML'), + u"Spinner should not be in {}".format(elem.get_attribute('innerHTML')) + ) + def place_item(self, item_value, zone_id, action_key=None): if action_key is None: self.drag_item_to_zone(item_value, zone_id) @@ -117,7 +131,7 @@ def move_item_to_zone(self, item_value, zone_id, action_key): def assert_grabbed_item(self, item): self.assertEqual(item.get_attribute('aria-grabbed'), 'true') - def assert_placed_item(self, item_value, zone_title): + def assert_placed_item(self, item_value, zone_title, assessment_mode=False): item = self._get_placed_item_by_value(item_value) self.wait_until_visible(item) item_content = item.find_element_by_css_selector('.item-content') @@ -131,7 +145,10 @@ def assert_placed_item(self, item_value, zone_title): self.assertEqual(item.get_attribute('data-drag-disabled'), 'true') self.assertEqual(item_content.get_attribute('aria-describedby'), item_description_id) self.assertEqual(item_description.get_attribute('id'), item_description_id) - self.assertEqual(item_description.text, 'Correctly placed in: {}'.format(zone_title)) + if assessment_mode: + self.assertEqual(item_description.text, 'Placed in: {}'.format(zone_title)) + else: + self.assertEqual(item_description.text, 'Correctly placed in: {}'.format(zone_title)) def assert_reverted_item(self, item_value): item = self._get_item_by_value(item_value) @@ -152,16 +169,28 @@ def assert_reverted_item(self, item_value): else: self.fail('Reverted item should not have .sr description.') - def assert_decoy_items(self, items_map): + def place_decoy_items(self, items_map, action_key): + decoy_items = self._get_items_without_zone(items_map) + # Place decoy items into first available zone. + zone_id, zone_title = self.all_zones[0] + for definition in decoy_items.values(): + self.place_item(definition.item_id, zone_id, action_key) + self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True) + + def assert_decoy_items(self, items_map, assessment_mode=False): decoy_items = self._get_items_without_zone(items_map) for item_key in decoy_items: item = self._get_item_by_value(item_key) - - self.assertEqual(item.get_attribute('class'), 'option fade') self.assertEqual(item.get_attribute('aria-grabbed'), 'false') self.assertEqual(item.get_attribute('data-drag-disabled'), 'true') - - def parameterized_item_positive_feedback_on_good_move(self, items_map, scroll_down=100, action_key=None): + if assessment_mode: + self.assertEqual(item.get_attribute('class'), 'option') + else: + self.assertEqual(item.get_attribute('class'), 'option fade') + + def parameterized_item_positive_feedback_on_good_move( + self, items_map, scroll_down=100, action_key=None, assessment_mode=False + ): popup = self._get_popup() feedback_popup_content = self._get_popup_content() @@ -170,11 +199,20 @@ def parameterized_item_positive_feedback_on_good_move(self, items_map, scroll_do for definition in self._get_items_with_zone(items_map).values(): self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.wait_until_html_in(definition.feedback_positive, feedback_popup_content) - self.assertEqual(popup.get_attribute('class'), 'popup') - self.assert_placed_item(definition.item_id, definition.zone_title) + 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) + feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') + if assessment_mode: + 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.assertTrue(popup.is_displayed()) - def parameterized_item_negative_feedback_on_bad_move(self, items_map, all_zones, scroll_down=100, action_key=None): + def parameterized_item_negative_feedback_on_bad_move( + self, items_map, all_zones, scroll_down=100, action_key=None, assessment_mode=False + ): popup = self._get_popup() feedback_popup_content = self._get_popup_content() @@ -182,15 +220,31 @@ def parameterized_item_negative_feedback_on_bad_move(self, items_map, all_zones, self.scroll_down(pixels=scroll_down) for definition in items_map.values(): - for zone in all_zones: - if zone in definition.zone_ids: - continue - self.place_item(definition.item_id, zone, action_key) - self.wait_until_html_in(definition.feedback_negative, feedback_popup_content) - self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect') - self.assert_reverted_item(definition.item_id) - - def parameterized_final_feedback_and_reset(self, items_map, feedback, scroll_down=100, action_key=None): + # Get first zone that is not correct for this item. + 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. + 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)) + feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') + self.assertEqual(feedback_popup_html, '') + self.assertFalse(popup.is_displayed()) + 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.assertTrue(popup.is_displayed()) + self.assert_reverted_item(definition.item_id) + + def parameterized_final_feedback_and_reset( + self, items_map, feedback, scroll_down=100, action_key=None, assessment_mode=False + ): feedback_message = self._get_feedback_message() self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check @@ -206,12 +260,17 @@ def get_locations(): for item_key, definition in items.items(): self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.assert_placed_item(definition.item_id, definition.zone_title) + self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=assessment_mode) - self.wait_until_html_in(feedback['final'], self._get_feedback_message()) + if assessment_mode: + # In assessment mode we also place decoy items onto the board, + # to make sure they are correctly reverted back to the bank on problem reset. + self.place_decoy_items(items_map, action_key) + else: + self.wait_until_html_in(feedback['final'], self._get_feedback_message()) # Check decoy items - self.assert_decoy_items(items_map) + self.assert_decoy_items(items_map, assessment_mode=assessment_mode) # Scroll "Reset problem" button into view to make sure Selenium can successfully click it self.scroll_down(pixels=scroll_down+150) @@ -298,7 +357,11 @@ class DefaultDataTestMixin(object): 4: ItemDefinition(4, [], None, "", ITEM_NO_ZONE_FEEDBACK), } - all_zones = [TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID] + all_zones = [ + (TOP_ZONE_ID, TOP_ZONE_TITLE), + (MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE), + (BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE) + ] feedback = { "intro": START_FEEDBACK, @@ -309,21 +372,66 @@ def _get_scenario_xml(self): # pylint: disable=no-self-use return "" -class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase): +class DefaultAssessmentDataTestMixin(DefaultDataTestMixin): """ - Testing interactions with Drag and Drop XBlock against default data. If default data changes this will break. + Provides a test scenario with default options in assessment mode. """ - def test_item_positive_feedback_on_good_move(self): - self.parameterized_item_positive_feedback_on_good_move(self.items_map) + def _get_scenario_xml(self): # pylint: disable=no-self-use + return "" - def test_item_negative_feedback_on_bad_move(self): - self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones) - def test_final_feedback_and_reset(self): - self.parameterized_final_feedback_and_reset(self.items_map, self.feedback) +@ddt +class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): + """ + Testing interactions with Drag and Drop XBlock against default data. + All interactions are tested using mouse (action_key=None) and four different keyboard action keys. + If default data changes this will break. + """ + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + 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) - def test_keyboard_help(self): - self.interact_with_keyboard_help() + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + 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) + + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + def test_final_feedback_and_reset(self, action_key): + self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key) + + @data(False, True) + def test_keyboard_help(self, use_keyboard): + self.interact_with_keyboard_help(use_keyboard=use_keyboard) + + +@ddt +class AssessmentInteractionTest(DefaultAssessmentDataTestMixin, InteractionTestBase, BaseIntegrationTest): + """ + Testing interactions with Drag and Drop XBlock against default data in assessment mode. + All interactions are tested using mouse (action_key=None) and four different keyboard action keys. + If default data changes this will break. + """ + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + def test_item_no_feedback_on_good_move(self, action_key): + self.parameterized_item_positive_feedback_on_good_move( + self.items_map, action_key=action_key, assessment_mode=True + ) + + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + def test_item_no_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, assessment_mode=True + ) + + @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') + def test_final_feedback_and_reset(self, action_key): + self.parameterized_final_feedback_and_reset( + self.items_map, self.feedback, action_key=action_key, assessment_mode=True + ) + + @data(False, True) + def test_keyboard_help(self, use_keyboard): + self.interact_with_keyboard_help(use_keyboard=use_keyboard) class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): @@ -417,32 +525,14 @@ def test_event(self, index, event): ) -@ddt -class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest): - @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') - def test_item_positive_feedback_on_good_move_with_keyboard(self, action_key): - self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=action_key) - - @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') - def test_item_negative_feedback_on_bad_move_with_keyboard(self, action_key): - self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones, action_key=action_key) - - @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m') - def test_final_feedback_and_reset_with_keyboard(self, action_key): - self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key) - - def test_keyboard_help(self): - self.interact_with_keyboard_help(use_keyboard=True) - - -class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest): +class CustomDataInteractionTest(StandardInteractionTest): items_map = { 0: ItemDefinition(0, ['zone-1'], "Zone 1", "Yes 1", "No 1"), 1: ItemDefinition(1, ['zone-2'], "Zone 2", "Yes 2", "No 2"), 2: ItemDefinition(2, [], None, "", "No Zone for this") } - all_zones = ['zone-1', 'zone-2'] + all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')] feedback = { "intro": "Some Intro Feed", @@ -453,14 +543,14 @@ def _get_scenario_xml(self): return self._get_custom_scenario_xml("data/test_data.json") -class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest): +class CustomHtmlDataInteractionTest(StandardInteractionTest): items_map = { 0: ItemDefinition(0, ['zone-1'], 'Zone 1', "Yes 1", "No 1"), 1: ItemDefinition(1, ['zone-2'], 'Zone 2', "Yes 2", "No 2"), 2: ItemDefinition(2, [], None, "", "No Zone for X") } - all_zones = ['zone-1', 'zone-2'] + all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')] feedback = { "intro": "Intro Feed", @@ -492,8 +582,8 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): } all_zones = { - 'block1': ['zone-1', 'zone-2'], - 'block2': ['zone-51', 'zone-52'] + 'block1': [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')], + 'block2': [('zone-51', 'Zone 51'), ('zone-52', 'Zone 52')] } feedback = { diff --git a/tests/unit/data/html/config_out.json b/tests/unit/data/html/config_out.json index 13ea0f1fe..80f40880c 100644 --- a/tests/unit/data/html/config_out.json +++ b/tests/unit/data/html/config_out.json @@ -1,5 +1,6 @@ { "title": "DnDv2 XBlock with HTML instructions", + "mode": "standard", "show_title": false, "problem_text": "Solve this drag-and-drop problem.", "show_problem_header": false, diff --git a/tests/unit/data/old/config_out.json b/tests/unit/data/old/config_out.json index 3ef89fa3c..d2ef3efd1 100644 --- a/tests/unit/data/old/config_out.json +++ b/tests/unit/data/old/config_out.json @@ -1,5 +1,6 @@ { "title": "Drag and Drop", + "mode": "standard", "show_title": true, "problem_text": "", "show_problem_header": true, diff --git a/tests/unit/data/plain/config_out.json b/tests/unit/data/plain/config_out.json index 0afa35d41..6f60f442b 100644 --- a/tests/unit/data/plain/config_out.json +++ b/tests/unit/data/plain/config_out.json @@ -1,5 +1,6 @@ { "title": "DnDv2 XBlock with plain text instructions", + "mode": "standard", "show_title": true, "problem_text": "Can you solve this drag-and-drop problem?", "show_problem_header": true, diff --git a/tests/unit/test_advanced.py b/tests/unit/test_advanced.py index ef52190ef..4449fa345 100644 --- a/tests/unit/test_advanced.py +++ b/tests/unit/test_advanced.py @@ -5,6 +5,8 @@ from xblockutils.resources import ResourceLoader +from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock + from ..utils import make_block, TestCaseMixin @@ -90,6 +92,14 @@ def test_do_attempt_correct(self): "feedback": self.FEEDBACK[item_id]["correct"] }) + def test_do_attempt_in_assessment_mode(self): + self.block.mode = DragAndDropBlock.ASSESSMENT_MODE + item_id, zone_id = 0, self.ZONE_1 + data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"} + res = self.call_handler('do_attempt', data) + # In assessment mode, the do_attempt doesn't return any data. + self.assertEqual(res, {}) + def test_grading(self): published_grades = [] diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py index 31a833e64..4191a1bac 100644 --- a/tests/unit/test_basics.py +++ b/tests/unit/test_basics.py @@ -30,6 +30,7 @@ def test_get_configuration(self): zones = config.pop("zones") items = config.pop("items") self.assertEqual(config, { + "mode": DragAndDropBlock.STANDARD_MODE, "display_zone_borders": False, "display_zone_labels": False, "title": "Drag and Drop",