diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index 59df0f722b..e3df6fd672 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -227,6 +227,7 @@ define([ "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/pages/container_spec", "js/spec/views/pages/container_subviews_spec", diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index c1db0bb0e1..cd93a86fc6 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -95,7 +95,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Editing the container", function() { var updatedDisplayName = 'Updated Test Container', - expectEditCanceled, getDisplayNameWrapper; + getDisplayNameWrapper; afterEach(function() { edit_helpers.cancelModalIfShowing(); @@ -105,24 +105,6 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin return containerPage.$('.wrapper-xblock-field'); }; - expectEditCanceled = function(test, options) { - var initialRequests, displayNameWrapper, displayNameInput; - renderContainerPage(test, mockContainerXBlockHtml); - displayNameWrapper = getDisplayNameWrapper(); - initialRequests = requests.length; - displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, options.newTitle); - if (options.pressEscape) { - displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE }); - displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE }); - } else { - displayNameInput.change(); - } - // No requests should be made when the edit is cancelled client-side - expect(initialRequests).toBe(requests.length); - edit_helpers.verifyInlineEditChange(displayNameWrapper, initialDisplayName); - expect(containerPage.model.get('display_name')).toBe(initialDisplayName); - }; - it('can edit itself', function() { var editButtons, displayNameElement; renderContainerPage(this, mockContainerXBlockHtml); @@ -173,46 +155,6 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin edit_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); }); - - it('does not change the title when a display name update fails', function() { - var initialRequests, displayNameInput, displayNameWrapper; - renderContainerPage(this, mockContainerXBlockHtml); - displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName); - initialRequests = requests.length; - displayNameInput.change(); - create_sinon.respondWithError(requests); - // No fetch operation should occur. - expect(initialRequests + 1).toBe(requests.length); - edit_helpers.verifyInlineEditChange(displayNameWrapper, initialDisplayName, updatedDisplayName); - expect(containerPage.model.get('display_name')).toBe(initialDisplayName); - }); - - it('trims whitespace from the display name', function() { - var displayNameInput, displayNameWrapper; - renderContainerPage(this, mockContainerXBlockHtml); - displayNameWrapper = getDisplayNameWrapper(); - displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName + ' '); - displayNameInput.change(); - // This is the response for the change operation. - create_sinon.respondWithJson(requests, { }); - // This is the response for the subsequent fetch operation. - create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName}); - edit_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); - expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); - }); - - it('does not change the title when input is the empty string', function() { - expectEditCanceled(this, {newTitle: ''}); - }); - - it('does not change the title when input is whitespace-only', function() { - expectEditCanceled(this, {newTitle: ' '}); - }); - - it('can cancel an inline edit', function() { - expectEditCanceled(this, {newTitle: updatedDisplayName, pressEscape: true}); - }); }); describe("Editing an xblock", function() { diff --git a/cms/static/js/spec/views/xblock_string_field_editor_spec.js b/cms/static/js/spec/views/xblock_string_field_editor_spec.js new file mode 100644 index 0000000000..1b7e9c8d01 --- /dev/null +++ b/cms/static/js/spec/views/xblock_string_field_editor_spec.js @@ -0,0 +1,155 @@ +define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"], + function ($, create_sinon, view_helpers, edit_helpers, XBlockInfo, XBlockStringFieldEditor) { + describe("XBlockStringFieldEditorView", function () { + var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView; + + getXBlockInfo = function (displayName) { + return new XBlockInfo( + { + display_name: displayName, + id: "my_xblock" + }, + { parse: true } + ); + }; + + getFieldEditorView = function (xblockInfo) { + if (xblockInfo === undefined) { + xblockInfo = getXBlockInfo(initialDisplayName); + } + return new XBlockStringFieldEditor({ + model: xblockInfo, + el: $('.wrapper-xblock-field') + }); + }; + + beforeEach(function () { + initialDisplayName = "Default Display Name"; + updatedDisplayName = "Updated Display Name"; + view_helpers.installTemplate('xblock-string-field-editor'); + appendSetFixtures( + '
' + + '

' + initialDisplayName + '

' + + '
' + ); + }); + + describe('Editing', function () { + var expectPostedNewDisplayName, expectEditCanceled; + + expectPostedNewDisplayName = function (requests, displayName) { + create_sinon.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', { + metadata: { + display_name: displayName + } + }); + }; + + expectEditCanceled = function (test, fieldEditorView, options) { + var requests, initialRequests, displayNameInput; + requests = create_sinon.requests(test); + initialRequests = requests.length; + displayNameInput = edit_helpers.inlineEdit(fieldEditorView.$el, options.newTitle); + if (options.pressEscape) { + displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE }); + displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE }); + } else if (options.clickCancel) { + fieldEditorView.$('button[name=cancel]').click(); + } else { + displayNameInput.change(); + } + // No requests should be made when the edit is cancelled client-side + expect(initialRequests).toBe(requests.length); + edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName); + expect(fieldEditorView.model.get('display_name')).toBe(initialDisplayName); + }; + + it('can inline edit the display name', function () { + var requests, fieldEditorView; + requests = create_sinon.requests(this); + fieldEditorView = getFieldEditorView().render(); + edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + fieldEditorView.$('button[name=submit]').click(); + expectPostedNewDisplayName(requests, updatedDisplayName); + // This is the response for the change operation. + create_sinon.respondWithJson(requests, { }); + // This is the response for the subsequent fetch operation. + create_sinon.respondWithJson(requests, {display_name: updatedDisplayName}); + edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName); + }); + + it('does not change the title when a display name update fails', function () { + var requests, fieldEditorView, initialRequests; + requests = create_sinon.requests(this); + initialRequests = requests.length; + fieldEditorView = getFieldEditorView().render(); + edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + fieldEditorView.$('button[name=submit]').click(); + expectPostedNewDisplayName(requests, updatedDisplayName); + create_sinon.respondWithError(requests); + // No fetch operation should occur. + expect(initialRequests + 1).toBe(requests.length); + edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName); + }); + + it('trims whitespace from the display name', function () { + var requests, fieldEditorView; + requests = create_sinon.requests(this); + fieldEditorView = getFieldEditorView().render(); + updatedDisplayName += ' '; + edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName); + fieldEditorView.$('button[name=submit]').click(); + expectPostedNewDisplayName(requests, updatedDisplayName.trim()); + // This is the response for the change operation. + create_sinon.respondWithJson(requests, { }); + // This is the response for the subsequent fetch operation. + create_sinon.respondWithJson(requests, {display_name: updatedDisplayName.trim()}); + edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim()); + }); + + it('does not change the title when input is the empty string', function () { + var fieldEditorView = getFieldEditorView().render(); + expectEditCanceled(this, fieldEditorView, {newTitle: ''}); + }); + + it('does not change the title when input is whitespace-only', function () { + var fieldEditorView = getFieldEditorView().render(); + expectEditCanceled(this, fieldEditorView, {newTitle: ' '}); + }); + + it('can cancel an inline edit by pressing escape', function () { + var fieldEditorView = getFieldEditorView().render(); + expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, pressEscape: true}); + }); + + it('can cancel an inline edit by clicking cancel', function () { + var fieldEditorView = getFieldEditorView().render(); + expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true}); + }); + }); + + describe('Rendering', function () { + var expectInputMatchesModelDisplayName = function (displayName) { + var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render(); + expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName); + }; + + it('renders single quotes in input field', function () { + expectInputMatchesModelDisplayName('Updated \'Display Name\''); + }); + + it('renders double quotes in input field', function () { + expectInputMatchesModelDisplayName('Updated "Display Name"'); + }); + + it('renders open angle bracket in input field', function () { + expectInputMatchesModelDisplayName(updatedDisplayName + '<'); + }); + + it('renders close angle bracket in input field', function () { + expectInputMatchesModelDisplayName('>' + updatedDisplayName); + }); + }); + }); + }); diff --git a/cms/static/js/views/xblock_string_field_editor.js b/cms/static/js/views/xblock_string_field_editor.js index 033fdd5497..4002bb0be9 100644 --- a/cms/static/js/views/xblock_string_field_editor.js +++ b/cms/static/js/views/xblock_string_field_editor.js @@ -11,7 +11,8 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], var XBlockStringFieldEditor = BaseView.extend({ events: { 'click .xblock-field-value-edit': 'showInput', - 'click button[type=submit]': 'onClickSubmit', + 'click button[name=submit]': 'onClickSubmit', + 'click button[name=cancel]': 'onClickCancel', 'change .xblock-field-input': 'updateField', 'focusout .xblock-field-input': 'onInputFocusLost', 'keyup .xblock-field-input': 'handleKeyUp' @@ -29,7 +30,7 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], render: function() { this.$el.append(this.template({ - value: this.model.get(this.fieldName), + value: this.model.escape(this.fieldName), fieldName: this.fieldName, fieldDisplayName: this.fieldDisplayName })); @@ -56,6 +57,11 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], this.updateField(); }, + onClickCancel: function(event) { + event.preventDefault(); + this.cancelInput(); + }, + onChangeField: function() { var value = this.model.get(this.fieldName); this.getLabel().text(value);