Commit d1e4d124 by Matjaz Gregoric

Simplify test infrastructure & update tests.

Also includes Travis CI integration.
parent c7fd6641
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: pep8 drag_and_drop_v2 --max-line-length=120 && python run_tests.py
notifications:
email: false
\ No newline at end of file
......@@ -93,22 +93,15 @@ You can define an arbitrary number of drag items.
Testing
-------
In a virtualenv, run
Inside a fresh virtualenv, run
```bash
$ cd .../xblock-drag-and-drop-v2/
$ pip install -r tests/requirements.txt
$ sh install_test_deps.sh
```
To run the tests, from the xblock-drag-and-drop-v2 repository root:
```bash
$ tests/manage.py test --rednose
```
To include coverage report (although selenium tends to crash with
segmentation faults when collection test coverage):
```bash
$ tests/manage.py test --rednose --with-cover --cover-package=drag_and_drop_v2
$ python run_tests.py
```
# 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
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
locally-disabled,
missing-docstring,
too-many-ancestors,
too-many-public-methods,
unused-argument
[SIMILARITIES]
min-similarity-lines=8
#!/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)
......@@ -28,7 +28,10 @@ setup(
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',
},
......
......@@ -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": ""
}
......@@ -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": ""
}
......@@ -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": ""
}
......@@ -89,5 +89,7 @@
"start": "Intro Feed",
"finish": "Final <b>Feed</b>"
},
"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": ""
}
......@@ -4,32 +4,21 @@ from selenium.webdriver.support.ui import WebDriverWait
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 = {
'"': "&quot;",
"'": "&apos;"
}
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 """
<vertical_demo>
......@@ -48,8 +37,8 @@ class BaseIntegrationTest(SeleniumTest):
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 @@ class BaseIntegrationTest(SeleniumTest):
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 @@ class BaseIntegrationTest(SeleniumTest):
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')))
......@@ -48,15 +48,17 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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 @@ class InteractionTestFixture(BaseIntegrationTest):
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():
......
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 <i>HTML</i>', 'Question with <i>HTML</i>'),
('html2', '<span style="color:red">Title: HTML?</span>', '<span style="color:red">Span question</span>'),
])
)
def test_title_and_question_parameters(self, _, display_name, question_text):
const_page_name = 'Test block parameters'
const_page_id = 'test_block_title'
......
#!/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)
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 .
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}
......@@ -7,6 +7,7 @@ from webob import Request
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('<section class="xblock--drag-and-drop">',
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('<div class="xblock--drag-and-drop editor-with-buttons">',
studio_fragment.content)
assert_in('{{ value }}', studio_fragment.content)
def test_studio_submit():
block = make_block()
......@@ -176,7 +173,7 @@ class BaseDragAndDropAjaxFixture(object):
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 @@ class BaseDragAndDropAjaxFixture(object):
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 @@ class BaseDragAndDropAjaxFixture(object):
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 @@ class BaseDragAndDropAjaxFixture(object):
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 @@ class BaseDragAndDropAjaxFixture(object):
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("{}"))
......
from django.conf.urls import include, url
urlpatterns = [
url(r'^', include('workbench.urls')),
]
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