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( + ' ' + ); + $('#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( - ' ' - ); - $('#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('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