From 17fda08c8d73a78428b3803a663ffa8399adb82c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 11 Feb 2014 07:38:29 -0500 Subject: [PATCH 1/2] Enable XBlocks in bok_choy tests --- cms/envs/bok_choy.py | 4 ++++ lms/envs/bok_choy.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 1decc4fc87..bc2a61a8ea 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -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 diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 851ae22dd6..ae8f5bacf0 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -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 From 6125d973016ae79770d8c758e5bbb19138cab5c0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 15 Jan 2014 11:41:07 -0500 Subject: [PATCH 2/2] Add acceptance tests using the AcidBlock for LMS and Studio [LMS-1645] [LMS-2062] [LMS-2063] [LMS-2064] --- cms/envs/bok_choy.env.json | 3 +- common/djangoapps/xmodule_modifiers.py | 11 +- .../test/acceptance/pages/studio/overview.py | 143 +++++++++++++++++- common/test/acceptance/pages/studio/unit.py | 76 ++++++++++ .../test/acceptance/pages/xblock/__init__.py | 0 common/test/acceptance/pages/xblock/acid.py | 66 ++++++++ common/test/acceptance/tests/test_lms.py | 52 ++++++- common/test/acceptance/tests/test_studio.py | 76 +++++++++- requirements/edx/github.txt | 5 +- 9 files changed, 423 insertions(+), 9 deletions(-) create mode 100644 common/test/acceptance/pages/xblock/__init__.py create mode 100644 common/test/acceptance/pages/xblock/acid.py diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json index 0902499563..4536fb9df5 100644 --- a/cms/envs/bok_choy.env.json +++ b/cms/envs/bok_choy.env.json @@ -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 **", diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 2c34994c45..eb838d572d 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -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 @@ -195,7 +196,15 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a if mstart is not None: is_released = "Yes!" if (now > mstart) else "Not yet" - 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, diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index 3f6d5602e2..a3bb4439e4 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -1,16 +1,155 @@ """ 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): + """ + 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 "{}(, {!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) diff --git a/common/test/acceptance/pages/studio/unit.py b/common/test/acceptance/pages/studio/unit.py index 530ebe9ef3..9b5934abba 100644 --- a/common/test/acceptance/pages/studio/unit.py +++ b/common/test/acceptance/pages/studio/unit.py @@ -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') diff --git a/common/test/acceptance/pages/xblock/__init__.py b/common/test/acceptance/pages/xblock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/acceptance/pages/xblock/acid.py b/common/test/acceptance/pages/xblock/acid.py new file mode 100644 index 0000000000..add30c3b91 --- /dev/null +++ b/common/test/acceptance/pages/xblock/acid.py @@ -0,0 +1,66 @@ +""" +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 "{}(, {!r})".format(self.__class__.__name__, self.context_selector) diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/test_lms.py index b3573566f7..4f65729895 100644 --- a/common/test/acceptance/tests/test_lms.py +++ b/common/test/acceptance/tests/test_lms.py @@ -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')) diff --git a/common/test/acceptance/tests/test_studio.py b/common/test/acceptance/tests/test_studio.py index df5eb07a5b..989533145d 100644 --- a/common/test/acceptance/tests/test_studio.py +++ b/common/test/acceptance/tests/test_studio.py @@ -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')) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index e4a09dff5c..d32df83bb1 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -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