Commit d58358a4 by Calen Pennington

Merge pull request #2555 from cpennington/xblock-acid-acceptance-tests

Add acceptance tests of the Acid block in both LMS and Studio
parents c7c807f2 6125d973
......@@ -69,7 +69,8 @@
"ENABLE_S3_GRADE_DOWNLOADS": true,
"PREVIEW_LMS_BASE": "",
"SUBDOMAIN_BRANDING": false,
"SUBDOMAIN_COURSE_LISTINGS": false
"SUBDOMAIN_COURSE_LISTINGS": false,
"ALLOW_ALL_ADVANCED_COMPONENTS": true
},
"FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
......
......@@ -16,6 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from .aws import * # pylint: disable=W0401, W0614
from xmodule.x_module import prefer_xmodules
######################### Testing overrides ####################################
......@@ -48,5 +49,8 @@ for log_name, log_level in LOG_OVERRIDES:
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# Enable XBlocks
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True
......@@ -10,6 +10,7 @@ import static_replace
from django.conf import settings
from django.utils.timezone import UTC
from edxmako.shortcuts import render_to_string
from xblock.exceptions import InvalidScopeError
from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule
......@@ -199,7 +200,15 @@ def add_staff_debug_info(user, block, view, frag, context): # pylint: disable=u
if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(name, field.read_from(block)) for name, field in block.fields.items()],
field_contents = []
for name, field in block.fields.items():
try:
field_contents.append((name, field.read_from(block)))
except InvalidScopeError:
log.warning("Unable to read field in Staff Debug information", exc_info=True)
field_contents.append((name, "WARNING: Unable to read field"))
staff_context = {'fields': field_contents,
'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location,
'xqa_key': block.xqa_key,
......
"""
Course Outline page in Studio.
"""
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import EmptyPromise, fulfill
from .course_page import CoursePage
from .unit import UnitPage
class CourseOutlinePage(CoursePage):
class CourseOutlineContainer(object):
"""
Course Outline page in Studio.
A mixin to a CourseOutline page object that adds the ability to load
a child page object by title.
CHILD_CLASS must be a :class:`CourseOutlineChild` subclass.
"""
CHILD_CLASS = None
def child(self, title):
return self.CHILD_CLASS(
self.browser,
self.q(css=self.CHILD_CLASS.BODY_SELECTOR).filter(
SubQuery(css=self.CHILD_CLASS.NAME_SELECTOR).filter(text=title)
)[0]['data-locator']
)
class CourseOutlineChild(PageObject):
"""
A mixin to a CourseOutline page object that will be used as a child of
:class:`CourseOutlineContainer`.
"""
NAME_SELECTOR = None
BODY_SELECTOR = None
def __init__(self, browser, locator):
super(CourseOutlineChild, self).__init__(browser)
self.locator = locator
def is_browser_on_page(self):
return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
@property
def name(self):
"""
Return the display name of this object.
"""
titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
if titles:
return titles[0]
else:
return None
def __repr__(self):
return "{}(<browser>, {!r})".format(self.__class__.__name__, self.locator)
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to this particular `CourseOutlineChild` context
"""
return '{}[data-locator="{}"] {}'.format(
self.BODY_SELECTOR,
self.locator,
selector
)
class CourseOutlineUnit(CourseOutlineChild):
"""
PageObject that wraps a unit link on the Studio Course Overview page.
"""
url = None
BODY_SELECTOR = '.courseware-unit'
NAME_SELECTOR = '.unit-name'
def go_to(self):
"""
Open the unit page linked to by this unit link, and return
an initialized :class:`.UnitPage` for that unit.
"""
return UnitPage(self.browser, self.locator).visit()
class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
"""
:class`.PageObject` that wraps a subsection block on the Studio Course Overview page.
"""
url = None
BODY_SELECTOR = '.courseware-subsection'
NAME_SELECTOR = '.subsection-name-value'
CHILD_CLASS = CourseOutlineUnit
def unit(self, title):
"""
Return the :class:`.CourseOutlineUnit with the title `title`.
"""
return self.child(title)
def toggle_expand(self):
"""
Toggle the expansion of this subsection.
"""
self.disable_jquery_animations()
def subsection_expanded():
return all(
self.q(css=self._bounded_selector('.new-unit-item'))
.map(lambda el: el.visible)
.results
)
currently_expanded = subsection_expanded()
self.css_click(self._bounded_selector('.expand-collapse'))
fulfill(EmptyPromise(
lambda: subsection_expanded() != currently_expanded,
"Check that the subsection {} has been toggled".format(self.locator),
))
return self
class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer):
"""
:class`.PageObject` that wraps a section block on the Studio Course Overview page.
"""
url = None
BODY_SELECTOR = '.courseware-section'
NAME_SELECTOR = '.section-name-span'
CHILD_CLASS = CourseOutlineSubsection
def subsection(self, title):
"""
Return the :class:`.CourseOutlineSubsection` with the title `title`.
"""
return self.child(title)
class CourseOutlinePage(CoursePage, CourseOutlineContainer):
"""
Course Outline page in Studio.
"""
url_path = "course"
CHILD_CLASS = CourseOutlineSection
def is_browser_on_page(self):
return self.is_css_present('body.view-outline')
def section(self, title):
"""
Return the :class:`.CourseOutlineSection` with the title `title`.
"""
return self.child(title)
......@@ -3,6 +3,10 @@ Unit page in Studio
"""
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import EmptyPromise, fulfill
from . import BASE_URL
class UnitPage(PageObject):
......@@ -10,5 +14,77 @@ class UnitPage(PageObject):
Unit page in Studio
"""
def __init__(self, browser, unit_locator):
super(UnitPage, self).__init__(browser)
self.unit_locator = unit_locator
@property
def url(self):
"""URL to the static pages UI in a course."""
return "{}/unit/{}".format(BASE_URL, self.unit_locator)
def is_browser_on_page(self):
return self.is_css_present('body.view-unit')
def component(self, title):
return Component(
self.browser,
self.q(css=Component.BODY_SELECTOR).filter(
SubQuery(css=Component.NAME_SELECTOR).filter(text=title)
)[0]['data-locator']
)
class Component(PageObject):
"""
A PageObject representing an XBlock child on the Studio UnitPage (including
the editing controls).
"""
url = None
BODY_SELECTOR = '.component'
NAME_SELECTOR = '.component-header'
def __init__(self, browser, locator):
super(Component, self).__init__(browser)
self.locator = locator
def is_browser_on_page(self):
return self.is_css_present('{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator))
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to this particular `CourseOutlineChild` context
"""
return '{}[data-locator="{}"] {}'.format(
self.BODY_SELECTOR,
self.locator,
selector
)
@property
def name(self):
titles = self.css_text(self._bounded_selector(self.NAME_SELECTOR))
if titles:
return titles[0]
else:
return None
@property
def preview_selector(self):
return self._bounded_selector('.xblock-student_view')
def edit(self):
self.css_click(self._bounded_selector('.edit-button'))
fulfill(EmptyPromise(
lambda: all(
self.q(css=self._bounded_selector('.component-editor'))
.map(lambda el: el.visible)
.results
),
"Verify that the editor for component {} has been expanded".format(self.locator)
))
return self
@property
def editor_selector(self):
return self._bounded_selector('.xblock-studio_view')
"""
PageObjects related to the AcidBlock
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, BrokenPromise, fulfill
class AcidView(PageObject):
"""
A :class:`.PageObject` representing the rendered view of the :class:`.AcidBlock`.
"""
url = None
def __init__(self, browser, context_selector):
"""
Args:
browser (splinter.browser.Browser): The browser that this page is loaded in.
context_selector (str): The selector that identifies where this :class:`.AcidBlock` view
is on the page.
"""
super(AcidView, self).__init__(browser)
if isinstance(context_selector, unicode):
context_selector = context_selector.encode('utf-8')
self.context_selector = context_selector
def is_browser_on_page(self):
return (
self.is_css_present('{} .acid-block'.format(self.context_selector)) and
self.browser.evaluate_script("$({!r}).data('initialized')".format(self.context_selector))
)
def test_passed(self, test_selector):
"""
Return whether a particular :class:`.AcidBlock` test passed.
"""
selector = '{} .acid-block {} .pass'.format(self.context_selector, test_selector)
return bool(self.q(css=selector).execute(try_interval=0.1, timeout=3))
@property
def init_fn_passed(self):
"""
Whether the init-fn test passed in this view of the :class:`.AcidBlock`.
"""
return self.test_passed('.js-init-run')
@property
def doc_ready_passed(self):
"""
Whether the document-ready test passed in this view of the :class:`.AcidBlock`.
"""
return self.test_passed('.document-ready-run')
def scope_passed(self, scope):
return all(
self.test_passed('.scope-storage-test.scope-{} {}'.format(scope, test))
for test in (
".server-storage-test-returned",
".server-storage-test-succeeded",
".client-storage-test-returned",
".client-storage-test-succeeded",
)
)
def __repr__(self):
return "{}(<browser>, {!r})".format(self.__class__.__name__, self.context_selector)
......@@ -18,6 +18,7 @@ from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.progress import ProgressPage
from ..pages.lms.video import VideoPage
from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
......@@ -103,11 +104,13 @@ class HighLevelTabTest(UniqueCourseTest):
XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
XBlockFixtureDesc('html', 'Test HTML'),
)),
)
),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2'),
XBlockFixtureDesc('sequential', 'Test Subsection 3'),
)).install()
)
).install()
# Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit()
......@@ -252,3 +255,48 @@ class VideoTest(UniqueCourseTest):
# latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
class XBlockAcidTest(UniqueCourseTest):
"""
Tests that verify that XBlock integration is working correctly
"""
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
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('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)
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
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'))
......@@ -20,7 +20,8 @@ from ..pages.studio.settings_advanced import AdvancedSettingsPage
from ..pages.studio.settings_graders import GradingPage
from ..pages.studio.signup import SignupPage
from ..pages.studio.textbooks import TextbooksPage
from ..fixtures.course import CourseFixture
from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
......@@ -107,3 +108,76 @@ class CoursePagesTest(UniqueCourseTest):
# Verify that each page is available
for page in self.pages:
page.visit()
class XBlockAcidTest(WebAppTest):
"""
Tests that verify that XBlock integration is working correctly
"""
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
# Define a unique course identifier
self.course_info = {
'org': 'test_org',
'number': 'course_' + self.unique_id[:5],
'run': 'test_' + self.unique_id,
'display_name': 'Test Course ' + self.unique_id
}
self.auth_page = AutoAuthPage(self.browser, staff=True)
self.outline = CourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.course_id = '{org}.{number}.{run}'.format(**self.course_info)
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')
)
)
)
).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')
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'))
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'))
......@@ -4,6 +4,7 @@ Settings for bok choy tests
import os
from path import path
from xmodule.x_module import prefer_xmodules
CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120
......@@ -59,5 +60,8 @@ LOG_OVERRIDES = [
for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level)
# Enable XBlocks
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True
......@@ -15,11 +15,12 @@
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries:
-e git+https://github.com/edx/XBlock.git@3830ee50015b460fad63ff3b71f77bf1a2684195#egg=XBlock
-e git+https://github.com/edx/XBlock.git@6d431d786587bd8f3a19a893364914d6e2d6c28f#egg=XBlock
-e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@v0.1.0#egg=bok_choy
-e git+https://github.com/edx/bok-choy.git@v0.2.1#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@15bf143b15714e22fc451ff1b0f8a7a2a9483172#egg=django-splash
-e git+https://github.com/edx/acid-block.git@aa95a3c#egg=acid-xblock
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