diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js index c2570289b0..5412d9b5fd 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -1,86 +1,85 @@ /* globals AjaxPrefix */ -(function(AjaxPrefix) { +define([ + 'domReady', + 'jquery', + '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'); + } + }); + }; + main(); + return main; +}); diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index eb0d03c706..3ef278c85d 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -230,7 +230,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 +262,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/factories/login.js b/cms/static/js/factories/login.js index fa8bb454a9..b528e075a2 100644 --- a/cms/static/js/factories/login.js +++ b/cms/static/js/factories/login.js @@ -1,9 +1,10 @@ -import * as cookie from 'jquery.cookie'; -import * as utility from 'utility'; -import * as ViewUtils from 'common/js/components/utils/view_utils'; '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({ @@ -16,23 +17,23 @@ export default function LoginFactory(homepageURL) { } // Clear the login error message when credentials are edited - $('input#email').on('input', function() { + $('input#email').on('input', function () { $('#login_error').removeClass('is-shown'); }); - $('input#password').on('input', function() { + $('input#password').on('input', function () { $('#login_error').removeClass('is-shown'); }); - $('form#login_form').submit(function(event) { + $('form#login_form').submit(function (event) { event.preventDefault(); var $submitButton = $('#submit'), deferred = new $.Deferred(), promise = deferred.promise(); - ViewUtils.disableElementWhileRunning($submitButton, function() { return promise; }); + ViewUtils.disableElementWhileRunning($submitButton, function () { return promise; }); var submit_data = $('#login_form').serialize(); - postJSON('/login_post', submit_data, function(json) { + 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])) { @@ -59,4 +60,4 @@ export default function LoginFactory(homepageURL) { }); }; -export {LoginFactory} +export { LoginFactory } 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