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 @@ ...@@ -69,7 +69,8 @@
"ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_S3_GRADE_DOWNLOADS": true,
"PREVIEW_LMS_BASE": "", "PREVIEW_LMS_BASE": "",
"SUBDOMAIN_BRANDING": false, "SUBDOMAIN_BRANDING": false,
"SUBDOMAIN_COURSE_LISTINGS": false "SUBDOMAIN_COURSE_LISTINGS": false,
"ALLOW_ALL_ADVANCED_COMPONENTS": true
}, },
"FEEDBACK_SUBMISSION_EMAIL": "", "FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **", "GITHUB_REPO_ROOT": "** OVERRIDDEN **",
......
...@@ -16,6 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy' ...@@ -16,6 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120 os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from .aws import * # pylint: disable=W0401, W0614 from .aws import * # pylint: disable=W0401, W0614
from xmodule.x_module import prefer_xmodules
######################### Testing overrides #################################### ######################### Testing overrides ####################################
...@@ -48,5 +49,8 @@ for log_name, log_level in LOG_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 # Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# Enable XBlocks
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Unfortunately, we need to use debug mode to serve staticfiles # Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True DEBUG = True
...@@ -10,6 +10,7 @@ import static_replace ...@@ -10,6 +10,7 @@ import static_replace
from django.conf import settings from django.conf import settings
from django.utils.timezone import UTC from django.utils.timezone import UTC
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from xblock.exceptions import InvalidScopeError
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule from xmodule.seq_module import SequenceModule
...@@ -199,7 +200,15 @@ def add_staff_debug_info(user, block, view, frag, context): # pylint: disable=u ...@@ -199,7 +200,15 @@ def add_staff_debug_info(user, block, view, frag, context): # pylint: disable=u
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" 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', {}), 'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location, 'location': block.location,
'xqa_key': block.xqa_key, 'xqa_key': block.xqa_key,
......
""" """
Course Outline page in Studio. 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 .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" url_path = "course"
CHILD_CLASS = CourseOutlineSection
def is_browser_on_page(self): def is_browser_on_page(self):
return self.is_css_present('body.view-outline') 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 ...@@ -3,6 +3,10 @@ Unit page in Studio
""" """
from bok_choy.page_object import PageObject 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): class UnitPage(PageObject):
...@@ -10,5 +14,77 @@ class UnitPage(PageObject): ...@@ -10,5 +14,77 @@ class UnitPage(PageObject):
Unit page in Studio 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): def is_browser_on_page(self):
return self.is_css_present('body.view-unit') 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 ...@@ -18,6 +18,7 @@ from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.progress import ProgressPage from ..pages.lms.progress import ProgressPage
from ..pages.lms.video import VideoPage from ..pages.lms.video import VideoPage
from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
...@@ -103,11 +104,13 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -103,11 +104,13 @@ class HighLevelTabTest(UniqueCourseTest):
XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')), 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('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
XBlockFixtureDesc('html', 'Test HTML'), XBlockFixtureDesc('html', 'Test HTML'),
)), )
),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children( XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2'), XBlockFixtureDesc('sequential', 'Test Subsection 2'),
XBlockFixtureDesc('sequential', 'Test Subsection 3'), XBlockFixtureDesc('sequential', 'Test Subsection 3'),
)).install() )
).install()
# Auto-auth register for the course # Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit() AutoAuthPage(self.browser, course_id=self.course_id).visit()
...@@ -252,3 +255,48 @@ class VideoTest(UniqueCourseTest): ...@@ -252,3 +255,48 @@ class VideoTest(UniqueCourseTest):
# latency through the ssh tunnel # latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0) self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time) 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 ...@@ -20,7 +20,8 @@ from ..pages.studio.settings_advanced import AdvancedSettingsPage
from ..pages.studio.settings_graders import GradingPage from ..pages.studio.settings_graders import GradingPage
from ..pages.studio.signup import SignupPage from ..pages.studio.signup import SignupPage
from ..pages.studio.textbooks import TextbooksPage 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 from .helpers import UniqueCourseTest
...@@ -107,3 +108,76 @@ class CoursePagesTest(UniqueCourseTest): ...@@ -107,3 +108,76 @@ class CoursePagesTest(UniqueCourseTest):
# Verify that each page is available # Verify that each page is available
for page in self.pages: for page in self.pages:
page.visit() 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 ...@@ -4,6 +4,7 @@ Settings for bok choy tests
import os import os
from path import path from path import path
from xmodule.x_module import prefer_xmodules
CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120 CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120
...@@ -59,5 +60,8 @@ LOG_OVERRIDES = [ ...@@ -59,5 +60,8 @@ LOG_OVERRIDES = [
for log_name, log_level in LOG_OVERRIDES: for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level) logging.getLogger(log_name).setLevel(log_level)
# Enable XBlocks
XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Unfortunately, we need to use debug mode to serve staticfiles # Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True DEBUG = True
...@@ -15,11 +15,12 @@ ...@@ -15,11 +15,12 @@
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # 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/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/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/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/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/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-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