From be3c7e05c5bdc4b0cca63de0b8f5aa431fddb89b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2018 15:52:05 -0400 Subject: [PATCH] Reapply "Switch container factory to webpack" This reverts commit 18d93b00ba166de22a8d22db8a1080ec8d36349f. --- .../features/advanced_settings.py | 31 +- .../contentstore/features/common.py | 1 - .../contentstore/features/html-editor.feature | 5 - .../features/problem-editor.feature | 15 - cms/djangoapps/pipeline_js/js/xmodule.js | 60 + .../pipeline_js/templates/xmodule.js | 45 - cms/djangoapps/pipeline_js/urls.py | 10 - cms/djangoapps/pipeline_js/utils.py | 18 + cms/djangoapps/pipeline_js/views.py | 44 - cms/envs/acceptance.py | 4 - cms/envs/bok_choy.py | 3 + cms/envs/bok_choy_docker.py | 6 + cms/envs/test.py | 2 - cms/static/cms/js/build.js | 7 +- cms/static/cms/js/main.js | 159 +- cms/static/cms/js/spec/main.js | 15 +- cms/static/cms/js/spec/main_webpack.js | 35 + .../cms/js/spec/xblock/cms.runtime.v1_spec.js | 137 +- cms/static/js/base.js | 72 +- cms/static/js/factories/base.js | 2 + cms/static/js/factories/container.js | 43 +- cms/static/js/factories/context_course.js | 3 + cms/static/js/factories/edit_tabs.js | 41 +- cms/static/js/factories/library.js | 47 +- cms/static/js/factories/login.js | 108 +- cms/static/js/factories/textbooks.js | 41 +- cms/static/js/factories/xblock_validation.js | 35 +- cms/static/js/pages/course.js | 6 - cms/static/js/pages/login.js | 8 - cms/static/js/pages/textbooks.js | 7 - cms/static/js/sock.js | 70 +- .../spec/factories/xblock_validation_spec.js | 126 +- .../js/spec/utils/drag_and_drop_spec.js | 1 + cms/static/js/spec/views/container_spec.js | 397 +- cms/static/js/spec/views/login_studio_spec.js | 57 +- .../js/spec/views/modals/edit_xblock_spec.js | 398 +- cms/static/js/spec/views/module_edit_spec.js | 496 +-- cms/static/js/spec/views/move_xblock_spec.js | 1469 +++---- .../js/spec/views/pages/container_spec.js | 1484 +++---- .../views/pages/container_subviews_spec.js | 1127 ++--- .../spec/views/pages/course_outline_spec.js | 3774 +++++++++-------- cms/static/js/spec/views/textbook_spec.js | 5 +- .../js/spec/views/xblock_editor_spec.js | 201 +- .../views/xblock_string_field_editor_spec.js | 315 +- cms/static/js/spec_helpers/edit_helpers.js | 223 +- cms/static/js/utils/date_utils.js | 151 +- cms/static/js/views/container.js | 15 +- .../js/views/utils/move_xblock_utils.js | 3 +- cms/static/js/views/xblock.js | 19 +- cms/static/karma_cms.conf.js | 13 +- cms/static/karma_cms_webpack.conf.js | 55 + cms/templates/base.html | 35 +- cms/templates/container.html | 6 +- cms/templates/edit-tabs.html | 6 +- cms/templates/library.html | 6 +- cms/templates/login.html | 6 +- cms/templates/studio_xblock_wrapper.html | 25 +- cms/templates/textbooks.html | 6 +- cms/urls.py | 1 - .../templates/static_content.html | 25 - common/djangoapps/terrain/ui_helpers.py | 33 +- common/lib/conftest.py | 13 + .../public/js/vertical_student_view.js | 98 +- .../xmodule/assets/word_cloud/.eslintrc.js | 10 + .../xmodule/assets/word_cloud/.gitignore | 1 + .../assets/word_cloud/public/js/word_cloud.js | 5 - .../word_cloud/public/js/word_cloud_main.js | 349 -- .../{public => src}/js/d3.layout.cloud.js | 0 .../word_cloud/{public => src}/js/d3.min.js | 0 .../assets/word_cloud/src/js/word_cloud.js | 3 + .../word_cloud/src/js/word_cloud_main.js | 315 ++ .../assets/word_cloud/webpack.config.js | 56 + .../xmodule/xmodule/js/fixtures/video.html | 9 +- .../js/fixtures/video_autoadvance.html | 1 + .../fixtures/video_autoadvance_disabled.html | 1 + .../xmodule/js/fixtures/video_hls.html | 3 + .../xmodule/js/fixtures/video_html5.html | 3 + .../js/fixtures/video_no_captions.html | 3 + .../js/fixtures/video_with_bumper.html | 1 + .../js/fixtures/video_yt_multiple.html | 1 + .../xmodule/js/karma_runner_webpack.js | 82 + .../xmodule/xmodule/js/karma_xmodule.conf.js | 14 +- .../xmodule/js/karma_xmodule_webpack.conf.js | 45 + common/lib/xmodule/xmodule/js/spec/helper.js | 13 - .../lib/xmodule/xmodule/js/spec/time_spec.js | 89 +- .../js/spec/video/async_process_spec.js | 2 +- .../xmodule/js/spec/video/general_spec.js | 20 +- .../xmodule/js/spec/video/html5_video_spec.js | 2 + .../xmodule/js/spec/video/initialize_spec.js | 4 +- .../xmodule/js/spec/video/iterator_spec.js | 2 +- .../xmodule/js/spec/video/resizer_spec.js | 4 +- .../xmodule/js/spec/video/sjson_spec.js | 2 +- .../js/spec/video/video_events_plugin_spec.js | 2 + .../js/spec/video/video_player_spec.js | 4 +- .../spec/video/video_progress_slider_spec.js | 4 +- .../video/video_save_state_plugin_spec.js | 8 +- .../js/spec/video/video_storage_spec.js | 4 +- .../xmodule/xmodule/js/spec/video_helper.js | 16 + .../xmodule/xmodule/js/src/capa/display.js | 4 +- .../xmodule/xmodule/js/src/capa/schematic.js | 38 +- .../lib/xmodule/xmodule/js/src/poll/poll.js | 16 +- .../xmodule/js/src/sequence/display.js | 4 +- common/lib/xmodule/xmodule/js/src/time.js | 83 +- .../xmodule/js/src/video/03_video_player.js | 4 +- .../xmodule/js/src/video/04_video_control.js | 4 +- .../js/src/video/09_save_state_plugin.js | 2 +- .../xmodule/js/src/video/09_video_caption.js | 5 +- .../xmodule/xmodule/js/src/video/10_main.js | 24 +- common/lib/xmodule/xmodule/static_content.py | 55 +- .../lib/xmodule/xmodule/tests/test_content.py | 6 +- common/lib/xmodule/xmodule/vertical_block.py | 4 +- .../xmodule/video_module/video_module.py | 34 - .../lib/xmodule/xmodule/word_cloud_module.py | 11 +- common/lib/xmodule/xmodule/x_module.py | 7 + .../common/js/components/utils/view_utils.js | 31 +- common/static/common/js/karma.common.conf.js | 73 +- .../common/js/spec_helpers/view_helpers.js | 21 +- common/static/common/js/utils/page_factory.js | 27 - common/static/common/js/xblock/runtime.v1.js | 17 +- common/templates/xmodule_shim.html | 4 + .../test/acceptance/pages/lms/video/video.py | 3 +- .../acceptance/pages/studio/video/video.py | 11 +- common/test/acceptance/tests/helpers.py | 33 + .../tests/video/test_studio_video_module.py | 7 + .../tests/video/test_video_module.py | 11 +- .../test/test-theme/cms/templates/login.html | 6 +- conftest.py | 4 + lms/envs/acceptance.py | 1 + lms/envs/bok_choy.py | 3 + lms/envs/bok_choy_docker.py | 6 + lms/envs/test.py | 2 - lms/static/js/views/message_banner.js | 2 +- .../courseware/courseware-chromeless.html | 3 - lms/templates/courseware/courseware.html | 3 - lms/templates/main.html | 22 +- .../commands/tests/test_cache_programs.py | 4 +- openedx/core/lib/xblock_utils/__init__.py | 7 + .../course_search/js/course_search_factory.js | 5 +- openedx/tests/util/webpack_loader/__init__.py | 0 .../webpack_loader/templatetags/__init__.py | 0 .../templatetags/webpack_loader.py | 16 - package-lock.json | 152 +- package.json | 4 +- pavelib/assets.py | 5 + pavelib/js_test.py | 1 + pavelib/paver_tests/test_js_test.py | 3 +- pavelib/quality.py | 3 +- pavelib/utils/envs.py | 4 + pavelib/utils/test/suites/js_suite.py | 4 +- requirements/edx/base.in | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/testing.txt | 2 +- scripts/xsslint/xsslint/linters.py | 2 - themes/red-theme/cms/templates/login.html | 6 +- webpack-config/file-lists.js | 17 +- webpack.common.config.js | 178 +- webpack.prod.config.js | 2 +- 158 files changed, 7161 insertions(+), 6598 deletions(-) create mode 100644 cms/djangoapps/pipeline_js/js/xmodule.js delete mode 100644 cms/djangoapps/pipeline_js/templates/xmodule.js delete mode 100644 cms/djangoapps/pipeline_js/urls.py create mode 100644 cms/djangoapps/pipeline_js/utils.py delete mode 100644 cms/djangoapps/pipeline_js/views.py create mode 100644 cms/static/cms/js/spec/main_webpack.js create mode 100644 cms/static/js/factories/context_course.js delete mode 100644 cms/static/js/pages/course.js delete mode 100644 cms/static/js/pages/login.js delete mode 100644 cms/static/js/pages/textbooks.js create mode 100644 cms/static/karma_cms_webpack.conf.js create mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/.eslintrc.js create mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/.gitignore delete mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/public/js/word_cloud.js delete mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/public/js/word_cloud_main.js rename common/lib/xmodule/xmodule/assets/word_cloud/{public => src}/js/d3.layout.cloud.js (100%) rename common/lib/xmodule/xmodule/assets/word_cloud/{public => src}/js/d3.min.js (100%) create mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud.js create mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud_main.js create mode 100644 common/lib/xmodule/xmodule/assets/word_cloud/webpack.config.js create mode 100644 common/lib/xmodule/xmodule/js/karma_runner_webpack.js create mode 100644 common/lib/xmodule/xmodule/js/karma_xmodule_webpack.conf.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video_helper.js delete mode 100644 common/static/common/js/utils/page_factory.js create mode 100644 common/templates/xmodule_shim.html delete mode 100644 openedx/tests/util/webpack_loader/__init__.py delete mode 100644 openedx/tests/util/webpack_loader/templatetags/__init__.py delete mode 100644 openedx/tests/util/webpack_loader/templatetags/webpack_loader.py diff --git a/cms/djangoapps/contentstore/features/advanced_settings.py b/cms/djangoapps/contentstore/features/advanced_settings.py index 5125fac12f37..99add57d0396 100644 --- a/cms/djangoapps/contentstore/features/advanced_settings.py +++ b/cms/djangoapps/contentstore/features/advanced_settings.py @@ -17,17 +17,26 @@ @step('I select the Advanced Settings$') def i_select_advanced_settings(step): - world.click_course_settings() - - # The click handlers are set up so that if you click - # the menu disappears. This means that if we're even a *little* - # bit off on the last item ('Advanced Settings'), the menu - # will close and the test will fail. - # For this reason, we retrieve the link and visit it directly - # This is what the browser *should* be doing, since it's just a native - # link with no JavaScript involved. - link_css = 'li.nav-course-settings-advanced a' - world.wait_for_visible(link_css) + world.wait_for_js_to_load() # pylint: disable=no-member + world.wait_for_js_variable_truthy('window.studioNavMenuActive') # pylint: disable=no-member + + for _ in range(5): + world.click_course_settings() # pylint: disable=no-member + + # The click handlers are set up so that if you click + # the menu disappears. This means that if we're even a *little* + # bit off on the last item ('Advanced Settings'), the menu + # will close and the test will fail. + # For this reason, we retrieve the link and visit it directly + # This is what the browser *should* be doing, since it's just a native + # link with no JavaScript involved. + link_css = 'li.nav-course-settings-advanced a' + try: + world.wait_for_visible(link_css) # pylint: disable=no-member + break + except AssertionError: + continue + link = world.css_find(link_css).first['href'] world.visit(link) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 70ce70d0aedb..686c8ec1562d 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -247,7 +247,6 @@ def create_unit_from_course_outline(): world.css_click(selector) world.wait_for_mathjax() - world.wait_for_xmodule() world.wait_for_loading() assert world.is_css_present('ul.new-component-type') diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature index f4e9e0e37ad4..d417cb5ffa4f 100644 --- a/cms/djangoapps/contentstore/features/html-editor.feature +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -15,11 +15,6 @@ Feature: CMS.HTML Editor Then I can modify the display name And my display name change is persisted on save - Scenario: Edit High Level source is available for LaTeX html - Given I have created an E-text Written in LaTeX - When I edit and select Settings - Then Edit High Level Source is visible - Scenario: TinyMCE image plugin sets urls correctly Given I have created a Blank HTML Page When I edit the page diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index 026504ca7f2e..0af0007a76f9 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -82,22 +82,7 @@ Feature: CMS.Problem Editor And I can modify the display name Then If I press Cancel my changes are not persisted - Scenario: Edit High Level source is available for LaTeX problem - Given I have created a LaTeX Problem - When I edit and select Settings - Then Edit High Level Source is visible - Scenario: Cheat sheet visible on toggle Given I have created a Blank Common Problem And I can edit the problem Then I can see cheatsheet - - Scenario: Reply on Annotation and Return to Annotation link works for Annotation problem - Given I have created a unit with advanced module "annotatable" - And I have created an advanced component "Annotation" of type "annotatable" - And I have created an advanced problem of type "Blank Advanced Problem" - And I edit first blank advanced problem for annotation response - When I mouseover on "annotatable-span" - Then I can see Reply to Annotation link - And I see that page has scrolled "down" when I click on "annotatable-reply" link - And I see that page has scrolled "up" when I click on "annotation-return" link diff --git a/cms/djangoapps/pipeline_js/js/xmodule.js b/cms/djangoapps/pipeline_js/js/xmodule.js new file mode 100644 index 000000000000..881b6482a1f5 --- /dev/null +++ b/cms/djangoapps/pipeline_js/js/xmodule.js @@ -0,0 +1,60 @@ +// This file is designed to load all the XModule Javascript files in one wad +// using requirejs. It is passed through the Mako template system, which +// populates the `urls` variable with a list of paths to XModule JS files. +// These files assume that several libraries are available and bound to +// variables in the global context, so we load those libraries with requirejs +// and attach them to the global context manually. +define( + [ + 'jquery', 'underscore', 'codemirror', 'tinymce', 'scriptjs', + 'jquery.tinymce', 'jquery.qtip', 'jquery.scrollTo', 'jquery.flot', + 'jquery.cookie', + 'utility' + ], + function($, _, CodeMirror, tinymce, $script) { + 'use strict'; + + window.$ = $; + window._ = _; + $script( + '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js' + + '?config=TeX-MML-AM_SVG&delayStartupUntil=configured', + 'mathjax' + ); + window.CodeMirror = CodeMirror; + window.RequireJS = { + requirejs: {}, // This is never used by current xmodules + require: $script, // $script([deps], callback) acts approximately like the require function + define: define + }; + /** + * Loads all modules one-by-one in exact order. + * The module should be used until we'll use RequireJS for XModules. + * @param {Array} modules A list of urls. + * @return {jQuery Promise} + **/ + function requireQueue(modules) { + var deferred = $.Deferred(); + function loadScript(queue) { + $script.ready('mathjax', function() { + // Loads the next script if queue is not empty. + if (queue.length) { + $script([queue.shift()], function() { + loadScript(queue); + }); + } else { + deferred.resolve(); + } + }); + } + + loadScript(modules.concat()); + return deferred.promise(); + } + + // if (!window.xmoduleUrls) { + // throw Error('window.xmoduleUrls must be defined'); + // } + return requireQueue([]); + } +); diff --git a/cms/djangoapps/pipeline_js/templates/xmodule.js b/cms/djangoapps/pipeline_js/templates/xmodule.js deleted file mode 100644 index fb0c22d2c1e3..000000000000 --- a/cms/djangoapps/pipeline_js/templates/xmodule.js +++ /dev/null @@ -1,45 +0,0 @@ -## This file is designed to load all the XModule Javascript files in one wad -## using requirejs. It is passed through the Mako template system, which -## populates the `urls` variable with a list of paths to XModule JS files. -## These files assume that several libraries are available and bound to -## variables in the global context, so we load those libraries with requirejs -## and attach them to the global context manually. -define(["jquery", "underscore", "codemirror", "tinymce", - "jquery.tinymce", "jquery.qtip", "jquery.scrollTo", "jquery.flot", - "jquery.cookie", - "utility"], - function($, _, CodeMirror, tinymce) { - window.$ = $; - window._ = _; - require(['mathjax']); - window.CodeMirror = CodeMirror; - window.RequireJS = { - 'requirejs': requirejs, - 'require': require, - 'define': define - }; - /** - * Loads all modules one-by-one in exact order. - * The module should be used until we'll use RequireJS for XModules. - * @param {Array} modules A list of urls. - * @return {jQuery Promise} - **/ - var requireQueue = function(modules) { - var deferred = $.Deferred(); - var loadScript = function (queue) { - // Loads the next script if queue is not empty. - if (queue.length) { - require([queue.shift()], function() { - loadScript(queue); - }); - } else { - deferred.resolve(); - } - }; - - loadScript(modules.concat()); - return deferred.promise(); - }; - - return requireQueue(${urls}); -}); diff --git a/cms/djangoapps/pipeline_js/urls.py b/cms/djangoapps/pipeline_js/urls.py deleted file mode 100644 index 303573cfdbd3..000000000000 --- a/cms/djangoapps/pipeline_js/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -URL patterns for Javascript files used to load all of the XModule JS in one wad. -""" -from django.conf.urls import url -from pipeline_js.views import xmodule_js_files, requirejs_xmodule - -urlpatterns = [ - url(r'^files\.json$', xmodule_js_files, name='xmodule_js_files'), - url(r'^xmodule\.js$', requirejs_xmodule, name='requirejs_xmodule'), -] diff --git a/cms/djangoapps/pipeline_js/utils.py b/cms/djangoapps/pipeline_js/utils.py new file mode 100644 index 000000000000..91ce32d8af93 --- /dev/null +++ b/cms/djangoapps/pipeline_js/utils.py @@ -0,0 +1,18 @@ +""" +Utilities for returning XModule JS (used by requirejs) +""" + +from django.conf import settings +from django.contrib.staticfiles.storage import staticfiles_storage + + +def get_xmodule_urls(): + """ + Returns a list of the URLs to hit to grab all the XModule JS + """ + pipeline_js_settings = settings.PIPELINE_JS["module-js"] + if settings.DEBUG: + paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]] + else: + paths = [pipeline_js_settings["output_filename"]] + return [staticfiles_storage.url(path) for path in paths] diff --git a/cms/djangoapps/pipeline_js/views.py b/cms/djangoapps/pipeline_js/views.py deleted file mode 100644 index bfb05d1dd335..000000000000 --- a/cms/djangoapps/pipeline_js/views.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Views for returning XModule JS (used by requirejs) -""" - -import json - -from django.conf import settings -from django.contrib.staticfiles.storage import staticfiles_storage -from django.http import HttpResponse - -from edxmako.shortcuts import render_to_response - - -def get_xmodule_urls(): - """ - Returns a list of the URLs to hit to grab all the XModule JS - """ - pipeline_js_settings = settings.PIPELINE_JS["module-js"] - if settings.DEBUG: - paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]] - else: - paths = [pipeline_js_settings["output_filename"]] - return [staticfiles_storage.url(path) for path in paths] - - -def xmodule_js_files(request): # pylint: disable=unused-argument - """ - View function that returns XModule URLs as a JSON list; meant to be used - as an API - """ - urls = get_xmodule_urls() - return HttpResponse(json.dumps(urls), content_type="application/json") - - -def requirejs_xmodule(request): # pylint: disable=unused-argument - """ - View function that returns a requirejs-wrapped Javascript file that - loads all the XModule URLs; meant to be loaded via requireJS - """ - return render_to_response( - "xmodule.js", - {"urls": get_xmodule_urls()}, - content_type="text/javascript", - ) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 514e4a7d9d88..766496f4865c 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -110,10 +110,6 @@ def seed(): # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -# Override the test stub webpack_loader that is installed in test.py. -INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'openedx.tests.util.webpack_loader'] -INSTALLED_APPS.append('webpack_loader') - # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # django.contrib.staticfiles used to be loaded by lettuce, now we must add it ourselves # django.contrib.staticfiles is not added to lms as there is a ^/static$ route built in to the app diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index eb6366318d88..0c2fdbc99e9b 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -49,6 +49,9 @@ # Needed to enable licensing on video modules XBLOCK_SETTINGS.update({'VideoDescriptor': {'licensing_enabled': True}}) +# Capture the console log via template includes, until webdriver supports log capture again +CAPTURE_CONSOLE_LOG = True + ############################ STATIC FILES ############################# # Enable debug so that static assets are served by Django diff --git a/cms/envs/bok_choy_docker.py b/cms/envs/bok_choy_docker.py index 96dd3902cccb..cd09b6e95aea 100644 --- a/cms/envs/bok_choy_docker.py +++ b/cms/envs/bok_choy_docker.py @@ -16,3 +16,9 @@ } LOGGING['loggers']['tracking']['handlers'] = ['console'] + +# Point the URL used to test YouTube availability to our stub YouTube server +BOK_CHOY_HOST = os.environ['BOK_CHOY_HOSTNAME'] +YOUTUBE['API'] = "http://{}:{}/get_youtube_api/".format(BOK_CHOY_HOST, YOUTUBE_PORT) +YOUTUBE['METADATA_URL'] = "http://{}:{}/test_youtube/".format(BOK_CHOY_HOST, YOUTUBE_PORT) +YOUTUBE['TEXT_API']['url'] = "{}:{}/test_transcripts_youtube/".format(BOK_CHOY_HOST, YOUTUBE_PORT) diff --git a/cms/envs/test.py b/cms/envs/test.py index 6a1e05f7ceab..ed891b320b52 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -54,8 +54,6 @@ # Want static files in the same dir for running on jenkins. STATIC_ROOT = TEST_ROOT / "staticfiles" -INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'webpack_loader'] -INSTALLED_APPS.append('openedx.tests.util.webpack_loader') WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" GITHUB_REPO_ROOT = TEST_ROOT / "data" diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index 3f86a8c891e0..10dec34c5a09 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -19,24 +19,19 @@ modules: getModulesList([ 'js/factories/asset_index', 'js/factories/base', - 'js/factories/container', 'js/factories/course_create_rerun', 'js/factories/course_info', - 'js/factories/edit_tabs', 'js/factories/export', 'js/factories/group_configurations', 'js/certificates/factories/certificates_page_factory', 'js/factories/index', - 'js/factories/library', 'js/factories/manage_users', 'js/factories/outline', 'js/factories/register', 'js/factories/settings', 'js/factories/settings_advanced', 'js/factories/settings_graders', - 'js/factories/textbooks', - 'js/factories/videos_index', - 'js/factories/xblock_validation' + 'js/factories/videos_index' ]), /** * By default all the configuration for optimization happens from the command diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js index c2570289b0c6..875b57d013d1 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -1,86 +1,87 @@ /* globals AjaxPrefix */ -(function(AjaxPrefix) { +define([ + 'domReady', + 'jquery', + 'underscore', + 'underscore.string', + 'backbone', + 'gettext', + '../../common/js/components/views/feedback_notification', + 'jquery.cookie' +], function(domReady, $, _, str, Backbone, gettext, NotificationView) { 'use strict'; - define([ - 'domReady', - 'jquery', - 'underscore.string', - 'backbone', - 'gettext', - '../../common/js/components/views/feedback_notification', - 'jquery.cookie' - ], function(domReady, $, str, Backbone, gettext, NotificationView) { - var main, sendJSON; - main = function() { - AjaxPrefix.addAjaxPrefix(jQuery, function() { - return $("meta[name='path_prefix']").attr('content'); - }); - window.CMS = window.CMS || {}; - window.CMS.URL = window.CMS.URL || {}; - window.onTouchBasedDevice = function() { - return navigator.userAgent.match(/iPhone|iPod|iPad|Android/i); - }; - _.extend(window.CMS, Backbone.Events); - Backbone.emulateHTTP = true; - $.ajaxSetup({ - headers: { - 'X-CSRFToken': $.cookie('csrftoken') - }, - dataType: 'json', - content: { - script: false - } + + var main, sendJSON; + main = function() { + AjaxPrefix.addAjaxPrefix(jQuery, function() { + return $("meta[name='path_prefix']").attr('content'); + }); + window.CMS = window.CMS || {}; + window.CMS.URL = window.CMS.URL || {}; + window.onTouchBasedDevice = function() { + return navigator.userAgent.match(/iPhone|iPod|iPad|Android/i); + }; + _.extend(window.CMS, Backbone.Events); + Backbone.emulateHTTP = true; + $.ajaxSetup({ + headers: { + 'X-CSRFToken': $.cookie('csrftoken') + }, + dataType: 'json', + content: { + script: false + } + }); + $(document).ajaxError(function(event, jqXHR, ajaxSettings) { + var msg, contentType, + message = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len + if (ajaxSettings.notifyOnError === false) { + return; + } + contentType = jqXHR.getResponseHeader('content-type'); + if (contentType && contentType.indexOf('json') > -1 && jqXHR.responseText) { + message = JSON.parse(jqXHR.responseText).error; + } + msg = new NotificationView.Error({ + title: gettext("Studio's having trouble saving your work"), + message: message }); - $(document).ajaxError(function(event, jqXHR, ajaxSettings) { - var msg, contentType, - message = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len - if (ajaxSettings.notifyOnError === false) { - return; - } - contentType = jqXHR.getResponseHeader('content-type'); - if (contentType && contentType.indexOf('json') > -1 && jqXHR.responseText) { - message = JSON.parse(jqXHR.responseText).error; - } - msg = new NotificationView.Error({ - title: gettext("Studio's having trouble saving your work"), - message: message - }); - console.log('Studio AJAX Error', { // eslint-disable-line no-console - url: event.currentTarget.URL, - response: jqXHR.responseText, - status: jqXHR.status - }); - return msg.show(); + console.log('Studio AJAX Error', { // eslint-disable-line no-console + url: event.currentTarget.URL, + response: jqXHR.responseText, + status: jqXHR.status }); - sendJSON = function(url, data, callback, type) { // eslint-disable-line no-param-reassign - if ($.isFunction(data)) { - callback = data; - data = undefined; - } - return $.ajax({ - url: url, - type: type, - contentType: 'application/json; charset=utf-8', - dataType: 'json', - data: JSON.stringify(data), - success: callback, - global: data ? data.global : true // Trigger global AJAX error handler or not - }); - }; - $.postJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign - return sendJSON(url, data, callback, 'POST'); - }; - $.patchJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign - return sendJSON(url, data, callback, 'PATCH'); - }; - return domReady(function() { - if (window.onTouchBasedDevice()) { - return $('body').addClass('touch-based-device'); - } + return msg.show(); + }); + sendJSON = function(url, data, callback, type) { // eslint-disable-line no-param-reassign + if ($.isFunction(data)) { + callback = data; + data = undefined; + } + return $.ajax({ + url: url, + type: type, + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify(data), + success: callback, + global: data ? data.global : true // Trigger global AJAX error handler or not }); }; - main(); - return main; - }); -}).call(this, AjaxPrefix); + $.postJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign + return sendJSON(url, data, callback, 'POST'); + }; + $.patchJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign + return sendJSON(url, data, callback, 'PATCH'); + }; + return domReady(function() { + if (window.onTouchBasedDevice()) { + return $('body').addClass('touch-based-device'); + } + return null; + }); + }; + main(); + return main; +}); diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index eb0d03c70652..1f32ced8d6e6 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -4,6 +4,7 @@ (function(requirejs, requireSerial) { 'use strict'; + var i, specHelpers, testFiles; if (window) { define('add-a11y-deps', [ @@ -20,8 +21,6 @@ }); } - var i, specHelpers, testFiles; - requirejs.config({ baseUrl: '/base/', paths: { @@ -230,7 +229,6 @@ testFiles = [ 'cms/js/spec/main_spec', - 'cms/js/spec/xblock/cms.runtime.v1_spec', 'js/spec/models/course_spec', 'js/spec/models/metadata_spec', 'js/spec/models/section_spec', @@ -263,32 +261,21 @@ 'js/spec/views/previous_video_upload_list_spec', 'js/spec/views/assets_spec', 'js/spec/views/baseview_spec', - 'js/spec/views/container_spec', - 'js/spec/views/module_edit_spec', 'js/spec/views/paged_container_spec', 'js/spec/views/group_configuration_spec', 'js/spec/views/unit_outline_spec', 'js/spec/views/xblock_spec', - 'js/spec/views/xblock_editor_spec', - 'js/spec/views/xblock_string_field_editor_spec', 'js/spec/views/xblock_validation_spec', 'js/spec/views/license_spec', 'js/spec/views/paging_spec', - 'js/spec/views/login_studio_spec', - 'js/spec/views/pages/container_spec', - 'js/spec/views/pages/container_subviews_spec', 'js/spec/views/pages/group_configurations_spec', - 'js/spec/views/pages/course_outline_spec', 'js/spec/views/pages/course_rerun_spec', 'js/spec/views/pages/index_spec', 'js/spec/views/pages/library_users_spec', 'js/spec/views/modals/base_modal_spec', - 'js/spec/views/modals/edit_xblock_spec', 'js/spec/views/modals/move_xblock_modal_spec', 'js/spec/views/modals/validation_error_modal_spec', - 'js/spec/views/move_xblock_spec', 'js/spec/views/settings/main_spec', - 'js/spec/factories/xblock_validation_spec', 'js/certificates/spec/models/certificate_spec', 'js/certificates/spec/views/certificate_details_spec', 'js/certificates/spec/views/certificate_editor_spec', diff --git a/cms/static/cms/js/spec/main_webpack.js b/cms/static/cms/js/spec/main_webpack.js new file mode 100644 index 000000000000..5cea9bfd0a87 --- /dev/null +++ b/cms/static/cms/js/spec/main_webpack.js @@ -0,0 +1,35 @@ +jasmine.getFixtures().fixturesPath = '/base/templates'; + +import 'common/js/spec_helpers/jasmine-extensions'; +import 'common/js/spec_helpers/jasmine-stealth'; +import 'common/js/spec_helpers/jasmine-waituntil'; + +// These libraries are used by the tests (and the code under test) +// but not explicitly imported +import 'jquery.ui'; + +import _ from 'underscore'; +import str from 'underscore.string'; +import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils'; +import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; +window._ = _; +window._.str = str; +window.edx = window.edx || {}; +window.edx.HtmlUtils = HtmlUtils; +window.edx.StringUtils = StringUtils; + +// These are the tests that will be run +import './xblock/cms.runtime.v1_spec.js'; +import '../../../js/spec/factories/xblock_validation_spec.js'; +import '../../../js/spec/views/container_spec.js'; +import '../../../js/spec/views/login_studio_spec.js'; +import '../../../js/spec/views/modals/edit_xblock_spec.js'; +import '../../../js/spec/views/module_edit_spec.js'; +import '../../../js/spec/views/move_xblock_spec.js'; +import '../../../js/spec/views/pages/container_spec.js'; +import '../../../js/spec/views/pages/container_subviews_spec.js'; +import '../../../js/spec/views/pages/course_outline_spec.js'; +import '../../../js/spec/views/xblock_editor_spec.js'; +import '../../../js/spec/views/xblock_string_field_editor_spec.js'; + +window.__karma__.start(); // eslint-disable-line no-underscore-dangle diff --git a/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js index 893fe6827a6d..72c72b1e30f5 100644 --- a/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js +++ b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js @@ -1,81 +1,82 @@ -define(['js/spec_helpers/edit_helpers', 'js/views/modals/base_modal', 'xblock/cms.runtime.v1'], - function(EditHelpers, BaseModal) { - 'use strict'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import BaseModal from 'js/views/modals/base_modal'; +import 'xblock/cms.runtime.v1'; - describe('Studio Runtime v1', function() { - var runtime; +describe('Studio Runtime v1', function() { + 'use strict'; - beforeEach(function() { - EditHelpers.installEditTemplates(); - runtime = new window.StudioRuntime.v1(); - }); + var runtime; - it('allows events to be listened to', function() { - var canceled = false; - runtime.listenTo('cancel', function() { - canceled = true; - }); - expect(canceled).toBeFalsy(); - runtime.notify('cancel', {}); - expect(canceled).toBeTruthy(); - }); + beforeEach(function() { + EditHelpers.installEditTemplates(); + runtime = new window.StudioRuntime.v1(); + }); - it('shows save notifications', function() { - var title = 'Mock saving...', - notificationSpy = EditHelpers.createNotificationSpy(); - runtime.notify('save', { - state: 'start', - message: title - }); - EditHelpers.verifyNotificationShowing(notificationSpy, title); - runtime.notify('save', { - state: 'end' - }); - EditHelpers.verifyNotificationHidden(notificationSpy); - }); + it('allows events to be listened to', function() { + var canceled = false; + runtime.listenTo('cancel', function() { + canceled = true; + }); + expect(canceled).toBeFalsy(); + runtime.notify('cancel', {}); + expect(canceled).toBeTruthy(); + }); - it('shows error messages', function() { - var title = 'Mock Error', - message = 'This is a mock error.', - notificationSpy = EditHelpers.createNotificationSpy('Error'); - runtime.notify('error', { - title: title, - message: message - }); - EditHelpers.verifyNotificationShowing(notificationSpy, title); - }); + it('shows save notifications', function() { + var title = 'Mock saving...', + notificationSpy = EditHelpers.createNotificationSpy(); + runtime.notify('save', { + state: 'start', + message: title + }); + EditHelpers.verifyNotificationShowing(notificationSpy, title); + runtime.notify('save', { + state: 'end' + }); + EditHelpers.verifyNotificationHidden(notificationSpy); + }); - describe('Modal Dialogs', function() { - var MockModal, modal, showMockModal; + it('shows error messages', function() { + var title = 'Mock Error', + message = 'This is a mock error.', + notificationSpy = EditHelpers.createNotificationSpy('Error'); + runtime.notify('error', { + title: title, + message: message + }); + EditHelpers.verifyNotificationShowing(notificationSpy, title); + }); - MockModal = BaseModal.extend({ - getContentHtml: function() { - return readFixtures('mock/mock-modal.underscore'); - } - }); + describe('Modal Dialogs', function() { + var MockModal, modal, showMockModal; - showMockModal = function() { - modal = new MockModal({ - title: 'Mock Modal' - }); - modal.show(); - }; + MockModal = BaseModal.extend({ + getContentHtml: function() { + return readFixtures('mock/mock-modal.underscore'); + } + }); - beforeEach(function() { - EditHelpers.installEditTemplates(); - }); + showMockModal = function() { + modal = new MockModal({ + title: 'Mock Modal' + }); + modal.show(); + }; - afterEach(function() { - EditHelpers.hideModalIfShowing(modal); - }); + beforeEach(function() { + EditHelpers.installEditTemplates(); + }); - it('cancels a modal dialog', function() { - showMockModal(); - runtime.notify('modal-shown', modal); - expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); - runtime.notify('cancel'); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - }); - }); + afterEach(function() { + EditHelpers.hideModalIfShowing(modal); + }); + + it('cancels a modal dialog', function() { + showMockModal(); + runtime.notify('modal-shown', modal); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + runtime.notify('cancel'); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); }); }); +}); diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 4ea35feda959..0f7fa7f1da96 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -26,8 +26,35 @@ define([ IframeUtils, DropdownMenuView ) { + 'use strict'; var $body; + function smoothScrollLink(e) { + (e).preventDefault(); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') + }); + } + + function hideNotification(e) { + (e).preventDefault(); + $(this) + .closest('.wrapper-notification') + .removeClass('is-shown') + .addClass('is-hiding') + .attr('aria-hidden', 'true'); + } + + function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + } + domReady(function() { var dropdownMenuView; @@ -44,14 +71,14 @@ define([ $('.action-notification-close').bind('click', hideNotification); // nav - dropdown related - $body.click(function(e) { + $body.click(function() { $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .title').removeClass('is-selected'); }); $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { - $subnav = $(this).find('.wrapper-nav-sub'); - $title = $(this).find('.title'); + var $subnav = $(this).find('.wrapper-nav-sub'), + $title = $(this).find('.title'); if ($subnav.hasClass('is-shown')) { $subnav.removeClass('is-shown'); @@ -68,7 +95,8 @@ define([ }); // general link management - new window/tab - $('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab')); + $('a[rel="external"]:not([title])') + .attr('title', gettext('This link will open in a new browser window/tab')); $('a[rel="external"]').attr('target', '_blank'); // general link management - lean modal window @@ -97,39 +125,7 @@ define([ }); dropdownMenuView.postRender(); } - }); - - function smoothScrollLink(e) { - (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $(this).attr('href') - }); - } - - function smoothScrollTop(e) { - (e).preventDefault(); - - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $('#view-top') - }); - } - - function hideNotification(e) { - (e).preventDefault(); - $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true'); - } - - function hideAlert(e) { - (e).preventDefault(); - $(this).closest('.wrapper-alert').removeClass('is-shown'); - } + window.studioNavMenuActive = true; + }); }); // end require() diff --git a/cms/static/js/factories/base.js b/cms/static/js/factories/base.js index 7f61b473c9e9..a71439357307 100644 --- a/cms/static/js/factories/base.js +++ b/cms/static/js/factories/base.js @@ -1,3 +1,5 @@ +// We can't convert this to an es6 module until all factories that use it have been converted out +// of RequireJS define(['js/base', 'cms/js/main', 'js/src/logger', 'datepair', 'accessibility', 'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course'], function() { diff --git a/cms/static/js/factories/container.js b/cms/static/js/factories/container.js index 8861d6f1249a..cfca29885183 100644 --- a/cms/static/js/factories/container.js +++ b/cms/static/js/factories/container.js @@ -1,21 +1,26 @@ -define([ - 'jquery', 'underscore', 'js/models/xblock_container_info', 'js/views/pages/container', - 'js/collections/component_template', 'xmodule', 'cms/js/main', - 'xblock/cms.runtime.v1' -], -function($, _, XBlockContainerInfo, ContainerPage, ComponentTemplates, xmoduleLoader) { - 'use strict'; - return function(componentTemplates, XBlockInfoJson, action, options) { - var main_options = { - el: $('#content'), - model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}), - action: action, - templates: new ComponentTemplates(componentTemplates, {parse: true}) - }; +import * as $ from 'jquery'; +import * as _ from 'underscore'; +import * as XBlockContainerInfo from 'js/models/xblock_container_info'; +import * as ContainerPage from 'js/views/pages/container'; +import * as ComponentTemplates from 'js/collections/component_template'; +import * as xmoduleLoader from 'xmodule'; +import './base'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - xmoduleLoader.done(function() { - var view = new ContainerPage(_.extend(main_options, options)); - view.render(); - }); +'use strict'; +export default function ContainerFactory(componentTemplates, XBlockInfoJson, action, options) { + var main_options = { + el: $('#content'), + model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}), + action: action, + templates: new ComponentTemplates(componentTemplates, {parse: true}) }; -}); + + xmoduleLoader.done(function() { + var view = new ContainerPage(_.extend(main_options, options)); + view.render(); + }); +}; + +export {ContainerFactory} diff --git a/cms/static/js/factories/context_course.js b/cms/static/js/factories/context_course.js new file mode 100644 index 000000000000..475e5a6282c6 --- /dev/null +++ b/cms/static/js/factories/context_course.js @@ -0,0 +1,3 @@ +import * as ContextCourse from 'js/models/course'; + +export {ContextCourse} diff --git a/cms/static/js/factories/edit_tabs.js b/cms/static/js/factories/edit_tabs.js index fddb8648a617..9f2912fc912d 100644 --- a/cms/static/js/factories/edit_tabs.js +++ b/cms/static/js/factories/edit_tabs.js @@ -1,20 +1,25 @@ -define([ - 'js/models/explicit_url', 'js/views/tabs', 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1' -], function(TabsModel, TabsEditView, xmoduleLoader) { - 'use strict'; - return function(courseLocation, explicitUrl) { - xmoduleLoader.done(function() { - var model = new TabsModel({ - id: courseLocation, - explicit_url: explicitUrl - }), - editView; +import * as TabsModel from 'js/models/explicit_url'; +import * as TabsEditView from 'js/views/tabs'; +import * as xmoduleLoader from 'xmodule'; +import './base'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - editView = new TabsEditView({ - el: $('.tab-list'), - model: model, - mast: $('.wrapper-mast') - }); +'use strict'; +export default function EditTabsFactory(courseLocation, explicitUrl) { + xmoduleLoader.done(function() { + var model = new TabsModel({ + id: courseLocation, + explicit_url: explicitUrl + }), + editView; + + editView = new TabsEditView({ + el: $('.tab-list'), + model: model, + mast: $('.wrapper-mast') }); - }; -}); + }); +}; + +export {EditTabsFactory} diff --git a/cms/static/js/factories/library.js b/cms/static/js/factories/library.js index e6eb92906934..4cde6873f939 100644 --- a/cms/static/js/factories/library.js +++ b/cms/static/js/factories/library.js @@ -1,23 +1,28 @@ -define([ - 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/paged_container', - 'js/views/library_container', 'js/collections/component_template', 'xmodule', 'cms/js/main', - 'xblock/cms.runtime.v1' -], -function($, _, XBlockInfo, PagedContainerPage, LibraryContainerView, ComponentTemplates, xmoduleLoader) { - 'use strict'; - return function(componentTemplates, XBlockInfoJson, options) { - var main_options = { - el: $('#content'), - model: new XBlockInfo(XBlockInfoJson, {parse: true}), - templates: new ComponentTemplates(componentTemplates, {parse: true}), - action: 'view', - viewClass: LibraryContainerView, - canEdit: true - }; +import * as $ from 'jquery'; +import * as _ from 'underscore'; +import * as XBlockInfo from 'js/models/xblock_info'; +import * as PagedContainerPage from 'js/views/pages/paged_container'; +import * as LibraryContainerView from 'js/views/library_container'; +import * as ComponentTemplates from 'js/collections/component_template'; +import * as xmoduleLoader from 'xmodule'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - xmoduleLoader.done(function() { - var view = new PagedContainerPage(_.extend(main_options, options)); - view.render(); - }); +'use strict'; +export default function LibraryFactory(componentTemplates, XBlockInfoJson, options) { + var main_options = { + el: $('#content'), + model: new XBlockInfo(XBlockInfoJson, {parse: true}), + templates: new ComponentTemplates(componentTemplates, {parse: true}), + action: 'view', + viewClass: LibraryContainerView, + canEdit: true }; -}); + + xmoduleLoader.done(function() { + var view = new PagedContainerPage(_.extend(main_options, options)); + view.render(); + }); +}; + +export {LibraryFactory} diff --git a/cms/static/js/factories/login.js b/cms/static/js/factories/login.js index fdbcef31e8ec..b528e075a2ce 100644 --- a/cms/static/js/factories/login.js +++ b/cms/static/js/factories/login.js @@ -1,57 +1,63 @@ -define(['jquery.cookie', 'utility', 'common/js/components/utils/view_utils'], function(cookie, utility, ViewUtils) { - 'use strict'; - return function LoginFactory(homepageURL) { - function postJSON(url, data, callback) { - $.ajax({ - type: 'POST', - url: url, - dataType: 'json', - data: data, - success: callback - }); - } - - // Clear the login error message when credentials are edited - $('input#email').on('input', function() { - $('#login_error').removeClass('is-shown'); - }); - $('input#password').on('input', function() { - $('#login_error').removeClass('is-shown'); +'use strict'; + +import cookie from 'jquery.cookie'; +import utility from 'utility'; +import ViewUtils from 'common/js/components/utils/view_utils'; + +export default function LoginFactory(homepageURL) { + function postJSON(url, data, callback) { + $.ajax({ + type: 'POST', + url: url, + dataType: 'json', + data: data, + success: callback }); + } + + // Clear the login error message when credentials are edited + $('input#email').on('input', function () { + $('#login_error').removeClass('is-shown'); + }); - $('form#login_form').submit(function(event) { - event.preventDefault(); - var $submitButton = $('#submit'), - deferred = new $.Deferred(), - promise = deferred.promise(); - ViewUtils.disableElementWhileRunning($submitButton, function() { return promise; }); - var submit_data = $('#login_form').serialize(); - - postJSON('/login_post', submit_data, function(json) { - if (json.success) { - var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search)); - if (next && next.length > 1 && !isExternal(next[1])) { - ViewUtils.redirect(next[1]); - } else { - ViewUtils.redirect(homepageURL); - } - } else if ($('#login_error').length === 0) { - $('#login_form').prepend( - '
' + - json.value + - '
' - ); - $('#login_error').addClass('is-shown'); - deferred.resolve(); + $('input#password').on('input', function () { + $('#login_error').removeClass('is-shown'); + }); + + $('form#login_form').submit(function (event) { + event.preventDefault(); + var $submitButton = $('#submit'), + deferred = new $.Deferred(), + promise = deferred.promise(); + ViewUtils.disableElementWhileRunning($submitButton, function () { return promise; }); + var submit_data = $('#login_form').serialize(); + + postJSON('/login_post', submit_data, function (json) { + if (json.success) { + var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search)); + if (next && next.length > 1 && !isExternal(next[1])) { + ViewUtils.redirect(next[1]); } else { - $('#login_error') - .stop() - .addClass('is-shown') - .html(json.value); - deferred.resolve(); + ViewUtils.redirect(homepageURL); } - }); + } else if ($('#login_error').length === 0) { + $('#login_form').prepend( + '
' + + json.value + + '
' + ); + $('#login_error').addClass('is-shown'); + deferred.resolve(); + } else { + $('#login_error') + .stop() + .addClass('is-shown') + .html(json.value); + deferred.resolve(); + } }); - }; -}); + }); +}; + +export { LoginFactory } diff --git a/cms/static/js/factories/textbooks.js b/cms/static/js/factories/textbooks.js index 0641e025d36a..2fd11e407f18 100644 --- a/cms/static/js/factories/textbooks.js +++ b/cms/static/js/factories/textbooks.js @@ -1,20 +1,23 @@ -define([ - 'gettext', 'js/models/section', 'js/collections/textbook', 'js/views/list_textbooks' -], function(gettext, Section, TextbookCollection, ListTextbooksView) { - 'use strict'; - return function(textbooksJson) { - var textbooks = new TextbookCollection(textbooksJson, {parse: true}), - tbView = new ListTextbooksView({collection: textbooks}); +import * as gettext from 'gettext'; +import * as Section from 'js/models/section'; +import * as TextbookCollection from 'js/collections/textbook'; +import * as ListTextbooksView from 'js/views/list_textbooks'; - $('.content-primary').append(tbView.render().el); - $('.nav-actions .new-button').click(function(event) { - tbView.addOne(event); - }); - $(window).on('beforeunload', function() { - var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); - if (dirty) { - return gettext('You have unsaved changes. Do you really want to leave this page?'); - } - }); - }; -}); +'use strict'; +export default function TextbooksFactory(textbooksJson) { + var textbooks = new TextbookCollection(textbooksJson, {parse: true}), + tbView = new ListTextbooksView({collection: textbooks}); + + $('.content-primary').append(tbView.render().el); + $('.nav-actions .new-button').click(function(event) { + tbView.addOne(event); + }); + $(window).on('beforeunload', function() { + var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); + if (dirty) { + return gettext('You have unsaved changes. Do you really want to leave this page?'); + } + }); +}; + +export {TextbooksFactory} diff --git a/cms/static/js/factories/xblock_validation.js b/cms/static/js/factories/xblock_validation.js index 4bcfc3b33917..56786c89d9c1 100644 --- a/cms/static/js/factories/xblock_validation.js +++ b/cms/static/js/factories/xblock_validation.js @@ -1,19 +1,22 @@ -define(['js/views/xblock_validation', 'js/models/xblock_validation'], -function(XBlockValidationView, XBlockValidationModel) { - 'use strict'; - return function(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { - var model, response; - if (hasEditingUrl && !isRoot) { - validationMessages.showSummaryOnly = true; - } - response = validationMessages; - response.isUnit = isUnit; +import * as XBlockValidationView from 'js/views/xblock_validation'; +import * as XBlockValidationModel from 'js/models/xblock_validation'; - model = new XBlockValidationModel(response, {parse: true}); +'use strict'; +export default function XBlockValidationFactory(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { + var model, response; - if (!model.get('empty')) { - new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render(); - } - }; -}); + if (hasEditingUrl && !isRoot) { + validationMessages.showSummaryOnly = true; + } + response = validationMessages; + response.isUnit = isUnit; + + model = new XBlockValidationModel(response, {parse: true}); + + if (!model.get('empty')) { + new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render(); + } +}; + +export {XBlockValidationFactory} diff --git a/cms/static/js/pages/course.js b/cms/static/js/pages/course.js deleted file mode 100644 index cf0ca8d5ef4a..000000000000 --- a/cms/static/js/pages/course.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ['js/models/course'], - function(ContextCourse) { - window.course = new ContextCourse(window.pageFactoryArguments.ContextCourse); - } -); diff --git a/cms/static/js/pages/login.js b/cms/static/js/pages/login.js deleted file mode 100644 index 449a1190f641..000000000000 --- a/cms/static/js/pages/login.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ['js/factories/login', 'common/js/utils/page_factory', 'js/factories/base'], - function(LoginFactory, invokePageFactory) { - 'use strict'; - invokePageFactory('LoginFactory', LoginFactory); - } -); - diff --git a/cms/static/js/pages/textbooks.js b/cms/static/js/pages/textbooks.js deleted file mode 100644 index 7d524fbd2909..000000000000 --- a/cms/static/js/pages/textbooks.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ['js/factories/textbooks', 'common/js/utils/page_factory', 'js/factories/base', 'js/pages/course'], - function(TextbooksFactory, invokePageFactory) { - 'use strict'; - invokePageFactory('TextbooksFactory', TextbooksFactory); - } -); diff --git a/cms/static/js/sock.js b/cms/static/js/sock.js index 44653981ff64..fe6b331e46d0 100644 --- a/cms/static/js/sock.js +++ b/cms/static/js/sock.js @@ -1,39 +1,41 @@ -define(['domReady', 'jquery', 'jquery.smoothScroll'], - function(domReady, $) { - 'use strict'; +import * as domReady from 'domReady'; +import * as $ from 'jquery'; +import 'jquery.smoothScroll'; - var toggleSock = function(e) { - e.preventDefault(); +'use strict'; - var $btnShowSockLabel = $(this).find('.copy-show'); - var $btnHideSockLabel = $(this).find('.copy-hide'); - var $sock = $('.wrapper-sock'); - var $sockContent = $sock.find('.wrapper-inner'); +var toggleSock = function (e) { + e.preventDefault(); - if ($sock.hasClass('is-shown')) { - $sock.removeClass('is-shown'); - $sockContent.hide('fast'); - $btnHideSockLabel.removeClass('is-shown').addClass('is-hidden'); - $btnShowSockLabel.removeClass('is-hidden').addClass('is-shown'); - } else { - $sock.addClass('is-shown'); - $sockContent.show('fast'); - $btnHideSockLabel.removeClass('is-hidden').addClass('is-shown'); - $btnShowSockLabel.removeClass('is-shown').addClass('is-hidden'); - } + var $btnShowSockLabel = $(this).find('.copy-show'); + var $btnHideSockLabel = $(this).find('.copy-hide'); + var $sock = $('.wrapper-sock'); + var $sockContent = $sock.find('.wrapper-inner'); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $sock - }); - }; - - domReady(function() { - // toggling footer additional support - $('.cta-show-sock').bind('click', toggleSock); - }); + if ($sock.hasClass('is-shown')) { + $sock.removeClass('is-shown'); + $sockContent.hide('fast'); + $btnHideSockLabel.removeClass('is-shown').addClass('is-hidden'); + $btnShowSockLabel.removeClass('is-hidden').addClass('is-shown'); + } else { + $sock.addClass('is-shown'); + $sockContent.show('fast'); + $btnHideSockLabel.removeClass('is-hidden').addClass('is-shown'); + $btnShowSockLabel.removeClass('is-shown').addClass('is-hidden'); } -); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $sock + }); +}; + +domReady(function () { + // toggling footer additional support + $('.cta-show-sock').bind('click', toggleSock); +}); + +export { toggleSock } diff --git a/cms/static/js/spec/factories/xblock_validation_spec.js b/cms/static/js/spec/factories/xblock_validation_spec.js index 5ba938098136..ca14f88c09a4 100644 --- a/cms/static/js/spec/factories/xblock_validation_spec.js +++ b/cms/static/js/spec/factories/xblock_validation_spec.js @@ -1,77 +1,77 @@ -define(['jquery', 'js/factories/xblock_validation', 'common/js/spec_helpers/template_helpers'], - function($, XBlockValidationFactory, TemplateHelpers) { - describe('XBlockValidationFactory', function() { - var $messageDiv; +import $ from 'jquery'; +import XBlockValidationFactory from 'js/factories/xblock_validation'; +import TemplateHelpers from 'common/js/spec_helpers/template_helpers'; - beforeEach(function() { - TemplateHelpers.installTemplate('xblock-validation-messages'); - appendSetFixtures($('
')); - $messageDiv = $('.messages'); - }); +describe('XBlockValidationFactory', () => { + var $messageDiv; - it('Does not attach a view if messages is empty', function() { - XBlockValidationFactory({empty: true}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(0); - }); + beforeEach(function() { + TemplateHelpers.installTemplate('xblock-validation-messages'); + appendSetFixtures($('
')); + $messageDiv = $('.messages'); + }); - it('Does attach a view if messages are not empty', function() { - XBlockValidationFactory({empty: false}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(1); - }); + it('Does not attach a view if messages is empty', function() { + XBlockValidationFactory({empty: true}, false, false, false, $messageDiv); + expect($messageDiv.children().length).toEqual(0); + }); - it('Passes through the root property to the view.', function() { - var noContainerContent = 'no-container-content'; + it('Does attach a view if messages are not empty', function() { + XBlockValidationFactory({empty: false}, false, false, false, $messageDiv); + expect($messageDiv.children().length).toEqual(1); + }); - var notConfiguredMessages = { - empty: false, - summary: {text: 'my summary', type: 'not-configured'}, - messages: [], - xblock_id: 'id' - }; - // Root is false, will not add noContainerContent. - XBlockValidationFactory(notConfiguredMessages, true, false, false, $messageDiv); - expect($messageDiv.find('.validation')).not.toHaveClass(noContainerContent); + it('Passes through the root property to the view.', function() { + var noContainerContent = 'no-container-content'; - // Root is true, will add noContainerContent. - XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); - expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); - }); + var notConfiguredMessages = { + empty: false, + summary: {text: 'my summary', type: 'not-configured'}, + messages: [], + xblock_id: 'id' + }; + // Root is false, will not add noContainerContent. + XBlockValidationFactory(notConfiguredMessages, true, false, false, $messageDiv); + expect($messageDiv.find('.validation')).not.toHaveClass(noContainerContent); - describe('Controls display of detailed messages based on url and root property', function() { - var messagesWithSummary, checkDetailedMessages; + // Root is true, will add noContainerContent. + XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); + expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); + }); - beforeEach(function() { - messagesWithSummary = { - empty: false, - summary: {text: 'my summary'}, - messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}], - xblock_id: 'id' - }; - }); + describe('Controls display of detailed messages based on url and root property', function() { + var messagesWithSummary, checkDetailedMessages; - checkDetailedMessages = function(expectedDetailedMessages) { - expect($messageDiv.children().length).toEqual(1); - expect($messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages); - }; + beforeEach(function() { + messagesWithSummary = { + empty: false, + summary: {text: 'my summary'}, + messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}], + xblock_id: 'id' + }; + }); + + checkDetailedMessages = function(expectedDetailedMessages) { + expect($messageDiv.children().length).toEqual(1); + expect($messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages); + }; - it('Does not show details if xblock has an editing URL and it is not rendered as root', function() { - XBlockValidationFactory(messagesWithSummary, true, false, false, $messageDiv); - checkDetailedMessages(0); - }); + it('Does not show details if xblock has an editing URL and it is not rendered as root', function() { + XBlockValidationFactory(messagesWithSummary, true, false, false, $messageDiv); + checkDetailedMessages(0); + }); - it('Shows details if xblock does not have its own editing URL, regardless of root value', function() { - XBlockValidationFactory(messagesWithSummary, false, false, false, $messageDiv); - checkDetailedMessages(2); + it('Shows details if xblock does not have its own editing URL, regardless of root value', function() { + XBlockValidationFactory(messagesWithSummary, false, false, false, $messageDiv); + checkDetailedMessages(2); - XBlockValidationFactory(messagesWithSummary, false, true, false, $messageDiv); - checkDetailedMessages(2); - }); + XBlockValidationFactory(messagesWithSummary, false, true, false, $messageDiv); + checkDetailedMessages(2); + }); - it('Shows details if xblock has its own editing URL and is rendered as root', function() { - XBlockValidationFactory(messagesWithSummary, true, true, false, $messageDiv); - checkDetailedMessages(2); - }); - }); + it('Shows details if xblock has its own editing URL and is rendered as root', function() { + XBlockValidationFactory(messagesWithSummary, true, true, false, $messageDiv); + checkDetailedMessages(2); }); - } -); + }); +}); diff --git a/cms/static/js/spec/utils/drag_and_drop_spec.js b/cms/static/js/spec/utils/drag_and_drop_spec.js index b61313de3187..1c56f9a4798c 100644 --- a/cms/static/js/spec/utils/drag_and_drop_spec.js +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -309,6 +309,7 @@ define(['sinon', 'js/utils/drag_and_drop', 'common/js/components/views/feedback_ }); afterEach(function() { this.clock.restore(); + jasmine.stealth.clearSpies(); }); it('should send an update on reorder from one parent to another', function() { var requests, request, savingOptions; diff --git a/cms/static/js/spec/views/container_spec.js b/cms/static/js/spec/views/container_spec.js index bc33ccd31a65..194d405d0cfd 100644 --- a/cms/static/js/spec/views/container_spec.js +++ b/cms/static/js/spec/views/container_spec.js @@ -1,198 +1,205 @@ -define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers', - 'js/views/container', 'js/models/xblock_info', 'jquery.simulate', - 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'], - function($, AjaxHelpers, EditHelpers, ContainerView, XBlockInfo) { - describe('Container View', function() { - describe('Supports reordering components', function() { - var model, containerView, mockContainerHTML, init, getComponent, - getDragHandle, dragComponentVertically, dragComponentAbove, - verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy, - - rootLocator = 'locator-container', - containerTestUrl = '/xblock/' + rootLocator, - - groupAUrl = '/xblock/locator-group-A', - groupA = 'locator-group-A', - groupAComponent1 = 'locator-component-A1', - groupAComponent2 = 'locator-component-A2', - groupAComponent3 = 'locator-component-A3', - - groupBUrl = '/xblock/locator-group-B', - groupB = 'locator-group-B', - groupBComponent1 = 'locator-component-B1', - groupBComponent2 = 'locator-component-B2', - groupBComponent3 = 'locator-component-B3'; - - mockContainerHTML = readFixtures('mock/mock-container-xblock.underscore'); - - beforeEach(function() { - EditHelpers.installMockXBlock(); - EditHelpers.installViewTemplates(); - appendSetFixtures('
'); - notificationSpy = EditHelpers.createNotificationSpy(); - model = new XBlockInfo({ - id: rootLocator, - display_name: 'Test AB Test', - category: 'split_test' - }); - - containerView = new ContainerView({ - model: model, - view: 'container_preview', - el: $('.wrapper-xblock') - }); - }); - - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - containerView.remove(); - }); - - init = function(caller) { - var requests = AjaxHelpers.requests(caller); - containerView.render(); - - AjaxHelpers.respondWithJson(requests, { - html: mockContainerHTML, - resources: [] - }); - - $('body').append(containerView.$el); - - // Give the whole container enough height to contain everything. - $('.xblock[data-locator=locator-container]').css('height', 2000); - - // Give the groups enough height to contain their child vertical elements. - $('.is-draggable[data-locator=locator-group-A]').css('height', 800); - $('.is-draggable[data-locator=locator-group-B]').css('height', 800); - - - // Give the leaf elements some height to mimic actual components. Otherwise - // drag and drop fails as the elements on bunched on top of each other. - $('.level-element').css('height', 230); - - return requests; - }; - - getComponent = function(locator) { - return containerView.$('.studio-xblock-wrapper[data-locator="' + locator + '"]'); - }; - - getDragHandle = function(locator) { - var component = getComponent(locator); - return $(component.find('.drag-handle')[0]); - }; - - dragComponentVertically = function(locator, dy) { - var handle = getDragHandle(locator); - handle.simulate('drag', {dy: dy}); - }; - - dragComponentAbove = function(sourceLocator, targetLocator) { - var targetElement = getComponent(targetLocator), - targetTop = targetElement.offset().top + 1, - handle = getDragHandle(sourceLocator), - handleY = handle.offset().top, - dy = targetTop - handleY; - handle.simulate('drag', {dy: dy}); - }; - - verifyRequest = function(requests, reorderCallIndex, expectedURL, expectedChildren) { - var actualIndex, request, children, i; - // 0th call is the response to the initial render call to get HTML. - actualIndex = reorderCallIndex + 1; - expect(requests.length).toBeGreaterThan(actualIndex); - request = requests[actualIndex]; - expect(request.url).toEqual(expectedURL); - children = (JSON.parse(request.requestBody)).children; - expect(children.length).toEqual(expectedChildren.length); - for (i = 0; i < children.length; i++) { - expect(children[i]).toEqual(expectedChildren[i]); - } - }; - - verifyNumReorderCalls = function(requests, expectedCalls) { - // Number of calls will be 1 more than expected because of the initial render call to get HTML. - expect(requests.length).toEqual(expectedCalls + 1); - }; - - respondToRequest = function(requests, reorderCallIndex, status) { - var actualIndex; - // Number of calls will be 1 more than expected because of the initial render call to get HTML. - actualIndex = reorderCallIndex + 1; - expect(requests.length).toBeGreaterThan(actualIndex); - requests[actualIndex].respond(status); - }; - - it('can reorder within a group', function() { - var requests = init(this); - // Drag the third component in Group A to be the first - dragComponentAbove(groupAComponent3, groupAComponent1); - respondToRequest(requests, 0, 200); - verifyRequest(requests, 0, groupAUrl, [groupAComponent3, groupAComponent1, groupAComponent2]); - }); - - it('can drag from one group to another', function() { - var requests = init(this); - // Drag the first component in Group B to the top of group A. - dragComponentAbove(groupBComponent1, groupAComponent1); - - // Respond to the two requests: add the component to Group A, then remove it from Group B. - respondToRequest(requests, 0, 200); - respondToRequest(requests, 1, 200); - - verifyRequest(requests, 0, groupAUrl, - [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); - verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]); - }); - - it('does not remove from old group if addition to new group fails', function() { - var requests = init(this); - // Drag the first component in Group B to the first group. - dragComponentAbove(groupBComponent1, groupAComponent1); - respondToRequest(requests, 0, 500); - // Send failure for addition to new group -- no removal event should be received. - verifyRequest(requests, 0, groupAUrl, - [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); - // Verify that a second request was not issued - verifyNumReorderCalls(requests, 1); - }); - - it('can swap group A and group B', function() { - var requests = init(this); - // Drag Group B before group A. - dragComponentAbove(groupB, groupA); - respondToRequest(requests, 0, 200); - verifyRequest(requests, 0, containerTestUrl, [groupB, groupA]); - }); - - describe('Shows a saving message', function() { - it('hides saving message upon success', function() { - var requests, savingOptions; - requests = init(this); - - // Drag the first component in Group B to the first group. - dragComponentAbove(groupBComponent1, groupAComponent1); - EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); - respondToRequest(requests, 0, 200); - EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); - respondToRequest(requests, 1, 200); - EditHelpers.verifyNotificationHidden(notificationSpy); - }); - - it('does not hide saving message if failure', function() { - var requests = init(this); - - // Drag the first component in Group B to the first group. - dragComponentAbove(groupBComponent1, groupAComponent1); - EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); - respondToRequest(requests, 0, 500); - EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); - - // Since the first reorder call failed, the removal will not be called. - verifyNumReorderCalls(requests, 1); - }); - }); +import $ from 'jquery'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import ContainerView from 'js/views/container'; +import XBlockInfo from 'js/models/xblock_info'; +import 'jquery.simulate'; +import 'xmodule/js/src/xmodule'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; + +describe('Container View', () => { + describe('Supports reordering components', () => { + var model, containerView, mockContainerHTML, init, getComponent, + getDragHandle, dragComponentVertically, dragComponentAbove, + verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy, + + rootLocator = 'locator-container', + containerTestUrl = '/xblock/' + rootLocator, + + groupAUrl = '/xblock/locator-group-A', + groupA = 'locator-group-A', + groupAComponent1 = 'locator-component-A1', + groupAComponent2 = 'locator-component-A2', + groupAComponent3 = 'locator-component-A3', + + groupBUrl = '/xblock/locator-group-B', + groupB = 'locator-group-B', + groupBComponent1 = 'locator-component-B1', + groupBComponent2 = 'locator-component-B2', + groupBComponent3 = 'locator-component-B3'; + + mockContainerHTML = readFixtures('templates/mock/mock-container-xblock.underscore'); + + beforeEach(() => { + EditHelpers.installMockXBlock(); + EditHelpers.installViewTemplates(); + appendSetFixtures('
'); + notificationSpy = EditHelpers.createNotificationSpy(); + model = new XBlockInfo({ + id: rootLocator, + display_name: 'Test AB Test', + category: 'split_test' + }); + + containerView = new ContainerView({ + model: model, + view: 'container_preview', + el: $('.wrapper-xblock') + }); + }); + + afterEach(() => { + EditHelpers.uninstallMockXBlock(); + containerView.remove(); + }); + + init = function(caller) { + var requests = AjaxHelpers.requests(caller); + containerView.render(); + + AjaxHelpers.respondWithJson(requests, { + html: mockContainerHTML, + resources: [] + }); + + $('body').append(containerView.$el); + + // Give the whole container enough height to contain everything. + $('.xblock[data-locator=locator-container]').css('height', 2000); + + // Give the groups enough height to contain their child vertical elements. + $('.is-draggable[data-locator=locator-group-A]').css('height', 800); + $('.is-draggable[data-locator=locator-group-B]').css('height', 800); + + + // Give the leaf elements some height to mimic actual components. Otherwise + // drag and drop fails as the elements on bunched on top of each other. + $('.level-element').css('height', 230); + + return requests; + }; + + getComponent = function(locator) { + return containerView.$('.studio-xblock-wrapper[data-locator="' + locator + '"]'); + }; + + getDragHandle = function(locator) { + var component = getComponent(locator); + return $(component.find('.drag-handle')[0]); + }; + + dragComponentVertically = function(locator, dy) { + var handle = getDragHandle(locator); + handle.simulate('drag', {dy: dy}); + }; + + dragComponentAbove = function(sourceLocator, targetLocator) { + var targetElement = getComponent(targetLocator), + targetTop = targetElement.offset().top + 1, + handle = getDragHandle(sourceLocator), + handleY = handle.offset().top, + dy = targetTop - handleY; + handle.simulate('drag', {dy: dy}); + }; + + verifyRequest = function(requests, reorderCallIndex, expectedURL, expectedChildren) { + var actualIndex, request, children, i; + // 0th call is the response to the initial render call to get HTML. + actualIndex = reorderCallIndex + 1; + expect(requests.length).toBeGreaterThan(actualIndex); + request = requests[actualIndex]; + expect(request.url).toEqual(expectedURL); + children = (JSON.parse(request.requestBody)).children; + expect(children.length).toEqual(expectedChildren.length); + for (i = 0; i < children.length; i++) { + expect(children[i]).toEqual(expectedChildren[i]); + } + }; + + verifyNumReorderCalls = function(requests, expectedCalls) { + // Number of calls will be 1 more than expected because of the initial render call to get HTML. + expect(requests.length).toEqual(expectedCalls + 1); + }; + + respondToRequest = function(requests, reorderCallIndex, status) { + var actualIndex; + // Number of calls will be 1 more than expected because of the initial render call to get HTML. + actualIndex = reorderCallIndex + 1; + expect(requests.length).toBeGreaterThan(actualIndex); + + // Now process the actual request + AjaxHelpers.respond(requests, {statusCode: status}); + }; + + it('can reorder within a group', () => { + var requests = init(this); + // Drag the third component in Group A to be the first + dragComponentAbove(groupAComponent3, groupAComponent1); + respondToRequest(requests, 0, 200); + verifyRequest(requests, 0, groupAUrl, [groupAComponent3, groupAComponent1, groupAComponent2]); + }); + + it('can drag from one group to another', () => { + var requests = init(this); + // Drag the first component in Group B to the top of group A. + dragComponentAbove(groupBComponent1, groupAComponent1); + + // Respond to the two requests: add the component to Group A, then remove it from Group B. + respondToRequest(requests, 0, 200); + respondToRequest(requests, 1, 200); + + verifyRequest(requests, 0, groupAUrl, + [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); + verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]); + }); + + it('does not remove from old group if addition to new group fails', () => { + var requests = init(this); + // Drag the first component in Group B to the first group. + dragComponentAbove(groupBComponent1, groupAComponent1); + respondToRequest(requests, 0, 500); + // Send failure for addition to new group -- no removal event should be received. + verifyRequest(requests, 0, groupAUrl, + [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); + // Verify that a second request was not issued + verifyNumReorderCalls(requests, 1); + }); + + it('can swap group A and group B', () => { + var requests = init(this); + // Drag Group B before group A. + dragComponentAbove(groupB, groupA); + respondToRequest(requests, 0, 200); + verifyRequest(requests, 0, containerTestUrl, [groupB, groupA]); + }); + + describe('Shows a saving message', () => { + it('hides saving message upon success', () => { + var requests, savingOptions; + requests = init(this); + + // Drag the first component in Group B to the first group. + dragComponentAbove(groupBComponent1, groupAComponent1); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); + respondToRequest(requests, 0, 200); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); + respondToRequest(requests, 1, 200); + EditHelpers.verifyNotificationHidden(notificationSpy); + }); + + it('does not hide saving message if failure', () => { + var requests = init(this); + + // Drag the first component in Group B to the first group. + dragComponentAbove(groupBComponent1, groupAComponent1); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); + respondToRequest(requests, 0, 500); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); + + // Since the first reorder call failed, the removal will not be called. + verifyNumReorderCalls(requests, 1); }); }); }); +}); diff --git a/cms/static/js/spec/views/login_studio_spec.js b/cms/static/js/spec/views/login_studio_spec.js index d20ca233efe5..38a01dc311aa 100644 --- a/cms/static/js/spec/views/login_studio_spec.js +++ b/cms/static/js/spec/views/login_studio_spec.js @@ -1,32 +1,35 @@ -define(['jquery', 'js/factories/login', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/components/utils/view_utils'], -function($, LoginFactory, AjaxHelpers, ViewUtils) { - 'use strict'; - describe('Studio Login Page', function() { - var $submitButton; - beforeEach(function() { - loadFixtures('mock/login.underscore'); - var login_factory = new LoginFactory('/home/'); - $submitButton = $('#submit'); - }); +'use strict'; - it('disable the submit button once it is clicked', function() { - spyOn(ViewUtils, 'redirect').and.callFake(function() {}); - var requests = AjaxHelpers.requests(this); - expect($submitButton).not.toHaveClass('is-disabled'); - $submitButton.click(); - AjaxHelpers.respondWithJson(requests, {success: true}); - expect($submitButton).toHaveClass('is-disabled'); - }); +import $ from 'jquery'; +import LoginFactory from 'js/factories/login'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import ViewUtils from 'common/js/components/utils/view_utils'; - it('It will not disable the submit button if there are errors in ajax request', function() { - var requests = AjaxHelpers.requests(this); - expect($submitButton).not.toHaveClass('is-disabled'); - $submitButton.click(); - expect($submitButton).toHaveClass('is-disabled'); - AjaxHelpers.respondWithError(requests, {}); - expect($submitButton).not.toHaveClass('is-disabled'); - }); +describe('Studio Login Page', () => { + var $submitButton; + + beforeEach(function() { + loadFixtures('mock/login.underscore'); + var login_factory = LoginFactory('/home/'); + $submitButton = $('#submit'); + }); + + it('disable the submit button once it is clicked', function() { + spyOn(ViewUtils, 'redirect').and.callFake(function() {}); + var requests = AjaxHelpers.requests(this); + expect($submitButton).not.toHaveClass('is-disabled'); + $submitButton.click(); + AjaxHelpers.respondWithJson(requests, {success: true}); + expect($submitButton).toHaveClass('is-disabled'); + }); + + it('It will not disable the submit button if there are errors in ajax request', function() { + var requests = AjaxHelpers.requests(this); + expect($submitButton).not.toHaveClass('is-disabled'); + $submitButton.click(); + expect($submitButton).toHaveClass('is-disabled'); + AjaxHelpers.respondWithError(requests, {}); + expect($submitButton).not.toHaveClass('is-disabled'); }); }); diff --git a/cms/static/js/spec/views/modals/edit_xblock_spec.js b/cms/static/js/spec/views/modals/edit_xblock_spec.js index a076c5ee4c23..8a28acab5bcb 100644 --- a/cms/static/js/spec/views/modals/edit_xblock_spec.js +++ b/cms/static/js/spec/views/modals/edit_xblock_spec.js @@ -1,211 +1,215 @@ -define(['jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'js/spec_helpers/edit_helpers', 'js/views/modals/edit_xblock', 'js/models/xblock_info'], - function($, _, Backbone, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) { - 'use strict'; - describe('EditXBlockModal', function() { - var model, modal, showModal; - - showModal = function(requests, mockHtml, options) { - var $xblockElement = $('.xblock'); - return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options); - }; - - beforeEach(function() { - EditHelpers.installEditTemplates(); - appendSetFixtures('
'); - model = new XBlockInfo({ - id: 'testCourse/branch/draft/block/verticalFFF', - display_name: 'Test Unit', - category: 'vertical' - }); - }); +'use strict'; + +import $ from 'jquery'; +import _ from 'underscore'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import EditXBlockModal from 'js/views/modals/edit_xblock'; +import XBlockInfo from 'js/models/xblock_info'; + +describe('EditXBlockModal', function() { + var model, modal, showModal; + + showModal = function(requests, mockHtml, options) { + var $xblockElement = $('.xblock'); + return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options); + }; + + beforeEach(function() { + EditHelpers.installEditTemplates(); + appendSetFixtures('
'); + model = new XBlockInfo({ + id: 'testCourse/branch/draft/block/verticalFFF', + display_name: 'Test Unit', + category: 'vertical' + }); + }); + + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); + + describe('XBlock Editor', function() { + var mockXBlockEditorHtml; + + mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore'); + + beforeEach(function() { + EditHelpers.installMockXBlock(); + spyOn(Backbone, 'trigger').and.callThrough(); + }); + + afterEach(function() { + EditHelpers.uninstallMockXBlock(); + }); + + it('can show itself', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + }); + + it('does not show the "Save" button', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(modal.$('.action-save')).not.toBeVisible(); + expect(modal.$('.action-cancel').text()).toBe('Close'); + }); + + it('shows the correct title', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); + }); + + it('does not show any editor mode buttons', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(modal.$('.editor-modes a').length).toBe(0); + }); + + it('hides itself and refreshes after save notification', function() { + var requests = AjaxHelpers.requests(this), + refreshed = false, + refresh = function() { + refreshed = true; + }; + modal = showModal(requests, mockXBlockEditorHtml, {refresh: refresh}); + modal.editorView.notifyRuntime('save', {state: 'start'}); + modal.editorView.notifyRuntime('save', {state: 'end'}); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + expect(refreshed).toBeTruthy(); + expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden'); + }); + + it('hides itself and does not refresh after cancel notification', function() { + var requests = AjaxHelpers.requests(this), + refreshed = false, + refresh = function() { + refreshed = true; + }; + modal = showModal(requests, mockXBlockEditorHtml, {refresh: refresh}); + modal.editorView.notifyRuntime('cancel'); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + expect(refreshed).toBeFalsy(); + expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden'); + }); + + describe('Custom Buttons', function() { + var mockCustomButtonsHtml; + + mockCustomButtonsHtml = readFixtures('templates/mock/mock-xblock-editor-with-custom-buttons.underscore'); - afterEach(function() { - EditHelpers.cancelModalIfShowing(); + it('hides the modal\'s button bar', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockCustomButtonsHtml); + expect(modal.$('.modal-actions')).toBeHidden(); }); + }); + }); + + describe('XModule Editor', function() { + var mockXModuleEditorHtml; + + mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-editor.underscore'); + + beforeEach(function() { + EditHelpers.installMockXModule(); + }); + + afterEach(function() { + EditHelpers.uninstallMockXModule(); + }); + + it('can render itself', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + }); + + it('shows the correct title', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); + }); - describe('XBlock Editor', function() { - var mockXBlockEditorHtml; - - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); - - beforeEach(function() { - EditHelpers.installMockXBlock(); - spyOn(Backbone, 'trigger').and.callThrough(); - }); - - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - }); - - it('can show itself', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXBlockEditorHtml); - expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); - EditHelpers.cancelModal(modal); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - }); - - it('does not show the "Save" button', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXBlockEditorHtml); - expect(modal.$('.action-save')).not.toBeVisible(); - expect(modal.$('.action-cancel').text()).toBe('Close'); - }); - - it('shows the correct title', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXBlockEditorHtml); - expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); - }); - - it('does not show any editor mode buttons', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXBlockEditorHtml); - expect(modal.$('.editor-modes a').length).toBe(0); - }); - - it('hides itself and refreshes after save notification', function() { - var requests = AjaxHelpers.requests(this), - refreshed = false, - refresh = function() { - refreshed = true; - }; - modal = showModal(requests, mockXBlockEditorHtml, {refresh: refresh}); - modal.editorView.notifyRuntime('save', {state: 'start'}); - modal.editorView.notifyRuntime('save', {state: 'end'}); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - expect(refreshed).toBeTruthy(); - expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden'); - }); - - it('hides itself and does not refresh after cancel notification', function() { - var requests = AjaxHelpers.requests(this), - refreshed = false, - refresh = function() { - refreshed = true; - }; - modal = showModal(requests, mockXBlockEditorHtml, {refresh: refresh}); - modal.editorView.notifyRuntime('cancel'); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - expect(refreshed).toBeFalsy(); - expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden'); - }); - - describe('Custom Buttons', function() { - var mockCustomButtonsHtml; - - mockCustomButtonsHtml = readFixtures('mock/mock-xblock-editor-with-custom-buttons.underscore'); - - it('hides the modal\'s button bar', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomButtonsHtml); - expect(modal.$('.modal-actions')).toBeHidden(); - }); - }); + it('shows the correct default buttons', function() { + var requests = AjaxHelpers.requests(this), + editorButton, + settingsButton; + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.editor-modes a').length).toBe(2); + editorButton = modal.$('.editor-button'); + settingsButton = modal.$('.settings-button'); + expect(editorButton.length).toBe(1); + expect(editorButton).toHaveClass('is-set'); + expect(settingsButton.length).toBe(1); + expect(settingsButton).not.toHaveClass('is-set'); + }); + + it('can switch tabs', function() { + var requests = AjaxHelpers.requests(this), + editorButton, + settingsButton; + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.editor-modes a').length).toBe(2); + editorButton = modal.$('.editor-button'); + settingsButton = modal.$('.settings-button'); + expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); + settingsButton.click(); + expect(modal.$('.metadata_edit')).toHaveClass('is-active'); + editorButton.click(); + expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); + }); + + describe('Custom Tabs', function() { + var mockCustomTabsHtml; + + mockCustomTabsHtml = readFixtures('templates/mock/mock-xmodule-editor-with-custom-tabs.underscore'); + + it('hides the modal\'s header', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockCustomTabsHtml); + expect(modal.$('.modal-header')).toBeHidden(); }); - describe('XModule Editor', function() { - var mockXModuleEditorHtml; - - mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore'); - - beforeEach(function() { - EditHelpers.installMockXModule(); - }); - - afterEach(function() { - EditHelpers.uninstallMockXModule(); - }); - - it('can render itself', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXModuleEditorHtml); - expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); - EditHelpers.cancelModal(modal); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - }); - - it('shows the correct title', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXModuleEditorHtml); - expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); - }); - - it('shows the correct default buttons', function() { - var requests = AjaxHelpers.requests(this), - editorButton, - settingsButton; - modal = showModal(requests, mockXModuleEditorHtml); - expect(modal.$('.editor-modes a').length).toBe(2); - editorButton = modal.$('.editor-button'); - settingsButton = modal.$('.settings-button'); - expect(editorButton.length).toBe(1); - expect(editorButton).toHaveClass('is-set'); - expect(settingsButton.length).toBe(1); - expect(settingsButton).not.toHaveClass('is-set'); - }); - - it('can switch tabs', function() { - var requests = AjaxHelpers.requests(this), - editorButton, - settingsButton; - modal = showModal(requests, mockXModuleEditorHtml); - expect(modal.$('.editor-modes a').length).toBe(2); - editorButton = modal.$('.editor-button'); - settingsButton = modal.$('.settings-button'); - expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); - settingsButton.click(); - expect(modal.$('.metadata_edit')).toHaveClass('is-active'); - editorButton.click(); - expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); - }); - - describe('Custom Tabs', function() { - var mockCustomTabsHtml; - - mockCustomTabsHtml = readFixtures('mock/mock-xmodule-editor-with-custom-tabs.underscore'); - - it('hides the modal\'s header', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomTabsHtml); - expect(modal.$('.modal-header')).toBeHidden(); - }); - - it('shows the correct title', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomTabsHtml); - expect(modal.$('.component-name').text()).toBe('Editing: Component'); - }); - }); + it('shows the correct title', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockCustomTabsHtml); + expect(modal.$('.component-name').text()).toBe('Editing: Component'); }); + }); + }); - describe('XModule Editor (settings only)', function() { - var mockXModuleEditorHtml; + describe('XModule Editor (settings only)', function() { + var mockXModuleEditorHtml; - mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore'); + mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-settings-only-editor.underscore'); - beforeEach(function() { - EditHelpers.installMockXModule(); - }); + beforeEach(function() { + EditHelpers.installMockXModule(); + }); - afterEach(function() { - EditHelpers.uninstallMockXModule(); - }); + afterEach(function() { + EditHelpers.uninstallMockXModule(); + }); - it('can render itself', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXModuleEditorHtml); - expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); - EditHelpers.cancelModal(modal); - expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); - }); + it('can render itself', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + }); - it('does not show any mode buttons', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXModuleEditorHtml); - expect(modal.$('.editor-modes li').length).toBe(0); - }); - }); + it('does not show any mode buttons', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.editor-modes li').length).toBe(0); }); }); +}); diff --git a/cms/static/js/spec/views/module_edit_spec.js b/cms/static/js/spec/views/module_edit_spec.js index 21903b222640..04f2d2797e75 100644 --- a/cms/static/js/spec/views/module_edit_spec.js +++ b/cms/static/js/spec/views/module_edit_spec.js @@ -1,37 +1,58 @@ -(function() { - 'use strict'; - define([ - 'jquery', 'common/js/components/utils/view_utils', 'js/spec_helpers/edit_helpers', - 'js/views/module_edit', 'js/models/module_info', 'xmodule'], - function($, ViewUtils, edit_helpers, ModuleEdit, ModuleModel) { - describe('ModuleEdit', function() { + +import $ from 'jquery'; +import ViewUtils from 'common/js/components/utils/view_utils'; +import edit_helpers from 'js/spec_helpers/edit_helpers'; +import ModuleEdit from 'js/views/module_edit'; +import ModuleModel from 'js/models/module_info'; +import 'xmodule/js/src/xmodule'; + +describe('ModuleEdit', function() { + beforeEach(function() { + this.stubModule = new ModuleModel({ + id: 'stub-id' + }); + setFixtures(''); + edit_helpers.installEditTemplates(true); + spyOn($, 'ajax').and.returnValue(this.moduleData); + this.moduleEdit = new ModuleEdit({ + el: $('.component'), + model: this.stubModule, + onDelete: jasmine.createSpy() + }); + return this.moduleEdit; + }); + describe('class definition', function() { + it('sets the correct tagName', function() { + return expect(this.moduleEdit.tagName).toEqual('li'); + }); + it('sets the correct className', function() { + return expect(this.moduleEdit.className).toEqual('component'); + }); + }); + describe('methods', function() { + describe('initialize', function() { beforeEach(function() { - this.stubModule = new ModuleModel({ - id: 'stub-id' - }); - setFixtures(''); - edit_helpers.installEditTemplates(true); - spyOn($, 'ajax').and.returnValue(this.moduleData); + spyOn(ModuleEdit.prototype, 'render'); this.moduleEdit = new ModuleEdit({ el: $('.component'), model: this.stubModule, @@ -39,227 +60,206 @@ }); return this.moduleEdit; }); - describe('class definition', function() { - it('sets the correct tagName', function() { - return expect(this.moduleEdit.tagName).toEqual('li'); + it('renders the module editor', function() { + return expect(ModuleEdit.prototype.render).toHaveBeenCalled(); + }); + }); + describe('render', function() { + beforeEach(function () { + edit_helpers.installEditTemplates(true); + spyOn(this.moduleEdit, 'loadDisplay'); + spyOn(this.moduleEdit, 'delegateEvents'); + spyOn($.fn, 'append'); + spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise()); + window.MockXBlock = function() { + return {}; + }; + window.loadedXBlockResources = void 0; + this.moduleEdit.render(); + return $.ajax.calls.mostRecent().args[0].success({ + html: '
Response html
', + resources: [ + [ + 'hash1', { + kind: 'text', + mimetype: 'text/css', + data: 'inline-css' + } + ], [ + 'hash2', { + kind: 'url', + mimetype: 'text/css', + data: 'css-url' + } + ], [ + 'hash3', { + kind: 'text', + mimetype: 'application/javascript', + data: 'inline-js' + } + ], [ + 'hash4', { + kind: 'url', + mimetype: 'application/javascript', + data: 'js-url' + } + ], [ + 'hash5', { + placement: 'head', + mimetype: 'text/html', + data: 'head-html' + } + ], [ + 'hash6', { + placement: 'not-head', + mimetype: 'text/html', + data: 'not-head-html' + } + ] + ] + }); + }); + afterEach(function() { + window.MockXBlock = null; + return window.MockXBlock; + }); + it('loads the module preview via ajax on the view element', function() { + expect($.ajax).toHaveBeenCalledWith({ + url: '/xblock/' + this.moduleEdit.model.id + '/student_view', + type: 'GET', + cache: false, + headers: { + Accept: 'application/json' + }, + success: jasmine.any(Function) }); - it('sets the correct className', function() { - return expect(this.moduleEdit.className).toEqual('component'); + expect($.ajax).not.toHaveBeenCalledWith({ + url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', + type: 'GET', + headers: { + Accept: 'application/json' + }, + success: jasmine.any(Function) }); + expect(this.moduleEdit.loadDisplay).toHaveBeenCalled(); + return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled(); }); - describe('methods', function() { - describe('initialize', function() { - beforeEach(function() { - spyOn(ModuleEdit.prototype, 'render'); - this.moduleEdit = new ModuleEdit({ - el: $('.component'), - model: this.stubModule, - onDelete: jasmine.createSpy() - }); - return this.moduleEdit; - }); - it('renders the module editor', function() { - return expect(ModuleEdit.prototype.render).toHaveBeenCalled(); - }); + it('loads the editing view via ajax on demand', function() { + var mockXBlockEditorHtml; + expect($.ajax).not.toHaveBeenCalledWith({ + url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', + type: 'GET', + cache: false, + headers: { + Accept: 'application/json' + }, + success: jasmine.any(Function) }); - describe('render', function() { - beforeEach(function() { - spyOn(this.moduleEdit, 'loadDisplay'); - spyOn(this.moduleEdit, 'delegateEvents'); - spyOn($.fn, 'append'); - spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise()); - window.MockXBlock = function() { - return {}; - }; - window.loadedXBlockResources = void 0; - this.moduleEdit.render(); - return $.ajax.calls.mostRecent().args[0].success({ - html: '
Response html
', - resources: [ - [ - 'hash1', { - kind: 'text', - mimetype: 'text/css', - data: 'inline-css' - } - ], [ - 'hash2', { - kind: 'url', - mimetype: 'text/css', - data: 'css-url' - } - ], [ - 'hash3', { - kind: 'text', - mimetype: 'application/javascript', - data: 'inline-js' - } - ], [ - 'hash4', { - kind: 'url', - mimetype: 'application/javascript', - data: 'js-url' - } - ], [ - 'hash5', { - placement: 'head', - mimetype: 'text/html', - data: 'head-html' - } - ], [ - 'hash6', { - placement: 'not-head', - mimetype: 'text/html', - data: 'not-head-html' - } - ] - ] - }); - }); - afterEach(function() { - window.MockXBlock = null; - return window.MockXBlock; - }); - it('loads the module preview via ajax on the view element', function() { - expect($.ajax).toHaveBeenCalledWith({ - url: '/xblock/' + this.moduleEdit.model.id + '/student_view', - type: 'GET', - cache: false, - headers: { - Accept: 'application/json' - }, - success: jasmine.any(Function) - }); - expect($.ajax).not.toHaveBeenCalledWith({ - url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', - type: 'GET', - headers: { - Accept: 'application/json' - }, - success: jasmine.any(Function) - }); - expect(this.moduleEdit.loadDisplay).toHaveBeenCalled(); - return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled(); - }); - it('loads the editing view via ajax on demand', function() { - var mockXBlockEditorHtml; - edit_helpers.installEditTemplates(true); - expect($.ajax).not.toHaveBeenCalledWith({ - url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', - type: 'GET', - cache: false, - headers: { - Accept: 'application/json' - }, - success: jasmine.any(Function) - }); - this.moduleEdit.clickEditButton({ - preventDefault: jasmine.createSpy('event.preventDefault') - }); - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); - $.ajax.calls.mostRecent().args[0].success({ - html: mockXBlockEditorHtml, - resources: [ - [ - 'hash1', { - kind: 'text', - mimetype: 'text/css', - data: 'inline-css' - } - ], [ - 'hash2', { - kind: 'url', - mimetype: 'text/css', - data: 'css-url' - } - ], [ - 'hash3', { - kind: 'text', - mimetype: 'application/javascript', - data: 'inline-js' - } - ], [ - 'hash4', { - kind: 'url', - mimetype: 'application/javascript', - data: 'js-url' - } - ], [ - 'hash5', { - placement: 'head', - mimetype: 'text/html', - data: 'head-html' - } - ], [ - 'hash6', { - placement: 'not-head', - mimetype: 'text/html', - data: 'not-head-html' - } - ] - ] - }); - expect($.ajax).toHaveBeenCalledWith({ - url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', - type: 'GET', - cache: false, - headers: { - Accept: 'application/json' - }, - success: jasmine.any(Function) - }); - return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled(); - }); - it('loads inline css from fragments', function() { - var args = ""; - return expect($('head').append).toHaveBeenCalledWith(args); - }); - it('loads css urls from fragments', function() { - var args = ""; - return expect($('head').append).toHaveBeenCalledWith(args); - }); - it('loads inline js from fragments', function() { - return expect($('head').append).toHaveBeenCalledWith(''); - }); - it('loads js urls from fragments', function() { - return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url'); - }); - it('loads head html', function() { - return expect($('head').append).toHaveBeenCalledWith('head-html'); - }); - it("doesn't load body html", function() { - return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html'); - }); - it("doesn't reload resources", function() { - var count; - count = $('head').append.calls.count(); - $.ajax.calls.mostRecent().args[0].success({ - html: '
Response html 2
', - resources: [ - [ - 'hash1', { - kind: 'text', - mimetype: 'text/css', - data: 'inline-css' - } - ] - ] - }); - return expect($('head').append.calls.count()).toBe(count); - }); + this.moduleEdit.clickEditButton({ + preventDefault: jasmine.createSpy('event.preventDefault') }); - describe('loadDisplay', function() { - beforeEach(function() { - spyOn(XBlock, 'initializeBlock'); - return this.moduleEdit.loadDisplay(); - }); - it('loads the .xmodule-display inside the module editor', function() { - expect(XBlock.initializeBlock).toHaveBeenCalled(); - var sel = '.xblock-student_view'; - return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0)); - }); + mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore'); + $.ajax.calls.mostRecent().args[0].success({ + html: mockXBlockEditorHtml, + resources: [ + [ + 'hash1', { + kind: 'text', + mimetype: 'text/css', + data: 'inline-css' + } + ], [ + 'hash2', { + kind: 'url', + mimetype: 'text/css', + data: 'css-url' + } + ], [ + 'hash3', { + kind: 'text', + mimetype: 'application/javascript', + data: 'inline-js' + } + ], [ + 'hash4', { + kind: 'url', + mimetype: 'application/javascript', + data: 'js-url' + } + ], [ + 'hash5', { + placement: 'head', + mimetype: 'text/html', + data: 'head-html' + } + ], [ + 'hash6', { + placement: 'not-head', + mimetype: 'text/html', + data: 'not-head-html' + } + ] + ] }); + expect($.ajax).toHaveBeenCalledWith({ + url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', + type: 'GET', + cache: false, + headers: { + Accept: 'application/json' + }, + success: jasmine.any(Function) + }); + return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled(); + }); + it('loads inline css from fragments', function() { + var args = ""; + return expect($('head').append).toHaveBeenCalledWith(args); + }); + it('loads css urls from fragments', function() { + var args = ""; + return expect($('head').append).toHaveBeenCalledWith(args); + }); + it('loads inline js from fragments', function() { + return expect($('head').append).toHaveBeenCalledWith(''); + }); + it('loads js urls from fragments', function() { + return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url'); + }); + it('loads head html', function() { + return expect($('head').append).toHaveBeenCalledWith('head-html'); + }); + it("doesn't load body html", function() { + return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html'); + }); + it("doesn't reload resources", function() { + var count; + count = $('head').append.calls.count(); + $.ajax.calls.mostRecent().args[0].success({ + html: '
Response html 2
', + resources: [ + [ + 'hash1', { + kind: 'text', + mimetype: 'text/css', + data: 'inline-css' + } + ] + ] + }); + return expect($('head').append.calls.count()).toBe(count); + }); + }); + describe('loadDisplay', function() { + beforeEach(function() { + spyOn(XBlock, 'initializeBlock'); + return this.moduleEdit.loadDisplay(); + }); + it('loads the .xmodule-display inside the module editor', function() { + expect(XBlock.initializeBlock).toHaveBeenCalled(); + var sel = '.xblock-student_view'; + return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0)); }); }); }); -}).call(this); +}); diff --git a/cms/static/js/spec/views/move_xblock_spec.js b/cms/static/js/spec/views/move_xblock_spec.js index 7e7610fdfee8..f8a045f95d90 100644 --- a/cms/static/js/spec/views/move_xblock_spec.js +++ b/cms/static/js/spec/views/move_xblock_spec.js @@ -1,766 +1,785 @@ -define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers', - 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/view_helpers', - 'js/views/modals/move_xblock_modal', 'js/views/pages/container', 'edx-ui-toolkit/js/utils/html-utils', - 'edx-ui-toolkit/js/utils/string-utils', 'js/models/xblock_info'], - function($, _, AjaxHelpers, EditHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, ContainerPage, HtmlUtils, - StringUtils, XBlockInfo) { - 'use strict'; - describe('MoveXBlock', function() { - var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions, - parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline, - verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton, - clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests, - verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, getMovedAlertNotification, - verifyConfirmationFeedbackTitleText, verifyConfirmationFeedbackRedirectLinkText, - verifyUndoConfirmationFeedbackTitleText, verifyConfirmationFeedbackUndoMoveActionText, - sourceParentXBlockInfo, mockContainerPage, createContainerPage, containerPage, - sourceDisplayName = 'component_display_name_0', - sourceLocator = 'component_ID_0', - sourceParentLocator = 'unit_ID_0'; - - parentChildMap = { - course: 'section', - section: 'subsection', - subsection: 'unit', - unit: 'component' - }; - - categoryMap = { - section: 'chapter', - subsection: 'sequential', - unit: 'vertical', - component: 'component' - }; - - courseOutlineOptions = { - section: 2, - subsection: 2, - unit: 2, - component: 2 - }; - - xblockAncestorInfo = { - ancestors: [ - { - category: 'vertical', - display_name: 'unit_display_name_0', - id: 'unit_ID_0' - }, - { - category: 'sequential', - display_name: 'subsection_display_name_0', - id: 'subsection_ID_0' - }, - { - category: 'chapter', - display_name: 'section_display_name_0', - id: 'section_ID_0' - }, - { - category: 'course', - display_name: 'Demo Course', - id: 'COURSE_ID_101' - } - ] - }; - - sourceParentXBlockInfo = new XBlockInfo({ - id: sourceParentLocator, +import $ from 'jquery'; +import _ from 'underscore'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import TemplateHelpers from 'common/js/spec_helpers/template_helpers'; +import ViewHelpers from 'common/js/spec_helpers/view_helpers'; +import MoveXBlockModal from 'js/views/modals/move_xblock_modal'; +import ContainerPage from 'js/views/pages/container'; +import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils'; +import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; +import XBlockInfo from 'js/models/xblock_info'; +import Course from 'js/models/course'; +import 'mock-ajax'; + +describe('MoveXBlock', function() { + + 'use strict'; + var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions, + parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline, + verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton, + clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests, + verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, getMovedAlertNotification, + verifyConfirmationFeedbackTitleText, verifyConfirmationFeedbackRedirectLinkText, + verifyUndoConfirmationFeedbackTitleText, verifyConfirmationFeedbackUndoMoveActionText, + sourceParentXBlockInfo, mockContainerPage, createContainerPage, containerPage, + sourceDisplayName = 'component_display_name_0', + sourceLocator = 'component_ID_0', + sourceParentLocator = 'unit_ID_0'; + + parentChildMap = { + course: 'section', + section: 'subsection', + subsection: 'unit', + unit: 'component' + }; + + categoryMap = { + section: 'chapter', + subsection: 'sequential', + unit: 'vertical', + component: 'component' + }; + + courseOutlineOptions = { + section: 2, + subsection: 2, + unit: 2, + component: 2 + }; + + xblockAncestorInfo = { + ancestors: [ + { + category: 'vertical', display_name: 'unit_display_name_0', - category: 'vertical' - }); - - createContainerPage = function() { - containerPage = new ContainerPage({ - model: sourceParentXBlockInfo, - templates: EditHelpers.mockComponentTemplates, - el: $('#content'), - isUnitPage: true - }); - }; - - beforeEach(function() { - setFixtures("
"); - mockContainerPage = readFixtures('mock/mock-container-page.underscore'); - TemplateHelpers.installTemplates([ - 'basic-modal', - 'modal-button', - 'move-xblock-modal' - ]); - appendSetFixtures(mockContainerPage); - createContainerPage(); - courseOutline = createCourseOutline(courseOutlineOptions); - showModal(); - }); + id: 'unit_ID_0' + }, + { + category: 'sequential', + display_name: 'subsection_display_name_0', + id: 'subsection_ID_0' + }, + { + category: 'chapter', + display_name: 'section_display_name_0', + id: 'section_ID_0' + }, + { + category: 'course', + display_name: 'Demo Course', + id: 'COURSE_ID_101' + } + ] + }; + + sourceParentXBlockInfo = new XBlockInfo({ + id: sourceParentLocator, + display_name: 'unit_display_name_0', + category: 'vertical' + }); - afterEach(function() { - modal.hide(); - courseOutline = null; - containerPage.remove(); - }); + createContainerPage = function() { + containerPage = new ContainerPage({ + model: sourceParentXBlockInfo, + templates: EditHelpers.mockComponentTemplates, + el: $('#content'), + isUnitPage: true + }); + }; + + beforeEach(function() { + setFixtures("
"); + mockContainerPage = readFixtures('templates/mock/mock-container-page.underscore'); + TemplateHelpers.installTemplates([ + 'basic-modal', + 'modal-button', + 'move-xblock-modal' + ]); + appendSetFixtures(mockContainerPage); + + window.course = new Course({ + id: "5", + name: "Course Name", + url_name: "course_name", + org: "course_org", + num: "course_num", + revision: "course_rev" + }); - showModal = function() { - modal = new MoveXBlockModal({ - sourceXBlockInfo: new XBlockInfo({ - id: sourceLocator, - display_name: sourceDisplayName, - category: 'component' - }), - sourceParentXBlockInfo: sourceParentXBlockInfo, - XBlockUrlRoot: '/xblock' - }); - modal.show(); - }; + createContainerPage(); + courseOutline = createCourseOutline(courseOutlineOptions); + showModal(); + }); - /** - * Create child XBlock info. - * - * @param {String} category XBlock category - * @param {Object} outlineOptions options according to which outline was created - * @param {Object} xblockIndex XBlock Index - * @returns - */ - createChildXBlockInfo = function(category, outlineOptions, xblockIndex) { - var childInfo = { - category: categoryMap[category], - display_name: category + '_display_name_' + xblockIndex, - id: category + '_ID_' + xblockIndex - }; - return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo); - }; + afterEach(function() { + modal.hide(); + courseOutline = null; + containerPage.remove(); + delete window.course; + }); - /** - * Create parent XBlock info. - * - * @param {String} category XBlock category - * @param {Object} outlineOptions options according to which outline was created - * @param {Object} outline ouline info being constructed - * @returns {Object} - */ - createXBlockInfo = function(category, outlineOptions, outline) { - var childInfo = { - category: categoryMap[category], - display_name: category, - children: [] - }, - xblocks; - - xblocks = outlineOptions[category]; - if (!xblocks) { - return outline; + showModal = function() { + modal = new MoveXBlockModal({ + sourceXBlockInfo: new XBlockInfo({ + id: sourceLocator, + display_name: sourceDisplayName, + category: 'component' + }), + sourceParentXBlockInfo: sourceParentXBlockInfo, + XBlockUrlRoot: '/xblock' + }); + modal.show(); + }; + + /** + * Create child XBlock info. + * + * @param {String} category XBlock category + * @param {Object} outlineOptions options according to which outline was created + * @param {Object} xblockIndex XBlock Index + * @returns + */ + createChildXBlockInfo = function(category, outlineOptions, xblockIndex) { + var childInfo = { + category: categoryMap[category], + display_name: category + '_display_name_' + xblockIndex, + id: category + '_ID_' + xblockIndex + }; + return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo); + }; + + /** + * Create parent XBlock info. + * + * @param {String} category XBlock category + * @param {Object} outlineOptions options according to which outline was created + * @param {Object} outline ouline info being constructed + * @returns {Object} + */ + createXBlockInfo = function(category, outlineOptions, outline) { + var childInfo = { + category: categoryMap[category], + display_name: category, + children: [] + }, + xblocks; + + xblocks = outlineOptions[category]; + if (!xblocks) { + return outline; + } + + outline.child_info = childInfo; // eslint-disable-line no-param-reassign + _.each(_.range(xblocks), function(xblockIndex) { + childInfo.children.push( + createChildXBlockInfo(category, outlineOptions, xblockIndex) + ); + }); + return outline; + }; + + /** + * Create course outline. + * + * @param {Object} outlineOptions options according to which outline was created + * @returns {Object} + */ + createCourseOutline = function(outlineOptions) { + var courseXBlockInfo = { + category: 'course', + display_name: 'Demo Course', + id: 'COURSE_ID_101' + }; + return createXBlockInfo('section', outlineOptions, courseXBlockInfo); + }; + + /** + * Render breadcrumb and XBlock list view. + * + * @param {any} courseOutlineInfo course outline info + * @param {any} ancestorInfo ancestors info + */ + renderViews = function(courseOutlineInfo, ancestorInfo) { + var ancestorInfo = ancestorInfo || {ancestors: []}; // eslint-disable-line no-redeclare + modal.renderViews(courseOutlineInfo, ancestorInfo); + }; + + /** + * Extract displayed XBlock list info. + * + * @returns {Object} + */ + getDisplayedInfo = function() { + var viewEl = modal.moveXBlockListView.$el; + return { + categoryText: viewEl.find('.category-text').text().trim(), + currentLocationText: viewEl.find('.current-location').text().trim(), + xblockCount: viewEl.find('.xblock-item').length, + xblockDisplayNames: viewEl.find('.xblock-item .xblock-displayname').map( + function() { return $(this).text().trim(); } + ).get(), + forwardButtonSRTexts: viewEl.find('.xblock-item .forward-sr-text').map( + function() { return $(this).text().trim(); } + ).get(), + forwardButtonCount: viewEl.find('.fa-arrow-right.forward-sr-icon').length + }; + }; + + /** + * Verify displayed XBlock list info. + * + * @param {String} category XBlock category + * @param {Integer} expectedXBlocksCount number of XBlock childs displayed + * @param {Boolean} hasCurrentLocation do we need to check current location + */ + verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) { + var displayedInfo = getDisplayedInfo(); + expect(displayedInfo.categoryText).toEqual(modal.moveXBlockListView.categoriesText[category] + ':'); + expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount); + expect(displayedInfo.xblockDisplayNames).toEqual( + _.map(_.range(expectedXBlocksCount), function(xblockIndex) { + return category + '_display_name_' + xblockIndex; + }) + ); + if (category === 'component') { + if (hasCurrentLocation) { + expect(displayedInfo.currentLocationText).toEqual('(Currently selected)'); + } + } else { + if (hasCurrentLocation) { + expect(displayedInfo.currentLocationText).toEqual('(Current location)'); + } + expect(displayedInfo.forwardButtonSRTexts).toEqual( + _.map(_.range(expectedXBlocksCount), function() { + return 'View child items'; + }) + ); + expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount); + } + }; + + /** + * Verify rendered breadcrumb info. + * + * @param {any} category XBlock category + * @param {any} xblockIndex XBlock index + */ + verifyBreadcrumbViewInfo = function(category, xblockIndex) { + var displayedBreadcrumbs = modal.moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map( + function() { return $(this).text().trim(); } + ).get(), + categories = _.keys(parentChildMap).concat(['component']), + visitedCategories = categories.slice(0, _.indexOf(categories, category)); + + expect(displayedBreadcrumbs).toEqual( + _.map(visitedCategories, function(visitedCategory) { + return visitedCategory === 'course' ? + 'Course Outline' : visitedCategory + '_display_name_' + xblockIndex; + }) + ); + }; + + /** + * Click forward button in the list of displayed XBlocks. + * + * @param {any} buttonIndex forward button index + */ + clickForwardButton = function(buttonIndex) { + buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign + modal.moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click(); + }; + + /** + * Click on last clickable breadcrumb button. + */ + clickBreadcrumbButton = function() { + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); + }; + + /** + * Returns the parent or child category of current XBlock. + * + * @param {String} direction `forward` or `backward` + * @param {String} category XBlock category + * @returns {String} + */ + nextCategory = function(direction, category) { + return direction === 'forward' ? parentChildMap[category] : _.invert(parentChildMap)[category]; + }; + + /** + * Verify renderd info of breadcrumbs and XBlock list. + * + * @param {Object} outlineOptions options according to which outline was created + * @param {String} category XBlock category + * @param {Integer} buttonIndex forward button index + * @param {String} direction `forward` or `backward` + * @param {String} hasCurrentLocation do we need to check current location + * @returns + */ + verifyXBlockInfo = function(outlineOptions, category, buttonIndex, direction, hasCurrentLocation) { + var expectedXBlocksCount = outlineOptions[category]; + + verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation); + verifyBreadcrumbViewInfo(category, buttonIndex); + verifyMoveEnabled(category, hasCurrentLocation); + + if (direction === 'forward') { + if (category === 'component') { + return; + } + clickForwardButton(buttonIndex); + } else if (direction === 'backward') { + if (category === 'section') { + return; + } + clickBreadcrumbButton(); + } + category = nextCategory(direction, category); // eslint-disable-line no-param-reassign + + verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation); + }; + + /** + * Verify move button is enabled. + * + * @param {String} category XBlock category + * @param {String} hasCurrentLocation do we need to check current location + */ + verifyMoveEnabled = function(category, hasCurrentLocation) { + var isMoveEnabled = !modal.$el.find('.modal-actions .action-move').hasClass('is-disabled'); + if (category === 'component' && !hasCurrentLocation) { + expect(isMoveEnabled).toBeTruthy(); + } else { + expect(isMoveEnabled).toBeFalsy(); + } + }; + + /** + * Verify notification status. + * + * @param {Object} requests requests object + * @param {Object} notificationSpy notification spy + * @param {String} notificationText notification text to be verified + * @param {Integer} sourceIndex source index of the xblock + */ + verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) { + var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare + ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText); + AjaxHelpers.respondWithJson(requests, { + move_source_locator: sourceLocator, + parent_locator: sourceParentLocator, + target_index: sourceIndex + }); + ViewHelpers.verifyNotificationHidden(notificationSpy); + }; + + /** + * Get move alert confirmation message HTML + */ + getMovedAlertNotification = function() { + return $('#page-alert'); + }; + + /** + * Send move xblock request. + * + * @param {Object} requests requests object + * @param {Object} xblockLocator Xblock id location + * @param {Integer} targetIndex target index of the xblock + * @param {Integer} sourceIndex source index of the xblock + */ + sendMoveXBlockRequest = function(requests, xblockLocator, targetIndex, sourceIndex) { + var responseData, + expectedData, + sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare + + responseData = expectedData = { + move_source_locator: xblockLocator, + parent_locator: modal.targetParentXBlockInfo.id + }; + + if (targetIndex !== undefined) { + expectedData = _.extend(expectedData, { + targetIndex: targetIndex + }); + } + + // verify content of request + AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData); + + // send the response + AjaxHelpers.respondWithJson(requests, _.extend(responseData, { + source_index: sourceIndex + })); + }; + + /** + * Move xblock with success. + * + * @param {Object} requests requests object + */ + moveXBlockWithSuccess = function(requests) { + // select a target item and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + modal.$el.find('.modal-actions .action-move').click(); + sendMoveXBlockRequest(requests, sourceLocator); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/' + sourceParentLocator); + AjaxHelpers.respondWithJson(requests, sourceParentXBlockInfo); + expect(getMovedAlertNotification().html().length).not.toEqual(0); + verifyConfirmationFeedbackTitleText(sourceDisplayName); + verifyConfirmationFeedbackRedirectLinkText(); + verifyConfirmationFeedbackUndoMoveActionText(); + }; + + /** + * Verify success banner message html has correct title text. + * + * @param {String} displayName XBlock display name + */ + verifyConfirmationFeedbackTitleText = function(displayName) { + expect(getMovedAlertNotification().find('.title').html() + .trim()) + .toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.', + { + displayName: displayName + }) + ); + }; + + /** + * Verify undo success banner message html has correct title text. + * + * @param {String} displayName XBlock display name + */ + verifyUndoConfirmationFeedbackTitleText = function(displayName) { + expect(getMovedAlertNotification().find('.title').html()).toEqual( + StringUtils.interpolate( + 'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.', + { + sourceDisplayName: displayName } + ) + ); + }; + + /** + * Verify success banner message html has correct redirect link text. + */ + verifyConfirmationFeedbackRedirectLinkText = function() { + expect(getMovedAlertNotification().find('.nav-actions .action-secondary').html()) + .toEqual('Take me to the new location'); + }; + + /** + * Verify success banner message html has correct undo move text. + */ + verifyConfirmationFeedbackUndoMoveActionText = function() { + expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move'); + }; + + /** + * Get sent requests. + * + * @returns {Object} + */ + getSentRequests = function() { + return jasmine.Ajax.requests.filter(function(request) { + return request.readyState > 0; + }); + }; - outline.child_info = childInfo; // eslint-disable-line no-param-reassign - _.each(_.range(xblocks), function(xblockIndex) { - childInfo.children.push( - createChildXBlockInfo(category, outlineOptions, xblockIndex) - ); - }); - return outline; - }; - - /** - * Create course outline. - * - * @param {Object} outlineOptions options according to which outline was created - * @returns {Object} - */ - createCourseOutline = function(outlineOptions) { - var courseXBlockInfo = { - category: 'course', - display_name: 'Demo Course', - id: 'COURSE_ID_101' - }; - return createXBlockInfo('section', outlineOptions, courseXBlockInfo); - }; - - /** - * Render breadcrumb and XBlock list view. - * - * @param {any} courseOutlineInfo course outline info - * @param {any} ancestorInfo ancestors info - */ - renderViews = function(courseOutlineInfo, ancestorInfo) { - var ancestorInfo = ancestorInfo || {ancestors: []}; // eslint-disable-line no-redeclare - modal.renderViews(courseOutlineInfo, ancestorInfo); - }; - - /** - * Extract displayed XBlock list info. - * - * @returns {Object} - */ - getDisplayedInfo = function() { - var viewEl = modal.moveXBlockListView.$el; - return { - categoryText: viewEl.find('.category-text').text().trim(), - currentLocationText: viewEl.find('.current-location').text().trim(), - xblockCount: viewEl.find('.xblock-item').length, - xblockDisplayNames: viewEl.find('.xblock-item .xblock-displayname').map( - function() { return $(this).text().trim(); } - ).get(), - forwardButtonSRTexts: viewEl.find('.xblock-item .forward-sr-text').map( - function() { return $(this).text().trim(); } - ).get(), - forwardButtonCount: viewEl.find('.fa-arrow-right.forward-sr-icon').length - }; - }; + it('renders views with correct information', function() { + var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1}, + outline = createCourseOutline(outlineOptions); - /** - * Verify displayed XBlock list info. - * - * @param {String} category XBlock category - * @param {Integer} expectedXBlocksCount number of XBlock childs displayed - * @param {Boolean} hasCurrentLocation do we need to check current location - */ - verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) { - var displayedInfo = getDisplayedInfo(); - expect(displayedInfo.categoryText).toEqual(modal.moveXBlockListView.categoriesText[category] + ':'); - expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount); - expect(displayedInfo.xblockDisplayNames).toEqual( - _.map(_.range(expectedXBlocksCount), function(xblockIndex) { - return category + '_display_name_' + xblockIndex; - }) - ); - if (category === 'component') { - if (hasCurrentLocation) { - expect(displayedInfo.currentLocationText).toEqual('(Currently selected)'); - } - } else { - if (hasCurrentLocation) { - expect(displayedInfo.currentLocationText).toEqual('(Current location)'); - } - expect(displayedInfo.forwardButtonSRTexts).toEqual( - _.map(_.range(expectedXBlocksCount), function() { - return 'View child items'; - }) - ); - expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount); - } - }; + renderViews(outline, xblockAncestorInfo); + verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); + verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true); + }); - /** - * Verify rendered breadcrumb info. - * - * @param {any} category XBlock category - * @param {any} xblockIndex XBlock index - */ - verifyBreadcrumbViewInfo = function(category, xblockIndex) { - var displayedBreadcrumbs = modal.moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map( - function() { return $(this).text().trim(); } - ).get(), - categories = _.keys(parentChildMap).concat(['component']), - visitedCategories = categories.slice(0, _.indexOf(categories, category)); - - expect(displayedBreadcrumbs).toEqual( - _.map(visitedCategories, function(visitedCategory) { - return visitedCategory === 'course' ? - 'Course Outline' : visitedCategory + '_display_name_' + xblockIndex; - }) - ); - }; + it('shows correct behavior on breadcrumb navigation', function() { + var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1}); - /** - * Click forward button in the list of displayed XBlocks. - * - * @param {any} buttonIndex forward button index - */ - clickForwardButton = function(buttonIndex) { - buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign - modal.moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click(); - }; + renderViews(outline); + _.each(_.range(3), function() { + clickForwardButton(); + }); - /** - * Click on last clickable breadcrumb button. - */ - clickBreadcrumbButton = function() { + _.each(['component', 'unit', 'subsection', 'section'], function(category) { + verifyListViewInfo(category, 1); + if (category !== 'section') { modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); - }; - - /** - * Returns the parent or child category of current XBlock. - * - * @param {String} direction `forward` or `backward` - * @param {String} category XBlock category - * @returns {String} - */ - nextCategory = function(direction, category) { - return direction === 'forward' ? parentChildMap[category] : _.invert(parentChildMap)[category]; - }; - - /** - * Verify renderd info of breadcrumbs and XBlock list. - * - * @param {Object} outlineOptions options according to which outline was created - * @param {String} category XBlock category - * @param {Integer} buttonIndex forward button index - * @param {String} direction `forward` or `backward` - * @param {String} hasCurrentLocation do we need to check current location - * @returns - */ - verifyXBlockInfo = function(outlineOptions, category, buttonIndex, direction, hasCurrentLocation) { - var expectedXBlocksCount = outlineOptions[category]; - - verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation); - verifyBreadcrumbViewInfo(category, buttonIndex); - verifyMoveEnabled(category, hasCurrentLocation); - - if (direction === 'forward') { - if (category === 'component') { - return; - } - clickForwardButton(buttonIndex); - } else if (direction === 'backward') { - if (category === 'section') { - return; - } - clickBreadcrumbButton(); - } - category = nextCategory(direction, category); // eslint-disable-line no-param-reassign + } + }); + }); - verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation); - }; + it('shows the correct current location', function() { + var outlineOptions = {section: 2, subsection: 2, unit: 2, component: 2}, + outline = createCourseOutline(outlineOptions); + renderViews(outline, xblockAncestorInfo); + verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); + // click the outline breadcrumb to render sections + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); + verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false); + }); - /** - * Verify move button is enabled. - * - * @param {String} category XBlock category - * @param {String} hasCurrentLocation do we need to check current location - */ - verifyMoveEnabled = function(category, hasCurrentLocation) { - var isMoveEnabled = !modal.$el.find('.modal-actions .action-move').hasClass('is-disabled'); - if (category === 'component' && !hasCurrentLocation) { - expect(isMoveEnabled).toBeTruthy(); - } else { - expect(isMoveEnabled).toBeFalsy(); - } - }; + it('shows correct message when parent has no children', function() { + var outlinesInfo = [ + { + outline: createCourseOutline({}), + message: 'This course has no sections' + }, + { + outline: createCourseOutline({section: 1}), + message: 'This section has no subsections', + forwardClicks: 1 + }, + { + outline: createCourseOutline({section: 1, subsection: 1}), + message: 'This subsection has no units', + forwardClicks: 2 + }, + { + outline: createCourseOutline({section: 1, subsection: 1, unit: 1}), + message: 'This unit has no components', + forwardClicks: 3 + } + ]; + + _.each(outlinesInfo, function(info) { + renderViews(info.outline); + _.each(_.range(info.forwardClicks), function() { + clickForwardButton(); + }); + expect(modal.moveXBlockListView.$el.find('.xblock-no-child-message').text().trim()) + .toEqual(info.message); + modal.moveXBlockListView.undelegateEvents(); + modal.moveXBlockBreadcrumbView.undelegateEvents(); + }); + }); - /** - * Verify notification status. - * - * @param {Object} requests requests object - * @param {Object} notificationSpy notification spy - * @param {String} notificationText notification text to be verified - * @param {Integer} sourceIndex source index of the xblock - */ - verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) { - var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare - ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText); - AjaxHelpers.respondWithJson(requests, { - move_source_locator: sourceLocator, - parent_locator: sourceParentLocator, - target_index: sourceIndex - }); - ViewHelpers.verifyNotificationHidden(notificationSpy); - }; + describe('Move button', function() { + it('is disabled when navigating to same parent', function() { + // select a target parent as the same as source parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(0); + }); + verifyMoveEnabled('component', true); + }); - /** - * Get move alert confirmation message HTML - */ - getMovedAlertNotification = function() { - return $('#page-alert'); - }; + it('is enabled when navigating to different parent', function() { + // select a target parent as the different as source parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + verifyMoveEnabled('component', false); + }); - /** - * Send move xblock request. - * - * @param {Object} requests requests object - * @param {Object} xblockLocator Xblock id location - * @param {Integer} targetIndex target index of the xblock - * @param {Integer} sourceIndex source index of the xblock - */ - sendMoveXBlockRequest = function(requests, xblockLocator, targetIndex, sourceIndex) { - var responseData, - expectedData, - sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare - - responseData = expectedData = { - move_source_locator: xblockLocator, - parent_locator: modal.targetParentXBlockInfo.id - }; - - if (targetIndex !== undefined) { - expectedData = _.extend(expectedData, { - targetIndex: targetIndex - }); - } + it('verify move state while navigating', function() { + renderViews(courseOutline, xblockAncestorInfo); + verifyXBlockInfo(courseOutlineOptions, 'section', 0, 'forward', true); + // start from course outline again + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); + verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false); + }); - // verify content of request - AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData); + it('is disbabled when navigating to same source xblock', function() { + var outline, + libraryContentXBlockInfo = { + category: 'library_content', + display_name: 'Library Content', + has_children: true, + id: 'LIBRARY_CONTENT_ID' + }, + outlineOptions = {library_content: 1, component: 1}; + + // make above xblock source xblock. + modal.sourceXBlockInfo = libraryContentXBlockInfo; + outline = createXBlockInfo('component', outlineOptions, libraryContentXBlockInfo); + renderViews(outline); + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + + // select a target parent + clickForwardButton(0); + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + }); - // send the response - AjaxHelpers.respondWithJson(requests, _.extend(responseData, { - source_index: sourceIndex - })); - }; + it('is disabled when navigating inside source content experiment', function() { + var outline, + splitTestXBlockInfo = { + category: 'split_test', + display_name: 'Content Experiment', + has_children: true, + id: 'SPLIT_TEST_ID' + }, + outlineOptions = {split_test: 1, unit: 2, component: 1}; + + // make above xblock source xblock. + modal.sourceXBlockInfo = splitTestXBlockInfo; + outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo); + renderViews(outline); + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + + // navigate to groups level + clickForwardButton(0); + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + + // navigate to component level inside a group + clickForwardButton(0); + + // move should be disabled because we are navigating inside source xblock + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + }); - /** - * Move xblock with success. - * - * @param {Object} requests requests object - */ - moveXBlockWithSuccess = function(requests) { - // select a target item and click - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(1); - }); - modal.$el.find('.modal-actions .action-move').click(); - sendMoveXBlockRequest(requests, sourceLocator); - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/' + sourceParentLocator); - AjaxHelpers.respondWithJson(requests, sourceParentXBlockInfo); - expect(getMovedAlertNotification().html().length).not.toEqual(0); - verifyConfirmationFeedbackTitleText(sourceDisplayName); - verifyConfirmationFeedbackRedirectLinkText(); - verifyConfirmationFeedbackUndoMoveActionText(); - }; + it('is disabled when navigating to any content experiment groups', function() { + var outline, + splitTestXBlockInfo = { + category: 'split_test', + display_name: 'Content Experiment', + has_children: true, + id: 'SPLIT_TEST_ID' + }, + outlineOptions = {split_test: 1, unit: 2, component: 1}; + + // group level should be disabled but component level inside groups should be movable + outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo); + renderViews(outline); + + // move is disabled on groups level + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + + // navigate to component level inside a group + clickForwardButton(1); + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); + }); - /** - * Verify success banner message html has correct title text. - * - * @param {String} displayName XBlock display name - */ - verifyConfirmationFeedbackTitleText = function(displayName) { - expect(getMovedAlertNotification().find('.title').html() - .trim()) - .toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.', - { - displayName: displayName - }) - ); + it('is enabled when navigating to any parentable component', function() { + var parentableXBlockInfo = { + category: 'vertical', + display_name: 'Parentable Component', + has_children: true, + id: 'PARENTABLE_ID' }; + renderViews(parentableXBlockInfo); - /** - * Verify undo success banner message html has correct title text. - * - * @param {String} displayName XBlock display name - */ - verifyUndoConfirmationFeedbackTitleText = function(displayName) { - expect(getMovedAlertNotification().find('.title').html()).toEqual( - StringUtils.interpolate( - 'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.', - { - sourceDisplayName: displayName - } - ) - ); - }; + // move is enabled on parentable xblocks. + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); + }); - /** - * Verify success banner message html has correct redirect link text. - */ - verifyConfirmationFeedbackRedirectLinkText = function() { - expect(getMovedAlertNotification().find('.nav-actions .action-secondary').html()) - .toEqual('Take me to the new location'); - }; + it('is enabled when moving a component inside a parentable component', function() { + // create a source parent with has_childern set true + modal.sourceParentXBlockInfo = new XBlockInfo({ + category: 'conditional', + display_name: 'Parentable Component', + has_children: true, + id: 'PARENTABLE_ID' + }); + // navigate and verify move button is enabled + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(0); + }); - /** - * Verify success banner message html has correct undo move text. - */ - verifyConfirmationFeedbackUndoMoveActionText = function() { - expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move'); - }; + // move is enabled when moving a component. + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); + }); - /** - * Get sent requests. - * - * @returns {Object} - */ - getSentRequests = function() { - return jasmine.Ajax.requests.filter(function(request) { - return request.readyState > 0; - }); + it('is disabled when navigating to any non-parentable component', function() { + var nonParentableXBlockInfo = { + category: 'html', + display_name: 'Non Parentable Component', + has_children: false, + id: 'NON_PARENTABLE_ID' }; + renderViews(nonParentableXBlockInfo); - it('renders views with correct information', function() { - var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1}, - outline = createCourseOutline(outlineOptions); - - renderViews(outline, xblockAncestorInfo); - verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); - verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true); - }); + // move is disabled on non-parent xblocks. + expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); + }); + }); - it('shows correct behavior on breadcrumb navigation', function() { - var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1}); + describe('Move an xblock', function() { + it('can not move in a disabled state', function() { + verifyMoveEnabled(false); + modal.$el.find('.modal-actions .action-move').click(); + expect(getMovedAlertNotification().html().length).toEqual(0); + expect(getSentRequests().length).toEqual(0); + }); - renderViews(outline); - _.each(_.range(3), function() { - clickForwardButton(); - }); + it('move an xblock when move button is clicked', function() { + var requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + }); - _.each(['component', 'unit', 'subsection', 'section'], function(category) { - verifyListViewInfo(category, 1); - if (category !== 'section') { - modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); - } - }); - }); + it('do not move an xblock when cancel button is clicked', function() { + modal.$el.find('.modal-actions .action-cancel').click(); + expect(getMovedAlertNotification().html().length).toEqual(0); + expect(getSentRequests().length).toEqual(0); + }); - it('shows the correct current location', function() { - var outlineOptions = {section: 2, subsection: 2, unit: 2, component: 2}, - outline = createCourseOutline(outlineOptions); - renderViews(outline, xblockAncestorInfo); - verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); - // click the outline breadcrumb to render sections - modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); - verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false); + it('undo move an xblock when undo move link is clicked', function() { + var sourceIndex = 0, + requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + getMovedAlertNotification().find('.action-save').click(); + AjaxHelpers.respondWithJson(requests, { + move_source_locator: sourceLocator, + parent_locator: sourceParentLocator, + target_index: sourceIndex }); + verifyUndoConfirmationFeedbackTitleText(sourceDisplayName); + }); + }); - it('shows correct message when parent has no children', function() { - var outlinesInfo = [ - { - outline: createCourseOutline({}), - message: 'This course has no sections' - }, - { - outline: createCourseOutline({section: 1}), - message: 'This section has no subsections', - forwardClicks: 1 - }, - { - outline: createCourseOutline({section: 1, subsection: 1}), - message: 'This subsection has no units', - forwardClicks: 2 - }, - { - outline: createCourseOutline({section: 1, subsection: 1, unit: 1}), - message: 'This unit has no components', - forwardClicks: 3 - } - ]; - - _.each(outlinesInfo, function(info) { - renderViews(info.outline); - _.each(_.range(info.forwardClicks), function() { - clickForwardButton(); - }); - expect(modal.moveXBlockListView.$el.find('.xblock-no-child-message').text().trim()) - .toEqual(info.message); - modal.moveXBlockListView.undelegateEvents(); - modal.moveXBlockBreadcrumbView.undelegateEvents(); - }); + describe('shows a notification', function() { + it('mini operation message when moving an xblock', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy(); + // navigate to a target parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); }); + modal.$el.find('.modal-actions .action-move').click(); + verifyNotificationStatus(requests, notificationSpy, 'Moving'); + }); - describe('Move button', function() { - it('is disabled when navigating to same parent', function() { - // select a target parent as the same as source parent and click - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(0); - }); - verifyMoveEnabled('component', true); - }); - - it('is enabled when navigating to different parent', function() { - // select a target parent as the different as source parent and click - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(1); - }); - verifyMoveEnabled('component', false); - }); - - it('verify move state while navigating', function() { - renderViews(courseOutline, xblockAncestorInfo); - verifyXBlockInfo(courseOutlineOptions, 'section', 0, 'forward', true); - // start from course outline again - modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); - verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false); - }); - - it('is disbabled when navigating to same source xblock', function() { - var outline, - libraryContentXBlockInfo = { - category: 'library_content', - display_name: 'Library Content', - has_children: true, - id: 'LIBRARY_CONTENT_ID' - }, - outlineOptions = {library_content: 1, component: 1}; - - // make above xblock source xblock. - modal.sourceXBlockInfo = libraryContentXBlockInfo; - outline = createXBlockInfo('component', outlineOptions, libraryContentXBlockInfo); - renderViews(outline); - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - - // select a target parent - clickForwardButton(0); - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - }); - - it('is disabled when navigating inside source content experiment', function() { - var outline, - splitTestXBlockInfo = { - category: 'split_test', - display_name: 'Content Experiment', - has_children: true, - id: 'SPLIT_TEST_ID' - }, - outlineOptions = {split_test: 1, unit: 2, component: 1}; - - // make above xblock source xblock. - modal.sourceXBlockInfo = splitTestXBlockInfo; - outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo); - renderViews(outline); - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - - // navigate to groups level - clickForwardButton(0); - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - - // navigate to component level inside a group - clickForwardButton(0); - - // move should be disabled because we are navigating inside source xblock - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - }); - - it('is disabled when navigating to any content experiment groups', function() { - var outline, - splitTestXBlockInfo = { - category: 'split_test', - display_name: 'Content Experiment', - has_children: true, - id: 'SPLIT_TEST_ID' - }, - outlineOptions = {split_test: 1, unit: 2, component: 1}; - - // group level should be disabled but component level inside groups should be movable - outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo); - renderViews(outline); - - // move is disabled on groups level - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - - // navigate to component level inside a group - clickForwardButton(1); - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); - }); - - it('is enabled when navigating to any parentable component', function() { - var parentableXBlockInfo = { - category: 'vertical', - display_name: 'Parentable Component', - has_children: true, - id: 'PARENTABLE_ID' - }; - renderViews(parentableXBlockInfo); - - // move is enabled on parentable xblocks. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); - }); - - it('is enabled when moving a component inside a parentable component', function() { - // create a source parent with has_childern set true - modal.sourceParentXBlockInfo = new XBlockInfo({ - category: 'conditional', - display_name: 'Parentable Component', - has_children: true, - id: 'PARENTABLE_ID' - }); - // navigate and verify move button is enabled - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(0); - }); - - // move is enabled when moving a component. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); - }); - - it('is disabled when navigating to any non-parentable component', function() { - var nonParentableXBlockInfo = { - category: 'html', - display_name: 'Non Parentable Component', - has_children: false, - id: 'NON_PARENTABLE_ID' - }; - renderViews(nonParentableXBlockInfo); - - // move is disabled on non-parent xblocks. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - }); - }); + it('mini operation message when undo moving an xblock', function() { + var notificationSpy, + requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + notificationSpy = ViewHelpers.createNotificationSpy(); + getMovedAlertNotification().find('.action-save').click(); + verifyNotificationStatus(requests, notificationSpy, 'Undo moving'); + }); - describe('Move an xblock', function() { - it('can not move in a disabled state', function() { - verifyMoveEnabled(false); - modal.$el.find('.modal-actions .action-move').click(); - expect(getMovedAlertNotification().html().length).toEqual(0); - expect(getSentRequests().length).toEqual(0); - }); - - it('move an xblock when move button is clicked', function() { - var requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - }); - - it('do not move an xblock when cancel button is clicked', function() { - modal.$el.find('.modal-actions .action-cancel').click(); - expect(getMovedAlertNotification().html().length).toEqual(0); - expect(getSentRequests().length).toEqual(0); - }); - - it('undo move an xblock when undo move link is clicked', function() { - var sourceIndex = 0, - requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - getMovedAlertNotification().find('.action-save').click(); - AjaxHelpers.respondWithJson(requests, { - move_source_locator: sourceLocator, - parent_locator: sourceParentLocator, - target_index: sourceIndex - }); - verifyUndoConfirmationFeedbackTitleText(sourceDisplayName); - }); + it('error message when move request fails', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy('Error'); + // select a target item and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); }); + modal.$el.find('.modal-actions .action-move').click(); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); + }); - describe('shows a notification', function() { - it('mini operation message when moving an xblock', function() { - var requests = AjaxHelpers.requests(this), - notificationSpy = ViewHelpers.createNotificationSpy(); - // navigate to a target parent and click - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(1); - }); - modal.$el.find('.modal-actions .action-move').click(); - verifyNotificationStatus(requests, notificationSpy, 'Moving'); - }); - - it('mini operation message when undo moving an xblock', function() { - var notificationSpy, - requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - notificationSpy = ViewHelpers.createNotificationSpy(); - getMovedAlertNotification().find('.action-save').click(); - verifyNotificationStatus(requests, notificationSpy, 'Undo moving'); - }); - - it('error message when move request fails', function() { - var requests = AjaxHelpers.requests(this), - notificationSpy = ViewHelpers.createNotificationSpy('Error'); - // select a target item and click - renderViews(courseOutline); - _.each(_.range(3), function() { - clickForwardButton(1); - }); - modal.$el.find('.modal-actions .action-move').click(); - AjaxHelpers.respondWithError(requests); - ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); - }); - - it('error message when undo move request fails', function() { - var requests = AjaxHelpers.requests(this), - notificationSpy = ViewHelpers.createNotificationSpy('Error'); - moveXBlockWithSuccess(requests); - getMovedAlertNotification().find('.action-save').click(); - AjaxHelpers.respondWithError(requests); - ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); - }); - }); + it('error message when undo move request fails', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy('Error'); + moveXBlockWithSuccess(requests); + getMovedAlertNotification().find('.action-save').click(); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); }); }); +}); diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index a27b66bb4916..3beca50b7973 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -1,839 +1,855 @@ -define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', 'js/spec_helpers/edit_helpers', - 'js/views/pages/container', 'js/views/pages/paged_container', 'js/models/xblock_info', - 'js/collections/component_template', 'jquery.simulate'], - function($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, - XBlockInfo, ComponentTemplates) { - 'use strict'; - - function parameterized_suite(label, globalPageOptions) { - describe(label + ' ContainerPage', function() { - var getContainerPage, renderContainerPage, handleContainerPageRefresh, expectComponents, - respondWithHtml, model, containerPage, requests, initialDisplayName, - mockContainerPage = readFixtures('mock/mock-container-page.underscore'), - mockContainerXBlockHtml = readFixtures(globalPageOptions.initial), - mockXBlockHtml = readFixtures(globalPageOptions.addResponse), - mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'), - mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'), - mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'), - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'), - mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'), - PageClass = globalPageOptions.page, - pagedSpecificTests = globalPageOptions.pagedSpecificTests, - hasVisibilityEditor = globalPageOptions.hasVisibilityEditor, - hasMoveModal = globalPageOptions.hasMoveModal; - - beforeEach(function() { - var newDisplayName = 'New Display Name'; - - EditHelpers.installEditTemplates(); - TemplateHelpers.installTemplate('xblock-string-field-editor'); - TemplateHelpers.installTemplate('container-message'); - appendSetFixtures(mockContainerPage); - - EditHelpers.installMockXBlock({ - data: '

Some HTML

', - metadata: { - display_name: newDisplayName - } - }); - - initialDisplayName = 'Test Container'; - - model = new XBlockInfo({ - id: 'locator-container', - display_name: initialDisplayName, - category: 'vertical' - }); - }); +'use strict'; +'use strict'; + +import $ from 'jquery'; +import _ from 'underscore'; +import str from 'underscore.string'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import TemplateHelpers from 'common/js/spec_helpers/template_helpers'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import ContainerPage from 'js/views/pages/container'; +import PagedContainerPage from 'js/views/pages/paged_container'; +import XBlockInfo from 'js/models/xblock_info'; +import ComponentTemplates from 'js/collections/component_template'; +import Course from 'js/models/course'; +import 'jquery.simulate'; + +function parameterized_suite(label, globalPageOptions) { + describe(label + ' ContainerPage', function() { + var getContainerPage, renderContainerPage, handleContainerPageRefresh, expectComponents, + respondWithHtml, model, containerPage, requests, initialDisplayName, + mockContainerPage = readFixtures('templates/mock/mock-container-page.underscore'), + mockContainerXBlockHtml = readFixtures(globalPageOptions.initial), + mockXBlockHtml = readFixtures(globalPageOptions.addResponse), + mockBadContainerXBlockHtml = readFixtures('templates/mock/mock-bad-javascript-container-xblock.underscore'), + mockBadXBlockContainerXBlockHtml = readFixtures('templates/mock/mock-bad-xblock-container-xblock.underscore'), + mockUpdatedContainerXBlockHtml = readFixtures('templates/mock/mock-updated-container-xblock.underscore'), + mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore'), + mockXBlockVisibilityEditorHtml = readFixtures('templates/mock/mock-xblock-visibility-editor.underscore'), + PageClass = globalPageOptions.page, + pagedSpecificTests = globalPageOptions.pagedSpecificTests, + hasVisibilityEditor = globalPageOptions.hasVisibilityEditor, + hasMoveModal = globalPageOptions.hasMoveModal; + + beforeEach(function() { + var newDisplayName = 'New Display Name'; + + EditHelpers.installEditTemplates(); + TemplateHelpers.installTemplate('xblock-string-field-editor'); + TemplateHelpers.installTemplate('container-message'); + appendSetFixtures(mockContainerPage); + + EditHelpers.installMockXBlock({ + data: '

Some HTML

', + metadata: { + display_name: newDisplayName + } + }); - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - if (containerPage !== undefined) { - containerPage.remove(); - } - }); + initialDisplayName = 'Test Container'; - respondWithHtml = function(html) { - AjaxHelpers.respondWithJson( - requests, - {html: html, resources: []} - ); - }; + model = new XBlockInfo({ + id: 'locator-container', + display_name: initialDisplayName, + category: 'vertical' + }); + window.course = new Course({ + id: "5", + name: "Course Name", + url_name: "course_name", + org: "course_org", + num: "course_num", + revision: "course_rev" + }); + }); - getContainerPage = function(options, componentTemplates) { - var default_options = { - model: model, - templates: componentTemplates === undefined ? - EditHelpers.mockComponentTemplates : componentTemplates, - el: $('#content') - }; - return new PageClass(_.extend(options || {}, globalPageOptions, default_options)); - }; + afterEach(function() { + EditHelpers.uninstallMockXBlock(); + if (containerPage !== undefined) { + containerPage.remove(); + } + delete window.course; + }); + + respondWithHtml = function(html) { + AjaxHelpers.respondWithJson( + requests, + {html: html, resources: []} + ); + }; + + getContainerPage = function(options, componentTemplates) { + var default_options = { + model: model, + templates: componentTemplates === undefined ? + EditHelpers.mockComponentTemplates : componentTemplates, + el: $('#content') + }; + return new PageClass(_.extend(options || {}, globalPageOptions, default_options)); + }; + + renderContainerPage = function(test, html, options, componentTemplates) { + requests = AjaxHelpers.requests(test); + containerPage = getContainerPage(options, componentTemplates); + containerPage.render(); + respondWithHtml(html); + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.respondWithJson(requests, options || {}); + }; + + handleContainerPageRefresh = function(requests) { + var request = AjaxHelpers.currentRequest(requests); + expect(str.startsWith(request.url, + '/xblock/locator-container/container_preview')).toBeTruthy(); + AjaxHelpers.respondWithJson(requests, { + html: mockUpdatedContainerXBlockHtml, + resources: [] + }); + }; + + expectComponents = function(container, locators) { + // verify expected components (in expected order) by their locators + var components = $(container).find('.studio-xblock-wrapper'); + expect(components.length).toBe(locators.length); + _.each(locators, function(locator, locator_index) { + expect($(components[locator_index]).data('locator')).toBe(locator); + }); + }; - renderContainerPage = function(test, html, options, componentTemplates) { - requests = AjaxHelpers.requests(test); - containerPage = getContainerPage(options, componentTemplates); - containerPage.render(); - respondWithHtml(html); - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); - AjaxHelpers.respondWithJson(requests, options || {}); - }; + describe('Initial display', function() { + it('can render itself', function() { + renderContainerPage(this, mockContainerXBlockHtml); + expect(containerPage.$('.xblock-header').length).toBe(9); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + }); - handleContainerPageRefresh = function(requests) { - var request = AjaxHelpers.currentRequest(requests); - expect(str.startsWith(request.url, - '/xblock/locator-container/container_preview')).toBeTruthy(); - AjaxHelpers.respondWithJson(requests, { - html: mockUpdatedContainerXBlockHtml, - resources: [] - }); - }; + it('shows a loading indicator', function() { + requests = AjaxHelpers.requests(this); + containerPage = getContainerPage(); + containerPage.render(); + expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden'); + respondWithHtml(mockContainerXBlockHtml); + expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); + }); - expectComponents = function(container, locators) { - // verify expected components (in expected order) by their locators - var components = $(container).find('.studio-xblock-wrapper'); - expect(components.length).toBe(locators.length); - _.each(locators, function(locator, locator_index) { - expect($(components[locator_index]).data('locator')).toBe(locator); - }); - }; + it('can show an xblock with broken JavaScript', function() { + renderContainerPage(this, mockBadContainerXBlockHtml); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); + }); - describe('Initial display', function() { - it('can render itself', function() { - renderContainerPage(this, mockContainerXBlockHtml); - expect(containerPage.$('.xblock-header').length).toBe(9); - expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); - }); + it('can show an xblock with an invalid XBlock', function() { + renderContainerPage(this, mockBadXBlockContainerXBlockHtml); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); + }); - it('shows a loading indicator', function() { - requests = AjaxHelpers.requests(this); - containerPage = getContainerPage(); - containerPage.render(); - expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden'); - respondWithHtml(mockContainerXBlockHtml); - expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); - }); + it('inline edits the display name when performing a new action', function() { + renderContainerPage(this, mockContainerXBlockHtml, { + action: 'new' + }); + expect(containerPage.$('.xblock-header').length).toBe(9); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + expect(containerPage.$('.xblock-field-input')).not.toHaveClass('is-hidden'); + }); + }); - it('can show an xblock with broken JavaScript', function() { - renderContainerPage(this, mockBadContainerXBlockHtml); - expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); - expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); - }); + describe('Editing the container', function() { + var updatedDisplayName = 'Updated Test Container', + getDisplayNameWrapper; - it('can show an xblock with an invalid XBlock', function() { - renderContainerPage(this, mockBadXBlockContainerXBlockHtml); - expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); - expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); - }); + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); - it('inline edits the display name when performing a new action', function() { - renderContainerPage(this, mockContainerXBlockHtml, { - action: 'new' - }); - expect(containerPage.$('.xblock-header').length).toBe(9); - expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); - expect(containerPage.$('.xblock-field-input')).not.toHaveClass('is-hidden'); - }); + getDisplayNameWrapper = function() { + return containerPage.$('.wrapper-xblock-field'); + }; + + it('can edit itself', function() { + var editButtons, displayNameElement, request; + renderContainerPage(this, mockContainerXBlockHtml); + displayNameElement = containerPage.$('.page-header-title'); + + // Click the root edit button + editButtons = containerPage.$('.nav-actions .edit-button'); + editButtons.first().click(); + + // Expect a request to be made to show the studio view for the container + request = AjaxHelpers.currentRequest(requests); + expect(str.startsWith(request.url, '/xblock/locator-container/studio_view')).toBeTruthy(); + AjaxHelpers.respondWithJson(requests, { + html: mockContainerXBlockHtml, + resources: [] }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); - describe('Editing the container', function() { - var updatedDisplayName = 'Updated Test Container', - getDisplayNameWrapper; - - afterEach(function() { - EditHelpers.cancelModalIfShowing(); - }); - - getDisplayNameWrapper = function() { - return containerPage.$('.wrapper-xblock-field'); - }; + // Expect the correct title to be shown + expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); - it('can edit itself', function() { - var editButtons, displayNameElement, request; - renderContainerPage(this, mockContainerXBlockHtml); - displayNameElement = containerPage.$('.page-header-title'); - - // Click the root edit button - editButtons = containerPage.$('.nav-actions .edit-button'); - editButtons.first().click(); - - // Expect a request to be made to show the studio view for the container - request = AjaxHelpers.currentRequest(requests); - expect(str.startsWith(request.url, '/xblock/locator-container/studio_view')).toBeTruthy(); - AjaxHelpers.respondWithJson(requests, { - html: mockContainerXBlockHtml, - resources: [] - }); - expect(EditHelpers.isShowingModal()).toBeTruthy(); + // Press the save button and respond with a success message to the save + EditHelpers.pressModalButton('.action-save'); + AjaxHelpers.respondWithJson(requests, { }); + expect(EditHelpers.isShowingModal()).toBeFalsy(); - // Expect the correct title to be shown - expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); + // Expect the last request be to refresh the container page + handleContainerPageRefresh(requests); - // Press the save button and respond with a success message to the save - EditHelpers.pressModalButton('.action-save'); - AjaxHelpers.respondWithJson(requests, { }); - expect(EditHelpers.isShowingModal()).toBeFalsy(); + // Respond to the subsequent xblock info fetch request. + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); - // Expect the last request be to refresh the container page - handleContainerPageRefresh(requests); + // Expect the title to have been updated + expect(displayNameElement.text().trim()).toBe(updatedDisplayName); + }); - // Respond to the subsequent xblock info fetch request. - AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + it('can inline edit the display name', function() { + var displayNameInput, displayNameWrapper; + renderContainerPage(this, mockContainerXBlockHtml); + displayNameWrapper = getDisplayNameWrapper(); + displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); + displayNameInput.change(); + // This is the response for the change operation. + AjaxHelpers.respondWithJson(requests, { }); + // This is the response for the subsequent fetch operation. + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); + expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); + }); + }); - // Expect the title to have been updated - expect(displayNameElement.text().trim()).toBe(updatedDisplayName); - }); + describe('Editing an xblock', function() { + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); - it('can inline edit the display name', function() { - var displayNameInput, displayNameWrapper; - renderContainerPage(this, mockContainerXBlockHtml); - displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); - displayNameInput.change(); - // This is the response for the change operation. - AjaxHelpers.respondWithJson(requests, { }); - // This is the response for the subsequent fetch operation. - AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); - EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); - expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); - }); + it('can show an edit modal for a child xblock', function() { + var editButtons, request; + renderContainerPage(this, mockContainerXBlockHtml); + editButtons = containerPage.$('.wrapper-xblock .edit-button'); + // The container should have rendered six mock xblocks + expect(editButtons.length).toBe(6); + editButtons[0].click(); + // Make sure that the correct xblock is requested to be edited + request = AjaxHelpers.currentRequest(requests); + expect(str.startsWith(request.url, '/xblock/locator-component-A1/studio_view')).toBeTruthy(); + AjaxHelpers.respondWithJson(requests, { + html: mockXBlockEditorHtml, + resources: [] }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + }); - describe('Editing an xblock', function() { - afterEach(function() { - EditHelpers.cancelModalIfShowing(); - }); + it('can show an edit modal for a child xblock with broken JavaScript', function() { + var editButtons; + renderContainerPage(this, mockBadContainerXBlockHtml); + editButtons = containerPage.$('.wrapper-xblock .edit-button'); + editButtons[0].click(); + AjaxHelpers.respondWithJson(requests, { + html: mockXBlockEditorHtml, + resources: [] + }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + }); - it('can show an edit modal for a child xblock', function() { - var editButtons, request; - renderContainerPage(this, mockContainerXBlockHtml); - editButtons = containerPage.$('.wrapper-xblock .edit-button'); - // The container should have rendered six mock xblocks - expect(editButtons.length).toBe(6); - editButtons[0].click(); - // Make sure that the correct xblock is requested to be edited - request = AjaxHelpers.currentRequest(requests); - expect(str.startsWith(request.url, '/xblock/locator-component-A1/studio_view')).toBeTruthy(); - AjaxHelpers.respondWithJson(requests, { - html: mockXBlockEditorHtml, - resources: [] - }); - expect(EditHelpers.isShowingModal()).toBeTruthy(); + it('can show a visibility modal for a child xblock if supported for the page', function() { + var accessButtons, request; + renderContainerPage(this, mockContainerXBlockHtml); + accessButtons = containerPage.$('.wrapper-xblock .access-button'); + if (hasVisibilityEditor) { + expect(accessButtons.length).toBe(6); + accessButtons[0].click(); + request = AjaxHelpers.currentRequest(requests); + expect(str.startsWith(request.url, '/xblock/locator-component-A1/visibility_view')) + .toBeTruthy(); + AjaxHelpers.respondWithJson(requests, { + html: mockXBlockVisibilityEditorHtml, + resources: [] }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + } else { + expect(accessButtons.length).toBe(0); + } + }); - it('can show an edit modal for a child xblock with broken JavaScript', function() { - var editButtons; - renderContainerPage(this, mockBadContainerXBlockHtml); - editButtons = containerPage.$('.wrapper-xblock .edit-button'); - editButtons[0].click(); - AjaxHelpers.respondWithJson(requests, { - html: mockXBlockEditorHtml, - resources: [] - }); - expect(EditHelpers.isShowingModal()).toBeTruthy(); - }); + it('can show a move modal for a child xblock', function() { + var moveButtons; + renderContainerPage(this, mockContainerXBlockHtml); + moveButtons = containerPage.$('.wrapper-xblock .move-button'); + if (hasMoveModal) { + expect(moveButtons.length).toBe(6); + moveButtons[0].click(); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + } else { + expect(moveButtons.length).toBe(0); + } + }); + }); - it('can show a visibility modal for a child xblock if supported for the page', function() { - var accessButtons, request; - renderContainerPage(this, mockContainerXBlockHtml); - accessButtons = containerPage.$('.wrapper-xblock .access-button'); - if (hasVisibilityEditor) { - expect(accessButtons.length).toBe(6); - accessButtons[0].click(); - request = AjaxHelpers.currentRequest(requests); - expect(str.startsWith(request.url, '/xblock/locator-component-A1/visibility_view')) - .toBeTruthy(); - AjaxHelpers.respondWithJson(requests, { - html: mockXBlockVisibilityEditorHtml, - resources: [] - }); - expect(EditHelpers.isShowingModal()).toBeTruthy(); - } else { - expect(accessButtons.length).toBe(0); - } - }); + describe('Editing an xmodule', function() { + var mockXModuleEditor = readFixtures('templates/mock/mock-xmodule-editor.underscore'), + newDisplayName = 'New Display Name'; - it('can show a move modal for a child xblock', function() { - var moveButtons; - renderContainerPage(this, mockContainerXBlockHtml); - moveButtons = containerPage.$('.wrapper-xblock .move-button'); - if (hasMoveModal) { - expect(moveButtons.length).toBe(6); - moveButtons[0].click(); - expect(EditHelpers.isShowingModal()).toBeTruthy(); - } else { - expect(moveButtons.length).toBe(0); - } - }); + beforeEach(function() { + EditHelpers.installMockXModule({ + data: '

Some HTML

', + metadata: { + display_name: newDisplayName + } }); + }); - describe('Editing an xmodule', function() { - var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'), - newDisplayName = 'New Display Name'; + afterEach(function() { + EditHelpers.uninstallMockXModule(); + EditHelpers.cancelModalIfShowing(); + }); - beforeEach(function() { - EditHelpers.installMockXModule({ - data: '

Some HTML

', - metadata: { - display_name: newDisplayName - } - }); - }); + it('can save changes to settings', function() { + var editButtons, $modal, mockUpdatedXBlockHtml; + mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore'); + renderContainerPage(this, mockContainerXBlockHtml); + editButtons = containerPage.$('.wrapper-xblock .edit-button'); + // The container should have rendered six mock xblocks + expect(editButtons.length).toBe(6); + editButtons[0].click(); + AjaxHelpers.respondWithJson(requests, { + html: mockXModuleEditor, + resources: [] + }); - afterEach(function() { - EditHelpers.uninstallMockXModule(); - EditHelpers.cancelModalIfShowing(); - }); + $modal = $('.edit-xblock-modal'); + expect($modal.length).toBe(1); + // Click on the settings tab + $modal.find('.settings-button').click(); + // Change the display name's text + $modal.find('.setting-input').text('Mock Update'); + // Press the save button + $modal.find('.action-save').click(); + // Respond to the save + AjaxHelpers.respondWithJson(requests, { + id: model.id + }); - it('can save changes to settings', function() { - var editButtons, $modal, mockUpdatedXBlockHtml; - mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore'); - renderContainerPage(this, mockContainerXBlockHtml); - editButtons = containerPage.$('.wrapper-xblock .edit-button'); - // The container should have rendered six mock xblocks - expect(editButtons.length).toBe(6); - editButtons[0].click(); - AjaxHelpers.respondWithJson(requests, { - html: mockXModuleEditor, - resources: [] - }); + // Respond to the request to refresh + respondWithHtml(mockUpdatedXBlockHtml); - $modal = $('.edit-xblock-modal'); - expect($modal.length).toBe(1); - // Click on the settings tab - $modal.find('.settings-button').click(); - // Change the display name's text - $modal.find('.setting-input').text('Mock Update'); - // Press the save button - $modal.find('.action-save').click(); - // Respond to the save - AjaxHelpers.respondWithJson(requests, { - id: model.id - }); + // Verify that the xblock was updated + expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update'); + }); + }); + + describe('xblock operations', function() { + var getGroupElement, + NUM_COMPONENTS_PER_GROUP = 3, + GROUP_TO_TEST = 'A', + allComponentsInGroup = _.map( + _.range(NUM_COMPONENTS_PER_GROUP), + function(index) { + return 'locator-component-' + GROUP_TO_TEST + (index + 1); + } + ); - // Respond to the request to refresh - respondWithHtml(mockUpdatedXBlockHtml); + getGroupElement = function() { + return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']"); + }; - // Verify that the xblock was updated - expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update'); - }); + describe('Deleting an xblock', function() { + var clickDelete, deleteComponent, deleteComponentWithSuccess, + promptSpy; + + beforeEach(function() { + promptSpy = EditHelpers.createPromptSpy(); }); - describe('xblock operations', function() { - var getGroupElement, - NUM_COMPONENTS_PER_GROUP = 3, - GROUP_TO_TEST = 'A', - allComponentsInGroup = _.map( - _.range(NUM_COMPONENTS_PER_GROUP), - function(index) { - return 'locator-component-' + GROUP_TO_TEST + (index + 1); - } - ); - getGroupElement = function() { - return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']"); - }; + clickDelete = function(componentIndex, clickNo) { + // find all delete buttons for the given group + var deleteButtons = getGroupElement().find('.delete-button'); + expect(deleteButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); - describe('Deleting an xblock', function() { - var clickDelete, deleteComponent, deleteComponentWithSuccess, - promptSpy; + // click the requested delete button + deleteButtons[componentIndex].click(); - beforeEach(function() { - promptSpy = EditHelpers.createPromptSpy(); - }); + // click the 'yes' or 'no' button in the prompt + EditHelpers.confirmPrompt(promptSpy, clickNo); + }; + deleteComponent = function(componentIndex) { + clickDelete(componentIndex); - clickDelete = function(componentIndex, clickNo) { - // find all delete buttons for the given group - var deleteButtons = getGroupElement().find('.delete-button'); - expect(deleteButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); + // first request to delete the component + AjaxHelpers.expectJsonRequest(requests, 'DELETE', + '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), + null); + AjaxHelpers.respondWithNoContent(requests); - // click the requested delete button - deleteButtons[componentIndex].click(); + // then handle the request to refresh the preview + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(requests); + } - // click the 'yes' or 'no' button in the prompt - EditHelpers.confirmPrompt(promptSpy, clickNo); - }; + // final request to refresh the xblock info + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.respondWithJson(requests, {}); + }; - deleteComponent = function(componentIndex) { - clickDelete(componentIndex); + deleteComponentWithSuccess = function(componentIndex) { + deleteComponent(componentIndex); - // first request to delete the component - AjaxHelpers.expectJsonRequest(requests, 'DELETE', - '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), - null); - AjaxHelpers.respondWithNoContent(requests); + // verify the new list of components within the group (unless reloading) + if (!globalPageOptions.requiresPageRefresh) { + expectComponents( + getGroupElement(), + _.without(allComponentsInGroup, allComponentsInGroup[componentIndex]) + ); + } + }; - // then handle the request to refresh the preview - if (globalPageOptions.requiresPageRefresh) { - handleContainerPageRefresh(requests); - } + it('can delete the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(0); + }); - // final request to refresh the xblock info - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); - AjaxHelpers.respondWithJson(requests, {}); - }; + it('can delete a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(1); + }); - deleteComponentWithSuccess = function(componentIndex) { - deleteComponent(componentIndex); + it('can delete the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); + }); - // verify the new list of components within the group (unless reloading) - if (!globalPageOptions.requiresPageRefresh) { - expectComponents( - getGroupElement(), - _.without(allComponentsInGroup, allComponentsInGroup[componentIndex]) - ); - } - }; + it('can delete an xblock with broken JavaScript', function() { + renderContainerPage(this, mockBadContainerXBlockHtml); + containerPage.$('.delete-button').first().click(); + EditHelpers.confirmPrompt(promptSpy); - it('can delete the first xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(0); - }); + // expect the second to last request to be a delete of the xblock + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript'); + AjaxHelpers.respondWithNoContent(requests); - it('can delete a middle xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(1); - }); + // handle the refresh request for pages that require a full refresh on delete + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(requests); + } - it('can delete the last xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); - }); + // expect the last request to be a fetch of the xblock info for the parent container + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + }); - it('can delete an xblock with broken JavaScript', function() { - renderContainerPage(this, mockBadContainerXBlockHtml); - containerPage.$('.delete-button').first().click(); - EditHelpers.confirmPrompt(promptSpy); + it('does not delete when clicking No in prompt', function() { + renderContainerPage(this, mockContainerXBlockHtml); - // expect the second to last request to be a delete of the xblock - AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript'); - AjaxHelpers.respondWithNoContent(requests); + // click delete on the first component but press no + clickDelete(0, true); - // handle the refresh request for pages that require a full refresh on delete - if (globalPageOptions.requiresPageRefresh) { - handleContainerPageRefresh(requests); - } + // all components should still exist + expectComponents(getGroupElement(), allComponentsInGroup); - // expect the last request to be a fetch of the xblock info for the parent container - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); - }); + // no requests should have been sent to the server + AjaxHelpers.expectNoRequests(requests); + }); - it('does not delete when clicking No in prompt', function() { - renderContainerPage(this, mockContainerXBlockHtml); + it('shows a notification during the delete operation', function() { + var notificationSpy = EditHelpers.createNotificationSpy(); + renderContainerPage(this, mockContainerXBlockHtml); + clickDelete(0); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondWithJson(requests, {}); + EditHelpers.verifyNotificationHidden(notificationSpy); + }); - // click delete on the first component but press no - clickDelete(0, true); + it('does not delete an xblock upon failure', function() { + var notificationSpy = EditHelpers.createNotificationSpy(); + renderContainerPage(this, mockContainerXBlockHtml); + clickDelete(0); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + AjaxHelpers.respondWithError(requests); + EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); + expectComponents(getGroupElement(), allComponentsInGroup); + }); + }); - // all components should still exist - expectComponents(getGroupElement(), allComponentsInGroup); + describe('Duplicating an xblock', function() { + var clickDuplicate, duplicateComponentWithSuccess, + refreshXBlockSpies; - // no requests should have been sent to the server - AjaxHelpers.expectNoRequests(requests); - }); + clickDuplicate = function(componentIndex) { + // find all duplicate buttons for the given group + var duplicateButtons = getGroupElement().find('.duplicate-button'); + expect(duplicateButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); - it('shows a notification during the delete operation', function() { - var notificationSpy = EditHelpers.createNotificationSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - clickDelete(0); - EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); - AjaxHelpers.respondWithJson(requests, {}); - EditHelpers.verifyNotificationHidden(notificationSpy); - }); + // click the requested duplicate button + duplicateButtons[componentIndex].click(); + }; - it('does not delete an xblock upon failure', function() { - var notificationSpy = EditHelpers.createNotificationSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - clickDelete(0); - EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); - AjaxHelpers.respondWithError(requests); - EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); - expectComponents(getGroupElement(), allComponentsInGroup); - }); + duplicateComponentWithSuccess = function(componentIndex) { + refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); + + clickDuplicate(componentIndex); + + // verify content of request + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { + duplicate_source_locator: 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1), + parent_locator: 'locator-group-' + GROUP_TO_TEST }); - describe('Duplicating an xblock', function() { - var clickDuplicate, duplicateComponentWithSuccess, - refreshXBlockSpies; + // send the response + AjaxHelpers.respondWithJson(requests, { + locator: 'locator-duplicated-component' + }); - clickDuplicate = function(componentIndex) { - // find all duplicate buttons for the given group - var duplicateButtons = getGroupElement().find('.duplicate-button'); - expect(duplicateButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); + // expect parent container to be refreshed + expect(refreshXBlockSpies).toHaveBeenCalled(); + }; - // click the requested duplicate button - duplicateButtons[componentIndex].click(); - }; + it('can duplicate the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(0); + }); - duplicateComponentWithSuccess = function(componentIndex) { - refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); + it('can duplicate a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(1); + }); - clickDuplicate(componentIndex); + it('can duplicate the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); + }); - // verify content of request - AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { - duplicate_source_locator: 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1), - parent_locator: 'locator-group-' + GROUP_TO_TEST - }); + it('can duplicate an xblock with broken JavaScript', function() { + renderContainerPage(this, mockBadContainerXBlockHtml); + containerPage.$('.duplicate-button').first().click(); + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { + duplicate_source_locator: 'locator-broken-javascript', + parent_locator: 'locator-container' + }); + }); - // send the response - AjaxHelpers.respondWithJson(requests, { - locator: 'locator-duplicated-component' - }); + it('shows a notification when duplicating', function() { + var notificationSpy = EditHelpers.createNotificationSpy(); + renderContainerPage(this, mockContainerXBlockHtml); + clickDuplicate(0); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + AjaxHelpers.respondWithJson(requests, {locator: 'new_item'}); + EditHelpers.verifyNotificationHidden(notificationSpy); + }); - // expect parent container to be refreshed - expect(refreshXBlockSpies).toHaveBeenCalled(); - }; + it('does not duplicate an xblock upon failure', function() { + var notificationSpy = EditHelpers.createNotificationSpy(); + renderContainerPage(this, mockContainerXBlockHtml); + refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); + clickDuplicate(0); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + AjaxHelpers.respondWithError(requests); + expectComponents(getGroupElement(), allComponentsInGroup); + expect(refreshXBlockSpies).not.toHaveBeenCalled(); + EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); + }); + }); - it('can duplicate the first xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(0); - }); + describe('Previews', function() { + var getButtonIcon, getButtonText; - it('can duplicate a middle xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(1); - }); + getButtonIcon = function(containerPage) { + return containerPage.$('.action-toggle-preview .fa'); + }; - it('can duplicate the last xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); - }); + getButtonText = function(containerPage) { + return containerPage.$('.action-toggle-preview .preview-text').text().trim(); + }; - it('can duplicate an xblock with broken JavaScript', function() { - renderContainerPage(this, mockBadContainerXBlockHtml); - containerPage.$('.duplicate-button').first().click(); - AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { - duplicate_source_locator: 'locator-broken-javascript', - parent_locator: 'locator-container' - }); - }); + if (pagedSpecificTests) { + it('has no text on the preview button to start with', function() { + containerPage = getContainerPage(); + expect(getButtonIcon(containerPage)).toHaveClass('fa-refresh'); + expect(getButtonIcon(containerPage).parent()).toHaveClass('is-hidden'); + expect(getButtonText(containerPage)).toBe(''); + }); - it('shows a notification when duplicating', function() { - var notificationSpy = EditHelpers.createNotificationSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - clickDuplicate(0); - EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - AjaxHelpers.respondWithJson(requests, {locator: 'new_item'}); - EditHelpers.verifyNotificationHidden(notificationSpy); + var updatePreviewButtonTest = function(show_previews, expected_text) { + it('can set preview button to "' + expected_text + '"', function() { + containerPage = getContainerPage(); + containerPage.updatePreviewButton(show_previews); + expect(getButtonText(containerPage)).toBe(expected_text); }); + }; - it('does not duplicate an xblock upon failure', function() { - var notificationSpy = EditHelpers.createNotificationSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); - clickDuplicate(0); - EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - AjaxHelpers.respondWithError(requests); - expectComponents(getGroupElement(), allComponentsInGroup); - expect(refreshXBlockSpies).not.toHaveBeenCalled(); - EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - }); + updatePreviewButtonTest(true, 'Hide Previews'); + updatePreviewButtonTest(false, 'Show Previews'); + + it('triggers underlying view togglePreviews when preview button clicked', function() { + containerPage = getContainerPage(); + containerPage.render(); + spyOn(containerPage.xblockView, 'togglePreviews'); + + containerPage.$('.toggle-preview-button').click(); + expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled(); }); + } + }); - describe('Previews', function() { - var getButtonIcon, getButtonText; + describe('createNewComponent ', function() { + var clickNewComponent; - getButtonIcon = function(containerPage) { - return containerPage.$('.action-toggle-preview .fa'); - }; + clickNewComponent = function(index) { + containerPage.$('.new-component .new-component-type button.single-template')[index].click(); + }; - getButtonText = function(containerPage) { - return containerPage.$('.action-toggle-preview .preview-text').text().trim(); - }; + it('Attaches a handler to new component button', function() { + containerPage = getContainerPage(); + containerPage.render(); + // Stub jQuery.scrollTo module. + $.scrollTo = jasmine.createSpy('jQuery.scrollTo'); + containerPage.$('.new-component-button').click(); + expect($.scrollTo).toHaveBeenCalled(); + }); - if (pagedSpecificTests) { - it('has no text on the preview button to start with', function() { - containerPage = getContainerPage(); - expect(getButtonIcon(containerPage)).toHaveClass('fa-refresh'); - expect(getButtonIcon(containerPage).parent()).toHaveClass('is-hidden'); - expect(getButtonText(containerPage)).toBe(''); - }); - - var updatePreviewButtonTest = function(show_previews, expected_text) { - it('can set preview button to "' + expected_text + '"', function() { - containerPage = getContainerPage(); - containerPage.updatePreviewButton(show_previews); - expect(getButtonText(containerPage)).toBe(expected_text); - }); - }; - - updatePreviewButtonTest(true, 'Hide Previews'); - updatePreviewButtonTest(false, 'Show Previews'); - - it('triggers underlying view togglePreviews when preview button clicked', function() { - containerPage = getContainerPage(); - containerPage.render(); - spyOn(containerPage.xblockView, 'togglePreviews'); - - containerPage.$('.toggle-preview-button').click(); - expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled(); - }); - } + it('sends the correct JSON to the server', function() { + renderContainerPage(this, mockContainerXBlockHtml); + clickNewComponent(0); + EditHelpers.verifyXBlockRequest(requests, { + category: 'discussion', + type: 'discussion', + parent_locator: 'locator-group-A' }); + }); - describe('createNewComponent ', function() { - var clickNewComponent; + it('also works for older-style add component links', function() { + // Some third party xblocks (problem-builder in particular) expect add + // event handlers on custom add buttons which is what the platform + // used to use instead of