diff --git a/requirements/test.in b/requirements/test.in index 08aaab81f..26cc962f1 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -7,7 +7,6 @@ pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support ddt # data-driven tests -bok-choy # integration tests selenium # integration tests mock # required by the workbench openedx-django-pyfs # required by the workbench diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py index 75a55358b..e1fb662a5 100644 --- a/tests/integration/test_base.py +++ b/tests/integration/test_base.py @@ -4,22 +4,17 @@ from __future__ import absolute_import -import json from collections import namedtuple -from xml.sax.saxutils import escape -from bok_choy.promise import EmptyPromise 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 six.moves import range -from workbench import scenarios -from xblock.utils.base_test import SeleniumBaseTest from xblock.utils.resources import ResourceLoader from drag_and_drop_v2.default_data import (BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE, - DEFAULT_DATA, FINISH_FEEDBACK, + FINISH_FEEDBACK, ITEM_ANY_ZONE_FEEDBACK, ITEM_ANY_ZONE_NAME, ITEM_BOTTOM_ZONE_NAME, @@ -33,13 +28,11 @@ ITEM_TOP_ZONE_NAME, MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE, START_FEEDBACK, TOP_ZONE_ID, TOP_ZONE_TITLE) -from drag_and_drop_v2.utils import Constants # Globals ########################################################### loader = ResourceLoader(__name__) - # Classes ########################################################### ItemDefinition = namedtuple( # pylint: disable=invalid-name @@ -56,179 +49,6 @@ ) -class BaseIntegrationTest(SeleniumBaseTest): - default_css_selector = '.themed-xblock.xblock--drag-and-drop' - module_name = __name__ - - _additional_escapes = { - '"': """, - "'": "'" - } - - # pylint: disable=too-many-arguments - # pylint: disable=bad-continuation - @classmethod - def _make_scenario_xml( - cls, display_name="Test DnDv2", show_title=True, problem_text="Question", completed=False, - show_problem_header=True, max_items_per_zone=0, data=None, mode=Constants.STANDARD_MODE - ): - if not data: - data = json.dumps(DEFAULT_DATA) - return """ - - - - """.format( - display_name=escape(display_name), - show_title=show_title, - problem_text=escape(problem_text), - show_problem_header=show_problem_header, - completed=completed, - max_items_per_zone=max_items_per_zone, - mode=mode, - data=escape(data, cls._additional_escapes) - ) - - def _get_custom_scenario_xml(self, filename): - data = loader.load_unicode(filename) - return "".format( - data=escape(data, self._additional_escapes) - ) - - def _add_scenario(self, identifier, title, xml): - scenarios.add_xml_scenario(identifier, title, xml) - self.addCleanup(scenarios.remove_scenario, identifier) - - def _get_items(self): - items_container = self._page.find_element_by_css_selector('.item-bank') - return items_container.find_elements_by_css_selector('.option') - - def _get_zones(self): - return self._page.find_elements_by_css_selector(".drag-container .zone") - - def _get_popup(self): - return self._page.find_element_by_css_selector(".popup") - - def _get_popup_wrapper(self): - return self._page.find_element_by_css_selector(".popup-wrapper") - - def _get_popup_content(self): - return self._page.find_element_by_css_selector(".popup .popup-content") - - def _get_keyboard_help(self): - return self._page.find_element_by_css_selector(".keyboard-help") - - def _get_keyboard_help_button(self): - return self._page.find_element_by_css_selector(".keyboard-help-button") - - def _get_keyboard_help_dialog(self): - return self._page.find_element_by_css_selector(".keyboard-help-dialog") - - def _get_go_to_beginning_button(self): - return self._page.find_element_by_css_selector('.go-to-beginning-button') - - def _get_reset_button(self): - return self._page.find_element_by_css_selector('.problem-action-button-wrapper .reset') - - def _get_show_answer_button(self): - return self._page.find_element_by_css_selector('.problem-action-button-wrapper .show') - - def _get_submit_button(self): - return self._page.find_element_by_css_selector('.submit-attempt-container .submit') - - def _get_attempts_info(self): - return self._page.find_element_by_css_selector('.submission-feedback') - - def _get_feedback(self): - return self._page.find_element_by_css_selector(".feedback-content") - - def _get_feedback_message(self): - return self._page.find_element_by_css_selector(".feedback .message") - - def _get_explanation(self): - return self._page.find_element_by_css_selector(".solution-span") - - def scroll_down(self, pixels=50): - self.browser.execute_script("$(window).scrollTop({})".format(pixels)) - - def is_element_in_viewport(self, element): - """Determines if the element lies at least partially in the viewport.""" - viewport = self.browser.execute_script( - "return {" - "top: window.scrollY," - "left: window.scrollX," - "bottom: window.scrollY + window.outerHeight," - "right: window.scrollX + window.outerWidth" - "};" - ) - - return all([ - any([ - viewport["top"] <= element.rect["y"] <= viewport["bottom"], - viewport["top"] <= element.rect["y"] + element.rect["height"] <= viewport["bottom"] - ]), - any([ - viewport["left"] <= element.rect["x"] <= viewport["right"], - viewport["left"] <= element.rect["x"] + element.rect["width"] <= viewport["right"] - ]) - ]) - - def _get_style(self, selector, style, computed=True): - if computed: - query = 'return getComputedStyle($("{selector}").get(0)).{style}' - else: - query = 'return $("{selector}").get(0).style.{style}' - return self.browser.execute_script(query.format(selector=selector, style=style)) - - def assertFocused(self, element): - focused_element = self.browser.switch_to.active_element - self.assertTrue(element == focused_element, 'expected element to have focus') - - def assertNotFocused(self, element): - focused_element = self.browser.switch_to.active_element - self.assertTrue(element != focused_element, 'expected element to not have focus') - - @staticmethod - def get_element_html(element): - return element.get_attribute('innerHTML').strip() - - @staticmethod - def get_element_classes(element): - return element.get_attribute('class').split() - - def wait_until_html_in(self, html, elem): - wait = WebDriverWait(elem, 2) - wait.until(lambda e: html in e.get_attribute('innerHTML'), - u"{} should be in {}".format(html, elem.get_attribute('innerHTML'))) - - @staticmethod - def wait_until_has_class(class_name, elem): - wait = WebDriverWait(elem, 2) - wait.until(lambda e: class_name in e.get_attribute('class').split(), - u"Class name {} not in {}".format(class_name, elem.get_attribute('class'))) - - def wait_for_ajax(self, timeout=15): - """ - Wait for jQuery to be loaded and for all ajax requests to finish. - Same as bok-choy's PageObject.wait_for_ajax() - """ - def is_ajax_finished(): - """ Check if all the ajax calls on the current page have completed. """ - return self.browser.execute_script("return typeof(jQuery)!='undefined' && jQuery.active==0") - - EmptyPromise(is_ajax_finished, "Finished waiting for ajax requests.", timeout=timeout).fulfill() - - class DefaultDataTestMixin(object): """ Provides a test scenario with default options. diff --git a/tests/integration/test_studio.py b/tests/integration/test_studio.py deleted file mode 100644 index b9ad65a57..000000000 --- a/tests/integration/test_studio.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import absolute_import - -import time - -import six -from six.moves import range -from xblock.utils.studio_editable_test import StudioEditableBaseTest - - -class TestStudio(StudioEditableBaseTest): - """ - Tests that cover the editing interface in the Studio. - """ - - def load_scenario(self, xml=''): - self.set_scenario_xml(xml) - self.element = self.go_to_view('studio_view') - self.fix_js_environment() - - def click_continue(self): - continue_button = self.element.find_element_by_css_selector('.continue-button') - self.scroll_into_view(continue_button) - continue_button.click() - - def scroll_into_view(self, element): - """ - Scrolls to the element and places cursor above it. - Useful when you want to click an element that is scrolled off - the visible area of the screen. - """ - # We have to use block: 'end' rather than the default 'start' because there's a fixed - # title bar in the studio view in the workbench that can obstruct the element. - script = "arguments[0].scrollIntoView({behavior: 'instant', block: 'end'})" - self.browser.execute_script(script, element) - - @property - def feedback_tab(self): - return self.element.find_element_by_css_selector('.feedback-tab') - - @property - def zones_tab(self): - return self.element.find_element_by_css_selector('.zones-tab') - - @property - def items_tab(self): - return self.element.find_element_by_css_selector('.items-tab') - - @property - def background_image_type_radio_buttons(self): - radio_buttons = self.zones_tab.find_elements_by_css_selector('.background-image-type input[type="radio"]') - self.assertEqual(len(radio_buttons), 2) - self.assertEqual(radio_buttons[0].get_attribute('value'), 'manual') - self.assertEqual(radio_buttons[1].get_attribute('value'), 'auto') - return {'manual': radio_buttons[0], 'auto': radio_buttons[1]} - - @property - def display_labels_checkbox(self): - return self.zones_tab.find_element_by_css_selector('.display-labels') - - @property - def background_image_url_field(self): - return self.zones_tab.find_element_by_css_selector('.background-manual .background-url') - - @property - def background_image_url_button(self): - return self.zones_tab.find_element_by_css_selector('.background-manual button') - - @property - def autozone_cols_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-cols') - - @property - def autozone_rows_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-rows') - - @property - def autozone_width_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-width') - - @property - def autozone_height_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-height') - - @property - def autozone_generate_button(self): - return self.zones_tab.find_element_by_css_selector('.background-auto button') - - @property - def target_preview_img(self): - return self.zones_tab.find_element_by_css_selector('.target-img') - - @property - def zones(self): - return self.zones_tab.find_elements_by_css_selector('.zone-row') - - def go_to_view(self, view_name='student_view', student_id="student_1"): - element = super().go_to_view(view_name, student_id) - time.sleep(0.1) # This method is unreliable without a delay. - return element - - def test_defaults(self): - """ - Basic test to verify stepping through the editor steps and saving works. - """ - self.load_scenario() - # We start on the feedback tab. - self.assertTrue(self.feedback_tab.is_displayed()) - self.assertFalse(self.zones_tab.is_displayed()) - self.assertFalse(self.items_tab.is_displayed()) - # Continue to the zones tab. - self.click_continue() - self.assertFalse(self.feedback_tab.is_displayed()) - self.assertTrue(self.zones_tab.is_displayed()) - self.assertFalse(self.items_tab.is_displayed()) - # And finally to the items tab. - self.click_continue() - self.assertFalse(self.feedback_tab.is_displayed()) - self.assertFalse(self.zones_tab.is_displayed()) - self.assertTrue(self.items_tab.is_displayed()) - # Save the block and expect success. - self.click_save(expect_success=True) - - def test_custom_image(self): - """" - Verify user can provide a custom background image URL. - """ - default_bg_img_src = 'http://localhost:8081/resource/drag-and-drop-v2/public/img/triangle.png' - - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - # Manual mode should be selected by default. - self.assertTrue(radio_buttons['manual'].is_selected()) - self.assertFalse(radio_buttons['auto'].is_selected()) - url_field = self.background_image_url_field - self.assertEqual(url_field.get_attribute('value'), '') - self.assertIn( - default_bg_img_src.split('http://localhost:8081/')[1], self.target_preview_img.get_attribute('src') - ) - - custom_bg_img_src = '{}?my-custom-image=true'.format(self.target_preview_img.get_attribute('src')) - - url_field.send_keys(custom_bg_img_src) - self.scroll_into_view(self.background_image_url_button) - self.background_image_url_button.click() - self.assertEqual(self.target_preview_img.get_attribute('src'), custom_bg_img_src) - self.click_continue() - self.click_save(expect_success=True) - - # Verify the custom image src was saved successfully. - self.element = self.go_to_view('student_view') - target_img = self.element.find_element_by_css_selector('.target-img') - self.assertEqual(target_img.get_attribute('src'), custom_bg_img_src) - - # Verify the background image URL field is set to custom image src when we go back to studio view. - self.element = self.go_to_view('studio_view') - self.click_continue() - self.assertEqual(self.background_image_url_field.get_attribute('value'), custom_bg_img_src) - - def _verify_autogenerated_zones(self, cols, rows, zone_width, zone_height, padding): - zones = self.zones - self.assertEqual(len(zones), rows * cols) - for col in range(cols): - for row in range(rows): - idx = col + (row * cols) - zone = zones[idx] - expected_values = { - 'zone-title': 'Zone {}'.format(idx + 1), - 'zone-width': zone_width, - 'zone-height': zone_height, - 'zone-x': (zone_width * col) + (padding * (col + 1)), - 'zone-y': (zone_height * row) + (padding * (row + 1)), - } - for name, expected_value in six.iteritems(expected_values): - field = zone.find_element_by_css_selector('.' + name) - self.assertEqual(field.get_attribute('value'), str(expected_value)) - - def test_auto_generated_image(self): - """ - Verify that background image and zones get generated successfully. - """ - cols = 3 - rows = 2 - zone_width = 150 - zone_height = 100 - padding = 20 - - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.scroll_into_view(radio_buttons['auto']) - radio_buttons['auto'].click() - # Manual background controls should be hidden. - self.assertFalse(self.background_image_url_field.is_displayed()) - self.assertFalse(self.background_image_url_button.is_displayed()) - # Display labels checkbox should be unchecked by default. - self.assertFalse(self.display_labels_checkbox.is_selected()) - # Enter zone properties for automatic generation. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys(cols) - self.autozone_rows_field.clear() - self.autozone_rows_field.send_keys(rows) - self.autozone_width_field.clear() - self.autozone_width_field.send_keys(zone_width) - self.autozone_height_field.clear() - self.autozone_height_field.send_keys(zone_height) - # Click the generate button. - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # Verify generated data-uri was set successfully. - generated_url = self.target_preview_img.get_attribute('src') - self.assertTrue(generated_url.startswith('data:image/svg+xml;')) - expected_width = (zone_width * cols) + (padding * (cols + 1)) - expected_height = (zone_height * rows) + (padding * (rows + 1)) - self.assertEqual(self.target_preview_img.get_attribute('naturalWidth'), str(expected_width)) - self.assertEqual(self.target_preview_img.get_attribute('naturalHeight'), str(expected_height)) - # Display labels checkbox should be automatically selected. - self.assertTrue(self.display_labels_checkbox.is_selected()) - # Verify there are exactly 6 zones, and their properties are correct. - self._verify_autogenerated_zones(cols, rows, zone_width, zone_height, padding) - - # Fill in zone descriptions to make the form valid (zone descriptions are required). - for zone in self.zones: - zone.find_element_by_css_selector('.zone-description').send_keys('Description') - - # Save the block. - self.click_continue() - self.click_save(expect_success=True) - - # Verify the custom image src was saved successfully. - self.element = self.go_to_view('student_view') - target_img = self.element.find_element_by_css_selector('.target-img') - self.assertTrue(target_img.get_attribute('src').startswith('data:image/svg+xml')) - self.assertEqual(target_img.get_attribute('naturalWidth'), str(expected_width)) - self.assertEqual(target_img.get_attribute('naturalHeight'), str(expected_height)) - - # Verify the background image URL field is set to custom image src when we go back to studio view. - self.element = self.go_to_view('studio_view') - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.assertFalse(radio_buttons['manual'].is_selected()) - self.assertTrue(radio_buttons['auto'].is_selected()) - self.assertEqual(self.autozone_cols_field.get_attribute('value'), str(cols)) - self.assertEqual(self.autozone_rows_field.get_attribute('value'), str(rows)) - self.assertEqual(self.autozone_width_field.get_attribute('value'), str(zone_width)) - self.assertEqual(self.autozone_height_field.get_attribute('value'), str(zone_height)) - - def test_autozone_parameter_validation(self): - """ - Test that autozone parameters are verified to be valid. - """ - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.scroll_into_view(radio_buttons['auto']) - radio_buttons['auto'].click() - # All fields are valid initially. - self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class')) - # Set two of the fields to invalid values. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys('2.5') - self.autozone_height_field.clear() - self.autozone_height_field.send_keys('100A') - # Try to generate the image. - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # The two bad fields should show errors. - self.assertTrue('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertTrue('field-error' in self.autozone_height_field.get_attribute('class')) - # Fix the faulty values. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys('2') - self.autozone_height_field.clear() - self.autozone_height_field.send_keys('100') - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # All good now. - self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class'))