diff --git a/cms/djangoapps/contentstore/features/advanced_settings.py b/cms/djangoapps/contentstore/features/advanced_settings.py index 5125fac12f..99add57d03 100644 --- a/cms/djangoapps/contentstore/features/advanced_settings.py +++ b/cms/djangoapps/contentstore/features/advanced_settings.py @@ -17,17 +17,26 @@ DEPRECATED_SETTINGS = ["CSS Class for Course Reruns", "Hide Progress Tab", "XQA @step('I select the Advanced Settings$') def i_select_advanced_settings(step): - world.click_course_settings() + 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 - # 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 70ce70d0ae..686c8ec156 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -247,7 +247,6 @@ def create_unit_from_course_outline(): world.css_click(selector) world.wait_for_mathjax() - world.wait_for_xmodule() world.wait_for_loading() assert world.is_css_present('ul.new-component-type') diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature index f4e9e0e37a..d417cb5ffa 100644 --- a/cms/djangoapps/contentstore/features/html-editor.feature +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -15,11 +15,6 @@ Feature: CMS.HTML Editor Then I can modify the display name And my display name change is persisted on save - Scenario: Edit High Level source is available for LaTeX html - Given I have created an E-text Written in LaTeX - When I edit and select Settings - Then Edit High Level Source is visible - Scenario: TinyMCE image plugin sets urls correctly Given I have created a Blank HTML Page When I edit the page diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index 026504ca7f..0af0007a76 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -82,22 +82,7 @@ Feature: CMS.Problem Editor And I can modify the display name Then If I press Cancel my changes are not persisted - Scenario: Edit High Level source is available for LaTeX problem - Given I have created a LaTeX Problem - When I edit and select Settings - Then Edit High Level Source is visible - Scenario: Cheat sheet visible on toggle Given I have created a Blank Common Problem And I can edit the problem Then I can see cheatsheet - - Scenario: Reply on Annotation and Return to Annotation link works for Annotation problem - Given I have created a unit with advanced module "annotatable" - And I have created an advanced component "Annotation" of type "annotatable" - And I have created an advanced problem of type "Blank Advanced Problem" - And I edit first blank advanced problem for annotation response - When I mouseover on "annotatable-span" - Then I can see Reply to Annotation link - And I see that page has scrolled "down" when I click on "annotatable-reply" link - And I see that page has scrolled "up" when I click on "annotation-return" link diff --git a/cms/djangoapps/pipeline_js/js/xmodule.js b/cms/djangoapps/pipeline_js/js/xmodule.js new file mode 100644 index 0000000000..881b6482a1 --- /dev/null +++ b/cms/djangoapps/pipeline_js/js/xmodule.js @@ -0,0 +1,60 @@ +// This file is designed to load all the XModule Javascript files in one wad +// using requirejs. It is passed through the Mako template system, which +// populates the `urls` variable with a list of paths to XModule JS files. +// These files assume that several libraries are available and bound to +// variables in the global context, so we load those libraries with requirejs +// and attach them to the global context manually. +define( + [ + 'jquery', 'underscore', 'codemirror', 'tinymce', 'scriptjs', + 'jquery.tinymce', 'jquery.qtip', 'jquery.scrollTo', 'jquery.flot', + 'jquery.cookie', + 'utility' + ], + function($, _, CodeMirror, tinymce, $script) { + 'use strict'; + + window.$ = $; + window._ = _; + $script( + '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js' + + '?config=TeX-MML-AM_SVG&delayStartupUntil=configured', + 'mathjax' + ); + window.CodeMirror = CodeMirror; + window.RequireJS = { + requirejs: {}, // This is never used by current xmodules + require: $script, // $script([deps], callback) acts approximately like the require function + define: define + }; + /** + * Loads all modules one-by-one in exact order. + * The module should be used until we'll use RequireJS for XModules. + * @param {Array} modules A list of urls. + * @return {jQuery Promise} + **/ + function requireQueue(modules) { + var deferred = $.Deferred(); + function loadScript(queue) { + $script.ready('mathjax', function() { + // Loads the next script if queue is not empty. + if (queue.length) { + $script([queue.shift()], function() { + loadScript(queue); + }); + } else { + deferred.resolve(); + } + }); + } + + loadScript(modules.concat()); + return deferred.promise(); + } + + // if (!window.xmoduleUrls) { + // throw Error('window.xmoduleUrls must be defined'); + // } + return requireQueue([]); + } +); diff --git a/cms/djangoapps/pipeline_js/templates/xmodule.js b/cms/djangoapps/pipeline_js/templates/xmodule.js deleted file mode 100644 index fb0c22d2c1..0000000000 --- a/cms/djangoapps/pipeline_js/templates/xmodule.js +++ /dev/null @@ -1,45 +0,0 @@ -## This file is designed to load all the XModule Javascript files in one wad -## using requirejs. It is passed through the Mako template system, which -## populates the `urls` variable with a list of paths to XModule JS files. -## These files assume that several libraries are available and bound to -## variables in the global context, so we load those libraries with requirejs -## and attach them to the global context manually. -define(["jquery", "underscore", "codemirror", "tinymce", - "jquery.tinymce", "jquery.qtip", "jquery.scrollTo", "jquery.flot", - "jquery.cookie", - "utility"], - function($, _, CodeMirror, tinymce) { - window.$ = $; - window._ = _; - require(['mathjax']); - window.CodeMirror = CodeMirror; - window.RequireJS = { - 'requirejs': requirejs, - 'require': require, - 'define': define - }; - /** - * Loads all modules one-by-one in exact order. - * The module should be used until we'll use RequireJS for XModules. - * @param {Array} modules A list of urls. - * @return {jQuery Promise} - **/ - var requireQueue = function(modules) { - var deferred = $.Deferred(); - var loadScript = function (queue) { - // Loads the next script if queue is not empty. - if (queue.length) { - require([queue.shift()], function() { - loadScript(queue); - }); - } else { - deferred.resolve(); - } - }; - - loadScript(modules.concat()); - return deferred.promise(); - }; - - return requireQueue(${urls}); -}); diff --git a/cms/djangoapps/pipeline_js/urls.py b/cms/djangoapps/pipeline_js/urls.py deleted file mode 100644 index 303573cfdb..0000000000 --- a/cms/djangoapps/pipeline_js/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -URL patterns for Javascript files used to load all of the XModule JS in one wad. -""" -from django.conf.urls import url -from pipeline_js.views import xmodule_js_files, requirejs_xmodule - -urlpatterns = [ - url(r'^files\.json$', xmodule_js_files, name='xmodule_js_files'), - url(r'^xmodule\.js$', requirejs_xmodule, name='requirejs_xmodule'), -] diff --git a/cms/djangoapps/pipeline_js/utils.py b/cms/djangoapps/pipeline_js/utils.py new file mode 100644 index 0000000000..91ce32d8af --- /dev/null +++ b/cms/djangoapps/pipeline_js/utils.py @@ -0,0 +1,18 @@ +""" +Utilities for returning XModule JS (used by requirejs) +""" + +from django.conf import settings +from django.contrib.staticfiles.storage import staticfiles_storage + + +def get_xmodule_urls(): + """ + Returns a list of the URLs to hit to grab all the XModule JS + """ + pipeline_js_settings = settings.PIPELINE_JS["module-js"] + if settings.DEBUG: + paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]] + else: + paths = [pipeline_js_settings["output_filename"]] + return [staticfiles_storage.url(path) for path in paths] diff --git a/cms/djangoapps/pipeline_js/views.py b/cms/djangoapps/pipeline_js/views.py deleted file mode 100644 index bfb05d1dd3..0000000000 --- a/cms/djangoapps/pipeline_js/views.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Views for returning XModule JS (used by requirejs) -""" - -import json - -from django.conf import settings -from django.contrib.staticfiles.storage import staticfiles_storage -from django.http import HttpResponse - -from edxmako.shortcuts import render_to_response - - -def get_xmodule_urls(): - """ - Returns a list of the URLs to hit to grab all the XModule JS - """ - pipeline_js_settings = settings.PIPELINE_JS["module-js"] - if settings.DEBUG: - paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]] - else: - paths = [pipeline_js_settings["output_filename"]] - return [staticfiles_storage.url(path) for path in paths] - - -def xmodule_js_files(request): # pylint: disable=unused-argument - """ - View function that returns XModule URLs as a JSON list; meant to be used - as an API - """ - urls = get_xmodule_urls() - return HttpResponse(json.dumps(urls), content_type="application/json") - - -def requirejs_xmodule(request): # pylint: disable=unused-argument - """ - View function that returns a requirejs-wrapped Javascript file that - loads all the XModule URLs; meant to be loaded via requireJS - """ - return render_to_response( - "xmodule.js", - {"urls": get_xmodule_urls()}, - content_type="text/javascript", - ) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 514e4a7d9d..766496f486 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -110,10 +110,6 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False # 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 eb6366318d..0c2fdbc99e 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -49,6 +49,9 @@ update_module_store_settings( # 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 96dd3902cc..cd09b6e95a 100644 --- a/cms/envs/bok_choy_docker.py +++ b/cms/envs/bok_choy_docker.py @@ -16,3 +16,9 @@ LOGGING['handlers']['local'] = LOGGING['handlers']['tracking'] = { } 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 6a1e05f7ce..ed891b320b 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -54,8 +54,6 @@ TEST_ROOT = path('test_root') # 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 3f86a8c891..10dec34c5a 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -19,24 +19,19 @@ modules: getModulesList([ 'js/factories/asset_index', 'js/factories/base', - 'js/factories/container', 'js/factories/course_create_rerun', 'js/factories/course_info', - 'js/factories/edit_tabs', 'js/factories/export', 'js/factories/group_configurations', 'js/certificates/factories/certificates_page_factory', 'js/factories/index', - 'js/factories/library', 'js/factories/manage_users', 'js/factories/outline', 'js/factories/register', 'js/factories/settings', 'js/factories/settings_advanced', 'js/factories/settings_graders', - 'js/factories/textbooks', - 'js/factories/videos_index', - 'js/factories/xblock_validation' + 'js/factories/videos_index' ]), /** * By default all the configuration for optimization happens from the command diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js index c2570289b0..875b57d013 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -1,86 +1,87 @@ /* globals AjaxPrefix */ -(function(AjaxPrefix) { +define([ + 'domReady', + 'jquery', + 'underscore', + 'underscore.string', + 'backbone', + 'gettext', + '../../common/js/components/views/feedback_notification', + 'jquery.cookie' +], function(domReady, $, _, str, Backbone, gettext, NotificationView) { 'use strict'; - define([ - 'domReady', - 'jquery', - 'underscore.string', - 'backbone', - 'gettext', - '../../common/js/components/views/feedback_notification', - 'jquery.cookie' - ], function(domReady, $, str, Backbone, gettext, NotificationView) { - var main, sendJSON; - main = function() { - AjaxPrefix.addAjaxPrefix(jQuery, function() { - return $("meta[name='path_prefix']").attr('content'); + + 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 }); - 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') - }, + 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', - 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'); - } + data: JSON.stringify(data), + success: callback, + global: data ? data.global : true // Trigger global AJAX error handler or not }); }; - main(); - return main; - }); -}).call(this, AjaxPrefix); + $.postJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign + return sendJSON(url, data, callback, 'POST'); + }; + $.patchJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign + return sendJSON(url, data, callback, 'PATCH'); + }; + return domReady(function() { + if (window.onTouchBasedDevice()) { + return $('body').addClass('touch-based-device'); + } + return null; + }); + }; + main(); + return main; +}); diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index eb0d03c706..1f32ced8d6 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -4,6 +4,7 @@ (function(requirejs, requireSerial) { 'use strict'; + var i, specHelpers, testFiles; if (window) { define('add-a11y-deps', [ @@ -20,8 +21,6 @@ }); } - var i, specHelpers, testFiles; - requirejs.config({ baseUrl: '/base/', paths: { @@ -230,7 +229,6 @@ testFiles = [ 'cms/js/spec/main_spec', - 'cms/js/spec/xblock/cms.runtime.v1_spec', 'js/spec/models/course_spec', 'js/spec/models/metadata_spec', 'js/spec/models/section_spec', @@ -263,32 +261,21 @@ 'js/spec/views/previous_video_upload_list_spec', 'js/spec/views/assets_spec', 'js/spec/views/baseview_spec', - 'js/spec/views/container_spec', - 'js/spec/views/module_edit_spec', 'js/spec/views/paged_container_spec', 'js/spec/views/group_configuration_spec', 'js/spec/views/unit_outline_spec', 'js/spec/views/xblock_spec', - 'js/spec/views/xblock_editor_spec', - 'js/spec/views/xblock_string_field_editor_spec', 'js/spec/views/xblock_validation_spec', 'js/spec/views/license_spec', 'js/spec/views/paging_spec', - 'js/spec/views/login_studio_spec', - 'js/spec/views/pages/container_spec', - 'js/spec/views/pages/container_subviews_spec', 'js/spec/views/pages/group_configurations_spec', - 'js/spec/views/pages/course_outline_spec', 'js/spec/views/pages/course_rerun_spec', 'js/spec/views/pages/index_spec', 'js/spec/views/pages/library_users_spec', 'js/spec/views/modals/base_modal_spec', - 'js/spec/views/modals/edit_xblock_spec', 'js/spec/views/modals/move_xblock_modal_spec', 'js/spec/views/modals/validation_error_modal_spec', - 'js/spec/views/move_xblock_spec', 'js/spec/views/settings/main_spec', - 'js/spec/factories/xblock_validation_spec', 'js/certificates/spec/models/certificate_spec', 'js/certificates/spec/views/certificate_details_spec', 'js/certificates/spec/views/certificate_editor_spec', diff --git a/cms/static/cms/js/spec/main_webpack.js b/cms/static/cms/js/spec/main_webpack.js new file mode 100644 index 0000000000..5cea9bfd0a --- /dev/null +++ b/cms/static/cms/js/spec/main_webpack.js @@ -0,0 +1,35 @@ +jasmine.getFixtures().fixturesPath = '/base/templates'; + +import 'common/js/spec_helpers/jasmine-extensions'; +import 'common/js/spec_helpers/jasmine-stealth'; +import 'common/js/spec_helpers/jasmine-waituntil'; + +// These libraries are used by the tests (and the code under test) +// but not explicitly imported +import 'jquery.ui'; + +import _ from 'underscore'; +import str from 'underscore.string'; +import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils'; +import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; +window._ = _; +window._.str = str; +window.edx = window.edx || {}; +window.edx.HtmlUtils = HtmlUtils; +window.edx.StringUtils = StringUtils; + +// These are the tests that will be run +import './xblock/cms.runtime.v1_spec.js'; +import '../../../js/spec/factories/xblock_validation_spec.js'; +import '../../../js/spec/views/container_spec.js'; +import '../../../js/spec/views/login_studio_spec.js'; +import '../../../js/spec/views/modals/edit_xblock_spec.js'; +import '../../../js/spec/views/module_edit_spec.js'; +import '../../../js/spec/views/move_xblock_spec.js'; +import '../../../js/spec/views/pages/container_spec.js'; +import '../../../js/spec/views/pages/container_subviews_spec.js'; +import '../../../js/spec/views/pages/course_outline_spec.js'; +import '../../../js/spec/views/xblock_editor_spec.js'; +import '../../../js/spec/views/xblock_string_field_editor_spec.js'; + +window.__karma__.start(); // eslint-disable-line no-underscore-dangle diff --git a/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js index 893fe6827a..72c72b1e30 100644 --- a/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js +++ b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js @@ -1,81 +1,82 @@ -define(['js/spec_helpers/edit_helpers', 'js/views/modals/base_modal', 'xblock/cms.runtime.v1'], - function(EditHelpers, BaseModal) { - 'use strict'; +import EditHelpers from 'js/spec_helpers/edit_helpers'; +import BaseModal from 'js/views/modals/base_modal'; +import 'xblock/cms.runtime.v1'; - describe('Studio Runtime v1', function() { - var runtime; +describe('Studio Runtime v1', function() { + 'use strict'; - beforeEach(function() { - EditHelpers.installEditTemplates(); - runtime = new window.StudioRuntime.v1(); + var runtime; + + 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('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); + }); + + describe('Modal Dialogs', function() { + var MockModal, modal, showMockModal; + + MockModal = BaseModal.extend({ + getContentHtml: function() { + return readFixtures('mock/mock-modal.underscore'); + } + }); + + showMockModal = function() { + modal = new MockModal({ + title: 'Mock Modal' }); + modal.show(); + }; - it('allows events to be listened to', function() { - var canceled = false; - runtime.listenTo('cancel', function() { - canceled = true; - }); - expect(canceled).toBeFalsy(); - runtime.notify('cancel', {}); - expect(canceled).toBeTruthy(); - }); + beforeEach(function() { + EditHelpers.installEditTemplates(); + }); - 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); - }); + afterEach(function() { + EditHelpers.hideModalIfShowing(modal); + }); - 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; - - MockModal = BaseModal.extend({ - getContentHtml: function() { - return readFixtures('mock/mock-modal.underscore'); - } - }); - - showMockModal = function() { - modal = new MockModal({ - title: 'Mock Modal' - }); - modal.show(); - }; - - beforeEach(function() { - EditHelpers.installEditTemplates(); - }); - - 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 4ea35feda9..0f7fa7f1da 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -26,8 +26,35 @@ define([ IframeUtils, DropdownMenuView ) { + 'use strict'; var $body; + function smoothScrollLink(e) { + (e).preventDefault(); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') + }); + } + + function hideNotification(e) { + (e).preventDefault(); + $(this) + .closest('.wrapper-notification') + .removeClass('is-shown') + .addClass('is-hiding') + .attr('aria-hidden', 'true'); + } + + function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + } + domReady(function() { var dropdownMenuView; @@ -44,14 +71,14 @@ define([ $('.action-notification-close').bind('click', hideNotification); // nav - dropdown related - $body.click(function(e) { + $body.click(function() { $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .title').removeClass('is-selected'); }); $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { - $subnav = $(this).find('.wrapper-nav-sub'); - $title = $(this).find('.title'); + var $subnav = $(this).find('.wrapper-nav-sub'), + $title = $(this).find('.title'); if ($subnav.hasClass('is-shown')) { $subnav.removeClass('is-shown'); @@ -68,7 +95,8 @@ define([ }); // general link management - new window/tab - $('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab')); + $('a[rel="external"]:not([title])') + .attr('title', gettext('This link will open in a new browser window/tab')); $('a[rel="external"]').attr('target', '_blank'); // general link management - lean modal window @@ -97,39 +125,7 @@ define([ }); dropdownMenuView.postRender(); } + + 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 7f61b473c9..a714393573 100644 --- a/cms/static/js/factories/base.js +++ b/cms/static/js/factories/base.js @@ -1,3 +1,5 @@ +// We can't convert this to an es6 module until all factories that use it have been converted out +// of RequireJS define(['js/base', 'cms/js/main', 'js/src/logger', 'datepair', 'accessibility', 'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course'], function() { diff --git a/cms/static/js/factories/container.js b/cms/static/js/factories/container.js index 8861d6f124..cfca298851 100644 --- a/cms/static/js/factories/container.js +++ b/cms/static/js/factories/container.js @@ -1,21 +1,26 @@ -define([ - 'jquery', 'underscore', 'js/models/xblock_container_info', 'js/views/pages/container', - 'js/collections/component_template', 'xmodule', 'cms/js/main', - 'xblock/cms.runtime.v1' -], -function($, _, XBlockContainerInfo, ContainerPage, ComponentTemplates, xmoduleLoader) { - 'use strict'; - return function(componentTemplates, XBlockInfoJson, action, options) { - var main_options = { - el: $('#content'), - model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}), - action: action, - templates: new ComponentTemplates(componentTemplates, {parse: true}) - }; +import * as $ from 'jquery'; +import * as _ from 'underscore'; +import * as XBlockContainerInfo from 'js/models/xblock_container_info'; +import * as ContainerPage from 'js/views/pages/container'; +import * as ComponentTemplates from 'js/collections/component_template'; +import * as xmoduleLoader from 'xmodule'; +import './base'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - xmoduleLoader.done(function() { - var view = new ContainerPage(_.extend(main_options, options)); - view.render(); - }); +'use strict'; +export default function ContainerFactory(componentTemplates, XBlockInfoJson, action, options) { + var main_options = { + el: $('#content'), + model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}), + action: action, + templates: new ComponentTemplates(componentTemplates, {parse: true}) }; -}); + + xmoduleLoader.done(function() { + var view = new ContainerPage(_.extend(main_options, options)); + view.render(); + }); +}; + +export {ContainerFactory} diff --git a/cms/static/js/factories/context_course.js b/cms/static/js/factories/context_course.js new file mode 100644 index 0000000000..475e5a6282 --- /dev/null +++ b/cms/static/js/factories/context_course.js @@ -0,0 +1,3 @@ +import * as ContextCourse from 'js/models/course'; + +export {ContextCourse} diff --git a/cms/static/js/factories/edit_tabs.js b/cms/static/js/factories/edit_tabs.js index fddb8648a6..9f2912fc91 100644 --- a/cms/static/js/factories/edit_tabs.js +++ b/cms/static/js/factories/edit_tabs.js @@ -1,20 +1,25 @@ -define([ - 'js/models/explicit_url', 'js/views/tabs', 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1' -], function(TabsModel, TabsEditView, xmoduleLoader) { - 'use strict'; - return function(courseLocation, explicitUrl) { - xmoduleLoader.done(function() { - var model = new TabsModel({ - id: courseLocation, - explicit_url: explicitUrl - }), - editView; +import * as TabsModel from 'js/models/explicit_url'; +import * as TabsEditView from 'js/views/tabs'; +import * as xmoduleLoader from 'xmodule'; +import './base'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - editView = new TabsEditView({ - el: $('.tab-list'), - model: model, - mast: $('.wrapper-mast') - }); +'use strict'; +export default function EditTabsFactory(courseLocation, explicitUrl) { + xmoduleLoader.done(function() { + var model = new TabsModel({ + id: courseLocation, + explicit_url: explicitUrl + }), + editView; + + editView = new TabsEditView({ + el: $('.tab-list'), + model: model, + mast: $('.wrapper-mast') }); - }; -}); + }); +}; + +export {EditTabsFactory} diff --git a/cms/static/js/factories/library.js b/cms/static/js/factories/library.js index e6eb929069..4cde6873f9 100644 --- a/cms/static/js/factories/library.js +++ b/cms/static/js/factories/library.js @@ -1,23 +1,28 @@ -define([ - 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/paged_container', - 'js/views/library_container', 'js/collections/component_template', 'xmodule', 'cms/js/main', - 'xblock/cms.runtime.v1' -], -function($, _, XBlockInfo, PagedContainerPage, LibraryContainerView, ComponentTemplates, xmoduleLoader) { - 'use strict'; - return function(componentTemplates, XBlockInfoJson, options) { - var main_options = { - el: $('#content'), - model: new XBlockInfo(XBlockInfoJson, {parse: true}), - templates: new ComponentTemplates(componentTemplates, {parse: true}), - action: 'view', - viewClass: LibraryContainerView, - canEdit: true - }; +import * as $ from 'jquery'; +import * as _ from 'underscore'; +import * as XBlockInfo from 'js/models/xblock_info'; +import * as PagedContainerPage from 'js/views/pages/paged_container'; +import * as LibraryContainerView from 'js/views/library_container'; +import * as ComponentTemplates from 'js/collections/component_template'; +import * as xmoduleLoader from 'xmodule'; +import 'cms/js/main'; +import 'xblock/cms.runtime.v1'; - xmoduleLoader.done(function() { - var view = new PagedContainerPage(_.extend(main_options, options)); - view.render(); - }); +'use strict'; +export default function LibraryFactory(componentTemplates, XBlockInfoJson, options) { + var main_options = { + el: $('#content'), + model: new XBlockInfo(XBlockInfoJson, {parse: true}), + templates: new ComponentTemplates(componentTemplates, {parse: true}), + action: 'view', + viewClass: LibraryContainerView, + canEdit: true }; -}); + + xmoduleLoader.done(function() { + var view = new PagedContainerPage(_.extend(main_options, options)); + view.render(); + }); +}; + +export {LibraryFactory} diff --git a/cms/static/js/factories/login.js b/cms/static/js/factories/login.js index fdbcef31e8..b528e075a2 100644 --- a/cms/static/js/factories/login.js +++ b/cms/static/js/factories/login.js @@ -1,57 +1,63 @@ -define(['jquery.cookie', 'utility', 'common/js/components/utils/view_utils'], function(cookie, utility, ViewUtils) { - 'use strict'; - return function LoginFactory(homepageURL) { - function postJSON(url, data, callback) { - $.ajax({ - type: 'POST', - url: url, - dataType: 'json', - data: data, - success: callback - }); - } - // Clear the login error message when credentials are edited - $('input#email').on('input', function() { - $('#login_error').removeClass('is-shown'); +'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 }); + } - $('input#password').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'); + }); - $('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]); - } else { - ViewUtils.redirect(homepageURL); - } - } else if ($('#login_error').length === 0) { - $('#login_form').prepend( - '' - ); - $('#login_error').addClass('is-shown'); - deferred.resolve(); + $('form#login_form').submit(function (event) { + event.preventDefault(); + var $submitButton = $('#submit'), + deferred = new $.Deferred(), + promise = deferred.promise(); + ViewUtils.disableElementWhileRunning($submitButton, function () { return promise; }); + var submit_data = $('#login_form').serialize(); + + postJSON('/login_post', submit_data, function (json) { + if (json.success) { + var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search)); + if (next && next.length > 1 && !isExternal(next[1])) { + ViewUtils.redirect(next[1]); } else { - $('#login_error') - .stop() - .addClass('is-shown') - .html(json.value); - deferred.resolve(); + ViewUtils.redirect(homepageURL); } - }); + } else if ($('#login_error').length === 0) { + $('#login_form').prepend( + '' + ); + $('#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 0641e025d3..2fd11e407f 100644 --- a/cms/static/js/factories/textbooks.js +++ b/cms/static/js/factories/textbooks.js @@ -1,20 +1,23 @@ -define([ - 'gettext', 'js/models/section', 'js/collections/textbook', 'js/views/list_textbooks' -], function(gettext, Section, TextbookCollection, ListTextbooksView) { - 'use strict'; - return function(textbooksJson) { - var textbooks = new TextbookCollection(textbooksJson, {parse: true}), - tbView = new ListTextbooksView({collection: textbooks}); +import * as gettext from 'gettext'; +import * as Section from 'js/models/section'; +import * as TextbookCollection from 'js/collections/textbook'; +import * as ListTextbooksView from 'js/views/list_textbooks'; - $('.content-primary').append(tbView.render().el); - $('.nav-actions .new-button').click(function(event) { - tbView.addOne(event); - }); - $(window).on('beforeunload', function() { - var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); - if (dirty) { - return gettext('You have unsaved changes. Do you really want to leave this page?'); - } - }); - }; -}); +'use strict'; +export default function TextbooksFactory(textbooksJson) { + var textbooks = new TextbookCollection(textbooksJson, {parse: true}), + tbView = new ListTextbooksView({collection: textbooks}); + + $('.content-primary').append(tbView.render().el); + $('.nav-actions .new-button').click(function(event) { + tbView.addOne(event); + }); + $(window).on('beforeunload', function() { + var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); + if (dirty) { + return gettext('You have unsaved changes. Do you really want to leave this page?'); + } + }); +}; + +export {TextbooksFactory} diff --git a/cms/static/js/factories/xblock_validation.js b/cms/static/js/factories/xblock_validation.js index 4bcfc3b339..56786c89d9 100644 --- a/cms/static/js/factories/xblock_validation.js +++ b/cms/static/js/factories/xblock_validation.js @@ -1,19 +1,22 @@ -define(['js/views/xblock_validation', 'js/models/xblock_validation'], -function(XBlockValidationView, XBlockValidationModel) { - 'use strict'; - return function(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { - var model, response; - if (hasEditingUrl && !isRoot) { - validationMessages.showSummaryOnly = true; - } - response = validationMessages; - response.isUnit = isUnit; +import * as XBlockValidationView from 'js/views/xblock_validation'; +import * as XBlockValidationModel from 'js/models/xblock_validation'; - model = new XBlockValidationModel(response, {parse: true}); +'use strict'; +export default function XBlockValidationFactory(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) { + var model, response; - if (!model.get('empty')) { - new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render(); - } - }; -}); + if (hasEditingUrl && !isRoot) { + validationMessages.showSummaryOnly = true; + } + response = validationMessages; + response.isUnit = isUnit; + + model = new XBlockValidationModel(response, {parse: true}); + + if (!model.get('empty')) { + new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render(); + } +}; + +export {XBlockValidationFactory} diff --git a/cms/static/js/pages/course.js b/cms/static/js/pages/course.js deleted file mode 100644 index cf0ca8d5ef..0000000000 --- a/cms/static/js/pages/course.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ['js/models/course'], - function(ContextCourse) { - window.course = new ContextCourse(window.pageFactoryArguments.ContextCourse); - } -); diff --git a/cms/static/js/pages/login.js b/cms/static/js/pages/login.js deleted file mode 100644 index 449a1190f6..0000000000 --- a/cms/static/js/pages/login.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ['js/factories/login', 'common/js/utils/page_factory', 'js/factories/base'], - function(LoginFactory, invokePageFactory) { - 'use strict'; - invokePageFactory('LoginFactory', LoginFactory); - } -); - diff --git a/cms/static/js/pages/textbooks.js b/cms/static/js/pages/textbooks.js deleted file mode 100644 index 7d524fbd29..0000000000 --- a/cms/static/js/pages/textbooks.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ['js/factories/textbooks', 'common/js/utils/page_factory', 'js/factories/base', 'js/pages/course'], - function(TextbooksFactory, invokePageFactory) { - 'use strict'; - invokePageFactory('TextbooksFactory', TextbooksFactory); - } -); diff --git a/cms/static/js/sock.js b/cms/static/js/sock.js index 44653981ff..fe6b331e46 100644 --- a/cms/static/js/sock.js +++ b/cms/static/js/sock.js @@ -1,39 +1,41 @@ -define(['domReady', 'jquery', 'jquery.smoothScroll'], - function(domReady, $) { - 'use strict'; +import * as domReady from 'domReady'; +import * as $ from 'jquery'; +import 'jquery.smoothScroll'; - var toggleSock = function(e) { - e.preventDefault(); +'use strict'; - var $btnShowSockLabel = $(this).find('.copy-show'); - var $btnHideSockLabel = $(this).find('.copy-hide'); - var $sock = $('.wrapper-sock'); - var $sockContent = $sock.find('.wrapper-inner'); +var toggleSock = function (e) { + e.preventDefault(); - if ($sock.hasClass('is-shown')) { - $sock.removeClass('is-shown'); - $sockContent.hide('fast'); - $btnHideSockLabel.removeClass('is-shown').addClass('is-hidden'); - $btnShowSockLabel.removeClass('is-hidden').addClass('is-shown'); - } else { - $sock.addClass('is-shown'); - $sockContent.show('fast'); - $btnHideSockLabel.removeClass('is-hidden').addClass('is-shown'); - $btnShowSockLabel.removeClass('is-shown').addClass('is-hidden'); - } + var $btnShowSockLabel = $(this).find('.copy-show'); + var $btnHideSockLabel = $(this).find('.copy-hide'); + var $sock = $('.wrapper-sock'); + var $sockContent = $sock.find('.wrapper-inner'); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $sock - }); - }; - - domReady(function() { - // toggling footer additional support - $('.cta-show-sock').bind('click', toggleSock); - }); + if ($sock.hasClass('is-shown')) { + $sock.removeClass('is-shown'); + $sockContent.hide('fast'); + $btnHideSockLabel.removeClass('is-shown').addClass('is-hidden'); + $btnShowSockLabel.removeClass('is-hidden').addClass('is-shown'); + } else { + $sock.addClass('is-shown'); + $sockContent.show('fast'); + $btnHideSockLabel.removeClass('is-hidden').addClass('is-shown'); + $btnShowSockLabel.removeClass('is-shown').addClass('is-hidden'); } -); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $sock + }); +}; + +domReady(function () { + // toggling footer additional support + $('.cta-show-sock').bind('click', toggleSock); +}); + +export { toggleSock } diff --git a/cms/static/js/spec/factories/xblock_validation_spec.js b/cms/static/js/spec/factories/xblock_validation_spec.js index 5ba9380981..ca14f88c09 100644 --- a/cms/static/js/spec/factories/xblock_validation_spec.js +++ b/cms/static/js/spec/factories/xblock_validation_spec.js @@ -1,77 +1,77 @@ -define(['jquery', 'js/factories/xblock_validation', 'common/js/spec_helpers/template_helpers'], - function($, XBlockValidationFactory, TemplateHelpers) { - describe('XBlockValidationFactory', function() { - var $messageDiv; +import $ from 'jquery'; +import XBlockValidationFactory from 'js/factories/xblock_validation'; +import TemplateHelpers from 'common/js/spec_helpers/template_helpers'; - beforeEach(function() { - TemplateHelpers.installTemplate('xblock-validation-messages'); - appendSetFixtures($('')); - $messageDiv = $('.messages'); - }); +describe('XBlockValidationFactory', () => { + var $messageDiv; - it('Does not attach a view if messages is empty', function() { - XBlockValidationFactory({empty: true}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(0); - }); + beforeEach(function() { + TemplateHelpers.installTemplate('xblock-validation-messages'); + appendSetFixtures($('')); + $messageDiv = $('.messages'); + }); - it('Does attach a view if messages are not empty', function() { - XBlockValidationFactory({empty: false}, false, false, false, $messageDiv); - expect($messageDiv.children().length).toEqual(1); - }); + it('Does not attach a view if messages is empty', function() { + XBlockValidationFactory({empty: true}, false, false, false, $messageDiv); + expect($messageDiv.children().length).toEqual(0); + }); - it('Passes through the root property to the view.', function() { - var noContainerContent = 'no-container-content'; + it('Does attach a view if messages are not empty', function() { + XBlockValidationFactory({empty: false}, false, false, false, $messageDiv); + expect($messageDiv.children().length).toEqual(1); + }); - var notConfiguredMessages = { - empty: false, - summary: {text: 'my summary', type: 'not-configured'}, - messages: [], - xblock_id: 'id' - }; - // Root is false, will not add noContainerContent. - XBlockValidationFactory(notConfiguredMessages, true, false, false, $messageDiv); - expect($messageDiv.find('.validation')).not.toHaveClass(noContainerContent); + it('Passes through the root property to the view.', function() { + var noContainerContent = 'no-container-content'; - // Root is true, will add noContainerContent. - XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); - expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); - }); + var notConfiguredMessages = { + empty: false, + summary: {text: 'my summary', type: 'not-configured'}, + messages: [], + xblock_id: 'id' + }; + // Root is false, will not add noContainerContent. + XBlockValidationFactory(notConfiguredMessages, true, false, false, $messageDiv); + expect($messageDiv.find('.validation')).not.toHaveClass(noContainerContent); - describe('Controls display of detailed messages based on url and root property', function() { - var messagesWithSummary, checkDetailedMessages; + // Root is true, will add noContainerContent. + XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv); + expect($messageDiv.find('.validation')).toHaveClass(noContainerContent); + }); - beforeEach(function() { - messagesWithSummary = { - empty: false, - summary: {text: 'my summary'}, - messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}], - xblock_id: 'id' - }; - }); + describe('Controls display of detailed messages based on url and root property', function() { + var messagesWithSummary, checkDetailedMessages; - checkDetailedMessages = function(expectedDetailedMessages) { - expect($messageDiv.children().length).toEqual(1); - expect($messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages); - }; - - 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); - - 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); - }); - }); + beforeEach(function() { + messagesWithSummary = { + empty: false, + summary: {text: 'my summary'}, + messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}], + xblock_id: 'id' + }; }); - } -); + + checkDetailedMessages = function(expectedDetailedMessages) { + expect($messageDiv.children().length).toEqual(1); + expect($messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages); + }; + + it('Does not show details if xblock has an editing URL and it is not rendered as root', function() { + XBlockValidationFactory(messagesWithSummary, true, false, false, $messageDiv); + checkDetailedMessages(0); + }); + + it('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); + }); + + 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 b61313de31..1c56f9a479 100644 --- a/cms/static/js/spec/utils/drag_and_drop_spec.js +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -309,6 +309,7 @@ define(['sinon', 'js/utils/drag_and_drop', 'common/js/components/views/feedback_ }); afterEach(function() { this.clock.restore(); + jasmine.stealth.clearSpies(); }); it('should send an update on reorder from one parent to another', function() { var requests, request, savingOptions; diff --git a/cms/static/js/spec/views/container_spec.js b/cms/static/js/spec/views/container_spec.js index bc33ccd31a..194d405d0c 100644 --- a/cms/static/js/spec/views/container_spec.js +++ b/cms/static/js/spec/views/container_spec.js @@ -1,198 +1,205 @@ -define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers', - 'js/views/container', 'js/models/xblock_info', 'jquery.simulate', - 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'], - function($, AjaxHelpers, EditHelpers, ContainerView, XBlockInfo) { - describe('Container View', function() { - describe('Supports reordering components', function() { - var model, containerView, mockContainerHTML, init, getComponent, - getDragHandle, dragComponentVertically, dragComponentAbove, - verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy, +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'; - rootLocator = 'locator-container', - containerTestUrl = '/xblock/' + rootLocator, +describe('Container View', () => { + describe('Supports reordering components', () => { + var model, containerView, mockContainerHTML, init, getComponent, + getDragHandle, dragComponentVertically, dragComponentAbove, + verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy, - groupAUrl = '/xblock/locator-group-A', - groupA = 'locator-group-A', - groupAComponent1 = 'locator-component-A1', - groupAComponent2 = 'locator-component-A2', - groupAComponent3 = 'locator-component-A3', + rootLocator = 'locator-container', + containerTestUrl = '/xblock/' + rootLocator, - groupBUrl = '/xblock/locator-group-B', - groupB = 'locator-group-B', - groupBComponent1 = 'locator-component-B1', - groupBComponent2 = 'locator-component-B2', - groupBComponent3 = 'locator-component-B3'; + groupAUrl = '/xblock/locator-group-A', + groupA = 'locator-group-A', + groupAComponent1 = 'locator-component-A1', + groupAComponent2 = 'locator-component-A2', + groupAComponent3 = 'locator-component-A3', - mockContainerHTML = readFixtures('mock/mock-container-xblock.underscore'); + groupBUrl = '/xblock/locator-group-B', + groupB = 'locator-group-B', + groupBComponent1 = 'locator-component-B1', + groupBComponent2 = 'locator-component-B2', + groupBComponent3 = 'locator-component-B3'; - beforeEach(function() { - EditHelpers.installMockXBlock(); - EditHelpers.installViewTemplates(); - appendSetFixtures(''); - notificationSpy = EditHelpers.createNotificationSpy(); - model = new XBlockInfo({ - id: rootLocator, - display_name: 'Test AB Test', - category: 'split_test' - }); + mockContainerHTML = readFixtures('templates/mock/mock-container-xblock.underscore'); - containerView = new ContainerView({ - model: model, - view: 'container_preview', - el: $('.wrapper-xblock') - }); - }); + beforeEach(() => { + EditHelpers.installMockXBlock(); + EditHelpers.installViewTemplates(); + appendSetFixtures(''); + notificationSpy = EditHelpers.createNotificationSpy(); + model = new XBlockInfo({ + id: rootLocator, + display_name: 'Test AB Test', + category: 'split_test' + }); - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - containerView.remove(); - }); + containerView = new ContainerView({ + model: model, + view: 'container_preview', + el: $('.wrapper-xblock') + }); + }); - init = function(caller) { - var requests = AjaxHelpers.requests(caller); - containerView.render(); + afterEach(() => { + EditHelpers.uninstallMockXBlock(); + containerView.remove(); + }); - AjaxHelpers.respondWithJson(requests, { - html: mockContainerHTML, - resources: [] - }); + init = function(caller) { + var requests = AjaxHelpers.requests(caller); + containerView.render(); - $('body').append(containerView.$el); + AjaxHelpers.respondWithJson(requests, { + html: mockContainerHTML, + resources: [] + }); - // Give the whole container enough height to contain everything. - $('.xblock[data-locator=locator-container]').css('height', 2000); + $('body').append(containerView.$el); - // 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 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); + // 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; - }; + return requests; + }; - getComponent = function(locator) { - return containerView.$('.studio-xblock-wrapper[data-locator="' + locator + '"]'); - }; + getComponent = function(locator) { + return containerView.$('.studio-xblock-wrapper[data-locator="' + locator + '"]'); + }; - getDragHandle = function(locator) { - var component = getComponent(locator); - return $(component.find('.drag-handle')[0]); - }; + 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}); - }; + 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}); - }; + 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]); - } - }; + 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); - }; + 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); - }; + 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); - 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]); - }); + // Now process the actual request + AjaxHelpers.respond(requests, {statusCode: status}); + }; - 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); + 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]); + }); - // 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); + 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); - verifyRequest(requests, 0, groupAUrl, - [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); - verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]); - }); + // 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); - 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); - }); + verifyRequest(requests, 0, groupAUrl, + [groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]); + verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]); + }); - 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]); - }); + 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); + }); - describe('Shows a saving message', function() { - it('hides saving message upon success', function() { - var requests, savingOptions; - requests = init(this); + 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]); + }); - // 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); - }); + describe('Shows a saving message', () => { + it('hides saving message upon success', () => { + var requests, savingOptions; + requests = init(this); - 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, 200); + EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving'); + respondToRequest(requests, 1, 200); + EditHelpers.verifyNotificationHidden(notificationSpy); + }); - // 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'); + it('does not hide saving message if failure', () => { + var requests = init(this); - // Since the first reorder call failed, the removal will not be called. - verifyNumReorderCalls(requests, 1); - }); - }); + // 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 d20ca233ef..38a01dc311 100644 --- a/cms/static/js/spec/views/login_studio_spec.js +++ b/cms/static/js/spec/views/login_studio_spec.js @@ -1,32 +1,35 @@ -define(['jquery', 'js/factories/login', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/components/utils/view_utils'], -function($, LoginFactory, AjaxHelpers, ViewUtils) { - 'use strict'; - describe('Studio Login Page', function() { - var $submitButton; - beforeEach(function() { - loadFixtures('mock/login.underscore'); - var login_factory = new LoginFactory('/home/'); - $submitButton = $('#submit'); - }); +'use strict'; - it('disable the submit button once it is clicked', function() { - spyOn(ViewUtils, 'redirect').and.callFake(function() {}); - var requests = AjaxHelpers.requests(this); - expect($submitButton).not.toHaveClass('is-disabled'); - $submitButton.click(); - AjaxHelpers.respondWithJson(requests, {success: true}); - expect($submitButton).toHaveClass('is-disabled'); - }); +import $ from 'jquery'; +import LoginFactory from 'js/factories/login'; +import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'; +import ViewUtils from 'common/js/components/utils/view_utils'; - it('It will not disable the submit button if there are errors in ajax request', function() { - var requests = AjaxHelpers.requests(this); - expect($submitButton).not.toHaveClass('is-disabled'); - $submitButton.click(); - expect($submitButton).toHaveClass('is-disabled'); - AjaxHelpers.respondWithError(requests, {}); - expect($submitButton).not.toHaveClass('is-disabled'); - }); +describe('Studio Login Page', () => { + var $submitButton; + + beforeEach(function() { + loadFixtures('mock/login.underscore'); + var login_factory = LoginFactory('/home/'); + $submitButton = $('#submit'); + }); + + it('disable the submit button once it is clicked', function() { + spyOn(ViewUtils, 'redirect').and.callFake(function() {}); + var requests = AjaxHelpers.requests(this); + expect($submitButton).not.toHaveClass('is-disabled'); + $submitButton.click(); + AjaxHelpers.respondWithJson(requests, {success: true}); + expect($submitButton).toHaveClass('is-disabled'); + }); + + it('It will not disable the submit button if there are errors in ajax request', function() { + var requests = AjaxHelpers.requests(this); + expect($submitButton).not.toHaveClass('is-disabled'); + $submitButton.click(); + expect($submitButton).toHaveClass('is-disabled'); + AjaxHelpers.respondWithError(requests, {}); + expect($submitButton).not.toHaveClass('is-disabled'); }); }); diff --git a/cms/static/js/spec/views/modals/edit_xblock_spec.js b/cms/static/js/spec/views/modals/edit_xblock_spec.js index a076c5ee4c..8a28acab5b 100644 --- a/cms/static/js/spec/views/modals/edit_xblock_spec.js +++ b/cms/static/js/spec/views/modals/edit_xblock_spec.js @@ -1,211 +1,215 @@ -define(['jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'js/spec_helpers/edit_helpers', 'js/views/modals/edit_xblock', 'js/models/xblock_info'], - function($, _, Backbone, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) { - 'use strict'; - describe('EditXBlockModal', function() { - var model, modal, showModal; +'use strict'; - showModal = function(requests, mockHtml, options) { - var $xblockElement = $('.xblock'); - return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options); - }; +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'; - beforeEach(function() { - EditHelpers.installEditTemplates(); - appendSetFixtures(''); - model = new XBlockInfo({ - id: 'testCourse/branch/draft/block/verticalFFF', - display_name: 'Test Unit', - category: 'vertical' - }); - }); +describe('EditXBlockModal', function() { + var model, modal, showModal; - afterEach(function() { - EditHelpers.cancelModalIfShowing(); - }); + showModal = function(requests, mockHtml, options) { + var $xblockElement = $('.xblock'); + return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options); + }; - describe('XBlock Editor', function() { - var mockXBlockEditorHtml; + beforeEach(function() { + EditHelpers.installEditTemplates(); + appendSetFixtures(''); + model = new XBlockInfo({ + id: 'testCourse/branch/draft/block/verticalFFF', + display_name: 'Test Unit', + category: 'vertical' + }); + }); - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); - beforeEach(function() { - EditHelpers.installMockXBlock(); - spyOn(Backbone, 'trigger').and.callThrough(); - }); + describe('XBlock Editor', function() { + var mockXBlockEditorHtml; - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - }); + mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore'); - 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(); - }); + beforeEach(function() { + EditHelpers.installMockXBlock(); + spyOn(Backbone, 'trigger').and.callThrough(); + }); - 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'); - }); + afterEach(function() { + EditHelpers.uninstallMockXBlock(); + }); - 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('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 any editor mode buttons', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockXBlockEditorHtml); - expect(modal.$('.editor-modes a').length).toBe(0); - }); + 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('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('shows the correct title', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); + }); - 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'); - }); + 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); + }); - describe('Custom Buttons', function() { - var mockCustomButtonsHtml; + 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'); + }); - mockCustomButtonsHtml = readFixtures('mock/mock-xblock-editor-with-custom-buttons.underscore'); + 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'); + }); - it('hides the modal\'s button bar', function() { - var requests = AjaxHelpers.requests(this); - modal = showModal(requests, mockCustomButtonsHtml); - expect(modal.$('.modal-actions')).toBeHidden(); - }); - }); - }); + describe('Custom Buttons', function() { + var mockCustomButtonsHtml; - describe('XModule Editor', function() { - var mockXModuleEditorHtml; + mockCustomButtonsHtml = readFixtures('templates/mock/mock-xblock-editor-with-custom-buttons.underscore'); - 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; - - mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-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('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('hides the modal\'s button bar', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockCustomButtonsHtml); + expect(modal.$('.modal-actions')).toBeHidden(); }); }); }); + + describe('XModule Editor', function() { + var mockXModuleEditorHtml; + + mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-editor.underscore'); + + beforeEach(function() { + EditHelpers.installMockXModule(); + }); + + afterEach(function() { + EditHelpers.uninstallMockXModule(); + }); + + it('can render itself', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); + EditHelpers.cancelModal(modal); + expect(EditHelpers.isShowingModal(modal)).toBeFalsy(); + }); + + it('shows the correct title', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.modal-window-title').text()).toBe('Editing: Component'); + }); + + it('shows the correct default buttons', function() { + var requests = AjaxHelpers.requests(this), + editorButton, + settingsButton; + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.editor-modes a').length).toBe(2); + editorButton = modal.$('.editor-button'); + settingsButton = modal.$('.settings-button'); + expect(editorButton.length).toBe(1); + expect(editorButton).toHaveClass('is-set'); + expect(settingsButton.length).toBe(1); + expect(settingsButton).not.toHaveClass('is-set'); + }); + + it('can switch tabs', function() { + var requests = AjaxHelpers.requests(this), + editorButton, + settingsButton; + modal = showModal(requests, mockXModuleEditorHtml); + expect(modal.$('.editor-modes a').length).toBe(2); + editorButton = modal.$('.editor-button'); + settingsButton = modal.$('.settings-button'); + expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); + settingsButton.click(); + expect(modal.$('.metadata_edit')).toHaveClass('is-active'); + editorButton.click(); + expect(modal.$('.metadata_edit')).toHaveClass('is-inactive'); + }); + + describe('Custom Tabs', function() { + var mockCustomTabsHtml; + + mockCustomTabsHtml = readFixtures('templates/mock/mock-xmodule-editor-with-custom-tabs.underscore'); + + it('hides the modal\'s header', function() { + var requests = AjaxHelpers.requests(this); + modal = showModal(requests, mockCustomTabsHtml); + expect(modal.$('.modal-header')).toBeHidden(); + }); + + 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; + + mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-settings-only-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('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 21903b2226..04f2d2797e 100644 --- a/cms/static/js/spec/views/module_edit_spec.js +++ b/cms/static/js/spec/views/module_edit_spec.js @@ -1,37 +1,58 @@ -(function() { - 'use strict'; - define([ - 'jquery', 'common/js/components/utils/view_utils', 'js/spec_helpers/edit_helpers', - 'js/views/module_edit', 'js/models/module_info', 'xmodule'], - function($, ViewUtils, edit_helpers, ModuleEdit, ModuleModel) { - describe('ModuleEdit', function() { + +import $ from 'jquery'; +import ViewUtils from 'common/js/components/utils/view_utils'; +import edit_helpers from 'js/spec_helpers/edit_helpers'; +import ModuleEdit from 'js/views/module_edit'; +import ModuleModel from 'js/models/module_info'; +import 'xmodule/js/src/xmodule'; + +describe('ModuleEdit', function() { + beforeEach(function() { + this.stubModule = new ModuleModel({ + id: 'stub-id' + }); + setFixtures('Some HTML
', - metadata: { - display_name: newDisplayName - } - }); + EditHelpers.installEditTemplates(); + TemplateHelpers.installTemplate('xblock-string-field-editor'); + TemplateHelpers.installTemplate('container-message'); + appendSetFixtures(mockContainerPage); - initialDisplayName = 'Test Container'; + EditHelpers.installMockXBlock({ + data: 'Some HTML
', + metadata: { + display_name: newDisplayName + } + }); - model = new XBlockInfo({ - id: 'locator-container', - display_name: initialDisplayName, - category: 'vertical' - }); + initialDisplayName = 'Test Container'; + + 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" + }); + }); + + 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); + }); + }; + + describe('Initial display', function() { + it('can render itself', function() { + renderContainerPage(this, mockContainerXBlockHtml); + expect(containerPage.$('.xblock-header').length).toBe(9); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + }); + + it('shows a loading indicator', function() { + requests = AjaxHelpers.requests(this); + containerPage = getContainerPage(); + containerPage.render(); + expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden'); + respondWithHtml(mockContainerXBlockHtml); + expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); + }); + + it('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'); + }); + + it('can show an xblock with an invalid XBlock', function() { + renderContainerPage(this, mockBadXBlockContainerXBlockHtml); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); + expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); + }); + + it('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'); + }); + }); - afterEach(function() { - EditHelpers.uninstallMockXBlock(); - if (containerPage !== undefined) { - containerPage.remove(); - } + describe('Editing the container', function() { + var updatedDisplayName = 'Updated Test Container', + getDisplayNameWrapper; + + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); + + 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(); - respondWithHtml = function(html) { - AjaxHelpers.respondWithJson( - requests, - {html: html, resources: []} - ); - }; + // Expect the correct title to be shown + expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container'); - 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)); - }; + // Press the save button and respond with a success message to the save + EditHelpers.pressModalButton('.action-save'); + AjaxHelpers.respondWithJson(requests, { }); + expect(EditHelpers.isShowingModal()).toBeFalsy(); - 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 || {}); - }; + // Expect the last request be to refresh the container page + handleContainerPageRefresh(requests); - handleContainerPageRefresh = function(requests) { - var request = AjaxHelpers.currentRequest(requests); - expect(str.startsWith(request.url, - '/xblock/locator-container/container_preview')).toBeTruthy(); + // Respond to the subsequent xblock info fetch request. + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + + // Expect the title to have been updated + expect(displayNameElement.text().trim()).toBe(updatedDisplayName); + }); + + it('can inline edit the display name', function() { + var displayNameInput, displayNameWrapper; + renderContainerPage(this, mockContainerXBlockHtml); + displayNameWrapper = getDisplayNameWrapper(); + displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); + displayNameInput.change(); + // This is the response for the change operation. + AjaxHelpers.respondWithJson(requests, { }); + // This is the response for the subsequent fetch operation. + AjaxHelpers.respondWithJson(requests, {display_name: updatedDisplayName}); + EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); + expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); + }); + }); + + describe('Editing an xblock', function() { + afterEach(function() { + EditHelpers.cancelModalIfShowing(); + }); + + it('can show an edit modal for a child xblock', function() { + var editButtons, request; + renderContainerPage(this, mockContainerXBlockHtml); + editButtons = containerPage.$('.wrapper-xblock .edit-button'); + // The container should have rendered six mock xblocks + expect(editButtons.length).toBe(6); + editButtons[0].click(); + // Make sure that the correct xblock is requested to be edited + request = AjaxHelpers.currentRequest(requests); + expect(str.startsWith(request.url, '/xblock/locator-component-A1/studio_view')).toBeTruthy(); + AjaxHelpers.respondWithJson(requests, { + html: mockXBlockEditorHtml, + resources: [] + }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + }); + + it('can show an edit modal for a child xblock with broken JavaScript', function() { + var editButtons; + renderContainerPage(this, mockBadContainerXBlockHtml); + editButtons = containerPage.$('.wrapper-xblock .edit-button'); + editButtons[0].click(); + AjaxHelpers.respondWithJson(requests, { + html: mockXBlockEditorHtml, + resources: [] + }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + }); + + it('can show a 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: mockUpdatedContainerXBlockHtml, + html: mockXBlockVisibilityEditorHtml, resources: [] }); + expect(EditHelpers.isShowingModal()).toBeTruthy(); + } else { + expect(accessButtons.length).toBe(0); + } + }); + + 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); + } + }); + }); + + describe('Editing an xmodule', function() { + var mockXModuleEditor = readFixtures('templates/mock/mock-xmodule-editor.underscore'), + newDisplayName = 'New Display Name'; + + beforeEach(function() { + EditHelpers.installMockXModule({ + data: 'Some HTML
', + metadata: { + display_name: newDisplayName + } + }); + }); + + 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: [] + }); + + $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 + }); + + // Respond to the request to refresh + respondWithHtml(mockUpdatedXBlockHtml); + + // 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); + } + ); + + getGroupElement = function() { + return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']"); + }; + + describe('Deleting an xblock', function() { + var clickDelete, deleteComponent, deleteComponentWithSuccess, + promptSpy; + + beforeEach(function() { + promptSpy = EditHelpers.createPromptSpy(); + }); + + + 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); + + // click the requested delete button + deleteButtons[componentIndex].click(); + + // click the 'yes' or 'no' button in the prompt + EditHelpers.confirmPrompt(promptSpy, clickNo); }; - 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); - }); + deleteComponent = function(componentIndex) { + clickDelete(componentIndex); + + // first request to delete the component + AjaxHelpers.expectJsonRequest(requests, 'DELETE', + '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), + null); + AjaxHelpers.respondWithNoContent(requests); + + // then handle the request to refresh the preview + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(requests); + } + + // final request to refresh the xblock info + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + AjaxHelpers.respondWithJson(requests, {}); }; - 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'); + 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]) + ); + } + }; + + it('can delete the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(0); + }); + + it('can delete a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(1); + }); + + it('can delete the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); + }); + + it('can delete an xblock with broken JavaScript', function() { + renderContainerPage(this, mockBadContainerXBlockHtml); + containerPage.$('.delete-button').first().click(); + EditHelpers.confirmPrompt(promptSpy); + + // expect the second to last request to be a delete of the xblock + AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript'); + AjaxHelpers.respondWithNoContent(requests); + + // handle the refresh request for pages that require a full refresh on delete + if (globalPageOptions.requiresPageRefresh) { + handleContainerPageRefresh(requests); + } + + // expect the last request to be a fetch of the xblock info for the parent container + AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); + }); + + it('does not delete when clicking No in prompt', function() { + renderContainerPage(this, mockContainerXBlockHtml); + + // click delete on the first component but press no + clickDelete(0, true); + + // all components should still exist + expectComponents(getGroupElement(), allComponentsInGroup); + + // no requests should have been sent to the server + AjaxHelpers.expectNoRequests(requests); + }); + + 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('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); + }); + }); + + describe('Duplicating an xblock', function() { + var clickDuplicate, duplicateComponentWithSuccess, + refreshXBlockSpies; + + 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); + + // click the requested duplicate button + duplicateButtons[componentIndex].click(); + }; + + duplicateComponentWithSuccess = function(componentIndex) { + refreshXBlockSpies = spyOn(containerPage, 'refreshXBlock'); + + clickDuplicate(componentIndex); + + // verify content of request + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { + duplicate_source_locator: 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1), + parent_locator: 'locator-group-' + GROUP_TO_TEST }); - it('shows a loading indicator', function() { - requests = AjaxHelpers.requests(this); + // send the response + AjaxHelpers.respondWithJson(requests, { + locator: 'locator-duplicated-component' + }); + + // expect parent container to be refreshed + expect(refreshXBlockSpies).toHaveBeenCalled(); + }; + + it('can duplicate the first xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(0); + }); + + it('can duplicate a middle xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(1); + }); + + it('can duplicate the last xblock', function() { + renderContainerPage(this, mockContainerXBlockHtml); + duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); + }); + + 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('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); + }); + + 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('Previews', function() { + var getButtonIcon, getButtonText; + + getButtonIcon = function(containerPage) { + return containerPage.$('.action-toggle-preview .fa'); + }; + + getButtonText = function(containerPage) { + return containerPage.$('.action-toggle-preview .preview-text').text().trim(); + }; + + if (pagedSpecificTests) { + it('has no text on the preview button to start with', function() { + containerPage = getContainerPage(); + expect(getButtonIcon(containerPage)).toHaveClass('fa-refresh'); + expect(getButtonIcon(containerPage).parent()).toHaveClass('is-hidden'); + expect(getButtonText(containerPage)).toBe(''); + }); + + var updatePreviewButtonTest = function(show_previews, expected_text) { + it('can set preview button to "' + expected_text + '"', function() { + containerPage = getContainerPage(); + containerPage.updatePreviewButton(show_previews); + expect(getButtonText(containerPage)).toBe(expected_text); + }); + }; + + updatePreviewButtonTest(true, 'Hide Previews'); + updatePreviewButtonTest(false, 'Show Previews'); + + it('triggers underlying view togglePreviews when preview button clicked', function() { containerPage = getContainerPage(); containerPage.render(); - expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden'); - respondWithHtml(mockContainerXBlockHtml); - expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); - }); + spyOn(containerPage.xblockView, 'togglePreviews'); - 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'); + containerPage.$('.toggle-preview-button').click(); + expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled(); }); + } + }); - 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'); - }); + describe('createNewComponent ', function() { + var clickNewComponent; - 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'); + 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(); + }); + + it('sends the correct JSON to the server', function() { + renderContainerPage(this, mockContainerXBlockHtml); + clickNewComponent(0); + EditHelpers.verifyXBlockRequest(requests, { + category: 'discussion', + type: 'discussion', + parent_locator: 'locator-group-A' }); }); - describe('Editing the container', function() { - var updatedDisplayName = 'Updated Test Container', - getDisplayNameWrapper; - - afterEach(function() { - EditHelpers.cancelModalIfShowing(); + 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