diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..aa081d8a3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - "2.7" +before_install: + - "export DISPLAY=:99" + - "sh -e /etc/init.d/xvfb start" +install: + - "sh install_test_deps.sh" + - "pip uninstall -y xblock-drag-and-drop-v2" + - "python setup.py sdist" + - "pip install dist/xblock-drag-and-drop-v2-0.1.tar.gz" +script: python run_tests.py +notifications: + email: false \ No newline at end of file diff --git a/install_test_deps.sh b/install_test_deps.sh new file mode 100644 index 000000000..c505d591f --- /dev/null +++ b/install_test_deps.sh @@ -0,0 +1,6 @@ +# Installs xblock-sdk and dependencies needed to run the tests suite. +# Run this script inside a fresh virtual environment. +pip install -e git://github.com/edx/xblock-sdk.git#egg=xblock-sdk +pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt +pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt +python setup.py develop diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 000000000..b620bee15 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +""" +Run tests for the Drag and Drop V2 XBlock. + +This script is required to run our selenium tests inside the xblock-sdk workbench +because the workbench SDK's settings file is not inside any python module. +""" + +import os +import sys +import workbench + +if __name__ == "__main__": + # Find the location of the XBlock SDK. Note: it must be installed in development mode. + # ('python setup.py develop' or 'pip install -e') + xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__)) + sys.path.append(xblock_sdk_dir) + + # Use the workbench settings file: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + # Configure a range of ports in case the default port of 8081 is in use + os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099") + + from django.core.management import execute_from_command_line + args = sys.argv[1:] + paths = [arg for arg in args if arg[0] != '-'] + if not paths: + paths = ["tests/"] + options = [arg for arg in args if arg not in paths] + execute_from_command_line([sys.argv[0], "test"] + paths + options) diff --git a/setup.py b/setup.py index a73ae8521..7895f8a10 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,10 @@ def package_data(pkg, root_list): packages=['drag_and_drop_v2'], install_requires=[ 'XBlock', + 'xblock-utils', + 'ddt' ], + dependency_links = ['http://github.com/edx/xblock-utils/tarball/master#egg=xblock-utils'], entry_points={ 'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock', }, diff --git a/tests/data/test_data.json b/tests/data/test_data.json index fb0bada83..8f20a7927 100644 --- a/tests/data/test_data.json +++ b/tests/data/test_data.json @@ -89,5 +89,7 @@ "start": "Intro Feed", "finish": "Final Feed" }, - "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png" + "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", + "title": "Drag and Drop", + "question_text": "" } diff --git a/tests/data/test_get_data.json b/tests/data/test_get_data.json index a19cefbf3..41b8ddf00 100644 --- a/tests/data/test_get_data.json +++ b/tests/data/test_get_data.json @@ -68,5 +68,7 @@ "feedback": { "start": "Intro Feed" }, - "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png" + "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", + "title": "Drag and Drop", + "question_text": "" } diff --git a/tests/data/test_get_html_data.json b/tests/data/test_get_html_data.json index 664c04d8a..a4b5ba638 100644 --- a/tests/data/test_get_html_data.json +++ b/tests/data/test_get_html_data.json @@ -68,5 +68,7 @@ "feedback": { "start": "Intro Feed" }, - "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png" + "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", + "title": "Drag and Drop", + "question_text": "" } diff --git a/tests/data/test_html_data.json b/tests/data/test_html_data.json index 8fc974933..5b8872b24 100644 --- a/tests/data/test_html_data.json +++ b/tests/data/test_html_data.json @@ -89,5 +89,7 @@ "start": "Intro Feed", "finish": "Final Feed" }, - "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png" + "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", + "title": "Drag and Drop", + "question_text": "" } diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py index 94660ae95..3600a723b 100644 --- a/tests/integration/test_base.py +++ b/tests/integration/test_base.py @@ -4,32 +4,21 @@ from tests.utils import load_resource from workbench import scenarios -from workbench.test.selenium_test import SeleniumTest + +from xblockutils.base_test import SeleniumBaseTest # Classes ########################################################### -class BaseIntegrationTest(SeleniumTest): +class BaseIntegrationTest(SeleniumBaseTest): + default_css_selector = 'section.xblock--drag-and-drop' + module_name = __name__ _additional_escapes = { '"': """, "'": "'" } - def setUp(self): - super(BaseIntegrationTest, self).setUp() - - # Use test scenarios - self.browser.get(self.live_server_url) # Needed to load tests once - scenarios.SCENARIOS.clear() - - # Suzy opens the browser to visit the workbench - self.browser.get(self.live_server_url) - - # She knows it's the site by the header - header1 = self.browser.find_element_by_css_selector('h1') - self.assertEqual(header1.text, 'XBlock scenarios') - def _make_scenario_xml(self, display_name, question_text, completed): return """ @@ -48,8 +37,8 @@ def _add_scenario(self, identifier, title, xml): self.addCleanup(scenarios.remove_scenario, identifier) def _get_items(self): - items_container = self._page.find_element_by_css_selector('ul.items') - return items_container.find_elements_by_css_selector('li.option') + items_container = self._page.find_element_by_css_selector('.items') + return items_container.find_elements_by_css_selector('.option') def _get_zones(self): return self._page.find_elements_by_css_selector(".drag-container .zone") @@ -57,25 +46,13 @@ def _get_zones(self): def _get_feedback_message(self): return self._page.find_element_by_css_selector(".feedback .message") - def go_to_page(self, page_name, css_selector='section.xblock--drag-and-drop'): - """ - Navigate to the page `page_name`, as listed on the workbench home - Returns the DOM element on the visited page located by the `css_selector` - """ - self.browser.get(self.live_server_url) - self.browser.find_element_by_link_text(page_name).click() - return self.browser.find_element_by_css_selector(css_selector) - def get_element_html(self, element): return element.get_attribute('innerHTML').strip() def get_element_classes(self, element): return element.get_attribute('class').split() - def scroll_to(self, y): - self.browser.execute_script('window.scrollTo(0, {0})'.format(y)) - - def wait_until_contains_html(self, html, elem): + 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'))) @@ -84,4 +61,3 @@ def wait_until_has_class(self, 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'))) - diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py index 840369cdc..8838fcd99 100644 --- a/tests/integration/test_interaction.py +++ b/tests/integration/test_interaction.py @@ -48,15 +48,17 @@ def setUp(self): scenario_xml = self._get_scenario_xml() self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - self.browser.get(self.live_server_url) 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, 800) def _get_item_by_value(self, item_value): - items_container = self._page.find_element_by_css_selector('ul.items') - return items_container.find_elements_by_xpath("//li[@data-value='{item_id}']".format(item_id=item_value))[0] + items_container = self._page.find_element_by_css_selector('.items') + return items_container.find_elements_by_xpath("//div[@data-value='{item_id}']".format(item_id=item_value))[0] def _get_zone_by_id(self, zone_id): - zones_container = self._page.find_element_by_css_selector('div.target') + zones_container = self._page.find_element_by_css_selector('.target') return zones_container.find_elements_by_xpath("//div[@data-zone='{zone_id}']".format(zone_id=zone_id))[0] def _get_input_div_by_value(self, item_value): @@ -68,7 +70,6 @@ def _send_input(self, item_value, value): element.find_element_by_class_name('input').send_keys(value) element.find_element_by_class_name('submit-input').click() - def drag_item_to_zone(self, item_value, zone_id): element = self._get_item_by_value(item_value) target = self._get_zone_by_id(zone_id) @@ -76,11 +77,11 @@ def drag_item_to_zone(self, item_value, zone_id): action_chains.drag_and_drop(element, target).perform() def test_item_positive_feedback_on_good_move(self): - feedback_popup = self._page.find_element_by_css_selector(".popup-content") for definition in self._get_correct_item_for_zone().values(): if not definition.input: self.drag_item_to_zone(definition.item_id, definition.zone_id) - self.wait_until_contains_html(definition.feedback_positive, feedback_popup) + feedback_popup = self._page.find_element_by_css_selector(".popup-content") + self.wait_until_html_in(definition.feedback_positive, feedback_popup) def test_item_positive_feedback_on_good_input(self): feedback_popup = self._page.find_element_by_css_selector(".popup-content") @@ -90,7 +91,7 @@ def test_item_positive_feedback_on_good_input(self): self._send_input(definition.item_id, definition.input) input_div = self._get_input_div_by_value(definition.item_id) self.wait_until_has_class('correct', input_div) - self.wait_until_contains_html(definition.feedback_positive, feedback_popup) + self.wait_until_html_in(definition.feedback_positive, feedback_popup) def test_item_negative_feedback_on_bad_move(self): feedback_popup = self._page.find_element_by_css_selector(".popup-content") @@ -100,7 +101,7 @@ def test_item_negative_feedback_on_bad_move(self): if zone == definition.zone_id: continue self.drag_item_to_zone(definition.item_id, zone) - self.wait_until_contains_html(definition.feedback_negative, feedback_popup) + self.wait_until_html_in(definition.feedback_negative, feedback_popup) def test_item_positive_feedback_on_bad_input(self): feedback_popup = self._page.find_element_by_css_selector(".popup-content") @@ -110,7 +111,7 @@ def test_item_positive_feedback_on_bad_input(self): self._send_input(definition.item_id, '1999999') input_div = self._get_input_div_by_value(definition.item_id) self.wait_until_has_class('incorrect', input_div) - self.wait_until_contains_html(definition.feedback_negative, feedback_popup) + self.wait_until_html_in(definition.feedback_negative, feedback_popup) def test_final_feedback_and_reset(self): feedback_message = self._get_feedback_message() @@ -128,16 +129,13 @@ def test_final_feedback_and_reset(self): input_div = self._get_input_div_by_value(item_key) self.wait_until_has_class('correct', input_div) - self.wait_until_contains_html(self.feedback['final'], feedback_message) + self.wait_until_exists('.reset-button') + self.wait_until_html_in(self.feedback['final'], self._get_feedback_message()) - # scrolling to have `reset` visible, otherwise it does not receive a click - # this is due to xblock workbench header that consumes top 40px - selenium scrolls so page so that target - # element is a the very top. - self.scroll_to(100) - reset = self._page.find_element_by_css_selector(".reset-button") + reset = self._page.find_element_by_css_selector('.reset-button') reset.click() - self.wait_until_contains_html(self.feedback['intro'], feedback_message) + self.wait_until_html_in(self.feedback['intro'], self._get_feedback_message()) locations_after_reset = get_locations() for item_key in items.keys(): diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index 84e28f976..470948066 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -59,4 +59,4 @@ def test_zones(self): def test_feedback(self): feedback_message = self._get_feedback_message() - self.assertEqual(feedback_message.text, "Intro Feed") \ No newline at end of file + self.assertEqual(feedback_message.text, "Intro Feed") diff --git a/tests/integration/test_title_and_question.py b/tests/integration/test_title_and_question.py index 5eb989c86..fe69d55f7 100644 --- a/tests/integration/test_title_and_question.py +++ b/tests/integration/test_title_and_question.py @@ -1,15 +1,17 @@ -from nose_parameterized import parameterized +from ddt import ddt, unpack, data from tests.integration.test_base import BaseIntegrationTest from workbench import scenarios +@ddt class TestDragAndDropTitleAndQuestion(BaseIntegrationTest): - @parameterized.expand([ + @unpack + @data( ('plain1', 'title1', 'question1'), ('plain2', 'title2', 'question2'), ('html1', 'title with HTML', 'Question with HTML'), ('html2', 'Title: HTML?', 'Span question'), - ]) + ) def test_title_and_question_parameters(self, _, display_name, question_text): const_page_name = 'Test block parameters' const_page_id = 'test_block_title' diff --git a/tests/manage.py b/tests/manage.py deleted file mode 100755 index 4d78be871..000000000 --- a/tests/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -"""Manage.py file for xblock-drag-and-drop-v2""" -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") - - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 732752908..000000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -bok_choy==0.3.2 -cookiecutter==0.7.1 -coverage==3.7.1 -diff-cover==0.7.2 -Django==1.4.16 -django_nose==1.2 -fs==0.5.0 -lazy==1.2 -lxml==3.4.1 -mock==1.0.1 -nose==1.3.4 -nose-parameterized==0.3.5 -pep8==1.5.7 -pylint==0.28 -pypng==0.0.17 -rednose==0.4.1 -requests==2.4.3 -selenium==2.44.0 -simplejson==3.6.5 -webob==1.4 - --e git+https://github.com/open-craft/XBlock.git@3ece535ee8e095f21de2f8b28cc720145749f0d6#egg=XBlock --e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock --e git+https://github.com/pmitros/django-pyfs.git@514607d78535fd80bfd23184cd292ee5799b500d#egg=djpyfs --e git+https://github.com/open-craft/xblock-sdk.git@39b00c58931664ce29bb4b38df827fe237a69613#egg=xblock-sdk - --e . diff --git a/tests/settings.py b/tests/settings.py deleted file mode 100644 index 44ab6dc6f..000000000 --- a/tests/settings.py +++ /dev/null @@ -1,39 +0,0 @@ -DEBUG = True - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'workbench', - 'sample_xblocks.basic', - 'django_nose', -) - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'drag_and_drop_v2.db' - } -} - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' - } -} - -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.eggs.Loader', -) - -ROOT_URLCONF = 'urls' - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - -STATIC_ROOT = '' -STATIC_URL = '/static/' - -WORKBENCH = {'reset_state_on_restart': False} diff --git a/tests/test_drag_and_drop_v2.py b/tests/test_drag_and_drop_v2.py index a22185b3d..5bf731f3c 100644 --- a/tests/test_drag_and_drop_v2.py +++ b/tests/test_drag_and_drop_v2.py @@ -7,6 +7,7 @@ from mock import Mock from workbench.runtime import WorkbenchRuntime +from xblock.fields import ScopeIds from xblock.runtime import KvsFieldData, DictKeyValueStore from nose.tools import ( @@ -33,10 +34,14 @@ def make_request(body, method='POST'): def make_block(): - runtime = WorkbenchRuntime() + block_type = 'drag_and_drop_v2' key_store = DictKeyValueStore() - db_model = KvsFieldData(key_store) - return drag_and_drop_v2.DragAndDropBlock(runtime, db_model, Mock()) + field_data = KvsFieldData(key_store) + runtime = WorkbenchRuntime() + def_id = runtime.id_generator.create_definition(block_type) + usage_id = runtime.id_generator.create_usage(def_id) + scope_ids = ScopeIds('user', block_type, def_id, usage_id) + return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids) def test_templates_contents(): @@ -46,17 +51,9 @@ def test_templates_contents(): block.question_text = "Question Drag & Drop" block.weight = 5 - student_fragment = block.render('student_view', Mock()) + student_fragment = block.runtime.render(block, 'student_view', ['ingore'])# block.render('student_view', Mock()) assert_in('
', student_fragment.content) - assert_in('{{ value }}', student_fragment.content) - assert_in("Test Drag & Drop", student_fragment.content) - assert_in("Question Drag & Drop", student_fragment.content) - - studio_fragment = block.render('studio_view', Mock()) - assert_in('
', - studio_fragment.content) - assert_in('{{ value }}', studio_fragment.content) def test_studio_submit(): block = make_block() @@ -176,7 +173,7 @@ def test_do_attempt_with_input(self): expected = self.get_data_response() expected["state"] = { "items": { - "1": {"top": "22px", "left": "222px", "correct_input": False} + "1": {"top": "22px", "left": "222px", "absolute": True, "correct_input": False} }, "finished": False } @@ -196,7 +193,8 @@ def test_do_attempt_with_input(self): expected = self.get_data_response() expected["state"] = { "items": { - "1": {"top": "22px", "left": "222px", "input": "250", "correct_input": False} + "1": {"top": "22px", "left": "222px", "absolute": True, + "input": "250", "correct_input": False} }, "finished": False } @@ -216,7 +214,8 @@ def test_do_attempt_with_input(self): expected = self.get_data_response() expected["state"] = { "items": { - "1": {"top": "22px", "left": "222px", "input": "103", "correct_input": True} + "1": {"top": "22px", "left": "222px", "absolute": True, + "input": "103", "correct_input": True} }, "finished": False } @@ -255,7 +254,8 @@ def test_do_attempt_final(self): expected = self.get_data_response() expected["state"] = { "items": { - "0": {"top": "11px", "left": "111px", "correct_input": True} + "0": {"top": "11px", "left": "111px", "absolute": True, + "correct_input": True} }, "finished": False } @@ -277,8 +277,9 @@ def test_do_attempt_final(self): expected = self.get_data_response() expected["state"] = { "items": { - "0": {"top": "11px", "left": "111px", "correct_input": True}, - "1": {"top": "22px", "left": "222px", "input": "99", "correct_input": True} + "0": {"top": "11px", "left": "111px", "absolute": True, "correct_input": True}, + "1": {"top": "22px", "left": "222px", "absolute": True, "input": "99", + "correct_input": True} }, "finished": True } @@ -336,8 +337,8 @@ def test_ajax_solve_and_reset(): block.handle('do_attempt', make_request(data)) assert_true(block.completed) - assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px"}, - '1': {"top": "22px", "left": "222px"}}) + assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px", "absolute": True}, + '1': {"top": "22px", "left": "222px", "absolute": True}}) block.handle('reset', make_request("{}")) diff --git a/tests/urls.py b/tests/urls.py deleted file mode 100644 index 708eef40d..000000000 --- a/tests/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.conf.urls import include, url - -urlpatterns = [ - url(r'^', include('workbench.urls')), -]