diff --git a/cms/djangoapps/contentstore/features/advanced_settings.py b/cms/djangoapps/contentstore/features/advanced_settings.py index 99add57d0396..5125fac12f37 100644 --- a/cms/djangoapps/contentstore/features/advanced_settings.py +++ b/cms/djangoapps/contentstore/features/advanced_settings.py @@ -17,26 +17,17 @@ @step('I select the Advanced Settings$') def i_select_advanced_settings(step): - 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 - + 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) 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 686c8ec1562d..70ce70d0aedb 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -247,6 +247,7 @@ 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 d417cb5ffa4f..f4e9e0e37ad4 100644 --- a/cms/djangoapps/contentstore/features/html-editor.feature +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -15,6 +15,11 @@ 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 0af0007a76f9..026504ca7f2e 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -82,7 +82,22 @@ 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 deleted file mode 100644 index 881b6482a1f5..000000000000 --- a/cms/djangoapps/pipeline_js/js/xmodule.js +++ /dev/null @@ -1,60 +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', '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 new file mode 100644 index 000000000000..fb0c22d2c1e3 --- /dev/null +++ b/cms/djangoapps/pipeline_js/templates/xmodule.js @@ -0,0 +1,45 @@ +## 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 new file mode 100644 index 000000000000..303573cfdbd3 --- /dev/null +++ b/cms/djangoapps/pipeline_js/urls.py @@ -0,0 +1,10 @@ +""" +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 deleted file mode 100644 index 91ce32d8af93..000000000000 --- a/cms/djangoapps/pipeline_js/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -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 new file mode 100644 index 000000000000..bfb05d1dd335 --- /dev/null +++ b/cms/djangoapps/pipeline_js/views.py @@ -0,0 +1,44 @@ +""" +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 766496f4865c..514e4a7d9d88 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -110,6 +110,10 @@ 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 0c2fdbc99e9b..eb6366318d88 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -49,9 +49,6 @@ # 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 cd09b6e95aea..96dd3902cccb 100644 --- a/cms/envs/bok_choy_docker.py +++ b/cms/envs/bok_choy_docker.py @@ -16,9 +16,3 @@ } 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 ed891b320b52..6a1e05f7ceab 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -54,6 +54,8 @@ # 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 10dec34c5a09..3f86a8c891e0 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -19,19 +19,24 @@ 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/videos_index' + 'js/factories/textbooks', + 'js/factories/videos_index', + 'js/factories/xblock_validation' ]), /** * 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 875b57d013d1..c2570289b0c6 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -1,87 +1,86 @@ /* globals AjaxPrefix */ -define([ - 'domReady', - 'jquery', - 'underscore', - 'underscore.string', - 'backbone', - 'gettext', - '../../common/js/components/views/feedback_notification', - 'jquery.cookie' -], function(domReady, $, _, str, Backbone, gettext, NotificationView) { +(function(AjaxPrefix) { 'use strict'; - - 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 + 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'); }); - console.log('Studio AJAX Error', { // eslint-disable-line no-console - url: event.currentTarget.URL, - response: jqXHR.responseText, - status: jqXHR.status - }); - 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', + 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', - data: JSON.stringify(data), - success: callback, - global: data ? data.global : true // Trigger global AJAX error handler or not + 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 + }); + console.log('Studio AJAX Error', { // eslint-disable-line no-console + url: event.currentTarget.URL, + response: jqXHR.responseText, + status: jqXHR.status + }); + 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 + }); + }; + $.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'); + } }); }; - $.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; -}); + main(); + return main; + }); +}).call(this, AjaxPrefix); diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 1f32ced8d6e6..eb0d03c70652 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -4,7 +4,6 @@ (function(requirejs, requireSerial) { 'use strict'; - var i, specHelpers, testFiles; if (window) { define('add-a11y-deps', [ @@ -21,6 +20,8 @@ }); } + var i, specHelpers, testFiles; + requirejs.config({ baseUrl: '/base/', paths: { @@ -229,6 +230,7 @@ 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', @@ -261,21 +263,32 @@ '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 deleted file mode 100644 index 5cea9bfd0a87..000000000000 --- a/cms/static/cms/js/spec/main_webpack.js +++ /dev/null @@ -1,35 +0,0 @@ -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 72c72b1e30f5..893fe6827a6d 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,82 +1,81 @@ -import EditHelpers from 'js/spec_helpers/edit_helpers'; -import BaseModal from 'js/views/modals/base_modal'; -import 'xblock/cms.runtime.v1'; +define(['js/spec_helpers/edit_helpers', 'js/views/modals/base_modal', 'xblock/cms.runtime.v1'], + function(EditHelpers, BaseModal) { + 'use strict'; -describe('Studio Runtime v1', function() { - 'use strict'; + describe('Studio Runtime v1', function() { + var runtime; - var runtime; - - beforeEach(function() { - EditHelpers.installEditTemplates(); - runtime = new window.StudioRuntime.v1(); - }); + beforeEach(function() { + EditHelpers.installEditTemplates(); + runtime = new window.StudioRuntime.v1(); + }); - 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('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 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('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('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 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); + }); - describe('Modal Dialogs', function() { - var MockModal, modal, showMockModal; + describe('Modal Dialogs', function() { + var MockModal, modal, showMockModal; - MockModal = BaseModal.extend({ - getContentHtml: function() { - return readFixtures('mock/mock-modal.underscore'); - } - }); + MockModal = BaseModal.extend({ + getContentHtml: function() { + return readFixtures('mock/mock-modal.underscore'); + } + }); - showMockModal = function() { - modal = new MockModal({ - title: 'Mock Modal' - }); - modal.show(); - }; + showMockModal = function() { + modal = new MockModal({ + title: 'Mock Modal' + }); + modal.show(); + }; - beforeEach(function() { - EditHelpers.installEditTemplates(); - }); + beforeEach(function() { + EditHelpers.installEditTemplates(); + }); - afterEach(function() { - EditHelpers.hideModalIfShowing(modal); - }); + 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(); + 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 0f7fa7f1da96..4ea35feda959 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -26,35 +26,8 @@ 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; @@ -71,14 +44,14 @@ define([ $('.action-notification-close').bind('click', hideNotification); // nav - dropdown related - $body.click(function() { + $body.click(function(e) { $('.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) { - var $subnav = $(this).find('.wrapper-nav-sub'), - $title = $(this).find('.title'); + $subnav = $(this).find('.wrapper-nav-sub'); + $title = $(this).find('.title'); if ($subnav.hasClass('is-shown')) { $subnav.removeClass('is-shown'); @@ -95,8 +68,7 @@ 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 @@ -125,7 +97,39 @@ define([ }); dropdownMenuView.postRender(); } - - window.studioNavMenuActive = true; }); + + 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'); + } }); // end require() diff --git a/cms/static/js/factories/base.js b/cms/static/js/factories/base.js index a71439357307..7f61b473c9e9 100644 --- a/cms/static/js/factories/base.js +++ b/cms/static/js/factories/base.js @@ -1,5 +1,3 @@ -// 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 cfca29885183..8861d6f1249a 100644 --- a/cms/static/js/factories/container.js +++ b/cms/static/js/factories/container.js @@ -1,26 +1,21 @@ -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'; +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}) + }; -'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(); + }); }; - - 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 deleted file mode 100644 index 475e5a6282c6..000000000000 --- a/cms/static/js/factories/context_course.js +++ /dev/null @@ -1,3 +0,0 @@ -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 9f2912fc912d..fddb8648a617 100644 --- a/cms/static/js/factories/edit_tabs.js +++ b/cms/static/js/factories/edit_tabs.js @@ -1,25 +1,20 @@ -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'; +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; -'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') + 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 4cde6873f939..e6eb92906934 100644 --- a/cms/static/js/factories/library.js +++ b/cms/static/js/factories/library.js @@ -1,28 +1,23 @@ -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'; +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 + }; -'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(); + }); }; - - 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 b528e075a2ce..fdbcef31e8ec 100644 --- a/cms/static/js/factories/login.js +++ b/cms/static/js/factories/login.js @@ -1,63 +1,57 @@ - -'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 +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'); }); - } - - // 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'); - }); - - $('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(); + $('input#password').on('input', function() { + $('#login_error').removeClass('is-shown'); + }); - 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]); + $('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(); } else { - ViewUtils.redirect(homepageURL); + $('#login_error') + .stop() + .addClass('is-shown') + .html(json.value); + deferred.resolve(); } - } 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 2fd11e407f18..0641e025d36a 100644 --- a/cms/static/js/factories/textbooks.js +++ b/cms/static/js/factories/textbooks.js @@ -1,23 +1,20 @@ -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'; +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}); -'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} + $('.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?'); + } + }); + }; +}); diff --git a/cms/static/js/factories/xblock_validation.js b/cms/static/js/factories/xblock_validation.js index 56786c89d9c1..4bcfc3b33917 100644 --- a/cms/static/js/factories/xblock_validation.js +++ b/cms/static/js/factories/xblock_validation.js @@ -1,22 +1,19 @@ +define(['js/views/xblock_validation', 'js/models/xblock_validation'], +function(XBlockValidationView, XBlockValidationModel) { + 'use strict'; + return function(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { + var model, response; -import * as XBlockValidationView from 'js/views/xblock_validation'; -import * as XBlockValidationModel from 'js/models/xblock_validation'; + if (hasEditingUrl && !isRoot) { + validationMessages.showSummaryOnly = true; + } + response = validationMessages; + response.isUnit = isUnit; -'use strict'; -export default function XBlockValidationFactory(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { - var model, response; + model = new XBlockValidationModel(response, {parse: true}); - 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} + if (!model.get('empty')) { + new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render(); + } + }; +}); diff --git a/cms/static/js/pages/course.js b/cms/static/js/pages/course.js new file mode 100644 index 000000000000..cf0ca8d5ef4a --- /dev/null +++ b/cms/static/js/pages/course.js @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000000..449a1190f641 --- /dev/null +++ b/cms/static/js/pages/login.js @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000000..7d524fbd2909 --- /dev/null +++ b/cms/static/js/pages/textbooks.js @@ -0,0 +1,7 @@ +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 fe6b331e46d0..44653981ff64 100644 --- a/cms/static/js/sock.js +++ b/cms/static/js/sock.js @@ -1,41 +1,39 @@ -import * as domReady from 'domReady'; -import * as $ from 'jquery'; -import 'jquery.smoothScroll'; +define(['domReady', 'jquery', 'jquery.smoothScroll'], + function(domReady, $) { + 'use strict'; -'use strict'; + var toggleSock = function(e) { + e.preventDefault(); -var toggleSock = function (e) { - e.preventDefault(); + var $btnShowSockLabel = $(this).find('.copy-show'); + var $btnHideSockLabel = $(this).find('.copy-hide'); + var $sock = $('.wrapper-sock'); + var $sockContent = $sock.find('.wrapper-inner'); - var $btnShowSockLabel = $(this).find('.copy-show'); - var $btnHideSockLabel = $(this).find('.copy-hide'); - var $sock = $('.wrapper-sock'); - var $sockContent = $sock.find('.wrapper-inner'); + 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'); + } - 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 - }); -}; + $.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 } + domReady(function() { + // toggling footer additional support + $('.cta-show-sock').bind('click', toggleSock); + }); + } +); diff --git a/cms/static/js/spec/factories/xblock_validation_spec.js b/cms/static/js/spec/factories/xblock_validation_spec.js index ca14f88c09a4..5ba938098136 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 @@ -import $ from 'jquery'; -import XBlockValidationFactory from 'js/factories/xblock_validation'; -import TemplateHelpers from 'common/js/spec_helpers/template_helpers'; +define(['jquery', 'js/factories/xblock_validation', 'common/js/spec_helpers/template_helpers'], + function($, XBlockValidationFactory, TemplateHelpers) { + describe('XBlockValidationFactory', function() { + var $messageDiv; -describe('XBlockValidationFactory', () => { - var $messageDiv; + beforeEach(function() { + TemplateHelpers.installTemplate('xblock-validation-messages'); + appendSetFixtures($('
')); + $messageDiv = $('.messages'); + }); - beforeEach(function() { - TemplateHelpers.installTemplate('xblock-validation-messages'); - appendSetFixtures($('
')); - $messageDiv = $('.messages'); - }); + 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('Does not attach a view if messages is empty', function() { - XBlockValidationFactory({empty: true}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(0); - }); + 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 attach a view if messages are not empty', function() { - XBlockValidationFactory({empty: false}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(1); - }); + it('Passes through the root property to the view.', function() { + var noContainerContent = 'no-container-content'; - it('Passes through the root property to the view.', function() { - var noContainerContent = 'no-container-content'; + 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); - 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); + // Root is true, will add noContainerContent. + XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); + expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); + }); - // Root is true, will add noContainerContent. - XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); - expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); - }); + describe('Controls display of detailed messages based on url and root property', function() { + var messagesWithSummary, checkDetailedMessages; - describe('Controls display of detailed messages based on url and root property', function() { - var messagesWithSummary, checkDetailedMessages; + beforeEach(function() { + messagesWithSummary = { + empty: false, + summary: {text: 'my summary'}, + messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}], + xblock_id: 'id' + }; + }); - 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); - }; + 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 1c56f9a4798c..b61313de3187 100644 --- a/cms/static/js/spec/utils/drag_and_drop_spec.js +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -309,7 +309,6 @@ 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 194d405d0cfd..bc33ccd31a65 100644 --- a/cms/static/js/spec/views/container_spec.js +++ b/cms/static/js/spec/views/container_spec.js @@ -1,205 +1,198 @@ -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); +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); + }); + }); }); }); }); -}); diff --git a/cms/static/js/spec/views/login_studio_spec.js b/cms/static/js/spec/views/login_studio_spec.js index 38a01dc311aa..d20ca233efe5 100644 --- a/cms/static/js/spec/views/login_studio_spec.js +++ b/cms/static/js/spec/views/login_studio_spec.js @@ -1,35 +1,32 @@ +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; -'use strict'; + beforeEach(function() { + loadFixtures('mock/login.underscore'); + var login_factory = new LoginFactory('/home/'); + $submitButton = $('#submit'); + }); -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('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'); + }); -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'); + 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 8a28acab5bcb..a076c5ee4c23 100644 --- a/cms/static/js/spec/views/modals/edit_xblock_spec.js +++ b/cms/static/js/spec/views/modals/edit_xblock_spec.js @@ -1,215 +1,211 @@ -'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'); - - it('hides the modal\'s button bar', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomButtonsHtml); - expect(modal.$('.modal-actions')).toBeHidden(); +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' + }); }); - }); - }); - - 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'); - }); - 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'); + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); - it('hides the modal\'s header', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomTabsHtml); - expect(modal.$('.modal-header')).toBeHidden(); + 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 title', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomTabsHtml); - expect(modal.$('.component-name').text()).toBe('Editing: Component'); + 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'); + }); + }); }); - }); - }); - describe('XModule Editor (settings only)', function() { - var mockXModuleEditorHtml; + describe('XModule Editor (settings only)', function() { + var mockXModuleEditorHtml; - mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-settings-only-editor.underscore'); + mockXModuleEditorHtml = readFixtures('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 04f2d2797e75..21903b222640 100644 --- a/cms/static/js/spec/views/module_edit_spec.js +++ b/cms/static/js/spec/views/module_edit_spec.js @@ -1,58 +1,37 @@ - -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() { +(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() { beforeEach(function() { - spyOn(ModuleEdit.prototype, 'render'); + 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, @@ -60,206 +39,227 @@ describe('ModuleEdit', function() { }); return this.moduleEdit; }); - 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) + describe('class definition', function() { + it('sets the correct tagName', function() { + return expect(this.moduleEdit.tagName).toEqual('li'); }); - expect($.ajax).not.toHaveBeenCalledWith({ - url: '/xblock/' + this.moduleEdit.model.id + '/studio_view', - type: 'GET', - headers: { - Accept: 'application/json' - }, - success: jasmine.any(Function) + it('sets the correct className', function() { + return expect(this.moduleEdit.className).toEqual('component'); }); - expect(this.moduleEdit.loadDisplay).toHaveBeenCalled(); - return expect(this.moduleEdit.delegateEvents).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('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(); + }); }); - this.moduleEdit.clickEditButton({ - preventDefault: jasmine.createSpy('event.preventDefault') + 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); + }); }); - 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' - } - ] - ] + 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)); + }); }); - 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 f8a045f95d90..7e7610fdfee8 100644 --- a/cms/static/js/spec/views/move_xblock_spec.js +++ b/cms/static/js/spec/views/move_xblock_spec.js @@ -1,785 +1,766 @@ -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', - 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' - }); +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' + }; - 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" - }); + categoryMap = { + section: 'chapter', + subsection: 'sequential', + unit: 'vertical', + component: 'component' + }; - createContainerPage(); - courseOutline = createCourseOutline(courseOutlineOptions); - showModal(); - }); + courseOutlineOptions = { + section: 2, + subsection: 2, + unit: 2, + component: 2 + }; - afterEach(function() { - modal.hide(); - courseOutline = null; - containerPage.remove(); - delete window.course; - }); + 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' + } + ] + }; - 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 + sourceParentXBlockInfo = new XBlockInfo({ + id: sourceParentLocator, + display_name: 'unit_display_name_0', + category: 'vertical' }); - } - - // 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 + + 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(); + }); + + afterEach(function() { + modal.hide(); + courseOutline = null; + containerPage.remove(); + }); + + 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; } - ) - ); - }; - - /** - * 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; - }); - }; - it('renders views with correct information', function() { - var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1}, - outline = createCourseOutline(outlineOptions); + outline.child_info = childInfo; // eslint-disable-line no-param-reassign + _.each(_.range(xblocks), function(xblockIndex) { + childInfo.children.push( + createChildXBlockInfo(category, outlineOptions, xblockIndex) + ); + }); + return outline; + }; - renderViews(outline, xblockAncestorInfo); - verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); - verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true); - }); + /** + * 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); + }; - it('shows correct behavior on breadcrumb navigation', function() { - var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1}); + /** + * 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); + }; - renderViews(outline); - _.each(_.range(3), function() { - clickForwardButton(); - }); + /** + * 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(); + }; - _.each(['component', 'unit', 'subsection', 'section'], function(category) { - verifyListViewInfo(category, 1); - if (category !== 'section') { + /** + * Click on last clickable breadcrumb button. + */ + clickBreadcrumbButton = function() { modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); - } - }); - }); + }; - 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); - }); + /** + * 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]; + }; - 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 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 - 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); - }); + verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation); + }; - 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); - }); + /** + * 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('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 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); + }; - 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(); - }); + /** + * Get move alert confirmation message HTML + */ + getMovedAlertNotification = function() { + return $('#page-alert'); + }; - 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(); - }); + /** + * 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('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 content of request + AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData); - it('is enabled when navigating to any parentable component', function() { - var parentableXBlockInfo = { - category: 'vertical', - display_name: 'Parentable Component', - has_children: true, - id: 'PARENTABLE_ID' + // send the response + AjaxHelpers.respondWithJson(requests, _.extend(responseData, { + source_index: sourceIndex + })); }; - renderViews(parentableXBlockInfo); - // move is enabled on parentable xblocks. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); - }); + /** + * 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 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 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 + }) + ); + }; - // move is enabled when moving a component. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy(); - }); + /** + * 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 + } + ) + ); + }; - 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' + /** + * 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'); }; - renderViews(nonParentableXBlockInfo); - // move is disabled on non-parent xblocks. - expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy(); - }); - }); + /** + * Verify success banner message html has correct undo move text. + */ + verifyConfirmationFeedbackUndoMoveActionText = function() { + expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move'); + }; - 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); - }); + /** + * Get sent requests. + * + * @returns {Object} + */ + getSentRequests = function() { + return jasmine.Ajax.requests.filter(function(request) { + return request.readyState > 0; + }); + }; - it('move an xblock when move button is clicked', function() { - var requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - }); + it('renders views with correct information', function() { + var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1}, + outline = createCourseOutline(outlineOptions); - 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); - }); + renderViews(outline, xblockAncestorInfo); + verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); + verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true); + }); - 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 + it('shows correct behavior on breadcrumb navigation', function() { + var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1}); + + renderViews(outline); + _.each(_.range(3), function() { + clickForwardButton(); + }); + + _.each(['component', 'unit', 'subsection', 'section'], function(category) { + verifyListViewInfo(category, 1); + if (category !== 'section') { + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); + } + }); }); - verifyUndoConfirmationFeedbackTitleText(sourceDisplayName); - }); - }); - 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); + 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); }); - 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('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(); + }); + }); - 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); + 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(); + }); }); - 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"); + 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); + }); + }); + + 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"); + }); + }); }); }); -}); diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index 3beca50b7973..a27b66bb4916 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -1,855 +1,839 @@ -'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 - } - }); +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; - initialDisplayName = 'Test Container'; + 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 + } + }); - 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" - }); - }); + initialDisplayName = 'Test Container'; - 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); - }); - }; + model = new XBlockInfo({ + id: 'locator-container', + display_name: initialDisplayName, + category: 'vertical' + }); + }); - 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'); - }); + afterEach(function() { + EditHelpers.uninstallMockXBlock(); + if (containerPage !== undefined) { + containerPage.remove(); + } + }); - 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'); - }); + respondWithHtml = function(html) { + AjaxHelpers.respondWithJson( + requests, + {html: html, resources: []} + ); + }; - 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'); - }); + 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)); + }; - 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'); - }); + 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 || {}); + }; - 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'); - }); - }); + 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: [] + }); + }; - describe('Editing the container', function() { - var updatedDisplayName = 'Updated Test Container', - getDisplayNameWrapper; + 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); + }); + }; - afterEach(function() { - EditHelpers.cancelModalIfShowing(); - }); + 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'); + }); - 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(); + 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'); + }); - // Expect the correct title to be shown - expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); + 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'); + }); - // Press the save button and respond with a success message to the save - EditHelpers.pressModalButton('.action-save'); - AjaxHelpers.respondWithJson(requests, { }); - expect(EditHelpers.isShowingModal()).toBeFalsy(); + 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'); + }); - // Expect the last request be to refresh the container page - handleContainerPageRefresh(requests); + 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'); + }); + }); - // Respond to the subsequent xblock info fetch request. - AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + describe('Editing the container', function() { + var updatedDisplayName = 'Updated Test Container', + getDisplayNameWrapper; - // Expect the title to have been updated - expect(displayNameElement.text().trim()).toBe(updatedDisplayName); - }); + 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); - }); - }); + getDisplayNameWrapper = function() { + return containerPage.$('.wrapper-xblock-field'); + }; - describe('Editing an xblock', function() { - afterEach(function() { - EditHelpers.cancelModalIfShowing(); - }); + 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(); - 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(); - }); + // Expect the correct title to be shown + expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); - 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(); - }); + // Press the save button and respond with a success message to the save + EditHelpers.pressModalButton('.action-save'); + AjaxHelpers.respondWithJson(requests, { }); + expect(EditHelpers.isShowingModal()).toBeFalsy(); - 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); - } - }); + // Expect the last request be to refresh the container page + handleContainerPageRefresh(requests); - 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); - } - }); - }); + // Respond to the subsequent xblock info fetch request. + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); - describe('Editing an xmodule', function() { - var mockXModuleEditor = readFixtures('templates/mock/mock-xmodule-editor.underscore'), - newDisplayName = 'New Display Name'; + // Expect the title to have been updated + expect(displayNameElement.text().trim()).toBe(updatedDisplayName); + }); - beforeEach(function() { - EditHelpers.installMockXModule({ - data: '

Some HTML

', - metadata: { - display_name: newDisplayName - } + 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); + }); }); - }); - afterEach(function() { - EditHelpers.uninstallMockXModule(); - EditHelpers.cancelModalIfShowing(); - }); + describe('Editing an xblock', function() { + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); - 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: [] - }); + 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(); + }); - $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 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(); + }); - // Respond to the request to refresh - respondWithHtml(mockUpdatedXBlockHtml); + 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); + } + }); - // 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); - } - ); + 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); + } + }); + }); - getGroupElement = function() { - return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']"); - }; + describe('Editing an xmodule', function() { + var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'), + newDisplayName = 'New Display Name'; - describe('Deleting an xblock', function() { - var clickDelete, deleteComponent, deleteComponentWithSuccess, - promptSpy; + beforeEach(function() { + EditHelpers.installMockXModule({ + data: '

Some HTML

', + metadata: { + display_name: newDisplayName + } + }); + }); - beforeEach(function() { - promptSpy = EditHelpers.createPromptSpy(); - }); + afterEach(function() { + EditHelpers.uninstallMockXModule(); + EditHelpers.cancelModalIfShowing(); + }); + 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: [] + }); - 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); + $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 + }); - // click the requested delete button - deleteButtons[componentIndex].click(); + // Respond to the request to refresh + respondWithHtml(mockUpdatedXBlockHtml); - // click the 'yes' or 'no' button in the prompt - EditHelpers.confirmPrompt(promptSpy, clickNo); - }; + // Verify that the xblock was updated + expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update'); + }); + }); - deleteComponent = function(componentIndex) { - clickDelete(componentIndex); + 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); + } + ); - // first request to delete the component - AjaxHelpers.expectJsonRequest(requests, 'DELETE', - '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), - null); - AjaxHelpers.respondWithNoContent(requests); + getGroupElement = function() { + return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']"); + }; - // then handle the request to refresh the preview - if (globalPageOptions.requiresPageRefresh) { - handleContainerPageRefresh(requests); - } + describe('Deleting an xblock', function() { + var clickDelete, deleteComponent, deleteComponentWithSuccess, + promptSpy; - // final request to refresh the xblock info - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); - AjaxHelpers.respondWithJson(requests, {}); - }; + beforeEach(function() { + promptSpy = EditHelpers.createPromptSpy(); + }); - deleteComponentWithSuccess = function(componentIndex) { - deleteComponent(componentIndex); - // verify the new list of components within the group (unless reloading) - if (!globalPageOptions.requiresPageRefresh) { - expectComponents( - getGroupElement(), - _.without(allComponentsInGroup, allComponentsInGroup[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); - it('can delete the first xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(0); - }); + // click the requested delete button + deleteButtons[componentIndex].click(); - it('can delete a middle xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(1); - }); + // click the 'yes' or 'no' button in the prompt + EditHelpers.confirmPrompt(promptSpy, clickNo); + }; - it('can delete the last xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); - }); + deleteComponent = function(componentIndex) { + clickDelete(componentIndex); - it('can delete an xblock with broken JavaScript', function() { - renderContainerPage(this, mockBadContainerXBlockHtml); - containerPage.$('.delete-button').first().click(); - EditHelpers.confirmPrompt(promptSpy); + // first request to delete the component + AjaxHelpers.expectJsonRequest(requests, 'DELETE', + '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), + null); + AjaxHelpers.respondWithNoContent(requests); - // expect the second to last request to be a delete of the xblock - AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript'); - AjaxHelpers.respondWithNoContent(requests); + // then handle the request to refresh the preview + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(requests); + } - // handle the refresh request for pages that require a full refresh on delete - if (globalPageOptions.requiresPageRefresh) { - handleContainerPageRefresh(requests); - } + // final request to refresh the xblock info + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.respondWithJson(requests, {}); + }; - // expect the last request to be a fetch of the xblock info for the parent container - AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); - }); + deleteComponentWithSuccess = function(componentIndex) { + deleteComponent(componentIndex); - it('does not delete when clicking No in prompt', function() { - renderContainerPage(this, mockContainerXBlockHtml); + // verify the new list of components within the group (unless reloading) + if (!globalPageOptions.requiresPageRefresh) { + expectComponents( + getGroupElement(), + _.without(allComponentsInGroup, allComponentsInGroup[componentIndex]) + ); + } + }; - // click delete on the first component but press no - clickDelete(0, true); + it('can delete the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(0); + }); - // all components should still exist - expectComponents(getGroupElement(), allComponentsInGroup); + it('can delete a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(1); + }); - // no requests should have been sent to the server - AjaxHelpers.expectNoRequests(requests); - }); + it('can delete the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); + }); - 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); - }); + it('can delete an xblock with broken JavaScript', function() { + renderContainerPage(this, mockBadContainerXBlockHtml); + containerPage.$('.delete-button').first().click(); + EditHelpers.confirmPrompt(promptSpy); - 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); - }); - }); + // expect the second to last request to be a delete of the xblock + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript'); + AjaxHelpers.respondWithNoContent(requests); - describe('Duplicating an xblock', function() { - var clickDuplicate, duplicateComponentWithSuccess, - refreshXBlockSpies; + // handle the refresh request for pages that require a full refresh on delete + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(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); + // expect the last request to be a fetch of the xblock info for the parent container + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + }); - // click the requested duplicate button - duplicateButtons[componentIndex].click(); - }; + it('does not delete when clicking No in prompt', function() { + renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess = function(componentIndex) { - refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); + // click delete on the first component but press no + clickDelete(0, true); - clickDuplicate(componentIndex); + // all components should still exist + expectComponents(getGroupElement(), allComponentsInGroup); - // 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 - }); + // no requests should have been sent to the server + AjaxHelpers.expectNoRequests(requests); + }); - // send the response - AjaxHelpers.respondWithJson(requests, { - locator: 'locator-duplicated-component' - }); + 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); + }); - // expect parent container to be refreshed - expect(refreshXBlockSpies).toHaveBeenCalled(); - }; + 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); + }); + }); - it('can duplicate the first xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(0); - }); + describe('Duplicating an xblock', function() { + var clickDuplicate, duplicateComponentWithSuccess, + refreshXBlockSpies; - it('can duplicate a middle xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(1); - }); + 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('can duplicate the last xblock', function() { - renderContainerPage(this, mockContainerXBlockHtml); - duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); - }); + // click the requested duplicate button + duplicateButtons[componentIndex].click(); + }; - 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' - }); - }); + duplicateComponentWithSuccess = function(componentIndex) { + refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); - 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); - }); + clickDuplicate(componentIndex); - 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/); - }); - }); + // 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('Previews', function() { - var getButtonIcon, getButtonText; + // send the response + AjaxHelpers.respondWithJson(requests, { + locator: 'locator-duplicated-component' + }); - getButtonIcon = function(containerPage) { - return containerPage.$('.action-toggle-preview .fa'); - }; + // expect parent container to be refreshed + expect(refreshXBlockSpies).toHaveBeenCalled(); + }; - getButtonText = function(containerPage) { - return containerPage.$('.action-toggle-preview .preview-text').text().trim(); - }; + it('can duplicate the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(0); + }); - 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('can duplicate a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(1); + }); - 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('can duplicate the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); }); - }; - updatePreviewButtonTest(true, 'Hide Previews'); - updatePreviewButtonTest(false, 'Show Previews'); + 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' + }); + }); - it('triggers underlying view togglePreviews when preview button clicked', function() { - containerPage = getContainerPage(); - containerPage.render(); - spyOn(containerPage.xblockView, 'togglePreviews'); + 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); + }); - containerPage.$('.toggle-preview-button').click(); - expect(containerPage.xblockView.togglePreviews).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/); + }); }); - } - }); - describe('createNewComponent ', function() { - var clickNewComponent; + describe('Previews', function() { + var getButtonIcon, getButtonText; - clickNewComponent = function(index) { - containerPage.$('.new-component .new-component-type button.single-template')[index].click(); - }; - - 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(); - }); + getButtonIcon = function(containerPage) { + return containerPage.$('.action-toggle-preview .fa'); + }; - 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' - }); - }); + getButtonText = function(containerPage) { + return containerPage.$('.action-toggle-preview .preview-text').text().trim(); + }; - 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