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:
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user