Merge pull request #2602 from cpennington/xblock-acid-child-tests

Add tests of the frontend children api using the AcidXBlock
This commit is contained in:
Calen Pennington
2014-02-21 16:07:23 -05:00
23 changed files with 279 additions and 110 deletions

View File

@@ -78,6 +78,7 @@ def get_test_system(course_id=''):
course_id=course_id,
error_descriptor_class=ErrorDescriptor,
get_user_role=Mock(is_staff=False),
descriptor_runtime=get_test_descriptor_system(),
)

View File

@@ -1048,7 +1048,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
"""
def __init__(
self, static_url, track_function, get_module, render_template,
replace_urls, user=None, filestore=None,
replace_urls, descriptor_runtime, user=None, filestore=None,
debug=False, hostname="", xqueue=None, publish=None, node_path="",
anonymous_student_id='', course_id=None,
open_ended_grading_interface=None, s3_interface=None,
@@ -1089,6 +1089,8 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
that capa_module can use to fix up the static urls in
ajax results.
descriptor_runtime - A `DescriptorSystem` to use for loading xblocks by id
anonymous_student_id - Used for tracking modules with student id
course_id - the course_id containing this module
@@ -1148,6 +1150,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
self.get_real_user = get_real_user
self.get_user_role = get_user_role
self.descriptor_runtime = descriptor_runtime
def get(self, attr):
""" provide uniform access to attributes (like etree)."""
@@ -1172,7 +1175,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
return self.handler_url(self.xmodule_instance, 'xmodule_handler', '', '').rstrip('/?')
def get_block(self, block_id):
raise NotImplementedError("XModules must use get_module to load other modules")
return self.get_module(self.descriptor_runtime.get_block(block_id))
def resource_url(self, resource):
raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls")

View File

@@ -31,8 +31,8 @@ describe "XBlock", ->
@missingInitBlock = XBlock.initializeBlock($('#missing-init')[0])
it "loads the right runtime version", ->
expect(TestRuntime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
expect(TestRuntime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
expect(TestRuntime.vA).toHaveBeenCalledWith()
expect(TestRuntime.vZ).toHaveBeenCalledWith()
it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, $('#vA')[0])

View File

@@ -9,12 +9,13 @@ describe "XBlock.Runtime.v1", ->
]
@element = $('.xblock')[0]
$(@element).prop('xblock_children', @children)
@runtime = new XBlock.Runtime.v1(@element, @children)
@runtime = new XBlock.Runtime.v1(@element)
it "provides a list of children", ->
expect(@runtime.children).toBe(@children)
expect(@runtime.children(@element)).toBe(@children)
it "maps children by name", ->
expect(@runtime.childMap.childA).toBe(@children[0])
expect(@runtime.childMap.childB).toBe(@children[1])
expect(@runtime.childMap(@element, 'childA')).toBe(@children[0])
expect(@runtime.childMap(@element, 'childB')).toBe(@children[1])

View File

@@ -7,8 +7,9 @@
runtime = $element.data("runtime-class")
version = $element.data("runtime-version")
initFnName = $element.data("init")
$element.prop('xblock_children', children)
if runtime? and version? and initFnName?
runtime = new window[runtime]["v#{version}"](element, children)
runtime = new window[runtime]["v#{version}"]
initFn = window[initFnName]
block = initFn(runtime, element) ? {}
else

View File

@@ -1,5 +1,5 @@
class XBlock.Runtime.v1
constructor: (@element, @children) ->
@childMap = {}
$.each @children, (idx, child) =>
@childMap[child.name] = child
children: (block) => $(block).prop('xblock_children')
childMap: (block, childName) =>
for child in @children(block)
return child if child.name == childName

View File

@@ -95,31 +95,20 @@ class XBlockFixtureDesc(object):
self.children.extend(args)
return self
def serialize(self, parent_loc=None):
def serialize(self):
"""
Return a JSON representation of the XBlock, suitable
for sending as POST data to /xblock
XBlocks are always set to public visibility.
"""
payload = {
'category': self.category,
return json.dumps({
'display_name': self.display_name,
'data': self.data,
'metadata': self.metadata,
'grader_type': self.grader_type,
'graderType': self.grader_type,
'publish': self.publish
}
# Need to handle detached categories differently, since they are not published
# This may change in the future.
if self.category in ['static_tab']:
del payload['publish']
if parent_loc is not None:
payload['parent_locator'] = parent_loc
return json.dumps(payload)
})
def __str__(self):
"""
@@ -395,15 +384,25 @@ class CourseFixture(StudioApiFixture):
loc = self._create_xblock(parent_loc, desc)
self._create_xblock_children(loc, desc.children)
self._publish_xblock(parent_loc)
def _create_xblock(self, parent_loc, xblock_desc):
"""
Create an XBlock with `parent_loc` (the location of the parent block)
and `xblock_desc` (an `XBlockFixtureDesc` instance).
"""
create_payload = {
'category': xblock_desc.category,
'display_name': xblock_desc.display_name,
}
if parent_loc is not None:
create_payload['parent_locator'] = parent_loc
# Create the new XBlock
response = self.session.post(
STUDIO_BASE_URL + '/xblock',
data=xblock_desc.serialize(parent_loc=parent_loc),
data=json.dumps(create_payload),
headers=self.headers,
)
@@ -417,24 +416,34 @@ class CourseFixture(StudioApiFixture):
except ValueError:
raise CourseFixtureError("Could not decode JSON from '{0}'".format(response.content))
if loc is not None:
# Configure the XBlock
response = self.session.post(
STUDIO_BASE_URL + '/xblock/' + loc,
data=xblock_desc.serialize(),
headers=self.headers,
)
if response.ok:
return loc
else:
raise CourseFixtureError(
"Could not update {0}. Status code: {1}".format(
xblock_desc, response.status_code))
# Configure the XBlock
response = self.session.post(
STUDIO_BASE_URL + '/xblock/' + loc,
data=xblock_desc.serialize(),
headers=self.headers,
)
if response.ok:
return loc
else:
raise CourseFixtureError("Could not retrieve location of {0}".format(xblock_desc))
raise CourseFixtureError(
"Could not update {0}. Status code: {1}".format(
xblock_desc, response.status_code))
def _publish_xblock(self, locator):
"""
Publish the xblock at `locator`.
"""
# Create the new XBlock
response = self.session.put(
"{}/xblock/{}".format(STUDIO_BASE_URL, locator),
data=json.dumps({'publish': 'make_public'}),
headers=self.headers,
)
if not response.ok:
msg = "Could not publish {}. Status was {}".format(locator, response.status_code)
raise CourseFixtureError(msg)
def _encode_post_dict(self, post_dict):
"""

View File

@@ -4,7 +4,7 @@ Unit page in Studio
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import Promise, EmptyPromise, fulfill
from bok_choy.promise import EmptyPromise, fulfill
from . import BASE_URL
@@ -24,20 +24,32 @@ class UnitPage(PageObject):
return "{}/unit/{}".format(BASE_URL, self.unit_locator)
def is_browser_on_page(self):
return self.is_css_present('body.view-unit')
# Wait until all components have been loaded
return (
self.is_css_present('body.view-unit') and
len(self.q(css=Component.BODY_SELECTOR)) == len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR)))
)
def component(self, title):
return Component(self.browser, self._locator(title))
@property
def components(self):
"""
Return a list of components loaded on the unit page.
"""
return self.q(css=Component.BODY_SELECTOR).map(lambda el: Component(self.browser, el['data-locator'])).results
def _locator(self, title):
def _check_func():
locators = self.q(css=Component.BODY_SELECTOR).filter(
SubQuery(css=Component.NAME_SELECTOR).filter(text=title)
).map(lambda el: el['data-locator']).results
return (len(locators) > 0, locators[0])
return fulfill(Promise(_check_func, "Found data locator for component"))
def edit_draft(self):
"""
Started editing a draft of this unit.
"""
fulfill(EmptyPromise(
lambda: self.q(css='.create-draft').present,
'Wait for edit draft link to be present'
))
self.q(css='.create-draft').click()
fulfill(EmptyPromise(
lambda: self.q(css='.editing-draft-alert').present,
'Wait for draft mode to be activated'
))
class Component(PageObject):

View File

@@ -51,6 +51,16 @@ class AcidView(PageObject):
"""
return self.test_passed('.document-ready-run')
@property
def child_tests_passed(self):
"""
Whether the tests of children passed
"""
return all([
self.test_passed('.child-counts-match'),
self.test_passed('.child-values-match')
])
def scope_passed(self, scope):
return all(
self.test_passed('.scope-storage-test.scope-{} {}'.format(scope, test))

View File

@@ -3,7 +3,7 @@
E2E tests for the LMS.
"""
from unittest import skip
from unittest import skip, expectedFailure
from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, fulfill_before, fulfill, Promise
@@ -292,8 +292,9 @@ class VideoTest(UniqueCourseTest):
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('video', 'Video')
))).install()
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('video', 'Video')
)))).install()
# Auto-auth register for the course
@@ -338,18 +339,48 @@ class VideoTest(UniqueCourseTest):
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
class XBlockAcidTest(UniqueCourseTest):
class XBlockAcidBase(UniqueCourseTest):
"""
Tests that verify that XBlock integration is working correctly
Base class for tests that verify that XBlock integration is working correctly
"""
__test__ = False
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
super(XBlockAcidBase, self).setUp()
self.setup_fixtures()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser)
def test_acid_block(self):
"""
Verify that all expected acid block tests pass in the lms.
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.child_tests_passed)
self.assertTrue(acid_block.scope_passed('user_state'))
class XBlockAcidNoChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with no children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
@@ -360,24 +391,43 @@ class XBlockAcidTest(UniqueCourseTest):
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('acid', 'Acid Block')
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('acid', 'Acid Block')
)
)
)
).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser)
class XBlockAcidChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with children
"""
__test__ = True
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('acid', 'Acid Block').add_children(
XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}),
XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}),
XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"),
)
)
)
)
).install()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
def test_acid_block(self):
"""
Verify that all expected acid block tests pass in the lms.
"""
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.scope_passed('user_state'))
super(XBlockAcidChildTest, self).test_acid_block()

View File

@@ -1,6 +1,8 @@
"""
Acceptance tests for Studio.
"""
from unittest import expectedFailure
from bok_choy.web_app_test import WebAppTest
from ..pages.studio.asset_index import AssetIndexPage
@@ -110,17 +112,18 @@ class CoursePagesTest(UniqueCourseTest):
page.visit()
class XBlockAcidTest(WebAppTest):
class XBlockAcidBase(WebAppTest):
"""
Tests that verify that XBlock integration is working correctly
Base class for tests that verify that XBlock integration is working correctly
"""
__test__ = False
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
super(XBlockAcidBase, self).setUp()
# Define a unique course identifier
self.course_info = {
@@ -140,6 +143,50 @@ class XBlockAcidTest(WebAppTest):
self.course_id = '{org}.{number}.{run}'.format(**self.course_info)
self.setup_fixtures()
self.auth_page.visit()
def test_acid_block_preview(self):
"""
Verify that all expected acid block tests pass in studio preview
"""
self.outline.visit()
unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
acid_block = AcidView(self.browser, unit.components[0].preview_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.child_tests_passed)
self.assertTrue(acid_block.scope_passed('user_state'))
def test_acid_block_editor(self):
"""
Verify that all expected acid block tests pass in studio preview
"""
self.outline.visit()
unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
unit.edit_draft()
acid_block = AcidView(self.browser, unit.components[0].edit().editor_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.child_tests_passed)
self.assertTrue(acid_block.scope_passed('content'))
self.assertTrue(acid_block.scope_passed('settings'))
class XBlockAcidNoChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with no children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
@@ -157,27 +204,42 @@ class XBlockAcidTest(WebAppTest):
)
).install()
self.auth_page.visit()
self.outline.visit()
unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
self.acid_component = unit.component('Acid Block')
class XBlockAcidChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('acid', 'Acid Block').add_children(
XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}),
XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}),
XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"),
)
)
)
)
).install()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
def test_acid_block_preview(self):
"""
Verify that all expected acid block tests pass in studio preview
"""
acid_block = AcidView(self.browser, self.acid_component.preview_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.scope_passed('user_state'))
super(XBlockAcidChildTest, self).test_acid_block_preview()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
def test_acid_block_editor(self):
"""
Verify that all expected acid block tests pass in studio preview
"""
acid_block = AcidView(self.browser, self.acid_component.edit().editor_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.scope_passed('content'))
self.assertTrue(acid_block.scope_passed('settings'))
super(XBlockAcidChildTest, self).test_acid_block_editor()