Extract a new webpack-based suite of cms javascript tests

This commit is contained in:
Calen Pennington
2018-04-25 22:11:58 -04:00
parent 28166f037c
commit 61855b2b30
29 changed files with 5467 additions and 5335 deletions

View File

@@ -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;
});

View File

@@ -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',

View File

@@ -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

View File

@@ -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();
});
});
});

View File

@@ -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 }

View File

@@ -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($('<div class="messages"></div>'));
$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($('<div class="messages"></div>'));
$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);
});
});
});

View File

@@ -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;

View File

@@ -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('<div class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="' + rootLocator + '"></div>');
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('<div class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="' + rootLocator + '"></div>');
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);
});
});
});
});

View File

@@ -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');
});
});

View File

@@ -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('<div class="xblock" data-locator="mock-xblock"></div>');
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('<div class="xblock" data-locator="mock-xblock"></div>');
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);
});
});
});

View File

@@ -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('<ul>\n' +
'<li class="component" id="stub-id" data-locator="stub-id">\n' +
' <div class="component-editor">\n' +
' <div class="module-editor">\n' +
' ${editor}\n' +
' </div>\n' +
' <a href="#" class="save-button">Save</a>\n' +
' <a href="#" class="cancel-button">Cancel</a>\n' +
' </div>\n' +
' <div class="component-actions">\n' +
' <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>\n' +
' <a href="#" class="delete-button"><span class="delete-icon white">' +
'</span>Delete</a>\n' +
' </div>\n' +
' <span class="drag-handle action"></span>\n' +
' <section class="xblock xblock-student_view xmodule_display xmodule_stub"' +
' data-type="StubModule">\n' +
' <div id="stub-module-content"/>\n' +
' </section>\n' +
'</li>\n' +
'</ul>');
edit_helpers.installEditTemplates(true);
spyOn($, 'ajax').and.returnValue(this.moduleData);
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
describe('class definition', function() {
it('sets the correct tagName', function() {
return expect(this.moduleEdit.tagName).toEqual('li');
});
it('sets the correct className', function() {
return expect(this.moduleEdit.className).toEqual('component');
});
});
describe('methods', function() {
describe('initialize', function() {
beforeEach(function() {
this.stubModule = new ModuleModel({
id: 'stub-id'
});
setFixtures('<ul>\n' +
'<li class="component" id="stub-id" data-locator="stub-id">\n' +
' <div class="component-editor">\n' +
' <div class="module-editor">\n' +
' ${editor}\n' +
' </div>\n' +
' <a href="#" class="save-button">Save</a>\n' +
' <a href="#" class="cancel-button">Cancel</a>\n' +
' </div>\n' +
' <div class="component-actions">\n' +
' <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>\n' +
' <a href="#" class="delete-button"><span class="delete-icon white">' +
'</span>Delete</a>\n' +
' </div>\n' +
' <span class="drag-handle action"></span>\n' +
' <section class="xblock xblock-student_view xmodule_display xmodule_stub"' +
' data-type="StubModule">\n' +
' <div id="stub-module-content"/>\n' +
' </section>\n' +
'</li>\n' +
'</ul>');
edit_helpers.installEditTemplates(true);
spyOn($, 'ajax').and.returnValue(this.moduleData);
spyOn(ModuleEdit.prototype, 'render');
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
@@ -39,227 +60,206 @@
});
return this.moduleEdit;
});
describe('class definition', function() {
it('sets the correct tagName', function() {
return expect(this.moduleEdit.tagName).toEqual('li');
});
it('sets the correct className', function() {
return expect(this.moduleEdit.className).toEqual('component');
it('renders the module editor', function() {
return expect(ModuleEdit.prototype.render).toHaveBeenCalled();
});
});
describe('render', function() {
beforeEach(function () {
edit_helpers.installEditTemplates(true);
spyOn(this.moduleEdit, 'loadDisplay');
spyOn(this.moduleEdit, 'delegateEvents');
spyOn($.fn, 'append');
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise());
window.MockXBlock = function() {
return {};
};
window.loadedXBlockResources = void 0;
this.moduleEdit.render();
return $.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
});
describe('methods', function() {
describe('initialize', function() {
beforeEach(function() {
spyOn(ModuleEdit.prototype, 'render');
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
it('renders the module editor', function() {
return expect(ModuleEdit.prototype.render).toHaveBeenCalled();
});
afterEach(function() {
window.MockXBlock = null;
return window.MockXBlock;
});
it('loads the module preview via ajax on the view element', function() {
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/student_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
describe('render', function() {
beforeEach(function() {
spyOn(this.moduleEdit, 'loadDisplay');
spyOn(this.moduleEdit, 'delegateEvents');
spyOn($.fn, 'append');
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise());
window.MockXBlock = function() {
return {};
};
window.loadedXBlockResources = void 0;
this.moduleEdit.render();
return $.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
});
afterEach(function() {
window.MockXBlock = null;
return window.MockXBlock;
});
it('loads the module preview via ajax on the view element', function() {
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/student_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect(this.moduleEdit.loadDisplay).toHaveBeenCalled();
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads the editing view via ajax on demand', function() {
var mockXBlockEditorHtml;
edit_helpers.installEditTemplates(true);
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
this.moduleEdit.clickEditButton({
preventDefault: jasmine.createSpy('event.preventDefault')
});
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
$.ajax.calls.mostRecent().args[0].success({
html: mockXBlockEditorHtml,
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads inline css from fragments', function() {
var args = "<style type='text/css'>inline-css</style>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads css urls from fragments', function() {
var args = "<link rel='stylesheet' href='css-url' type='text/css'>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads inline js from fragments', function() {
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
});
it('loads js urls from fragments', function() {
return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url');
});
it('loads head html', function() {
return expect($('head').append).toHaveBeenCalledWith('head-html');
});
it("doesn't load body html", function() {
return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html');
});
it("doesn't reload resources", function() {
var count;
count = $('head').append.calls.count();
$.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html 2</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
]
]
});
return expect($('head').append.calls.count()).toBe(count);
});
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
describe('loadDisplay', function() {
beforeEach(function() {
spyOn(XBlock, 'initializeBlock');
return this.moduleEdit.loadDisplay();
});
it('loads the .xmodule-display inside the module editor', function() {
expect(XBlock.initializeBlock).toHaveBeenCalled();
var sel = '.xblock-student_view';
return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0));
});
expect(this.moduleEdit.loadDisplay).toHaveBeenCalled();
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads the editing view via ajax on demand', function() {
var mockXBlockEditorHtml;
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
this.moduleEdit.clickEditButton({
preventDefault: jasmine.createSpy('event.preventDefault')
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
$.ajax.calls.mostRecent().args[0].success({
html: mockXBlockEditorHtml,
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads inline css from fragments', function() {
var args = "<style type='text/css'>inline-css</style>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads css urls from fragments', function() {
var args = "<link rel='stylesheet' href='css-url' type='text/css'>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads inline js from fragments', function() {
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
});
it('loads js urls from fragments', function() {
return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url');
});
it('loads head html', function() {
return expect($('head').append).toHaveBeenCalledWith('head-html');
});
it("doesn't load body html", function() {
return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html');
});
it("doesn't reload resources", function() {
var count;
count = $('head').append.calls.count();
$.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html 2</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
]
]
});
return expect($('head').append.calls.count()).toBe(count);
});
});
describe('loadDisplay', function() {
beforeEach(function() {
spyOn(XBlock, 'initializeBlock');
return this.moduleEdit.loadDisplay();
});
it('loads the .xmodule-display inside the module editor', function() {
expect(XBlock.initializeBlock).toHaveBeenCalled();
var sel = '.xblock-student_view';
return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0));
});
});
});
}).call(this);
});

View File

@@ -1,766 +1,785 @@
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers',
'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/view_helpers',
'js/views/modals/move_xblock_modal', 'js/views/pages/container', 'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils', 'js/models/xblock_info'],
function($, _, AjaxHelpers, EditHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, ContainerPage, HtmlUtils,
StringUtils, XBlockInfo) {
'use strict';
describe('MoveXBlock', function() {
var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions,
parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline,
verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton,
clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests,
verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, getMovedAlertNotification,
verifyConfirmationFeedbackTitleText, verifyConfirmationFeedbackRedirectLinkText,
verifyUndoConfirmationFeedbackTitleText, verifyConfirmationFeedbackUndoMoveActionText,
sourceParentXBlockInfo, mockContainerPage, createContainerPage, containerPage,
sourceDisplayName = 'component_display_name_0',
sourceLocator = 'component_ID_0',
sourceParentLocator = 'unit_ID_0';
import $ from 'jquery';
import _ from 'underscore';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import EditHelpers from 'js/spec_helpers/edit_helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import ViewHelpers from 'common/js/spec_helpers/view_helpers';
import MoveXBlockModal from 'js/views/modals/move_xblock_modal';
import ContainerPage from 'js/views/pages/container';
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
import XBlockInfo from 'js/models/xblock_info';
import Course from 'js/models/course';
import 'mock-ajax';
parentChildMap = {
course: 'section',
section: 'subsection',
subsection: 'unit',
unit: 'component'
};
describe('MoveXBlock', function() {
categoryMap = {
section: 'chapter',
subsection: 'sequential',
unit: 'vertical',
component: 'component'
};
'use strict';
var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions,
parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline,
verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton,
clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests,
verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, getMovedAlertNotification,
verifyConfirmationFeedbackTitleText, verifyConfirmationFeedbackRedirectLinkText,
verifyUndoConfirmationFeedbackTitleText, verifyConfirmationFeedbackUndoMoveActionText,
sourceParentXBlockInfo, mockContainerPage, createContainerPage, containerPage,
sourceDisplayName = 'component_display_name_0',
sourceLocator = 'component_ID_0',
sourceParentLocator = 'unit_ID_0';
courseOutlineOptions = {
section: 2,
subsection: 2,
unit: 2,
component: 2
};
parentChildMap = {
course: 'section',
section: 'subsection',
subsection: 'unit',
unit: 'component'
};
xblockAncestorInfo = {
ancestors: [
{
category: 'vertical',
display_name: 'unit_display_name_0',
id: 'unit_ID_0'
},
{
category: 'sequential',
display_name: 'subsection_display_name_0',
id: 'subsection_ID_0'
},
{
category: 'chapter',
display_name: 'section_display_name_0',
id: 'section_ID_0'
},
{
category: 'course',
display_name: 'Demo Course',
id: 'COURSE_ID_101'
}
]
};
categoryMap = {
section: 'chapter',
subsection: 'sequential',
unit: 'vertical',
component: 'component'
};
sourceParentXBlockInfo = new XBlockInfo({
id: sourceParentLocator,
courseOutlineOptions = {
section: 2,
subsection: 2,
unit: 2,
component: 2
};
xblockAncestorInfo = {
ancestors: [
{
category: 'vertical',
display_name: 'unit_display_name_0',
category: 'vertical'
id: 'unit_ID_0'
},
{
category: 'sequential',
display_name: 'subsection_display_name_0',
id: 'subsection_ID_0'
},
{
category: 'chapter',
display_name: 'section_display_name_0',
id: 'section_ID_0'
},
{
category: 'course',
display_name: 'Demo Course',
id: 'COURSE_ID_101'
}
]
};
sourceParentXBlockInfo = new XBlockInfo({
id: sourceParentLocator,
display_name: 'unit_display_name_0',
category: 'vertical'
});
createContainerPage = function() {
containerPage = new ContainerPage({
model: sourceParentXBlockInfo,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
};
beforeEach(function() {
setFixtures("<div id='page-alert'></div>");
mockContainerPage = readFixtures('templates/mock/mock-container-page.underscore');
TemplateHelpers.installTemplates([
'basic-modal',
'modal-button',
'move-xblock-modal'
]);
appendSetFixtures(mockContainerPage);
window.course = new Course({
id: "5",
name: "Course Name",
url_name: "course_name",
org: "course_org",
num: "course_num",
revision: "course_rev"
});
createContainerPage();
courseOutline = createCourseOutline(courseOutlineOptions);
showModal();
});
afterEach(function() {
modal.hide();
courseOutline = null;
containerPage.remove();
delete window.course;
});
showModal = function() {
modal = new MoveXBlockModal({
sourceXBlockInfo: new XBlockInfo({
id: sourceLocator,
display_name: sourceDisplayName,
category: 'component'
}),
sourceParentXBlockInfo: sourceParentXBlockInfo,
XBlockUrlRoot: '/xblock'
});
modal.show();
};
/**
* Create child XBlock info.
*
* @param {String} category XBlock category
* @param {Object} outlineOptions options according to which outline was created
* @param {Object} xblockIndex XBlock Index
* @returns
*/
createChildXBlockInfo = function(category, outlineOptions, xblockIndex) {
var childInfo = {
category: categoryMap[category],
display_name: category + '_display_name_' + xblockIndex,
id: category + '_ID_' + xblockIndex
};
return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo);
};
/**
* Create parent XBlock info.
*
* @param {String} category XBlock category
* @param {Object} outlineOptions options according to which outline was created
* @param {Object} outline ouline info being constructed
* @returns {Object}
*/
createXBlockInfo = function(category, outlineOptions, outline) {
var childInfo = {
category: categoryMap[category],
display_name: category,
children: []
},
xblocks;
xblocks = outlineOptions[category];
if (!xblocks) {
return outline;
}
outline.child_info = childInfo; // eslint-disable-line no-param-reassign
_.each(_.range(xblocks), function(xblockIndex) {
childInfo.children.push(
createChildXBlockInfo(category, outlineOptions, xblockIndex)
);
});
return outline;
};
/**
* Create course outline.
*
* @param {Object} outlineOptions options according to which outline was created
* @returns {Object}
*/
createCourseOutline = function(outlineOptions) {
var courseXBlockInfo = {
category: 'course',
display_name: 'Demo Course',
id: 'COURSE_ID_101'
};
return createXBlockInfo('section', outlineOptions, courseXBlockInfo);
};
/**
* Render breadcrumb and XBlock list view.
*
* @param {any} courseOutlineInfo course outline info
* @param {any} ancestorInfo ancestors info
*/
renderViews = function(courseOutlineInfo, ancestorInfo) {
var ancestorInfo = ancestorInfo || {ancestors: []}; // eslint-disable-line no-redeclare
modal.renderViews(courseOutlineInfo, ancestorInfo);
};
/**
* Extract displayed XBlock list info.
*
* @returns {Object}
*/
getDisplayedInfo = function() {
var viewEl = modal.moveXBlockListView.$el;
return {
categoryText: viewEl.find('.category-text').text().trim(),
currentLocationText: viewEl.find('.current-location').text().trim(),
xblockCount: viewEl.find('.xblock-item').length,
xblockDisplayNames: viewEl.find('.xblock-item .xblock-displayname').map(
function() { return $(this).text().trim(); }
).get(),
forwardButtonSRTexts: viewEl.find('.xblock-item .forward-sr-text').map(
function() { return $(this).text().trim(); }
).get(),
forwardButtonCount: viewEl.find('.fa-arrow-right.forward-sr-icon').length
};
};
/**
* Verify displayed XBlock list info.
*
* @param {String} category XBlock category
* @param {Integer} expectedXBlocksCount number of XBlock childs displayed
* @param {Boolean} hasCurrentLocation do we need to check current location
*/
verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) {
var displayedInfo = getDisplayedInfo();
expect(displayedInfo.categoryText).toEqual(modal.moveXBlockListView.categoriesText[category] + ':');
expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount);
expect(displayedInfo.xblockDisplayNames).toEqual(
_.map(_.range(expectedXBlocksCount), function(xblockIndex) {
return category + '_display_name_' + xblockIndex;
})
);
if (category === 'component') {
if (hasCurrentLocation) {
expect(displayedInfo.currentLocationText).toEqual('(Currently selected)');
}
} else {
if (hasCurrentLocation) {
expect(displayedInfo.currentLocationText).toEqual('(Current location)');
}
expect(displayedInfo.forwardButtonSRTexts).toEqual(
_.map(_.range(expectedXBlocksCount), function() {
return 'View child items';
})
);
expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount);
}
};
/**
* Verify rendered breadcrumb info.
*
* @param {any} category XBlock category
* @param {any} xblockIndex XBlock index
*/
verifyBreadcrumbViewInfo = function(category, xblockIndex) {
var displayedBreadcrumbs = modal.moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map(
function() { return $(this).text().trim(); }
).get(),
categories = _.keys(parentChildMap).concat(['component']),
visitedCategories = categories.slice(0, _.indexOf(categories, category));
expect(displayedBreadcrumbs).toEqual(
_.map(visitedCategories, function(visitedCategory) {
return visitedCategory === 'course' ?
'Course Outline' : visitedCategory + '_display_name_' + xblockIndex;
})
);
};
/**
* Click forward button in the list of displayed XBlocks.
*
* @param {any} buttonIndex forward button index
*/
clickForwardButton = function(buttonIndex) {
buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign
modal.moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click();
};
/**
* Click on last clickable breadcrumb button.
*/
clickBreadcrumbButton = function() {
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click();
};
/**
* Returns the parent or child category of current XBlock.
*
* @param {String} direction `forward` or `backward`
* @param {String} category XBlock category
* @returns {String}
*/
nextCategory = function(direction, category) {
return direction === 'forward' ? parentChildMap[category] : _.invert(parentChildMap)[category];
};
/**
* Verify renderd info of breadcrumbs and XBlock list.
*
* @param {Object} outlineOptions options according to which outline was created
* @param {String} category XBlock category
* @param {Integer} buttonIndex forward button index
* @param {String} direction `forward` or `backward`
* @param {String} hasCurrentLocation do we need to check current location
* @returns
*/
verifyXBlockInfo = function(outlineOptions, category, buttonIndex, direction, hasCurrentLocation) {
var expectedXBlocksCount = outlineOptions[category];
verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation);
verifyBreadcrumbViewInfo(category, buttonIndex);
verifyMoveEnabled(category, hasCurrentLocation);
if (direction === 'forward') {
if (category === 'component') {
return;
}
clickForwardButton(buttonIndex);
} else if (direction === 'backward') {
if (category === 'section') {
return;
}
clickBreadcrumbButton();
}
category = nextCategory(direction, category); // eslint-disable-line no-param-reassign
verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation);
};
/**
* Verify move button is enabled.
*
* @param {String} category XBlock category
* @param {String} hasCurrentLocation do we need to check current location
*/
verifyMoveEnabled = function(category, hasCurrentLocation) {
var isMoveEnabled = !modal.$el.find('.modal-actions .action-move').hasClass('is-disabled');
if (category === 'component' && !hasCurrentLocation) {
expect(isMoveEnabled).toBeTruthy();
} else {
expect(isMoveEnabled).toBeFalsy();
}
};
/**
* Verify notification status.
*
* @param {Object} requests requests object
* @param {Object} notificationSpy notification spy
* @param {String} notificationText notification text to be verified
* @param {Integer} sourceIndex source index of the xblock
*/
verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) {
var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare
ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText);
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
ViewHelpers.verifyNotificationHidden(notificationSpy);
};
/**
* Get move alert confirmation message HTML
*/
getMovedAlertNotification = function() {
return $('#page-alert');
};
/**
* Send move xblock request.
*
* @param {Object} requests requests object
* @param {Object} xblockLocator Xblock id location
* @param {Integer} targetIndex target index of the xblock
* @param {Integer} sourceIndex source index of the xblock
*/
sendMoveXBlockRequest = function(requests, xblockLocator, targetIndex, sourceIndex) {
var responseData,
expectedData,
sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare
responseData = expectedData = {
move_source_locator: xblockLocator,
parent_locator: modal.targetParentXBlockInfo.id
};
if (targetIndex !== undefined) {
expectedData = _.extend(expectedData, {
targetIndex: targetIndex
});
}
createContainerPage = function() {
containerPage = new ContainerPage({
model: sourceParentXBlockInfo,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
};
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData);
beforeEach(function() {
setFixtures("<div id='page-alert'></div>");
mockContainerPage = readFixtures('mock/mock-container-page.underscore');
TemplateHelpers.installTemplates([
'basic-modal',
'modal-button',
'move-xblock-modal'
]);
appendSetFixtures(mockContainerPage);
createContainerPage();
courseOutline = createCourseOutline(courseOutlineOptions);
showModal();
});
// send the response
AjaxHelpers.respondWithJson(requests, _.extend(responseData, {
source_index: sourceIndex
}));
};
afterEach(function() {
modal.hide();
courseOutline = null;
containerPage.remove();
});
/**
* Move xblock with success.
*
* @param {Object} requests requests object
*/
moveXBlockWithSuccess = function(requests) {
// select a target item and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
sendMoveXBlockRequest(requests, sourceLocator);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/' + sourceParentLocator);
AjaxHelpers.respondWithJson(requests, sourceParentXBlockInfo);
expect(getMovedAlertNotification().html().length).not.toEqual(0);
verifyConfirmationFeedbackTitleText(sourceDisplayName);
verifyConfirmationFeedbackRedirectLinkText();
verifyConfirmationFeedbackUndoMoveActionText();
};
showModal = function() {
modal = new MoveXBlockModal({
sourceXBlockInfo: new XBlockInfo({
id: sourceLocator,
display_name: sourceDisplayName,
category: 'component'
}),
sourceParentXBlockInfo: sourceParentXBlockInfo,
XBlockUrlRoot: '/xblock'
});
modal.show();
};
/**
* Verify success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()
.trim())
.toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.',
{
displayName: displayName
})
);
};
/**
* Create child XBlock info.
*
* @param {String} category XBlock category
* @param {Object} outlineOptions options according to which outline was created
* @param {Object} xblockIndex XBlock Index
* @returns
*/
createChildXBlockInfo = function(category, outlineOptions, xblockIndex) {
var childInfo = {
category: categoryMap[category],
display_name: category + '_display_name_' + xblockIndex,
id: category + '_ID_' + xblockIndex
};
return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo);
};
/**
* Create parent XBlock info.
*
* @param {String} category XBlock category
* @param {Object} outlineOptions options according to which outline was created
* @param {Object} outline ouline info being constructed
* @returns {Object}
*/
createXBlockInfo = function(category, outlineOptions, outline) {
var childInfo = {
category: categoryMap[category],
display_name: category,
children: []
},
xblocks;
xblocks = outlineOptions[category];
if (!xblocks) {
return outline;
/**
* Verify undo success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyUndoConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()).toEqual(
StringUtils.interpolate(
'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.',
{
sourceDisplayName: displayName
}
)
);
};
outline.child_info = childInfo; // eslint-disable-line no-param-reassign
_.each(_.range(xblocks), function(xblockIndex) {
childInfo.children.push(
createChildXBlockInfo(category, outlineOptions, xblockIndex)
);
});
return outline;
};
/**
* Verify success banner message html has correct redirect link text.
*/
verifyConfirmationFeedbackRedirectLinkText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-secondary').html())
.toEqual('Take me to the new location');
};
/**
* Create course outline.
*
* @param {Object} outlineOptions options according to which outline was created
* @returns {Object}
*/
createCourseOutline = function(outlineOptions) {
var courseXBlockInfo = {
category: 'course',
display_name: 'Demo Course',
id: 'COURSE_ID_101'
};
return createXBlockInfo('section', outlineOptions, courseXBlockInfo);
};
/**
* Verify success banner message html has correct undo move text.
*/
verifyConfirmationFeedbackUndoMoveActionText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move');
};
/**
* Render breadcrumb and XBlock list view.
*
* @param {any} courseOutlineInfo course outline info
* @param {any} ancestorInfo ancestors info
*/
renderViews = function(courseOutlineInfo, ancestorInfo) {
var ancestorInfo = ancestorInfo || {ancestors: []}; // eslint-disable-line no-redeclare
modal.renderViews(courseOutlineInfo, ancestorInfo);
};
/**
* Get sent requests.
*
* @returns {Object}
*/
getSentRequests = function() {
return jasmine.Ajax.requests.filter(function(request) {
return request.readyState > 0;
});
};
/**
* Extract displayed XBlock list info.
*
* @returns {Object}
*/
getDisplayedInfo = function() {
var viewEl = modal.moveXBlockListView.$el;
return {
categoryText: viewEl.find('.category-text').text().trim(),
currentLocationText: viewEl.find('.current-location').text().trim(),
xblockCount: viewEl.find('.xblock-item').length,
xblockDisplayNames: viewEl.find('.xblock-item .xblock-displayname').map(
function() { return $(this).text().trim(); }
).get(),
forwardButtonSRTexts: viewEl.find('.xblock-item .forward-sr-text').map(
function() { return $(this).text().trim(); }
).get(),
forwardButtonCount: viewEl.find('.fa-arrow-right.forward-sr-icon').length
};
};
it('renders views with correct information', function() {
var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1},
outline = createCourseOutline(outlineOptions);
/**
* Verify displayed XBlock list info.
*
* @param {String} category XBlock category
* @param {Integer} expectedXBlocksCount number of XBlock childs displayed
* @param {Boolean} hasCurrentLocation do we need to check current location
*/
verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) {
var displayedInfo = getDisplayedInfo();
expect(displayedInfo.categoryText).toEqual(modal.moveXBlockListView.categoriesText[category] + ':');
expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount);
expect(displayedInfo.xblockDisplayNames).toEqual(
_.map(_.range(expectedXBlocksCount), function(xblockIndex) {
return category + '_display_name_' + xblockIndex;
})
);
if (category === 'component') {
if (hasCurrentLocation) {
expect(displayedInfo.currentLocationText).toEqual('(Currently selected)');
}
} else {
if (hasCurrentLocation) {
expect(displayedInfo.currentLocationText).toEqual('(Current location)');
}
expect(displayedInfo.forwardButtonSRTexts).toEqual(
_.map(_.range(expectedXBlocksCount), function() {
return 'View child items';
})
);
expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount);
}
};
renderViews(outline, xblockAncestorInfo);
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true);
});
/**
* Verify rendered breadcrumb info.
*
* @param {any} category XBlock category
* @param {any} xblockIndex XBlock index
*/
verifyBreadcrumbViewInfo = function(category, xblockIndex) {
var displayedBreadcrumbs = modal.moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map(
function() { return $(this).text().trim(); }
).get(),
categories = _.keys(parentChildMap).concat(['component']),
visitedCategories = categories.slice(0, _.indexOf(categories, category));
it('shows correct behavior on breadcrumb navigation', function() {
var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1});
expect(displayedBreadcrumbs).toEqual(
_.map(visitedCategories, function(visitedCategory) {
return visitedCategory === 'course' ?
'Course Outline' : visitedCategory + '_display_name_' + xblockIndex;
})
);
};
renderViews(outline);
_.each(_.range(3), function() {
clickForwardButton();
});
/**
* Click forward button in the list of displayed XBlocks.
*
* @param {any} buttonIndex forward button index
*/
clickForwardButton = function(buttonIndex) {
buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign
modal.moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click();
};
/**
* Click on last clickable breadcrumb button.
*/
clickBreadcrumbButton = function() {
_.each(['component', 'unit', 'subsection', 'section'], function(category) {
verifyListViewInfo(category, 1);
if (category !== 'section') {
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click();
};
/**
* Returns the parent or child category of current XBlock.
*
* @param {String} direction `forward` or `backward`
* @param {String} category XBlock category
* @returns {String}
*/
nextCategory = function(direction, category) {
return direction === 'forward' ? parentChildMap[category] : _.invert(parentChildMap)[category];
};
/**
* Verify renderd info of breadcrumbs and XBlock list.
*
* @param {Object} outlineOptions options according to which outline was created
* @param {String} category XBlock category
* @param {Integer} buttonIndex forward button index
* @param {String} direction `forward` or `backward`
* @param {String} hasCurrentLocation do we need to check current location
* @returns
*/
verifyXBlockInfo = function(outlineOptions, category, buttonIndex, direction, hasCurrentLocation) {
var expectedXBlocksCount = outlineOptions[category];
verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation);
verifyBreadcrumbViewInfo(category, buttonIndex);
verifyMoveEnabled(category, hasCurrentLocation);
if (direction === 'forward') {
if (category === 'component') {
return;
}
clickForwardButton(buttonIndex);
} else if (direction === 'backward') {
if (category === 'section') {
return;
}
clickBreadcrumbButton();
}
category = nextCategory(direction, category); // eslint-disable-line no-param-reassign
verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation);
};
/**
* Verify move button is enabled.
*
* @param {String} category XBlock category
* @param {String} hasCurrentLocation do we need to check current location
*/
verifyMoveEnabled = function(category, hasCurrentLocation) {
var isMoveEnabled = !modal.$el.find('.modal-actions .action-move').hasClass('is-disabled');
if (category === 'component' && !hasCurrentLocation) {
expect(isMoveEnabled).toBeTruthy();
} else {
expect(isMoveEnabled).toBeFalsy();
}
};
/**
* Verify notification status.
*
* @param {Object} requests requests object
* @param {Object} notificationSpy notification spy
* @param {String} notificationText notification text to be verified
* @param {Integer} sourceIndex source index of the xblock
*/
verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) {
var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare
ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText);
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
ViewHelpers.verifyNotificationHidden(notificationSpy);
};
/**
* Get move alert confirmation message HTML
*/
getMovedAlertNotification = function() {
return $('#page-alert');
};
/**
* Send move xblock request.
*
* @param {Object} requests requests object
* @param {Object} xblockLocator Xblock id location
* @param {Integer} targetIndex target index of the xblock
* @param {Integer} sourceIndex source index of the xblock
*/
sendMoveXBlockRequest = function(requests, xblockLocator, targetIndex, sourceIndex) {
var responseData,
expectedData,
sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare
responseData = expectedData = {
move_source_locator: xblockLocator,
parent_locator: modal.targetParentXBlockInfo.id
};
if (targetIndex !== undefined) {
expectedData = _.extend(expectedData, {
targetIndex: targetIndex
});
}
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData);
// send the response
AjaxHelpers.respondWithJson(requests, _.extend(responseData, {
source_index: sourceIndex
}));
};
/**
* Move xblock with success.
*
* @param {Object} requests requests object
*/
moveXBlockWithSuccess = function(requests) {
// select a target item and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
sendMoveXBlockRequest(requests, sourceLocator);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/' + sourceParentLocator);
AjaxHelpers.respondWithJson(requests, sourceParentXBlockInfo);
expect(getMovedAlertNotification().html().length).not.toEqual(0);
verifyConfirmationFeedbackTitleText(sourceDisplayName);
verifyConfirmationFeedbackRedirectLinkText();
verifyConfirmationFeedbackUndoMoveActionText();
};
/**
* Verify success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()
.trim())
.toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.',
{
displayName: displayName
})
);
};
/**
* Verify undo success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyUndoConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()).toEqual(
StringUtils.interpolate(
'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.',
{
sourceDisplayName: displayName
}
)
);
};
/**
* Verify success banner message html has correct redirect link text.
*/
verifyConfirmationFeedbackRedirectLinkText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-secondary').html())
.toEqual('Take me to the new location');
};
/**
* Verify success banner message html has correct undo move text.
*/
verifyConfirmationFeedbackUndoMoveActionText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move');
};
/**
* Get sent requests.
*
* @returns {Object}
*/
getSentRequests = function() {
return jasmine.Ajax.requests.filter(function(request) {
return request.readyState > 0;
});
};
it('renders views with correct information', function() {
var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1},
outline = createCourseOutline(outlineOptions);
renderViews(outline, xblockAncestorInfo);
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true);
});
it('shows correct behavior on breadcrumb navigation', function() {
var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1});
renderViews(outline);
_.each(_.range(3), function() {
clickForwardButton();
});
_.each(['component', 'unit', 'subsection', 'section'], function(category) {
verifyListViewInfo(category, 1);
if (category !== 'section') {
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click();
}
});
});
it('shows the correct current location', function() {
var outlineOptions = {section: 2, subsection: 2, unit: 2, component: 2},
outline = createCourseOutline(outlineOptions);
renderViews(outline, xblockAncestorInfo);
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
// click the outline breadcrumb to render sections
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click();
verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false);
});
it('shows correct message when parent has no children', function() {
var outlinesInfo = [
{
outline: createCourseOutline({}),
message: 'This course has no sections'
},
{
outline: createCourseOutline({section: 1}),
message: 'This section has no subsections',
forwardClicks: 1
},
{
outline: createCourseOutline({section: 1, subsection: 1}),
message: 'This subsection has no units',
forwardClicks: 2
},
{
outline: createCourseOutline({section: 1, subsection: 1, unit: 1}),
message: 'This unit has no components',
forwardClicks: 3
}
];
_.each(outlinesInfo, function(info) {
renderViews(info.outline);
_.each(_.range(info.forwardClicks), function() {
clickForwardButton();
});
expect(modal.moveXBlockListView.$el.find('.xblock-no-child-message').text().trim())
.toEqual(info.message);
modal.moveXBlockListView.undelegateEvents();
modal.moveXBlockBreadcrumbView.undelegateEvents();
});
});
describe('Move button', function() {
it('is disabled when navigating to same parent', function() {
// select a target parent as the same as source parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(0);
});
verifyMoveEnabled('component', true);
});
it('is enabled when navigating to different parent', function() {
// select a target parent as the different as source parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
verifyMoveEnabled('component', false);
});
it('verify move state while navigating', function() {
renderViews(courseOutline, xblockAncestorInfo);
verifyXBlockInfo(courseOutlineOptions, 'section', 0, 'forward', true);
// start from course outline again
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click();
verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false);
});
it('is disbabled when navigating to same source xblock', function() {
var outline,
libraryContentXBlockInfo = {
category: 'library_content',
display_name: 'Library Content',
has_children: true,
id: 'LIBRARY_CONTENT_ID'
},
outlineOptions = {library_content: 1, component: 1};
// make above xblock source xblock.
modal.sourceXBlockInfo = libraryContentXBlockInfo;
outline = createXBlockInfo('component', outlineOptions, libraryContentXBlockInfo);
renderViews(outline);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// select a target parent
clickForwardButton(0);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
it('is disabled when navigating inside source content experiment', function() {
var outline,
splitTestXBlockInfo = {
category: 'split_test',
display_name: 'Content Experiment',
has_children: true,
id: 'SPLIT_TEST_ID'
},
outlineOptions = {split_test: 1, unit: 2, component: 1};
// make above xblock source xblock.
modal.sourceXBlockInfo = splitTestXBlockInfo;
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
renderViews(outline);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to groups level
clickForwardButton(0);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to component level inside a group
clickForwardButton(0);
// move should be disabled because we are navigating inside source xblock
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
it('is disabled when navigating to any content experiment groups', function() {
var outline,
splitTestXBlockInfo = {
category: 'split_test',
display_name: 'Content Experiment',
has_children: true,
id: 'SPLIT_TEST_ID'
},
outlineOptions = {split_test: 1, unit: 2, component: 1};
// group level should be disabled but component level inside groups should be movable
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
renderViews(outline);
// move is disabled on groups level
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to component level inside a group
clickForwardButton(1);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is enabled when navigating to any parentable component', function() {
var parentableXBlockInfo = {
category: 'vertical',
display_name: 'Parentable Component',
has_children: true,
id: 'PARENTABLE_ID'
};
renderViews(parentableXBlockInfo);
// move is enabled on parentable xblocks.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is enabled when moving a component inside a parentable component', function() {
// create a source parent with has_childern set true
modal.sourceParentXBlockInfo = new XBlockInfo({
category: 'conditional',
display_name: 'Parentable Component',
has_children: true,
id: 'PARENTABLE_ID'
});
// navigate and verify move button is enabled
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(0);
});
// move is enabled when moving a component.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is disabled when navigating to any non-parentable component', function() {
var nonParentableXBlockInfo = {
category: 'html',
display_name: 'Non Parentable Component',
has_children: false,
id: 'NON_PARENTABLE_ID'
};
renderViews(nonParentableXBlockInfo);
// move is disabled on non-parent xblocks.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
});
describe('Move an xblock', function() {
it('can not move in a disabled state', function() {
verifyMoveEnabled(false);
modal.$el.find('.modal-actions .action-move').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('move an xblock when move button is clicked', function() {
var requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
});
it('do not move an xblock when cancel button is clicked', function() {
modal.$el.find('.modal-actions .action-cancel').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('undo move an xblock when undo move link is clicked', function() {
var sourceIndex = 0,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
verifyUndoConfirmationFeedbackTitleText(sourceDisplayName);
});
});
describe('shows a notification', function() {
it('mini operation message when moving an xblock', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy();
// navigate to a target parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
verifyNotificationStatus(requests, notificationSpy, 'Moving');
});
it('mini operation message when undo moving an xblock', function() {
var notificationSpy,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
notificationSpy = ViewHelpers.createNotificationSpy();
getMovedAlertNotification().find('.action-save').click();
verifyNotificationStatus(requests, notificationSpy, 'Undo moving');
});
it('error message when move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
// select a target item and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
it('error message when undo move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
});
}
});
});
it('shows the correct current location', function() {
var outlineOptions = {section: 2, subsection: 2, unit: 2, component: 2},
outline = createCourseOutline(outlineOptions);
renderViews(outline, xblockAncestorInfo);
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
// click the outline breadcrumb to render sections
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click();
verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false);
});
it('shows correct message when parent has no children', function() {
var outlinesInfo = [
{
outline: createCourseOutline({}),
message: 'This course has no sections'
},
{
outline: createCourseOutline({section: 1}),
message: 'This section has no subsections',
forwardClicks: 1
},
{
outline: createCourseOutline({section: 1, subsection: 1}),
message: 'This subsection has no units',
forwardClicks: 2
},
{
outline: createCourseOutline({section: 1, subsection: 1, unit: 1}),
message: 'This unit has no components',
forwardClicks: 3
}
];
_.each(outlinesInfo, function(info) {
renderViews(info.outline);
_.each(_.range(info.forwardClicks), function() {
clickForwardButton();
});
expect(modal.moveXBlockListView.$el.find('.xblock-no-child-message').text().trim())
.toEqual(info.message);
modal.moveXBlockListView.undelegateEvents();
modal.moveXBlockBreadcrumbView.undelegateEvents();
});
});
describe('Move button', function() {
it('is disabled when navigating to same parent', function() {
// select a target parent as the same as source parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(0);
});
verifyMoveEnabled('component', true);
});
it('is enabled when navigating to different parent', function() {
// select a target parent as the different as source parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
verifyMoveEnabled('component', false);
});
it('verify move state while navigating', function() {
renderViews(courseOutline, xblockAncestorInfo);
verifyXBlockInfo(courseOutlineOptions, 'section', 0, 'forward', true);
// start from course outline again
modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click();
verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false);
});
it('is disbabled when navigating to same source xblock', function() {
var outline,
libraryContentXBlockInfo = {
category: 'library_content',
display_name: 'Library Content',
has_children: true,
id: 'LIBRARY_CONTENT_ID'
},
outlineOptions = {library_content: 1, component: 1};
// make above xblock source xblock.
modal.sourceXBlockInfo = libraryContentXBlockInfo;
outline = createXBlockInfo('component', outlineOptions, libraryContentXBlockInfo);
renderViews(outline);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// select a target parent
clickForwardButton(0);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
it('is disabled when navigating inside source content experiment', function() {
var outline,
splitTestXBlockInfo = {
category: 'split_test',
display_name: 'Content Experiment',
has_children: true,
id: 'SPLIT_TEST_ID'
},
outlineOptions = {split_test: 1, unit: 2, component: 1};
// make above xblock source xblock.
modal.sourceXBlockInfo = splitTestXBlockInfo;
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
renderViews(outline);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to groups level
clickForwardButton(0);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to component level inside a group
clickForwardButton(0);
// move should be disabled because we are navigating inside source xblock
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
it('is disabled when navigating to any content experiment groups', function() {
var outline,
splitTestXBlockInfo = {
category: 'split_test',
display_name: 'Content Experiment',
has_children: true,
id: 'SPLIT_TEST_ID'
},
outlineOptions = {split_test: 1, unit: 2, component: 1};
// group level should be disabled but component level inside groups should be movable
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
renderViews(outline);
// move is disabled on groups level
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
// navigate to component level inside a group
clickForwardButton(1);
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is enabled when navigating to any parentable component', function() {
var parentableXBlockInfo = {
category: 'vertical',
display_name: 'Parentable Component',
has_children: true,
id: 'PARENTABLE_ID'
};
renderViews(parentableXBlockInfo);
// move is enabled on parentable xblocks.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is enabled when moving a component inside a parentable component', function() {
// create a source parent with has_childern set true
modal.sourceParentXBlockInfo = new XBlockInfo({
category: 'conditional',
display_name: 'Parentable Component',
has_children: true,
id: 'PARENTABLE_ID'
});
// navigate and verify move button is enabled
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(0);
});
// move is enabled when moving a component.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});
it('is disabled when navigating to any non-parentable component', function() {
var nonParentableXBlockInfo = {
category: 'html',
display_name: 'Non Parentable Component',
has_children: false,
id: 'NON_PARENTABLE_ID'
};
renderViews(nonParentableXBlockInfo);
// move is disabled on non-parent xblocks.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
});
});
describe('Move an xblock', function() {
it('can not move in a disabled state', function() {
verifyMoveEnabled(false);
modal.$el.find('.modal-actions .action-move').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('move an xblock when move button is clicked', function() {
var requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
});
it('do not move an xblock when cancel button is clicked', function() {
modal.$el.find('.modal-actions .action-cancel').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('undo move an xblock when undo move link is clicked', function() {
var sourceIndex = 0,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
verifyUndoConfirmationFeedbackTitleText(sourceDisplayName);
});
});
describe('shows a notification', function() {
it('mini operation message when moving an xblock', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy();
// navigate to a target parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
verifyNotificationStatus(requests, notificationSpy, 'Moving');
});
it('mini operation message when undo moving an xblock', function() {
var notificationSpy,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
notificationSpy = ViewHelpers.createNotificationSpy();
getMovedAlertNotification().find('.action-save').click();
verifyNotificationStatus(requests, notificationSpy, 'Undo moving');
});
it('error message when move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
// select a target item and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
it('error message when undo move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
});
});

View File

@@ -1,839 +1,855 @@
define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'js/spec_helpers/edit_helpers',
'js/views/pages/container', 'js/views/pages/paged_container', 'js/models/xblock_info',
'js/collections/component_template', 'jquery.simulate'],
function($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage,
XBlockInfo, ComponentTemplates) {
'use strict';
'use strict';
'use strict';
function parameterized_suite(label, globalPageOptions) {
describe(label + ' ContainerPage', function() {
var getContainerPage, renderContainerPage, handleContainerPageRefresh, expectComponents,
respondWithHtml, model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures(globalPageOptions.initial),
mockXBlockHtml = readFixtures(globalPageOptions.addResponse),
mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'),
mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'),
mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'),
PageClass = globalPageOptions.page,
pagedSpecificTests = globalPageOptions.pagedSpecificTests,
hasVisibilityEditor = globalPageOptions.hasVisibilityEditor,
hasMoveModal = globalPageOptions.hasMoveModal;
import $ from 'jquery';
import _ from 'underscore';
import str from 'underscore.string';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import EditHelpers from 'js/spec_helpers/edit_helpers';
import ContainerPage from 'js/views/pages/container';
import PagedContainerPage from 'js/views/pages/paged_container';
import XBlockInfo from 'js/models/xblock_info';
import ComponentTemplates from 'js/collections/component_template';
import Course from 'js/models/course';
import 'jquery.simulate';
beforeEach(function() {
var newDisplayName = 'New Display Name';
function parameterized_suite(label, globalPageOptions) {
describe(label + ' ContainerPage', function() {
var getContainerPage, renderContainerPage, handleContainerPageRefresh, expectComponents,
respondWithHtml, model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('templates/mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures(globalPageOptions.initial),
mockXBlockHtml = readFixtures(globalPageOptions.addResponse),
mockBadContainerXBlockHtml = readFixtures('templates/mock/mock-bad-javascript-container-xblock.underscore'),
mockBadXBlockContainerXBlockHtml = readFixtures('templates/mock/mock-bad-xblock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('templates/mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore'),
mockXBlockVisibilityEditorHtml = readFixtures('templates/mock/mock-xblock-visibility-editor.underscore'),
PageClass = globalPageOptions.page,
pagedSpecificTests = globalPageOptions.pagedSpecificTests,
hasVisibilityEditor = globalPageOptions.hasVisibilityEditor,
hasMoveModal = globalPageOptions.hasMoveModal;
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
beforeEach(function() {
var newDisplayName = 'New Display Name';
EditHelpers.installMockXBlock({
data: '<p>Some HTML</p>',
metadata: {
display_name: newDisplayName
}
});
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
initialDisplayName = 'Test Container';
EditHelpers.installMockXBlock({
data: '<p>Some HTML</p>',
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: '<p>Some HTML</p>',
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 <a> add buttons which is what the platform
// used to use instead of <button>s.
// This can be removed once there is a proper API that XBlocks can use
// to add children or allow authors to add children.
renderContainerPage(this, mockContainerXBlockHtml);
$('.add-xblock-component-button').each(function() {
var $htmlAsLink = $($(this).prop('outerHTML').replace(/(<\/?)button/g, '$1a'));
$(this).replaceWith($htmlAsLink);
});
$('.add-xblock-component-button').first().click();
EditHelpers.verifyXBlockRequest(requests, {
category: 'discussion',
type: 'discussion',
parent_locator: 'locator-group-A'
});
});
getDisplayNameWrapper = function() {
return containerPage.$('.wrapper-xblock-field');
it('shows a notification while creating', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/);
AjaxHelpers.respondWithJson(requests, { });
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not insert component upon failure', function() {
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
AjaxHelpers.expectNoRequests(requests);
expectComponents(getGroupElement(), allComponentsInGroup);
});
describe('Template Picker', function() {
var showTemplatePicker, verifyCreateHtmlComponent;
showTemplatePicker = function() {
containerPage.$('.new-component .new-component-type button.multiple-templates')[0].click();
};
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();
// Expect the correct title to be shown
expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container');
// Press the save button and respond with a success message to the save
EditHelpers.pressModalButton('.action-save');
AjaxHelpers.respondWithJson(requests, { });
expect(EditHelpers.isShowingModal()).toBeFalsy();
// Expect the last request be to refresh the container page
handleContainerPageRefresh(requests);
// 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: 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('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
beforeEach(function() {
EditHelpers.installMockXModule({
data: '<p>Some HTML</p>',
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 + "']");
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html button')[templateIndex].click();
EditHelpers.verifyXBlockRequest(requests, expectedRequest);
AjaxHelpers.respondWithJson(requests, {locator: 'new_item'});
respondWithHtml(mockXBlockHtml);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
};
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);
};
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, {});
};
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);
it('can add an HTML component without a template', function() {
verifyCreateHtmlComponent(this, 0, {
category: 'html',
parent_locator: 'locator-group-A'
});
});
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
});
// 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/);
it('can add an HTML component with a template', function() {
verifyCreateHtmlComponent(this, 1, {
category: 'html',
boilerplate: 'announcement.yaml',
parent_locator: 'locator-group-A'
});
});
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();
spyOn(containerPage.xblockView, 'togglePreviews');
containerPage.$('.toggle-preview-button').click();
expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled();
});
}
it('does not show the support legend if show_legend is false', function() {
// By default, show_legend is false in the mock component Templates.
renderContainerPage(this, mockContainerXBlockHtml);
showTemplatePicker();
expect(containerPage.$('.support-documentation').length).toBe(0);
});
describe('createNewComponent ', function() {
var clickNewComponent;
it('does show the support legend if show_legend is true', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: false
}
}],
{
parse: true
}),
supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// On this page, groups are being shown, each of which has a new component menu.
expect(supportDocumentation.length).toBeGreaterThan(0);
clickNewComponent = function(index) {
containerPage.$('.new-component .new-component-type button.single-template')[index].click();
// check that the documentation label is displayed
expect($(supportDocumentation[0]).find('.support-documentation-link').text().trim())
.toBe('Documentation Label:');
// show_unsupported_xblocks is false, so only 2 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(2);
});
it('does show unsupported level if enabled', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: true
}
}],
{
parse: true
}),
supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// show_unsupported_xblocks is true, so 3 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(3);
// verify only one has the unsupported item
expect($(supportDocumentation[0]).find('.fa-circle-o').length).toBe(1);
});
it('does render support level indicators if present in JSON', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text',
support_level: 'fs'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement',
support_level: 'ps'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML',
support_level: 'us'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: true
}
}],
{
parse: true
}),
supportLevelIndicators, getScreenReaderText;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportLevelIndicators = $(containerPage.$('.new-component-template')[0])
.find('.support-level');
expect(supportLevelIndicators.length).toBe(3);
getScreenReaderText = function(index) {
return $($(supportLevelIndicators[index]).siblings()[0]).text().trim();
};
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'
});
});
it('also works for older-style add component links', function() {
// Some third party xblocks (problem-builder in particular) expect add
// event handlers on custom <a> add buttons which is what the platform
// used to use instead of <button>s.
// This can be removed once there is a proper API that XBlocks can use
// to add children or allow authors to add children.
renderContainerPage(this, mockContainerXBlockHtml);
$('.add-xblock-component-button').each(function() {
var $htmlAsLink = $($(this).prop('outerHTML').replace(/(<\/?)button/g, '$1a'));
$(this).replaceWith($htmlAsLink);
});
$('.add-xblock-component-button').first().click();
EditHelpers.verifyXBlockRequest(requests, {
category: 'discussion',
type: 'discussion',
parent_locator: 'locator-group-A'
});
});
it('shows a notification while creating', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/);
AjaxHelpers.respondWithJson(requests, { });
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not insert component upon failure', function() {
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
AjaxHelpers.expectNoRequests(requests);
expectComponents(getGroupElement(), allComponentsInGroup);
});
describe('Template Picker', function() {
var showTemplatePicker, verifyCreateHtmlComponent;
showTemplatePicker = function() {
containerPage.$('.new-component .new-component-type button.multiple-templates')[0].click();
};
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html button')[templateIndex].click();
EditHelpers.verifyXBlockRequest(requests, expectedRequest);
AjaxHelpers.respondWithJson(requests, {locator: 'new_item'});
respondWithHtml(mockXBlockHtml);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
};
it('can add an HTML component without a template', function() {
verifyCreateHtmlComponent(this, 0, {
category: 'html',
parent_locator: 'locator-group-A'
});
});
it('can add an HTML component with a template', function() {
verifyCreateHtmlComponent(this, 1, {
category: 'html',
boilerplate: 'announcement.yaml',
parent_locator: 'locator-group-A'
});
});
it('does not show the support legend if show_legend is false', function() {
// By default, show_legend is false in the mock component Templates.
renderContainerPage(this, mockContainerXBlockHtml);
showTemplatePicker();
expect(containerPage.$('.support-documentation').length).toBe(0);
});
it('does show the support legend if show_legend is true', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: false
}
}],
{
parse: true
}),
supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// On this page, groups are being shown, each of which has a new component menu.
expect(supportDocumentation.length).toBeGreaterThan(0);
// check that the documentation label is displayed
expect($(supportDocumentation[0]).find('.support-documentation-link').text().trim())
.toBe('Documentation Label:');
// show_unsupported_xblocks is false, so only 2 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(2);
});
it('does show unsupported level if enabled', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: true
}
}],
{
parse: true
}),
supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// show_unsupported_xblocks is true, so 3 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(3);
// verify only one has the unsupported item
expect($(supportDocumentation[0]).find('.fa-circle-o').length).toBe(1);
});
it('does render support level indicators if present in JSON', function() {
var templates = new ComponentTemplates([
{
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text',
support_level: 'fs'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement',
support_level: 'ps'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML',
support_level: 'us'
}],
type: 'html',
support_legend: {
show_legend: true,
documentation_label: 'Documentation Label:',
allow_unsupported_xblocks: true
}
}],
{
parse: true
}),
supportLevelIndicators, getScreenReaderText;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportLevelIndicators = $(containerPage.$('.new-component-template')[0])
.find('.support-level');
expect(supportLevelIndicators.length).toBe(3);
getScreenReaderText = function(index) {
return $($(supportLevelIndicators[index]).siblings()[0]).text().trim();
};
// Verify one level of each type was rendered.
expect(getScreenReaderText(0)).toBe('Fully Supported');
expect(getScreenReaderText(1)).toBe('Provisionally Supported');
expect(getScreenReaderText(2)).toBe('Not Supported');
});
});
// Verify one level of each type was rendered.
expect(getScreenReaderText(0)).toBe('Fully Supported');
expect(getScreenReaderText(1)).toBe('Provisionally Supported');
expect(getScreenReaderText(2)).toBe('Not Supported');
});
});
});
}
// Create a suite for a non-paged container that includes 'edit visibility' buttons
parameterized_suite('Non paged',
{
page: ContainerPage,
requiresPageRefresh: false,
initial: 'mock/mock-container-xblock.underscore',
addResponse: 'mock/mock-xblock.underscore',
hasVisibilityEditor: true,
pagedSpecificTests: false,
hasMoveModal: true
}
);
// Create a suite for a paged container that does not include 'edit visibility' buttons
parameterized_suite('Paged',
{
page: PagedContainerPage,
page_size: 42,
requiresPageRefresh: true,
initial: 'mock/mock-container-paged-xblock.underscore',
addResponse: 'mock/mock-xblock-paged.underscore',
hasVisibilityEditor: false,
pagedSpecificTests: true,
hasMoveModal: false
}
);
});
});
}
// Create a suite for a non-paged container that includes 'edit visibility' buttons
parameterized_suite('Non paged',
{
page: ContainerPage,
requiresPageRefresh: false,
initial: 'templates/mock/mock-container-xblock.underscore',
addResponse: 'templates/mock/mock-xblock.underscore',
hasVisibilityEditor: true,
pagedSpecificTests: false,
hasMoveModal: true
}
);
// Create a suite for a paged container that does not include 'edit visibility' buttons
parameterized_suite('Paged',
{
page: PagedContainerPage,
page_size: 42,
requiresPageRefresh: true,
initial: 'templates/mock/mock-container-paged-xblock.underscore',
addResponse: 'templates/mock/mock-xblock-paged.underscore',
hasVisibilityEditor: false,
pagedSpecificTests: true,
hasMoveModal: false
}
);

View File

@@ -1,620 +1,627 @@
define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'js/spec_helpers/edit_helpers',
'common/js/components/views/feedback_prompt', 'js/views/pages/container',
'js/views/pages/container_subviews', 'js/models/xblock_info', 'js/views/utils/xblock_utils',
'js/models/course'],
function($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, Prompt, ContainerPage, ContainerSubviews,
XBlockInfo, XBlockUtils, Course) {
var VisibilityState = XBlockUtils.VisibilityState;
import $ from 'jquery';
import _ from 'underscore';
import str from 'underscore.string';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import EditHelpers from 'js/spec_helpers/edit_helpers';
import Prompt from 'common/js/components/views/feedback_prompt';
import ContainerPage from 'js/views/pages/container';
import ContainerSubviews from 'js/views/pages/container_subviews';
import XBlockInfo from 'js/models/xblock_info';
import XBlockUtils from 'js/views/utils/xblock_utils';
import Course from 'js/models/course';
describe('Container Subviews', function() {
var model, containerPage, requests, createContainerPage, renderContainerPage,
respondWithHtml, fetch,
disabledCss = 'is-disabled',
defaultXBlockInfo, createXBlockInfo,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
var VisibilityState = XBlockUtils.VisibilityState;
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
describe('Container Subviews', function() {
var model, containerPage, requests, createContainerPage, renderContainerPage,
respondWithHtml, fetch,
disabledCss = 'is-disabled',
defaultXBlockInfo, createXBlockInfo,
mockContainerPage = readFixtures('templates/mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('templates/mock/mock-empty-container-xblock.underscore');
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('publish-xblock');
TemplateHelpers.installTemplate('publish-history');
TemplateHelpers.installTemplate('unit-outline');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
requests = AjaxHelpers.requests(this);
});
afterEach(function() {
delete window.course;
if (containerPage !== undefined) {
containerPage.remove();
}
});
defaultXBlockInfo = {
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
published: false,
has_changes: false,
visibility_state: VisibilityState.unscheduled,
edited_on: 'Jul 02, 2014 at 14:20 UTC', edited_by: 'joe',
published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako',
currently_visible_to_students: false
};
createXBlockInfo = function(options) {
return _.extend(_.extend({}, defaultXBlockInfo), options || {});
};
createContainerPage = function(test, options) {
model = new XBlockInfo(createXBlockInfo(options), {parse: true});
containerPage = new ContainerPage({
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
};
renderContainerPage = function(test, html, options) {
createContainerPage(test, options);
containerPage.render();
respondWithHtml(html, options);
};
respondWithHtml = function(html, options) {
AjaxHelpers.respondWithJson(
requests,
{html: html, resources: []}
);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, createXBlockInfo(options));
};
fetch = function(json) {
json = createXBlockInfo(json);
model.fetch();
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, json);
};
describe('ViewLiveButtonController', function() {
var viewPublishedCss = '.button-view',
visibilityNoteCss = '.note-visibility';
it('renders correctly for unscheduled unit', function() {
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
expect(containerPage.$(viewPublishedCss).attr('title')).toBe('Open the courseware in the LMS');
expect(containerPage.$('.button-preview')).not.toHaveClass(disabledCss);
expect(containerPage.$('.button-preview').attr('title')).toBe('Preview the courseware in the LMS');
});
it('updates when publish state changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: true});
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
fetch({published: false});
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
});
});
describe('Publisher', function() {
var headerCss = '.pub-status',
bitPublishingCss = 'div.bit-publishing',
liveClass = 'is-live',
readyClass = 'is-ready',
staffOnlyClass = 'is-staff-only',
scheduledClass = 'is-scheduled',
unscheduledClass = '',
hasWarningsClass = 'has-warnings',
publishButtonCss = '.action-publish',
discardChangesButtonCss = '.action-discard',
lastDraftCss = '.wrapper-last-draft',
releaseDateTitleCss = '.wrapper-release .title',
releaseDateContentCss = '.wrapper-release .copy',
releaseDateDateCss = '.wrapper-release .copy .release-date',
releaseDateWithCss = '.wrapper-release .copy .release-with',
promptSpies, sendDiscardChangesToServer, verifyPublishingBitUnscheduled;
sendDiscardChangesToServer = function() {
// Helper function to do the discard operation, up until the server response.
containerPage.render();
respondWithHtml(mockContainerXBlockHtml);
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
// Click discard changes
containerPage.$(discardChangesButtonCss).click();
// Confirm the discard.
expect(promptSpies.constructor).toHaveBeenCalled();
promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(promptSpies);
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container',
{publish: 'discard_changes'}
);
};
verifyPublishingBitUnscheduled = function() {
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(unscheduledClass);
};
beforeEach(function() {
promptSpies = jasmine.stealth.spyOnConstructor(Prompt, 'Warning', ['show', 'hide']);
promptSpies.show.and.returnValue(this.promptSpies);
});
afterEach(jasmine.stealth.clearSpies);
it('renders correctly with private content', function() {
var verifyPrivateState = function() {
expect(containerPage.$(headerCss).text()).toContain('Draft (Never published)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
};
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: false, has_changes: false, visibility_state: VisibilityState.needsAttention});
verifyPrivateState();
fetch({published: false, has_changes: true, visibility_state: VisibilityState.needsAttention});
verifyPrivateState();
});
it('renders correctly with published content', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, has_changes: false, visibility_state: VisibilityState.ready,
release_date: 'Jul 02, 2030 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Published (not yet released)');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({
published: true, has_changes: true, visibility_state: VisibilityState.needsAttention,
release_date: 'Jul 02, 2030 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({published: true, has_changes: false, visibility_state: VisibilityState.live,
release_date: 'Jul 02, 1990 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Published and Live');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({published: true, has_changes: false, visibility_state: VisibilityState.unscheduled,
release_date: null
});
expect(containerPage.$(headerCss).text()).toContain('Published (not yet released)');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
verifyPublishingBitUnscheduled();
});
it('can publish private content', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass);
// Click publish
containerPage.$(publishButtonCss).click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Publishing/);
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container',
{publish: 'make_public'}
);
// Response to publish call
AjaxHelpers.respondWithJson(requests, {id: 'locator-container', data: null, metadata: {}});
EditHelpers.verifyNotificationHidden(notificationSpy);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
// Response to fetch
AjaxHelpers.respondWithJson(
requests,
createXBlockInfo({
published: true, has_changes: false, visibility_state: VisibilityState.ready
})
);
// Verify updates displayed
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get('publish')).toBeNull();
});
it('does not refresh if publish fails', function() {
renderContainerPage(this, mockContainerXBlockHtml);
verifyPublishingBitUnscheduled();
// Click publish
containerPage.$(publishButtonCss).click();
// Respond with failure
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
// Verify still in draft (unscheduled) state.
verifyPublishingBitUnscheduled();
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get('publish')).toBeNull();
});
it('can discard changes', function() {
var notificationSpy, renderPageSpy, numRequests;
createContainerPage(this);
notificationSpy = EditHelpers.createNotificationSpy();
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').and.callThrough();
sendDiscardChangesToServer();
numRequests = requests.length;
// Respond with success.
AjaxHelpers.respondWithJson(requests, {id: 'locator-container'});
EditHelpers.verifyNotificationHidden(notificationSpy);
// Verify other requests are sent to the server to update page state.
// Response to fetch, specifying the very next request (as multiple requests will be sent to server)
expect(requests.length > numRequests).toBeTruthy();
expect(containerPage.model.get('publish')).toBeNull();
expect(renderPageSpy).toHaveBeenCalled();
});
it('does not fetch if discard changes fails', function() {
var renderPageSpy, numRequests;
createContainerPage(this);
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').and.callThrough();
sendDiscardChangesToServer();
// Respond with failure
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.model.get('publish')).toBeNull();
expect(renderPageSpy).not.toHaveBeenCalled();
});
it('does not discard changes on cancel', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
// Click discard changes
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
containerPage.$(discardChangesButtonCss).click();
// Click cancel to confirmation.
expect(promptSpies.constructor).toHaveBeenCalled();
promptSpies.constructor.calls.mostRecent().args[0].actions.secondary.click(promptSpies);
AjaxHelpers.expectNoRequests(requests);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
});
it('renders the last published date and user when there are no changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'});
expect(containerPage.$(lastDraftCss).text()).
toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
});
it('renders the last saved date and user when there are changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({has_changes: true, edited_on: 'Jul 02, 2014 at 14:20 UTC', edited_by: 'joe'});
expect(containerPage.$(lastDraftCss).text()).
toContain('Draft saved on Jul 02, 2014 at 14:20 UTC by joe');
});
describe('Release Date', function() {
it('renders correctly when unreleased', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.ready, released_to_students: false,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('publish-xblock');
TemplateHelpers.installTemplate('publish-history');
TemplateHelpers.installTemplate('unit-outline');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
requests = AjaxHelpers.requests(this);
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Scheduled:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
afterEach(function() {
delete window.course;
if (containerPage !== undefined) {
containerPage.remove();
it('renders correctly when released', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.live, released_to_students: true,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Released:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
it('renders correctly when the release date is not set', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.unscheduled, released_to_students: false,
release_date: null, release_date_from: null
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Release:');
expect(containerPage.$(releaseDateContentCss).text()).toContain('Unscheduled');
});
it('renders correctly when the unit is not published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.needsAttention, released_to_students: true,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
containerPage.xblockPublisher.render();
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Release:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
});
describe('Content Visibility', function() {
var requestStaffOnly, verifyStaffOnly, verifyExplicitStaffOnly, verifyImplicitStaffOnly, promptSpy,
visibilityTitleCss = '.wrapper-visibility .title';
requestStaffOnly = function(isStaffOnly) {
var newVisibilityState;
containerPage.$('.action-staff-lock').click();
// If removing explicit staff lock with no implicit staff lock, click 'Yes' to confirm
if (!isStaffOnly && !containerPage.model.get('ancestor_has_staff_lock')) {
EditHelpers.confirmPrompt(promptSpy);
}
});
defaultXBlockInfo = {
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
published: false,
has_changes: false,
visibility_state: VisibilityState.unscheduled,
edited_on: 'Jul 02, 2014 at 14:20 UTC', edited_by: 'joe',
published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako',
currently_visible_to_students: false
};
createXBlockInfo = function(options) {
return _.extend(_.extend({}, defaultXBlockInfo), options || {});
};
createContainerPage = function(test, options) {
model = new XBlockInfo(createXBlockInfo(options), {parse: true});
containerPage = new ContainerPage({
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container', {
publish: 'republish',
metadata: {visible_to_staff_only: isStaffOnly ? true : null}
});
AjaxHelpers.respondWithJson(requests, {
data: null,
id: 'locator-container',
metadata: {
visible_to_staff_only: isStaffOnly ? true : null
}
});
};
renderContainerPage = function(test, html, options) {
createContainerPage(test, options);
containerPage.render();
respondWithHtml(html, options);
};
respondWithHtml = function(html, options) {
AjaxHelpers.respondWithJson(
requests,
{html: html, resources: []}
);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, createXBlockInfo(options));
if (isStaffOnly || containerPage.model.get('ancestor_has_staff_lock')) {
newVisibilityState = VisibilityState.staffOnly;
} else {
newVisibilityState = VisibilityState.live;
}
AjaxHelpers.respondWithJson(requests, createXBlockInfo({
published: containerPage.model.get('published'),
has_explicit_staff_lock: isStaffOnly,
visibility_state: newVisibilityState,
release_date: 'Jul 02, 2000 at 14:20 UTC'
}));
};
fetch = function(json) {
json = createXBlockInfo(json);
model.fetch();
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, json);
};
describe('ViewLiveButtonController', function() {
var viewPublishedCss = '.button-view',
visibilityNoteCss = '.note-visibility';
it('renders correctly for unscheduled unit', function() {
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
expect(containerPage.$(viewPublishedCss).attr('title')).toBe('Open the courseware in the LMS');
expect(containerPage.$('.button-preview')).not.toHaveClass(disabledCss);
expect(containerPage.$('.button-preview').attr('title')).toBe('Preview the courseware in the LMS');
});
it('updates when publish state changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: true});
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
fetch({published: false});
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
});
});
describe('Publisher', function() {
var headerCss = '.pub-status',
bitPublishingCss = 'div.bit-publishing',
liveClass = 'is-live',
readyClass = 'is-ready',
staffOnlyClass = 'is-staff-only',
scheduledClass = 'is-scheduled',
unscheduledClass = '',
hasWarningsClass = 'has-warnings',
publishButtonCss = '.action-publish',
discardChangesButtonCss = '.action-discard',
lastDraftCss = '.wrapper-last-draft',
releaseDateTitleCss = '.wrapper-release .title',
releaseDateContentCss = '.wrapper-release .copy',
releaseDateDateCss = '.wrapper-release .copy .release-date',
releaseDateWithCss = '.wrapper-release .copy .release-with',
promptSpies, sendDiscardChangesToServer, verifyPublishingBitUnscheduled;
sendDiscardChangesToServer = function() {
// Helper function to do the discard operation, up until the server response.
containerPage.render();
respondWithHtml(mockContainerXBlockHtml);
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
// Click discard changes
containerPage.$(discardChangesButtonCss).click();
// Confirm the discard.
expect(promptSpies.constructor).toHaveBeenCalled();
promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(promptSpies);
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container',
{publish: 'discard_changes'}
);
};
verifyPublishingBitUnscheduled = function() {
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass);
verifyStaffOnly = function(isStaffOnly) {
var visibilityCopy = containerPage.$('.wrapper-visibility .copy').text().trim();
if (isStaffOnly) {
expect(visibilityCopy).toContain('Staff Only');
expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass);
} else {
expect(visibilityCopy).toBe('Staff and Learners');
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(unscheduledClass);
};
verifyExplicitStaffOnly(false);
verifyImplicitStaffOnly(false);
}
};
beforeEach(function() {
promptSpies = jasmine.stealth.spyOnConstructor(Prompt, 'Warning', ['show', 'hide']);
promptSpies.show.and.returnValue(this.promptSpies);
});
verifyExplicitStaffOnly = function(isStaffOnly) {
if (isStaffOnly) {
expect(containerPage.$('.action-staff-lock .fa')).toHaveClass('fa-check-square-o');
} else {
expect(containerPage.$('.action-staff-lock .fa')).toHaveClass('fa-square-o');
}
};
it('renders correctly with private content', function() {
var verifyPrivateState = function() {
expect(containerPage.$(headerCss).text()).toContain('Draft (Never published)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
};
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: false, has_changes: false, visibility_state: VisibilityState.needsAttention});
verifyPrivateState();
verifyImplicitStaffOnly = function(isStaffOnly) {
if (isStaffOnly) {
expect(containerPage.$('.wrapper-visibility .inherited-from')).toExist();
} else {
expect(containerPage.$('.wrapper-visibility .inherited-from')).not.toExist();
}
};
fetch({published: false, has_changes: true, visibility_state: VisibilityState.needsAttention});
verifyPrivateState();
});
it('renders correctly with published content', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, has_changes: false, visibility_state: VisibilityState.ready,
release_date: 'Jul 02, 2030 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Published (not yet released)');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({
published: true, has_changes: true, visibility_state: VisibilityState.needsAttention,
release_date: 'Jul 02, 2030 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({published: true, has_changes: false, visibility_state: VisibilityState.live,
release_date: 'Jul 02, 1990 at 14:20 UTC'
});
expect(containerPage.$(headerCss).text()).toContain('Published and Live');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
fetch({published: true, has_changes: false, visibility_state: VisibilityState.unscheduled,
release_date: null
});
expect(containerPage.$(headerCss).text()).toContain('Published (not yet released)');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
verifyPublishingBitUnscheduled();
});
it('can publish private content', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass);
// Click publish
containerPage.$(publishButtonCss).click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Publishing/);
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container',
{publish: 'make_public'}
);
// Response to publish call
AjaxHelpers.respondWithJson(requests, {id: 'locator-container', data: null, metadata: {}});
EditHelpers.verifyNotificationHidden(notificationSpy);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
// Response to fetch
AjaxHelpers.respondWithJson(
requests,
createXBlockInfo({
published: true, has_changes: false, visibility_state: VisibilityState.ready
})
);
// Verify updates displayed
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get('publish')).toBeNull();
});
it('does not refresh if publish fails', function() {
renderContainerPage(this, mockContainerXBlockHtml);
verifyPublishingBitUnscheduled();
// Click publish
containerPage.$(publishButtonCss).click();
// Respond with failure
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
// Verify still in draft (unscheduled) state.
verifyPublishingBitUnscheduled();
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get('publish')).toBeNull();
});
it('can discard changes', function() {
var notificationSpy, renderPageSpy, numRequests;
createContainerPage(this);
notificationSpy = EditHelpers.createNotificationSpy();
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').and.callThrough();
sendDiscardChangesToServer();
numRequests = requests.length;
// Respond with success.
AjaxHelpers.respondWithJson(requests, {id: 'locator-container'});
EditHelpers.verifyNotificationHidden(notificationSpy);
// Verify other requests are sent to the server to update page state.
// Response to fetch, specifying the very next request (as multiple requests will be sent to server)
expect(requests.length > numRequests).toBeTruthy();
expect(containerPage.model.get('publish')).toBeNull();
expect(renderPageSpy).toHaveBeenCalled();
});
it('does not fetch if discard changes fails', function() {
var renderPageSpy, numRequests;
createContainerPage(this);
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').and.callThrough();
sendDiscardChangesToServer();
// Respond with failure
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.model.get('publish')).toBeNull();
expect(renderPageSpy).not.toHaveBeenCalled();
});
it('does not discard changes on cancel', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
// Click discard changes
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
containerPage.$(discardChangesButtonCss).click();
// Click cancel to confirmation.
expect(promptSpies.constructor).toHaveBeenCalled();
promptSpies.constructor.calls.mostRecent().args[0].actions.secondary.click(promptSpies);
AjaxHelpers.expectNoRequests(requests);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
});
it('renders the last published date and user when there are no changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'});
expect(containerPage.$(lastDraftCss).text()).
toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
});
it('renders the last saved date and user when there are changes', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({has_changes: true, edited_on: 'Jul 02, 2014 at 14:20 UTC', edited_by: 'joe'});
expect(containerPage.$(lastDraftCss).text()).
toContain('Draft saved on Jul 02, 2014 at 14:20 UTC by joe');
});
describe('Release Date', function() {
it('renders correctly when unreleased', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.ready, released_to_students: false,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Scheduled:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
it('renders correctly when released', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.live, released_to_students: true,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Released:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
it('renders correctly when the release date is not set', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.unscheduled, released_to_students: false,
release_date: null, release_date_from: null
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Release:');
expect(containerPage.$(releaseDateContentCss).text()).toContain('Unscheduled');
});
it('renders correctly when the unit is not published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
visibility_state: VisibilityState.needsAttention, released_to_students: true,
release_date: 'Jul 02, 2014 at 14:20 UTC', release_date_from: 'Section "Week 1"'
});
containerPage.xblockPublisher.render();
expect(containerPage.$(releaseDateTitleCss).text()).toContain('Release:');
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
});
});
describe('Content Visibility', function() {
var requestStaffOnly, verifyStaffOnly, verifyExplicitStaffOnly, verifyImplicitStaffOnly, promptSpy,
visibilityTitleCss = '.wrapper-visibility .title';
requestStaffOnly = function(isStaffOnly) {
var newVisibilityState;
containerPage.$('.action-staff-lock').click();
// If removing explicit staff lock with no implicit staff lock, click 'Yes' to confirm
if (!isStaffOnly && !containerPage.model.get('ancestor_has_staff_lock')) {
EditHelpers.confirmPrompt(promptSpy);
}
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/locator-container', {
publish: 'republish',
metadata: {visible_to_staff_only: isStaffOnly ? true : null}
});
AjaxHelpers.respondWithJson(requests, {
data: null,
id: 'locator-container',
metadata: {
visible_to_staff_only: isStaffOnly ? true : null
}
});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
if (isStaffOnly || containerPage.model.get('ancestor_has_staff_lock')) {
newVisibilityState = VisibilityState.staffOnly;
} else {
newVisibilityState = VisibilityState.live;
}
AjaxHelpers.respondWithJson(requests, createXBlockInfo({
published: containerPage.model.get('published'),
has_explicit_staff_lock: isStaffOnly,
visibility_state: newVisibilityState,
release_date: 'Jul 02, 2000 at 14:20 UTC'
}));
};
verifyStaffOnly = function(isStaffOnly) {
var visibilityCopy = containerPage.$('.wrapper-visibility .copy').text().trim();
if (isStaffOnly) {
expect(visibilityCopy).toContain('Staff Only');
expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass);
} else {
expect(visibilityCopy).toBe('Staff and Learners');
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
verifyExplicitStaffOnly(false);
verifyImplicitStaffOnly(false);
}
};
verifyExplicitStaffOnly = function(isStaffOnly) {
if (isStaffOnly) {
expect(containerPage.$('.action-staff-lock .fa')).toHaveClass('fa-check-square-o');
} else {
expect(containerPage.$('.action-staff-lock .fa')).toHaveClass('fa-square-o');
}
};
verifyImplicitStaffOnly = function(isStaffOnly) {
if (isStaffOnly) {
expect(containerPage.$('.wrapper-visibility .inherited-from')).toExist();
} else {
expect(containerPage.$('.wrapper-visibility .inherited-from')).not.toExist();
}
};
it('is initially shown to all', function() {
renderContainerPage(this, mockContainerXBlockHtml);
verifyStaffOnly(false);
});
it("displays 'Is Visible To' when released and published", function() {
renderContainerPage(this, mockContainerXBlockHtml, {
released_to_students: true,
published: true,
has_changes: false
});
expect(containerPage.$(visibilityTitleCss).text()).toContain('Is Visible To');
});
it("displays 'Will Be Visible To' when not released or fully published", function() {
renderContainerPage(this, mockContainerXBlockHtml, {
released_to_students: false,
published: true,
has_changes: true
});
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To');
});
it('can be explicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml);
requestStaffOnly(true);
verifyExplicitStaffOnly(true);
verifyImplicitStaffOnly(false);
verifyStaffOnly(true);
});
it('can be implicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
staff_lock_from: 'Section Foo'
});
verifyImplicitStaffOnly(true);
verifyExplicitStaffOnly(false);
verifyStaffOnly(true);
});
it('can be explicitly and implicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
staff_lock_from: 'Section Foo'
});
requestStaffOnly(true);
// explicit staff lock overrides the display of implicit staff lock
verifyImplicitStaffOnly(false);
verifyExplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('can remove explicit staff only setting without having implicit staff only', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
has_explicit_staff_lock: true,
ancestor_has_staff_lock: false
});
requestStaffOnly(false);
verifyStaffOnly(false);
});
it('can remove explicit staff only setting while having implicit staff only', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
has_explicit_staff_lock: true,
staff_lock_from: 'Section Foo'
});
requestStaffOnly(false);
verifyExplicitStaffOnly(false);
verifyImplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('does not refresh if removing staff only is canceled', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
has_explicit_staff_lock: true,
ancestor_has_staff_lock: false
});
containerPage.$('.action-staff-lock').click();
EditHelpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
AjaxHelpers.expectNoRequests(requests);
verifyExplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('does not refresh when failing to set staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml);
containerPage.$('.action-staff-lock').click();
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
verifyStaffOnly(false);
});
});
it('is initially shown to all', function() {
renderContainerPage(this, mockContainerXBlockHtml);
verifyStaffOnly(false);
});
describe('PublishHistory', function() {
var lastPublishCss = '.wrapper-last-publish';
it('renders never published when the block is unpublished', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
published: false, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Never published');
});
it('renders the last published date and user when the block is published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'
});
expect(containerPage.$(lastPublishCss).text()).
toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
});
it('renders correctly when the block is published without publish info', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Previously published');
it("displays 'Is Visible To' when released and published", function() {
renderContainerPage(this, mockContainerXBlockHtml, {
released_to_students: true,
published: true,
has_changes: false
});
expect(containerPage.$(visibilityTitleCss).text()).toContain('Is Visible To');
});
describe('Message Area', function() {
var messageSelector = '.container-message .warning',
warningMessage = 'Caution: The last published version of this unit is live. ' +
'By publishing changes you will change the student experience.';
it('is empty for a unit that is not currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
it("displays 'Will Be Visible To' when not released or fully published", function() {
renderContainerPage(this, mockContainerXBlockHtml, {
released_to_students: false,
published: true,
has_changes: true
});
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To');
});
it('shows a message for a unit that is currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
it('can be explicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml);
requestStaffOnly(true);
verifyExplicitStaffOnly(true);
verifyImplicitStaffOnly(false);
verifyStaffOnly(true);
});
it('hides the message when the unit is hidden from students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
fetch({currently_visible_to_students: false});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
it('can be implicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
staff_lock_from: 'Section Foo'
});
verifyImplicitStaffOnly(true);
verifyExplicitStaffOnly(false);
verifyStaffOnly(true);
});
it('shows a message when a unit is made visible', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
fetch({currently_visible_to_students: true});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
it('can be explicitly and implicitly set to staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
staff_lock_from: 'Section Foo'
});
requestStaffOnly(true);
// explicit staff lock overrides the display of implicit staff lock
verifyImplicitStaffOnly(false);
verifyExplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('can remove explicit staff only setting without having implicit staff only', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
has_explicit_staff_lock: true,
ancestor_has_staff_lock: false
});
requestStaffOnly(false);
verifyStaffOnly(false);
});
it('can remove explicit staff only setting while having implicit staff only', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
ancestor_has_staff_lock: true,
has_explicit_staff_lock: true,
staff_lock_from: 'Section Foo'
});
requestStaffOnly(false);
verifyExplicitStaffOnly(false);
verifyImplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('does not refresh if removing staff only is canceled', function() {
promptSpy = EditHelpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly,
has_explicit_staff_lock: true,
ancestor_has_staff_lock: false
});
containerPage.$('.action-staff-lock').click();
EditHelpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
AjaxHelpers.expectNoRequests(requests);
verifyExplicitStaffOnly(true);
verifyStaffOnly(true);
});
it('does not refresh when failing to set staff only', function() {
renderContainerPage(this, mockContainerXBlockHtml);
containerPage.$('.action-staff-lock').click();
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
verifyStaffOnly(false);
});
});
});
describe('PublishHistory', function() {
var lastPublishCss = '.wrapper-last-publish';
it('renders never published when the block is unpublished', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
published: false, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Never published');
});
it('renders the last published date and user when the block is published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'
});
expect(containerPage.$(lastPublishCss).text()).
toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
});
it('renders correctly when the block is published without publish info', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Previously published');
});
});
describe('Message Area', function() {
var messageSelector = '.container-message .warning',
warningMessage = 'Caution: The last published version of this unit is live. ' +
'By publishing changes you will change the student experience.';
it('is empty for a unit that is not currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message for a unit that is currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
it('hides the message when the unit is hidden from students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
fetch({currently_visible_to_students: false});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message when a unit is made visible', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
fetch({currently_visible_to_students: true});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
});
});

View File

@@ -1,1984 +1,1988 @@
define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/components/utils/view_utils',
'js/views/pages/course_outline', 'js/models/xblock_outline_info', 'js/utils/date_utils',
'js/spec_helpers/edit_helpers', 'common/js/spec_helpers/template_helpers', 'js/models/course'],
function($, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils,
EditHelpers, TemplateHelpers, Course) {
describe('CourseOutlinePage', function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, getItemsOfType, getItemHeaders,
verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, selectBasicSettings,
selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
import $ from 'jquery';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import ViewUtils from 'common/js/components/utils/view_utils';
import CourseOutlinePage from 'js/views/pages/course_outline';
import XBlockOutlineInfo from 'js/models/xblock_outline_info';
import DateUtils from 'js/utils/date_utils';
import EditHelpers from 'js/spec_helpers/edit_helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import Course from 'js/models/course';
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-course',
display_name: 'Mock Course',
category: 'course',
enable_proctored_exams: true,
enable_timed_exams: true,
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
category: 'chapter',
display_name: 'Section',
children: []
},
user_partitions: [],
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false
}, options, {child_info: {children: children}});
describe('CourseOutlinePage', function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, getItemsOfType, getItemHeaders,
verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, selectBasicSettings,
selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
mockOutlinePage = readFixtures('templates/mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('templates/mock/mock-course-rerun-notification.underscore');
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-course',
display_name: 'Mock Course',
category: 'course',
enable_proctored_exams: true,
enable_timed_exams: true,
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
category: 'chapter',
display_name: 'Section',
children: []
},
user_partitions: [],
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false
}, options, {child_info: {children: children}});
};
createMockSectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-section',
display_name: 'Mock Section',
category: 'chapter',
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: []
},
user_partitions: [],
group_access: {},
user_partition_info: {},
highlights: [],
highlights_enabled: true
}, options, {child_info: {children: children}});
};
createMockSubsectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-subsection',
display_name: 'Mock Subsection',
category: 'sequential',
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
course_graders: ['Lab', 'Howework'],
has_explicit_staff_lock: false,
is_prereq: false,
prereqs: [],
prereq: '',
prereq_min_score: '',
show_correctness: 'always',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: []
},
user_partitions: [],
group_access: {},
user_partition_info: {}
}, options, {child_info: {children: children}});
};
createMockVerticalJSON = function(options) {
return $.extend(true, {}, {
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
user_partitions: [],
group_access: {},
user_partition_info: {}
}, options);
};
createMockIndexJSON = function(option) {
if (option) {
return JSON.stringify({
developer_message: 'Course has been successfully reindexed.',
user_message: 'Course has been successfully reindexed.'
});
} else {
return JSON.stringify({
developer_message: 'Could not reindex course.',
user_message: 'Could not reindex course.'
});
}
};
getItemsOfType = function(type) {
return outlinePage.$('.outline-' + type);
};
getItemHeaders = function(type) {
return getItemsOfType(type).find('> .' + type + '-header');
};
verifyItemsExpanded = function(type, isExpanded) {
var element = getItemsOfType(type);
if (isExpanded) {
expect(element).not.toHaveClass('is-collapsed');
} else {
expect(element).toHaveClass('is-collapsed');
}
};
expandItemsAndVerifyState = function(type) {
getItemHeaders(type).find('.ui-toggle-expansion').click();
verifyItemsExpanded(type, true);
};
collapseItemsAndVerifyState = function(type) {
getItemHeaders(type).find('.ui-toggle-expansion').click();
verifyItemsExpanded(type, false);
};
selectBasicSettings = function() {
$(".modal-section .settings-tab-button[data-tab='basic']").click();
};
selectVisibilitySettings = function() {
$(".modal-section .settings-tab-button[data-tab='visibility']").click();
};
selectAdvancedSettings = function() {
$(".modal-section .settings-tab-button[data-tab='advanced']").click();
};
setSelfPaced = function() {
/* global course */
course.set('self_paced', true);
};
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
outlinePage = new CourseOutlinePage({
model: model,
el: $('#content')
});
if (!createOnly) {
outlinePage.render();
}
return outlinePage;
};
verifyTypePublishable = function(type, getMockCourseJSON) {
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
createCourseOutlinePageAndShowUnit = function(test, courseJSON, createOnly) {
outlinePage = createCourseOutlinePage.apply(this, arguments);
if (type === 'unit') {
expandItemsAndVerifyState('subsection');
}
};
verifyPublishButton = function(test, courseJSON, createOnly) {
createCourseOutlinePageAndShowUnit.apply(this, arguments);
expect(getItemHeaders(type).find('.publish-button')).toExist();
};
it('can be published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
getItemHeaders(type).find('.publish-button').click();
$('.wrapper-modal-window .action-publish').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, {
publish: 'make_public'
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('should show publish button if it is not published and not changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is published and changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: true
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is not published, but changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should hide publish button if it is not changed, but published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
expect(getItemHeaders(type).find('.publish-button')).not.toExist();
});
};
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
EditHelpers.installMockAnalytics();
EditHelpers.installViewTemplates();
TemplateHelpers.installTemplates([
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor',
'course-highlights-enable'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
mockEmptyCourseJSON = createMockCourseJSON();
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON()
]);
mockCourseEntranceExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({is_header_visible: false}, [
createMockVerticalJSON()
])
])
]);
// Create a mock Course object as the JS now expects it.
window.course = new Course({
id: '333',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
});
afterEach(function() {
EditHelpers.cancelModalIfShowing();
EditHelpers.removeMockAnalytics();
// Clean up after the $.datepicker
$('#start_date').datepicker('destroy');
$('#due_date').datepicker('destroy');
$('.ui-datepicker').remove();
delete window.course;
});
describe('Initial display', function() {
it('can render itself', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(outlinePage.$('.list-sections')).toExist();
expect(outlinePage.$('.list-subsections')).toExist();
expect(outlinePage.$('.list-units')).toExist();
});
it('shows a loading indicator', function() {
createCourseOutlinePage(this, mockCourseJSON, true);
expect(outlinePage.$('.ui-loading')).not.toHaveClass('is-hidden');
outlinePage.render();
expect(outlinePage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('shows subsections initially collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
verifyItemsExpanded('subsection', false);
expect(getItemsOfType('unit')).not.toExist();
});
it('unit initially exist for entrance exam', function() {
createCourseOutlinePage(this, mockCourseEntranceExamJSON);
expect(getItemsOfType('unit')).toExist();
});
});
describe('Rerun notification', function() {
it('can be dismissed', function() {
appendSetFixtures(mockRerunNotification);
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden');
$('.dismiss-button').click();
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
AjaxHelpers.respondWithNoContent(requests);
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
});
});
describe('Button bar', function() {
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
outlinePage.$('.nav-actions .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section',
courseKey: 'slashes:MockCourse'
});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('can add a second section', function() {
var sectionElements;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
outlinePage.$('.nav-actions .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section-2',
courseKey: 'slashes:MockCourse'
});
// Expect the UI to just fetch the new section and repaint it
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
expect($(sectionElements[1]).data('locator')).toEqual('mock-section-2');
});
it('can expand and collapse all sections', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
verifyItemsExpanded('section', true);
outlinePage.$('.nav-actions .button-toggle-expand-collapse .collapse-all').click();
verifyItemsExpanded('section', false);
outlinePage.$('.nav-actions .button-toggle-expand-collapse .expand-all').click();
verifyItemsExpanded('section', true);
});
it('can start reindex of a course', function() {
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
var reindexSpy = spyOn(outlinePage, 'startReIndex').and.callThrough();
var successSpy = spyOn(outlinePage, 'onIndexSuccess').and.callThrough();
var reindexButton = outlinePage.$('.button.button-reindex');
var test_url = '/course/5/search_reindex';
reindexButton.attr('href', test_url);
reindexButton.trigger('click');
AjaxHelpers.expectJsonRequest(requests, 'GET', test_url);
AjaxHelpers.respondWithJson(requests, createMockIndexJSON(true));
expect(reindexSpy).toHaveBeenCalled();
expect(successSpy).toHaveBeenCalled();
});
it('shows an error message when reindexing fails', function() {
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
var reindexSpy = spyOn(outlinePage, 'startReIndex').and.callThrough();
var errorSpy = spyOn(outlinePage, 'onIndexError').and.callThrough();
var reindexButton = outlinePage.$('.button.button-reindex');
var test_url = '/course/5/search_reindex';
reindexButton.attr('href', test_url);
reindexButton.trigger('click');
AjaxHelpers.expectJsonRequest(requests, 'GET', test_url);
AjaxHelpers.respondWithError(requests, 500, createMockIndexJSON(false));
expect(reindexSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
});
});
describe('Duplicate an xblock', function() {
var duplicateXBlockWithSuccess;
duplicateXBlockWithSuccess = function(xblockLocator, parentLocator, xblockType, xblockIndex) {
getItemHeaders(xblockType).find('.duplicate-button')[xblockIndex].click();
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
duplicate_source_locator: xblockLocator,
parent_locator: parentLocator
});
// send the response
AjaxHelpers.respondWithJson(requests, {
locator: 'locator-duplicated-xblock'
});
};
it('section can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(outlinePage.$('.list-sections li.outline-section').length).toEqual(1);
expect(getItemsOfType('section').length, 1);
duplicateXBlockWithSuccess('mock-section', 'mock-course', 'section', 0);
expect(getItemHeaders('section').length, 2);
});
it('subsection can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemsOfType('subsection').length, 1);
duplicateXBlockWithSuccess('mock-subsection', 'mock-section', 'subsection', 0);
expect(getItemHeaders('subsection').length, 2);
});
it('unit can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
expect(getItemsOfType('unit').length, 1);
duplicateXBlockWithSuccess('mock-unit', 'mock-subsection', 'unit', 0);
expect(getItemHeaders('unit').length, 2);
});
it('shows a notification when duplicating', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithJson(requests, {locator: 'locator-duplicated-xblock'});
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not duplicate an xblock upon failure', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemHeaders('section').length, 1);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithError(requests);
expect(getItemHeaders('section').length, 2);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
});
});
describe('Empty course', function() {
it('shows an empty course message initially', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
$('.no-content .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section',
courseKey: 'slashes:MockCourse'
});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('remains empty if an add fails', function() {
var requestCount;
createCourseOutlinePage(this, mockEmptyCourseJSON);
$('.no-content .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
});
describe('Content Highlights', function() {
var createCourse, createCourseWithHighlights, createCourseWithHighlightsDisabled,
clickSaveOnModal, clickCancelOnModal;
beforeEach(function() {
setSelfPaced();
});
createCourse = function(sectionOptions, courseOptions) {
createCourseOutlinePage(this,
createMockCourseJSON(courseOptions, [
createMockSectionJSON(sectionOptions)
])
);
};
createCourseWithHighlights = function(highlights) {
createCourse({highlights: highlights});
};
createCourseWithHighlightsDisabled = function() {
var highlightsDisabled = {highlights_enabled: false};
createCourse(highlightsDisabled, highlightsDisabled);
};
clickSaveOnModal = function() {
$('.wrapper-modal-window .action-save').click();
};
clickCancelOnModal = function() {
$('.wrapper-modal-window .action-cancel').click();
};
describe('Course Highlights Setting', function() {
var highlightsSetting, expectHighlightsEnabledToBe, expectServerHandshake, openHighlightsSettings;
highlightsSetting = function() {
return $('.course-highlights-setting');
};
createMockSectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-section',
display_name: 'Mock Section',
category: 'chapter',
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: []
},
user_partitions: [],
group_access: {},
user_partition_info: {},
highlights: [],
highlights_enabled: true
}, options, {child_info: {children: children}});
};
createMockSubsectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-subsection',
display_name: 'Mock Subsection',
category: 'sequential',
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
course_graders: ['Lab', 'Howework'],
has_explicit_staff_lock: false,
is_prereq: false,
prereqs: [],
prereq: '',
prereq_min_score: '',
show_correctness: 'always',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: []
},
user_partitions: [],
group_access: {},
user_partition_info: {}
}, options, {child_info: {children: children}});
};
createMockVerticalJSON = function(options) {
return $.extend(true, {}, {
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
user_partitions: [],
group_access: {},
user_partition_info: {}
}, options);
};
createMockIndexJSON = function(option) {
if (option) {
return JSON.stringify({
developer_message: 'Course has been successfully reindexed.',
user_message: 'Course has been successfully reindexed.'
});
expectHighlightsEnabledToBe = function(expectedEnabled) {
if (expectedEnabled) {
expect('.status-highlights-enabled-value.button').not.toExist();
expect('.status-highlights-enabled-value.text').toExist();
} else {
return JSON.stringify({
developer_message: 'Could not reindex course.',
user_message: 'Could not reindex course.'
});
expect('.status-highlights-enabled-value.button').toExist();
expect('.status-highlights-enabled-value.text').not.toExist();
}
};
getItemsOfType = function(type) {
return outlinePage.$('.outline-' + type);
};
getItemHeaders = function(type) {
return getItemsOfType(type).find('> .' + type + '-header');
};
verifyItemsExpanded = function(type, isExpanded) {
var element = getItemsOfType(type);
if (isExpanded) {
expect(element).not.toHaveClass('is-collapsed');
} else {
expect(element).toHaveClass('is-collapsed');
}
};
expandItemsAndVerifyState = function(type) {
getItemHeaders(type).find('.ui-toggle-expansion').click();
verifyItemsExpanded(type, true);
};
collapseItemsAndVerifyState = function(type) {
getItemHeaders(type).find('.ui-toggle-expansion').click();
verifyItemsExpanded(type, false);
};
selectBasicSettings = function() {
this.$(".modal-section .settings-tab-button[data-tab='basic']").click();
};
selectVisibilitySettings = function() {
this.$(".modal-section .settings-tab-button[data-tab='visibility']").click();
};
selectAdvancedSettings = function() {
this.$(".modal-section .settings-tab-button[data-tab='advanced']").click();
};
setSelfPaced = function() {
/* global course */
course.set('self_paced', true);
};
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
outlinePage = new CourseOutlinePage({
model: model,
el: $('#content')
});
if (!createOnly) {
outlinePage.render();
}
return outlinePage;
};
verifyTypePublishable = function(type, getMockCourseJSON) {
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
createCourseOutlinePageAndShowUnit = function(test, courseJSON, createOnly) {
outlinePage = createCourseOutlinePage.apply(this, arguments);
if (type === 'unit') {
expandItemsAndVerifyState('subsection');
}
};
verifyPublishButton = function(test, courseJSON, createOnly) {
createCourseOutlinePageAndShowUnit.apply(this, arguments);
expect(getItemHeaders(type).find('.publish-button')).toExist();
};
it('can be published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
getItemHeaders(type).find('.publish-button').click();
$('.wrapper-modal-window .action-publish').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, {
publish: 'make_public'
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('should show publish button if it is not published and not changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is published and changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: true
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is not published, but changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should hide publish button if it is not changed, but published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
expect(getItemHeaders(type).find('.publish-button')).not.toExist();
});
};
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
EditHelpers.installMockAnalytics();
EditHelpers.installViewTemplates();
TemplateHelpers.installTemplates([
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor',
'course-highlights-enable'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
mockEmptyCourseJSON = createMockCourseJSON();
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON()
]);
mockCourseEntranceExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({is_header_visible: false}, [
createMockVerticalJSON()
])
])
]);
// Create a mock Course object as the JS now expects it.
window.course = new Course({
id: '333',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
});
afterEach(function() {
EditHelpers.cancelModalIfShowing();
EditHelpers.removeMockAnalytics();
// Clean up after the $.datepicker
$('#start_date').datepicker('destroy');
$('#due_date').datepicker('destroy');
$('.ui-datepicker').remove();
delete window.course;
});
describe('Initial display', function() {
it('can render itself', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(outlinePage.$('.list-sections')).toExist();
expect(outlinePage.$('.list-subsections')).toExist();
expect(outlinePage.$('.list-units')).toExist();
});
it('shows a loading indicator', function() {
createCourseOutlinePage(this, mockCourseJSON, true);
expect(outlinePage.$('.ui-loading')).not.toHaveClass('is-hidden');
outlinePage.render();
expect(outlinePage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('shows subsections initially collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
verifyItemsExpanded('subsection', false);
expect(getItemsOfType('unit')).not.toExist();
});
it('unit initially exist for entrance exam', function() {
createCourseOutlinePage(this, mockCourseEntranceExamJSON);
expect(getItemsOfType('unit')).toExist();
});
});
describe('Rerun notification', function() {
it('can be dismissed', function() {
appendSetFixtures(mockRerunNotification);
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden');
$('.dismiss-button').click();
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
AjaxHelpers.respondWithNoContent(requests);
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
});
});
describe('Button bar', function() {
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
outlinePage.$('.nav-actions .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section',
courseKey: 'slashes:MockCourse'
});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('can add a second section', function() {
var sectionElements;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
outlinePage.$('.nav-actions .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section-2',
courseKey: 'slashes:MockCourse'
});
// Expect the UI to just fetch the new section and repaint it
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
expect($(sectionElements[1]).data('locator')).toEqual('mock-section-2');
});
it('can expand and collapse all sections', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
verifyItemsExpanded('section', true);
outlinePage.$('.nav-actions .button-toggle-expand-collapse .collapse-all').click();
verifyItemsExpanded('section', false);
outlinePage.$('.nav-actions .button-toggle-expand-collapse .expand-all').click();
verifyItemsExpanded('section', true);
});
it('can start reindex of a course', function() {
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
var reindexSpy = spyOn(outlinePage, 'startReIndex').and.callThrough();
var successSpy = spyOn(outlinePage, 'onIndexSuccess').and.callThrough();
var reindexButton = outlinePage.$('.button.button-reindex');
var test_url = '/course/5/search_reindex';
reindexButton.attr('href', test_url);
reindexButton.trigger('click');
AjaxHelpers.expectJsonRequest(requests, 'GET', test_url);
AjaxHelpers.respondWithJson(requests, createMockIndexJSON(true));
expect(reindexSpy).toHaveBeenCalled();
expect(successSpy).toHaveBeenCalled();
});
it('shows an error message when reindexing fails', function() {
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
var reindexSpy = spyOn(outlinePage, 'startReIndex').and.callThrough();
var errorSpy = spyOn(outlinePage, 'onIndexError').and.callThrough();
var reindexButton = outlinePage.$('.button.button-reindex');
var test_url = '/course/5/search_reindex';
reindexButton.attr('href', test_url);
reindexButton.trigger('click');
AjaxHelpers.expectJsonRequest(requests, 'GET', test_url);
AjaxHelpers.respondWithError(requests, 500, createMockIndexJSON(false));
expect(reindexSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
});
});
describe('Duplicate an xblock', function() {
var duplicateXBlockWithSuccess;
duplicateXBlockWithSuccess = function(xblockLocator, parentLocator, xblockType, xblockIndex) {
getItemHeaders(xblockType).find('.duplicate-button')[xblockIndex].click();
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
duplicate_source_locator: xblockLocator,
parent_locator: parentLocator
});
// send the response
AjaxHelpers.respondWithJson(requests, {
locator: 'locator-duplicated-xblock'
});
};
it('section can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(outlinePage.$('.list-sections li.outline-section').length).toEqual(1);
expect(getItemsOfType('section').length, 1);
duplicateXBlockWithSuccess('mock-section', 'mock-course', 'section', 0);
expect(getItemHeaders('section').length, 2);
});
it('subsection can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemsOfType('subsection').length, 1);
duplicateXBlockWithSuccess('mock-subsection', 'mock-section', 'subsection', 0);
expect(getItemHeaders('subsection').length, 2);
});
it('unit can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
expect(getItemsOfType('unit').length, 1);
duplicateXBlockWithSuccess('mock-unit', 'mock-subsection', 'unit', 0);
expect(getItemHeaders('unit').length, 2);
});
it('shows a notification when duplicating', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithJson(requests, {locator: 'locator-duplicated-xblock'});
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not duplicate an xblock upon failure', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemHeaders('section').length, 1);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithError(requests);
expect(getItemHeaders('section').length, 2);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
});
});
describe('Empty course', function() {
it('shows an empty course message initially', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
$('.no-content .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'mock-section',
courseKey: 'slashes:MockCourse'
});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('remains empty if an add fails', function() {
var requestCount;
createCourseOutlinePage(this, mockEmptyCourseJSON);
$('.no-content .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'chapter',
display_name: 'Section',
parent_locator: 'mock-course'
});
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
});
describe('Content Highlights', function() {
var createCourse, createCourseWithHighlights, createCourseWithHighlightsDisabled,
clickSaveOnModal, clickCancelOnModal;
beforeEach(function() {
setSelfPaced();
});
createCourse = function(sectionOptions, courseOptions) {
createCourseOutlinePage(this,
createMockCourseJSON(courseOptions, [
createMockSectionJSON(sectionOptions)
])
);
};
createCourseWithHighlights = function(highlights) {
createCourse({highlights: highlights});
};
createCourseWithHighlightsDisabled = function() {
var highlightsDisabled = {highlights_enabled: false};
createCourse(highlightsDisabled, highlightsDisabled);
};
clickSaveOnModal = function() {
$('.wrapper-modal-window .action-save').click();
};
clickCancelOnModal = function() {
$('.wrapper-modal-window .action-cancel').click();
};
describe('Course Highlights Setting', function() {
var highlightsSetting, expectHighlightsEnabledToBe, expectServerHandshake, openHighlightsSettings;
highlightsSetting = function() {
return $('.course-highlights-setting');
};
expectHighlightsEnabledToBe = function(expectedEnabled) {
if (expectedEnabled) {
expect('.status-highlights-enabled-value.button').not.toExist();
expect('.status-highlights-enabled-value.text').toExist();
} else {
expect('.status-highlights-enabled-value.button').toExist();
expect('.status-highlights-enabled-value.text').not.toExist();
}
};
expectServerHandshake = function() {
// POST to update course
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-course', {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true
}
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated course
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(
requests, createMockCourseJSON({highlights_enabled_for_messaging: true})
);
};
openHighlightsSettings = function() {
$('button.status-highlights-enabled-value').click();
};
it('does not display settings when disabled', function() {
createCourseWithHighlightsDisabled();
expect(highlightsSetting()).not.toExist();
});
it('displays settings when enabled', function() {
createCourseWithHighlights([]);
expect(highlightsSetting()).toExist();
});
it('displays settings as not enabled for messaging', function() {
createCourse();
expectHighlightsEnabledToBe(false);
});
it('displays settings as enabled for messaging', function() {
createCourse({}, {highlights_enabled_for_messaging: true});
expectHighlightsEnabledToBe(true);
});
it('changes settings when enabled for messaging', function() {
createCourse();
openHighlightsSettings();
clickSaveOnModal();
expectServerHandshake();
expectHighlightsEnabledToBe(true);
});
it('does not change settings when enabling is cancelled', function() {
createCourse();
openHighlightsSettings();
clickCancelOnModal();
expectHighlightsEnabledToBe(false);
});
});
describe('Section Highlights', function() {
var mockHighlightValues, highlightsLink, highlightInputs, openHighlights, saveHighlights,
cancelHighlights, setHighlights, expectHighlightLinkNumberToBe, expectHighlightsToBe,
expectServerHandshakeWithHighlights, expectHighlightsToUpdate,
maxNumHighlights = 5;
mockHighlightValues = function(numberOfHighlights) {
var highlights = [],
i;
for (i = 0; i < numberOfHighlights; i++) {
highlights.push('Highlight' + (i + 1));
}
return highlights;
};
highlightsLink = function() {
return outlinePage.$('.section-status >> .highlights-button');
};
highlightInputs = function() {
return $('.highlight-input-text');
};
openHighlights = function() {
highlightsLink().click();
};
saveHighlights = function() {
clickSaveOnModal();
};
cancelHighlights = function() {
clickCancelOnModal();
};
setHighlights = function(highlights) {
var i;
for (i = 0; i < highlights.length; i++) {
$(highlightInputs()[i]).val(highlights[i]);
}
for (i = highlights.length; i < maxNumHighlights; i++) {
$(highlightInputs()[i]).val('');
}
};
expectHighlightLinkNumberToBe = function(expectedNumber) {
var link = highlightsLink();
expect(link).toContainText('Section Highlights');
expect(link.find('.number-highlights')).toHaveHtml(expectedNumber);
};
expectHighlightsToBe = function(expectedHighlights) {
var highlights = highlightInputs(),
i;
expect(highlights).toHaveLength(maxNumHighlights);
for (i = 0; i < expectedHighlights.length; i++) {
expect(highlights[i]).toHaveValue(expectedHighlights[i]);
}
for (i = expectedHighlights.length; i < maxNumHighlights; i++) {
expect(highlights[i]).toHaveValue('');
expect(highlights[i]).toHaveAttr(
'placeholder',
'A highlight to look forward to this week.'
);
}
};
expectServerHandshakeWithHighlights = function(highlights) {
// POST to update section
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
publish: 'republish',
metadata: {
highlights: highlights
}
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated section
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, createMockSectionJSON({highlights: highlights}));
};
expectHighlightsToUpdate = function(originalHighlights, updatedHighlights) {
createCourseWithHighlights(originalHighlights);
openHighlights();
setHighlights(updatedHighlights);
saveHighlights();
expectServerHandshakeWithHighlights(updatedHighlights);
expectHighlightLinkNumberToBe(updatedHighlights.length);
openHighlights();
expectHighlightsToBe(updatedHighlights);
};
it('does not display link when disabled', function() {
createCourseWithHighlightsDisabled();
expect(highlightsLink()).not.toExist();
});
it('displays link when no highlights exist', function() {
createCourseWithHighlights([]);
expectHighlightLinkNumberToBe(0);
});
it('displays link when highlights exist', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
expectHighlightLinkNumberToBe(2);
});
it('can view when no highlights exist', function() {
createCourseWithHighlights([]);
openHighlights();
expectHighlightsToBe([]);
});
it('can view existing highlights', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
openHighlights();
expectHighlightsToBe(highlights);
});
it('does not save highlights when cancelled', function() {
var originalHighlights = mockHighlightValues(2),
editedHighlights = originalHighlights;
editedHighlights[1] = 'A New Value';
createCourseWithHighlights(originalHighlights);
openHighlights();
setHighlights(editedHighlights);
cancelHighlights();
AjaxHelpers.expectNoRequests(requests);
openHighlights();
expectHighlightsToBe(originalHighlights);
});
it('can add highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(0),
mockHighlightValues(1)
);
});
it('can remove highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(5),
mockHighlightValues(3)
);
});
it('can edit highlights', function() {
var originalHighlights = mockHighlightValues(3),
editedHighlights = originalHighlights;
editedHighlights[2] = 'A New Value';
expectHighlightsToUpdate(originalHighlights, editedHighlights);
});
});
});
describe('Section', function() {
var getDisplayNameWrapper;
getDisplayNameWrapper = function() {
return getItemHeaders('section').find('.wrapper-xblock-field');
};
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON(),
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
]));
getItemHeaders('section').find('.delete-button').first().click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectNoRequests(requests); // No fetch should be performed
expect(outlinePage.$('[data-locator="mock-section"]')).not.toExist();
expect(outlinePage.$('[data-locator="mock-section-2"]')).toExist();
});
it('can be deleted if it is the only section', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
getItemHeaders('section').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('remains visible if its deletion fails', function() {
var promptSpy = EditHelpers.createPromptSpy(),
requestCount;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
getItemHeaders('section').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('can add a subsection', function() {
createCourseOutlinePage(this, mockCourseJSON);
getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'sequential',
display_name: 'Subsection',
parent_locator: 'mock-section'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'new-mock-subsection',
courseKey: 'slashes:MockCourse'
});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can be renamed inline', function() {
var updatedDisplayName = 'Updated Section Name',
displayNameWrapper,
sectionModel;
createCourseOutlinePage(this, mockCourseJSON);
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);
sectionModel = outlinePage.model.get('child_info').children[0];
expect(sectionModel.get('display_name')).toBe(updatedDisplayName);
});
it('can be expanded and collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
collapseItemsAndVerifyState('section');
expandItemsAndVerifyState('section');
collapseItemsAndVerifyState('section');
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
$('#start_date').val('1/2/2015');
// Section release date can't be cleared.
expect($('.wrapper-modal-window .action-clear')).not.toExist();
// Section does not contain due_date or grading type selector
expect($('due_date')).not.toExist();
expect($('grading_format')).not.toExist();
// Staff lock controls are always visible on the visibility tab
selectVisibilitySettings();
expect($('#staff_lock')).toExist();
selectBasicSettings();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
metadata: {
start: '2015-01-02T00:00:00.000Z'
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
var mockResponseSectionJSON = createMockSectionJSON({
release_date: 'Jan 02, 2015 at 00:00 UTC'
}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, mockResponseSectionJSON);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-section .status-release-value')).toContainText('Jan 02, 2015 at 00:00 UTC');
});
verifyTypePublishable('section', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON(options, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished subsections and units', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true, display_name: 'Unit 1'})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
]),
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
])
])
]),
modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('section').first().find('.publish-button').click();
$modalWindow = $('.wrapper-modal-window');
expect($modalWindow.find('.outline-unit').length).toBe(3);
expect(_.compact(_.map($modalWindow.find('.outline-unit').text().split('\n'), $.trim))).toEqual(
['Unit 100', 'Unit 50', 'Unit 1']
);
expect($modalWindow.find('.outline-subsection').length).toBe(2);
});
});
describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setContentVisibility, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, checkOptionFieldVisibility,
defaultModalSettings, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() {
return getItemHeaders('subsection').find('.wrapper-xblock-field');
};
setEditModalValues = function(start_date, due_date, grading_type) {
$('#start_date').val(start_date);
$('#due_date').val(due_date);
$('#grading_type').val(grading_type);
};
setContentVisibility = function(visibility) {
$('input[name=content-visibility][value=' + visibility + ']').prop('checked', true);
};
selectDisableSpecialExams = function() {
this.$('input.no_special_exam').prop('checked', true).trigger('change');
};
selectTimedExam = function(time_limit) {
this.$('input.timed_exam').prop('checked', true).trigger('change');
this.$('.field-time-limit input').val(time_limit);
this.$('.field-time-limit input').trigger('focusout');
setContentVisibility('hide_after_due');
};
selectProctoredExam = function(time_limit) {
this.$('input.proctored_exam').prop('checked', true).trigger('change');
this.$('.field-time-limit input').val(time_limit);
this.$('.field-time-limit input').trigger('focusout');
};
selectPracticeExam = function(time_limit) {
this.$('input.practice_exam').prop('checked', true).trigger('change');
this.$('.field-time-limit input').val(time_limit);
this.$('.field-time-limit input').trigger('focusout');
};
selectPrerequisite = function() {
this.$('#is_prereq').prop('checked', true).trigger('change');
};
selectLastPrerequisiteSubsection = function(minScore) {
this.$('#prereq option:last').prop('selected', true).trigger('change');
this.$('#prereq_min_score').val(minScore).trigger('keyup');
};
// Helper to validate oft-checked additional option fields' visibility
checkOptionFieldVisibility = function(time_limit, review_rules) {
expect($('.field-time-limit').is(':visible')).toBe(time_limit);
expect($('.field-exam-review-rules').is(':visible')).toBe(review_rules);
};
expectShowCorrectness = function(showCorrectness) {
expect($('input[name=show-correctness][value=' + showCorrectness + ']').is(':checked')).toBe(true);
};
getMockNoPrereqOrExamsCourseJSON = function() {
var mockVerticalJSON = createMockVerticalJSON({}, []);
var mockSubsectionJSON = createMockSubsectionJSON({}, [mockVerticalJSON]);
delete mockSubsectionJSON.is_prereq;
delete mockSubsectionJSON.prereqs;
delete mockSubsectionJSON.prereq;
delete mockSubsectionJSON.prereq_min_score;
return createMockCourseJSON({
enable_proctored_exams: false,
enable_timed_exams: false
}, [
createMockSectionJSON({}, [mockSubsectionJSON])
]);
};
defaultModalSettings = {
graderType: 'notgraded',
isPrereq: false,
expectServerHandshake = function() {
// POST to update course
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-course', {
publish: 'republish',
metadata: {
due: null,
is_practice_exam: false,
is_time_limited: false,
exam_review_rules: '',
is_proctored_enabled: false,
default_time_limit_minutes: null
highlights_enabled_for_messaging: true
}
};
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated course
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(
requests, createMockCourseJSON({highlights_enabled_for_messaging: true})
);
};
openHighlightsSettings = function() {
$('button.status-highlights-enabled-value').click();
};
it('does not display settings when disabled', function() {
createCourseWithHighlightsDisabled();
expect(highlightsSetting()).not.toExist();
});
it('displays settings when enabled', function() {
createCourseWithHighlights([]);
expect(highlightsSetting()).toExist();
});
it('displays settings as not enabled for messaging', function() {
createCourse();
expectHighlightsEnabledToBe(false);
});
it('displays settings as enabled for messaging', function() {
createCourse({}, {highlights_enabled_for_messaging: true});
expectHighlightsEnabledToBe(true);
});
it('changes settings when enabled for messaging', function() {
createCourse();
openHighlightsSettings();
clickSaveOnModal();
expectServerHandshake();
expectHighlightsEnabledToBe(true);
});
it('does not change settings when enabling is cancelled', function() {
createCourse();
openHighlightsSettings();
clickCancelOnModal();
expectHighlightsEnabledToBe(false);
});
});
describe('Section Highlights', function() {
var mockHighlightValues, highlightsLink, highlightInputs, openHighlights, saveHighlights,
cancelHighlights, setHighlights, expectHighlightLinkNumberToBe, expectHighlightsToBe,
expectServerHandshakeWithHighlights, expectHighlightsToUpdate,
maxNumHighlights = 5;
mockHighlightValues = function(numberOfHighlights) {
var highlights = [],
i;
for (i = 0; i < numberOfHighlights; i++) {
highlights.push('Highlight' + (i + 1));
}
return highlights;
};
highlightsLink = function() {
return outlinePage.$('.section-status >> .highlights-button');
};
highlightInputs = function() {
return $('.highlight-input-text');
};
openHighlights = function() {
highlightsLink().click();
};
saveHighlights = function() {
clickSaveOnModal();
};
cancelHighlights = function() {
clickCancelOnModal();
};
setHighlights = function(highlights) {
var i;
for (i = 0; i < highlights.length; i++) {
$(highlightInputs()[i]).val(highlights[i]);
}
for (i = highlights.length; i < maxNumHighlights; i++) {
$(highlightInputs()[i]).val('');
}
};
expectHighlightLinkNumberToBe = function(expectedNumber) {
var link = highlightsLink();
expect(link).toContainText('Section Highlights');
expect(link.find('.number-highlights')).toHaveHtml(expectedNumber);
};
expectHighlightsToBe = function(expectedHighlights) {
var highlights = highlightInputs(),
i;
expect(highlights).toHaveLength(maxNumHighlights);
for (i = 0; i < expectedHighlights.length; i++) {
expect(highlights[i]).toHaveValue(expectedHighlights[i]);
}
for (i = expectedHighlights.length; i < maxNumHighlights; i++) {
expect(highlights[i]).toHaveValue('');
expect(highlights[i]).toHaveAttr(
'placeholder',
'A highlight to look forward to this week.'
);
}
};
expectServerHandshakeWithHighlights = function(highlights) {
// POST to update section
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
publish: 'republish',
metadata: {
highlights: highlights
}
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated section
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, createMockSectionJSON({highlights: highlights}));
};
expectHighlightsToUpdate = function(originalHighlights, updatedHighlights) {
createCourseWithHighlights(originalHighlights);
openHighlights();
setHighlights(updatedHighlights);
saveHighlights();
expectServerHandshakeWithHighlights(updatedHighlights);
expectHighlightLinkNumberToBe(updatedHighlights.length);
openHighlights();
expectHighlightsToBe(updatedHighlights);
};
it('does not display link when disabled', function() {
createCourseWithHighlightsDisabled();
expect(highlightsLink()).not.toExist();
});
it('displays link when no highlights exist', function() {
createCourseWithHighlights([]);
expectHighlightLinkNumberToBe(0);
});
it('displays link when highlights exist', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
expectHighlightLinkNumberToBe(2);
});
it('can view when no highlights exist', function() {
createCourseWithHighlights([]);
openHighlights();
expectHighlightsToBe([]);
});
it('can view existing highlights', function() {
var highlights = mockHighlightValues(2);
createCourseWithHighlights(highlights);
openHighlights();
expectHighlightsToBe(highlights);
});
it('does not save highlights when cancelled', function() {
var originalHighlights = mockHighlightValues(2),
editedHighlights = originalHighlights;
editedHighlights[1] = 'A New Value';
createCourseWithHighlights(originalHighlights);
openHighlights();
setHighlights(editedHighlights);
cancelHighlights();
AjaxHelpers.expectNoRequests(requests);
openHighlights();
expectHighlightsToBe(originalHighlights);
});
it('can add highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(0),
mockHighlightValues(1)
);
});
it('can remove highlights', function() {
expectHighlightsToUpdate(
mockHighlightValues(5),
mockHighlightValues(3)
);
});
it('can edit highlights', function() {
var originalHighlights = mockHighlightValues(3),
editedHighlights = originalHighlights;
editedHighlights[2] = 'A New Value';
expectHighlightsToUpdate(originalHighlights, editedHighlights);
});
});
});
describe('Section', function() {
var getDisplayNameWrapper;
getDisplayNameWrapper = function() {
return getItemHeaders('section').find('.wrapper-xblock-field');
};
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON(),
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
]));
getItemHeaders('section').find('.delete-button').first().click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectNoRequests(requests); // No fetch should be performed
expect(outlinePage.$('[data-locator="mock-section"]')).not.toExist();
expect(outlinePage.$('[data-locator="mock-section-2"]')).toExist();
});
it('can be deleted if it is the only section', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
getItemHeaders('section').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
AjaxHelpers.respondWithJson(requests, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('remains visible if its deletion fails', function() {
var promptSpy = EditHelpers.createPromptSpy(),
requestCount;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
getItemHeaders('section').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
AjaxHelpers.respondWithError(requests);
AjaxHelpers.expectNoRequests(requests);
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});
it('can add a subsection', function() {
createCourseOutlinePage(this, mockCourseJSON);
getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'sequential',
display_name: 'Subsection',
parent_locator: 'mock-section'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'new-mock-subsection',
courseKey: 'slashes:MockCourse'
});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can be renamed inline', function() {
var updatedDisplayName = 'Updated Section Name',
displayNameWrapper,
sectionModel;
createCourseOutlinePage(this, mockCourseJSON);
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);
sectionModel = outlinePage.model.get('child_info').children[0];
expect(sectionModel.get('display_name')).toBe(updatedDisplayName);
});
it('can be expanded and collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
collapseItemsAndVerifyState('section');
expandItemsAndVerifyState('section');
collapseItemsAndVerifyState('section');
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
$('#start_date').val('1/2/2015');
// Section release date can't be cleared.
expect($('.wrapper-modal-window .action-clear')).not.toExist();
// Section does not contain due_date or grading type selector
expect($('due_date')).not.toExist();
expect($('grading_format')).not.toExist();
// Staff lock controls are always visible on the visibility tab
selectVisibilitySettings();
expect($('#staff_lock')).toExist();
selectBasicSettings();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
metadata: {
start: '2015-01-02T00:00:00.000Z'
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
var mockResponseSectionJSON = createMockSectionJSON({
release_date: 'Jan 02, 2015 at 00:00 UTC'
}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, mockResponseSectionJSON);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-section .status-release-value')).toContainText('Jan 02, 2015 at 00:00 UTC');
});
verifyTypePublishable('section', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON(options, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished subsections and units', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true, display_name: 'Unit 1'})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
]),
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
])
])
]),
modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('section').first().find('.publish-button').click();
var $modalWindow = $('.wrapper-modal-window');
expect($modalWindow.find('.outline-unit').length).toBe(3);
expect(_.compact(_.map($modalWindow.find('.outline-unit').text().split('\n'), $.trim))).toEqual(
['Unit 100', 'Unit 50', 'Unit 1']
);
expect($modalWindow.find('.outline-subsection').length).toBe(2);
});
});
describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setContentVisibility, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, checkOptionFieldVisibility,
defaultModalSettings, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() {
return getItemHeaders('subsection').find('.wrapper-xblock-field');
};
setEditModalValues = function(start_date, due_date, grading_type) {
$('#start_date').val(start_date);
$('#due_date').val(due_date);
$('#grading_type').val(grading_type);
};
setContentVisibility = function(visibility) {
$('input[name=content-visibility][value=' + visibility + ']').prop('checked', true);
};
selectDisableSpecialExams = function() {
$('input.no_special_exam').prop('checked', true).trigger('change');
};
selectTimedExam = function(time_limit) {
$('input.timed_exam').prop('checked', true).trigger('change');
$('.field-time-limit input').val(time_limit);
$('.field-time-limit input').trigger('focusout');
setContentVisibility('hide_after_due');
};
selectProctoredExam = function(time_limit) {
$('input.proctored_exam').prop('checked', true).trigger('change');
$('.field-time-limit input').val(time_limit);
$('.field-time-limit input').trigger('focusout');
};
selectPracticeExam = function(time_limit) {
$('input.practice_exam').prop('checked', true).trigger('change');
$('.field-time-limit input').val(time_limit);
$('.field-time-limit input').trigger('focusout');
};
selectPrerequisite = function() {
$('#is_prereq').prop('checked', true).trigger('change');
};
selectLastPrerequisiteSubsection = function(minScore) {
$('#prereq option:last').prop('selected', true).trigger('change');
$('#prereq_min_score').val(minScore).trigger('keyup');
};
// Helper to validate oft-checked additional option fields' visibility
checkOptionFieldVisibility = function(time_limit, review_rules) {
expect($('.field-time-limit').is(':visible')).toBe(time_limit);
expect($('.field-exam-review-rules').is(':visible')).toBe(review_rules);
};
expectShowCorrectness = function(showCorrectness) {
expect($('input[name=show-correctness][value=' + showCorrectness + ']').is(':checked')).toBe(true);
};
getMockNoPrereqOrExamsCourseJSON = function() {
var mockVerticalJSON = createMockVerticalJSON({}, []);
var mockSubsectionJSON = createMockSubsectionJSON({}, [mockVerticalJSON]);
delete mockSubsectionJSON.is_prereq;
delete mockSubsectionJSON.prereqs;
delete mockSubsectionJSON.prereq;
delete mockSubsectionJSON.prereq_min_score;
return createMockCourseJSON({
enable_proctored_exams: false,
enable_timed_exams: false
}, [
createMockSectionJSON({}, [mockSubsectionJSON])
]);
};
defaultModalSettings = {
graderType: 'notgraded',
isPrereq: false,
metadata: {
due: null,
is_practice_exam: false,
is_time_limited: false,
exam_review_rules: '',
is_proctored_enabled: false,
default_time_limit_minutes: null
}
};
// Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC'
}, [
createMockSubsectionJSON({
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: '2014-07-09T00:00:00Z',
format: 'Lab',
due: '2014-07-10T00:00:00Z',
has_explicit_staff_lock: true,
staff_only_message: true,
is_prereq: false,
show_correctness: 'never',
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: true
}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
getItemHeaders('subsection').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
AjaxHelpers.respondWithJson(requests, {});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can add a unit', function() {
var redirectSpy;
createCourseOutlinePage(this, mockCourseJSON);
redirectSpy = spyOn(ViewUtils, 'redirect');
getItemsOfType('subsection').find('> .outline-content > .add-unit .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'vertical',
display_name: 'Unit',
parent_locator: 'mock-subsection'
});
AjaxHelpers.respondWithJson(requests, {
locator: 'new-mock-unit',
courseKey: 'slashes:MockCourse'
});
expect(redirectSpy).toHaveBeenCalledWith('/container/new-mock-unit?action=new');
});
it('can be renamed inline', function() {
var updatedDisplayName = 'Updated Subsection Name',
displayNameWrapper,
subsectionModel;
createCourseOutlinePage(this, mockCourseJSON);
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 for the section.
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({}, [
createMockSubsectionJSON({
display_name: updatedDisplayName
})
])
);
// Find the display name again in the refreshed DOM and verify it
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0];
expect(subsectionModel.get('display_name')).toBe(updatedDisplayName);
});
it('can be expanded and collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
verifyItemsExpanded('subsection', false);
expandItemsAndVerifyState('subsection');
collapseItemsAndVerifyState('subsection');
expandItemsAndVerifyState('subsection');
});
it('subsection can show basic settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectBasicSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
});
it('subsection can show visibility settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
});
it('subsection can show advanced settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).toHaveClass('active');
});
it('subsection does not show advanced settings tab if no special exams or prerequisites', function() {
var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toExist();
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toExist();
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toExist();
});
it('unit does not show settings tab headers if there is only one tab to show', function() {
var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
outlinePage.$('.outline-unit .configure-button').click();
expect($('.settings-tabs-header').length).toBe(0);
});
it('can show correct editors for self_paced course', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
setSelfPaced();
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-settings-release').length).toBe(0);
expect($('.grading-due-date').length).toBe(0);
expect($('.edit-settings-grading').length).toBe(1);
expect($('.edit-content-visibility').length).toBe(1);
expect($('.edit-show-correctness').length).toBe(1);
});
it('can select valid time', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
var default_time = '00:30';
var valid_times = ['00:30', '23:00', '24:00', '99:00'];
var invalid_times = ['00:00', '100:00', '01:60'];
var time_limit, i;
for (i = 0; i < valid_times.length; i++) {
time_limit = valid_times[i];
selectTimedExam(time_limit);
expect($('.field-time-limit input').val()).toEqual(time_limit);
}
for (i = 0; i < invalid_times.length; i++) {
time_limit = invalid_times[i];
selectTimedExam(time_limit);
expect($('.field-time-limit input').val()).not.toEqual(time_limit);
expect($('.field-time-limit input').val()).toEqual(default_time);
}
});
it('can be saved', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', defaultModalSettings);
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectAdvancedSettings();
selectTimedExam('02:30');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
graderType: 'Lab',
publish: 'republish',
isPrereq: false,
metadata: {
visible_to_staff_only: null,
start: '2014-07-09T00:00:00.000Z',
due: '2014-07-10T00:00:00.000Z',
exam_review_rules: '',
is_time_limited: true,
is_practice_exam: false,
is_proctored_enabled: false,
default_time_limit_minutes: 150,
hide_after_due: true
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, mockServerValuesJson);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-subsection .status-release-value')).toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).toContainText(
'Due: Jul 10, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
expect($('.outline-item .outline-subsection .status-grading-value')).toContainText('Lab');
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#due_date').val()).toBe('7/10/2014');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
expectShowCorrectness('never');
});
it('can hide time limit and hide after due fields when the None radio box is selected', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectDisableSpecialExams();
// all additional options should be hidden
expect($('.exam-options').is(':hidden')).toBe(true);
});
it('can select the practice exam', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectPracticeExam('00:30');
// time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false);
$('.wrapper-modal-window .action-save').click();
});
it('can select the timed exam', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectAdvancedSettings();
selectTimedExam('00:30');
// time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false);
$('.wrapper-modal-window .action-save').click();
});
it('can select the Proctored exam option', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectProctoredExam('00:30');
// time limit and review rules should be visible
checkOptionFieldVisibility(true, true);
$('.wrapper-modal-window .action-save').click();
});
it('entering invalid time format uses default value of 30 minutes.', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectProctoredExam('abcd');
// time limit field should be visible and have the correct value
expect($('.field-time-limit').is(':visible')).toBe(true);
expect($('.field-time-limit input').val()).toEqual('00:30');
});
it('can show a saved non-special exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
// Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC'
}, [
createMockSubsectionJSON({
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: '2014-07-09T00:00:00Z',
format: 'Lab',
due: '2014-07-10T00:00:00Z',
has_explicit_staff_lock: true,
staff_only_message: true,
is_prereq: false,
show_correctness: 'never',
has_changes: true,
is_time_limited: false,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: false
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(true);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can show a saved timed exam correctly when hide_after_due is true', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 10,
hide_after_due: true
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('00:10');
});
it('can show a saved timed exam correctly when hide_after_due is true', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 10,
hide_after_due: false
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('00:10');
expect($('.field-hide-after-due input').is(':checked')).toBe(false);
});
it('can show a saved practice exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: true,
is_proctored_exam: true,
default_time_limit_minutes: 150
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(true);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can show a saved proctored exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: true,
default_time_limit_minutes: 150
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(true);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('does not show proctored settings if proctored exams not enabled', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: false,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: true
}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
getItemHeaders('subsection').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
AjaxHelpers.respondWithJson(requests, {});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can select prerequisite', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectPrerequisite();
expect($('#is_prereq').is(':checked')).toBe(true);
$('.wrapper-modal-window .action-save').click();
});
it('can add a unit', function() {
var redirectSpy;
createCourseOutlinePage(this, mockCourseJSON);
redirectSpy = spyOn(ViewUtils, 'redirect');
getItemsOfType('subsection').find('> .outline-content > .add-unit .button-new').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'vertical',
display_name: 'Unit',
parent_locator: 'mock-subsection'
it('can be deleted when it is a prerequisite', function() {
var promptSpy = EditHelpers.createPromptSpy();
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
is_prereq: true
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
getItemHeaders('subsection').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can show a saved prerequisite correctly', function() {
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
is_prereq: true
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#is_prereq').is(':checked')).toBe(true);
});
it('does not display prerequisite subsections if none are available', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(0);
});
it('can display available prerequisite subsections', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(1);
});
it('can select prerequisite subsection', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('80');
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
expect($('#prereq option:selected').val()).toBe('usage_key');
expect($('#prereq_min_score').val()).toBe('80');
$('.wrapper-modal-window .action-save').click();
});
it('can display gating correctly', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
visibility_state: 'gated',
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
prereq: 'usage_key',
prereq_min_score: '80'
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Prerequisite: Prereq Subsection 1'
);
});
it('can show a saved prerequisite subsection correctly', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
prereq: 'usage_key',
prereq_min_score: '80'
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(1);
expect($('#prereq option:selected').val()).toBe('usage_key');
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
expect($('#prereq_min_score').val()).toBe('80');
});
it('can display validation error on non-integer minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('abc');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('5.5');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
});
it('can display validation error on out of bounds minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('-5');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('105');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
});
it('does not display validation error on valid minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
selectLastPrerequisiteSubsection('');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('80');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('0');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('100');
expect($('#prereq_min_score_error').css('display')).toBe('none');
});
it('release date, due date, grading type, and staff lock can be cleared.', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
setContentVisibility('staff_only');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, mockServerValuesJson);
expect($('.outline-subsection .status-release-value')).toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).toContainText(
'Due: Jul 10, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#due_date').val()).toBe('7/10/2014');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
$('.wrapper-modal-window .scheduled-date-input .action-clear').click();
$('.wrapper-modal-window .due-date-input .action-clear').click();
expect($('#start_date').val()).toBe('');
expect($('#due_date').val()).toBe('');
$('#grading_type').val('notgraded');
setContentVisibility('visible');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
expect($('.outline-subsection .status-release-value')).not.toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).not.toExist();
expect($('.outline-subsection .status-grading-value')).not.toExist();
expect($('.outline-subsection .status-message-copy')).not.toContainText(
'Contains staff only content'
);
});
describe('Show correctness setting set as expected.', function() {
var setShowCorrectness;
setShowCorrectness = function(showCorrectness) {
$('input[name=show-correctness][value=' + showCorrectness + ']').click();
};
describe('Show correctness set by subsection metadata.', function() {
$.each(['always', 'never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '"', function() {
var mockCourseJSONCorrectness = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({show_correctness: showCorrectness}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSONCorrectness, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expectShowCorrectness(showCorrectness);
});
AjaxHelpers.respondWithJson(requests, {
locator: 'new-mock-unit',
courseKey: 'slashes:MockCourse'
});
expect(redirectSpy).toHaveBeenCalledWith('/container/new-mock-unit?action=new');
});
it('can be renamed inline', function() {
var updatedDisplayName = 'Updated Subsection Name',
displayNameWrapper,
subsectionModel;
createCourseOutlinePage(this, mockCourseJSON);
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 for the section.
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({}, [
createMockSubsectionJSON({
display_name: updatedDisplayName
})
])
);
// Find the display name again in the refreshed DOM and verify it
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0];
expect(subsectionModel.get('display_name')).toBe(updatedDisplayName);
});
it('can be expanded and collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
verifyItemsExpanded('subsection', false);
expandItemsAndVerifyState('subsection');
collapseItemsAndVerifyState('subsection');
expandItemsAndVerifyState('subsection');
});
it('subsection can show basic settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectBasicSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
});
it('subsection can show visibility settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
});
it('subsection can show advanced settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).toHaveClass('active');
});
it('subsection does not show advanced settings tab if no special exams or prerequisites', function() {
var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toExist();
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toExist();
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toExist();
});
it('unit does not show settings tab headers if there is only one tab to show', function() {
var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
outlinePage.$('.outline-unit .configure-button').click();
expect($('.settings-tabs-header').length).toBe(0);
});
it('can show correct editors for self_paced course', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
setSelfPaced();
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-settings-release').length).toBe(0);
expect($('.grading-due-date').length).toBe(0);
expect($('.edit-settings-grading').length).toBe(1);
expect($('.edit-content-visibility').length).toBe(1);
expect($('.edit-show-correctness').length).toBe(1);
});
it('can select valid time', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
var default_time = '00:30';
var valid_times = ['00:30', '23:00', '24:00', '99:00'];
var invalid_times = ['00:00', '100:00', '01:60'];
var time_limit, i;
for (i = 0; i < valid_times.length; i++) {
time_limit = valid_times[i];
selectTimedExam(time_limit);
expect($('.field-time-limit input').val()).toEqual(time_limit);
}
for (i = 0; i < invalid_times.length; i++) {
time_limit = invalid_times[i];
selectTimedExam(time_limit);
expect($('.field-time-limit input').val()).not.toEqual(time_limit);
expect($('.field-time-limit input').val()).toEqual(default_time);
}
});
it('can be saved', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', defaultModalSettings);
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectAdvancedSettings();
selectTimedExam('02:30');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
graderType: 'Lab',
publish: 'republish',
isPrereq: false,
metadata: {
visible_to_staff_only: null,
start: '2014-07-09T00:00:00.000Z',
due: '2014-07-10T00:00:00.000Z',
exam_review_rules: '',
is_time_limited: true,
is_practice_exam: false,
is_proctored_enabled: false,
default_time_limit_minutes: 150,
hide_after_due: true
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, mockServerValuesJson);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-subsection .status-release-value')).toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).toContainText(
'Due: Jul 10, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
expect($('.outline-item .outline-subsection .status-grading-value')).toContainText('Lab');
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#due_date').val()).toBe('7/10/2014');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
expectShowCorrectness('never');
});
it('can hide time limit and hide after due fields when the None radio box is selected', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectDisableSpecialExams();
// all additional options should be hidden
expect($('.exam-options').is(':hidden')).toBe(true);
});
it('can select the practice exam', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectPracticeExam('00:30');
// time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false);
$('.wrapper-modal-window .action-save').click();
});
it('can select the timed exam', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectAdvancedSettings();
selectTimedExam('00:30');
// time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false);
$('.wrapper-modal-window .action-save').click();
});
it('can select the Proctored exam option', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectProctoredExam('00:30');
// time limit and review rules should be visible
checkOptionFieldVisibility(true, true);
$('.wrapper-modal-window .action-save').click();
});
it('entering invalid time format uses default value of 30 minutes.', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings();
selectProctoredExam('abcd');
// time limit field should be visible and have the correct value
expect($('.field-time-limit').is(':visible')).toBe(true);
expect($('.field-time-limit input').val()).toEqual('00:30');
});
it('can show a saved non-special exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: false,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: false
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(true);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can show a saved timed exam correctly when hide_after_due is true', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 10,
hide_after_due: true
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('00:10');
});
it('can show a saved timed exam correctly when hide_after_due is true', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 10,
hide_after_due: false
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('00:10');
expect($('.field-hide-after-due input').is(':checked')).toBe(false);
});
it('can show a saved practice exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: true,
is_proctored_exam: true,
default_time_limit_minutes: 150
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(true);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can show a saved proctored exam correctly', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: true,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: true,
default_time_limit_minutes: 150
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(true);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('does not show proctored settings if proctored exams not enabled', function() {
var mockCourseWithSpecialExamJSON = createMockCourseJSON({}, [
createMockSectionJSON({
has_changes: true,
enable_proctored_exams: false,
enable_timed_exams: true
}, [
createMockSubsectionJSON({
has_changes: true,
is_time_limited: true,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: true
}, [
])
])
]);
createCourseOutlinePage(this, mockCourseWithSpecialExamJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('input.timed_exam').is(':checked')).toBe(true);
expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30');
});
it('can select prerequisite', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectPrerequisite();
expect($('#is_prereq').is(':checked')).toBe(true);
$('.wrapper-modal-window .action-save').click();
});
it('can be deleted when it is a prerequisite', function() {
var promptSpy = EditHelpers.createPromptSpy();
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
is_prereq: true
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
getItemHeaders('subsection').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('can show a saved prerequisite correctly', function() {
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
is_prereq: true
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#is_prereq').is(':checked')).toBe(true);
});
it('does not display prerequisite subsections if none are available', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(0);
});
it('can display available prerequisite subsections', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(1);
});
it('can select prerequisite subsection', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('80');
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
expect($('#prereq option:selected').val()).toBe('usage_key');
expect($('#prereq_min_score').val()).toBe('80');
$('.wrapper-modal-window .action-save').click();
});
it('can display gating correctly', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
visibility_state: 'gated',
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
prereq: 'usage_key',
prereq_min_score: '80'
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Prerequisite: Prereq Subsection 1'
);
});
it('can show a saved prerequisite subsection correctly', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
prereq: 'usage_key',
prereq_min_score: '80'
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.gating-prereq').length).toBe(1);
expect($('#prereq option:selected').val()).toBe('usage_key');
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
expect($('#prereq_min_score').val()).toBe('80');
});
it('can display validation error on non-integer minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('abc');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('5.5');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
});
it('can display validation error on out of bounds minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectLastPrerequisiteSubsection('-5');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('105');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
});
it('does not display validation error on valid minimum score', function() {
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
}, [])
])
]);
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
selectLastPrerequisiteSubsection('');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('80');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('0');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('100');
expect($('#prereq_min_score_error').css('display')).toBe('none');
});
it('release date, due date, grading type, and staff lock can be cleared.', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
setContentVisibility('staff_only');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, mockServerValuesJson);
expect($('.outline-subsection .status-release-value')).toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).toContainText(
'Due: Jul 10, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#due_date').val()).toBe('7/10/2014');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
$('.wrapper-modal-window .scheduled-date-input .action-clear').click();
$('.wrapper-modal-window .due-date-input .action-clear').click();
expect($('#start_date').val()).toBe('');
expect($('#due_date').val()).toBe('');
$('#grading_type').val('notgraded');
setContentVisibility('visible');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
expect($('.outline-subsection .status-release-value')).not.toContainText(
'Jul 09, 2014 at 00:00 UTC'
);
expect($('.outline-subsection .status-grading-date')).not.toExist();
expect($('.outline-subsection .status-grading-value')).not.toExist();
expect($('.outline-subsection .status-message-copy')).not.toContainText(
'Contains staff only content'
);
});
describe('Show correctness setting set as expected.', function() {
var setShowCorrectness;
setShowCorrectness = function(showCorrectness) {
$('input[name=show-correctness][value=' + showCorrectness + ']').click();
};
describe('Show correctness set by subsection metadata.', function() {
$.each(['always', 'never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '"', function() {
var mockCourseJSONCorrectness = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({show_correctness: showCorrectness}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSONCorrectness, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expectShowCorrectness(showCorrectness);
});
});
});
describe('Show correctness editor works as expected.', function() {
beforeEach(function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
});
it('show_correctness="always" (default, unchanged metadata)', function() {
setShowCorrectness('always');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
defaultModalSettings);
});
$.each(['never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '" updates settings, republishes', function() {
var expectedSettings = $.extend(true, {}, defaultModalSettings, {publish: 'republish'});
expectedSettings.metadata.show_correctness = showCorrectness;
setShowCorrectness(showCorrectness);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
expectedSettings);
});
});
});
});
verifyTypePublishable('subsection', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON(options, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished units', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
])
]),
$modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('subsection').first().find('.publish-button').click();
$modalWindow = $('.wrapper-modal-window');
expect($modalWindow.find('.outline-unit').length).toBe(2);
expect(_.compact(_.map($modalWindow.find('.outline-unit').text().split('\n'), $.trim))).toEqual(
['Unit 100', 'Unit 50']
);
expect($modalWindow.find('.outline-subsection')).not.toExist();
});
});
// Note: most tests for units can be found in Bok Choy
describe('Unit', function() {
var getUnitStatus = function(options) {
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
return getItemsOfType('unit').find('.unit-status .status-message');
};
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
getItemHeaders('unit').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit');
AjaxHelpers.respondWithJson(requests, {});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('has a link to the unit page', function() {
var unitAnchor;
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
unitAnchor = getItemsOfType('unit').find('.unit-title a');
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
it('shows partition group information', function() {
var messages = getUnitStatus({has_partition_group_components: true});
expect(messages.length).toBe(1);
expect(messages).toContainText(
'Access to some content in this unit is restricted to specific groups of learners'
);
});
it('shows partition group information with group_access set', function() {
var partitions = [
{
scheme: 'cohort',
id: 1,
groups: [
{
deleted: false,
selected: true,
id: 2,
name: 'Group 2'
},
{
deleted: false,
selected: true,
id: 3,
name: 'Group 3'
}
],
name: 'Content Group Configuration'
}
];
var messages = getUnitStatus({
has_partition_group_components: true,
user_partitions: partitions,
group_access: {1: [2, 3]},
user_partition_info: {
selected_partition_index: 1,
selected_groups_label: '1, 2',
selectable_partitions: partitions
}
});
expect(messages.length).toBe(1);
expect(messages).toContainText(
'Access to this unit is restricted to'
);
});
it('does not show partition group information if visible to all', function() {
var messages = getUnitStatus({});
expect(messages.length).toBe(0);
});
it('does not show partition group information if staff locked', function() {
var messages = getUnitStatus(
{has_partition_group_components: true, staff_only_message: true}
);
expect(messages.length).toBe(1);
expect(messages).toContainText('Contains staff only content');
});
verifyTypePublishable('unit', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
});
});
describe('Date and Time picker', function() {
// Two datetime formats can came from server: '%Y-%m-%dT%H:%M:%SZ' and %Y-%m-%dT%H:%M:%S+TZ:TZ'
it('can parse dates in both formats that can come from server', function() {
describe('Show correctness editor works as expected.', function() {
beforeEach(function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('');
expect($('#start_time').val()).toBe('');
DateUtils.setDate($('#start_date'), ('#start_time'), '2015-08-10T05:10:00Z');
expect($('#start_date').val()).toBe('8/10/2015');
expect($('#start_time').val()).toBe('05:10');
DateUtils.setDate($('#start_date'), ('#start_time'), '2014-07-09T00:00:00+00:00');
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#start_time').val()).toBe('00:00');
selectVisibilitySettings();
});
it('show_correctness="always" (default, unchanged metadata)', function() {
setShowCorrectness('always');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
defaultModalSettings);
});
$.each(['never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '" updates settings, republishes', function() {
var expectedSettings = $.extend(true, {}, defaultModalSettings, {publish: 'republish'});
expectedSettings.metadata.show_correctness = showCorrectness;
setShowCorrectness(showCorrectness);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
expectedSettings);
});
});
});
});
verifyTypePublishable('subsection', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON(options, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished units', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
])
]),
$modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('subsection').first().find('.publish-button').click();
$modalWindow = $('.wrapper-modal-window');
expect($modalWindow.find('.outline-unit').length).toBe(2);
expect(_.compact(_.map($modalWindow.find('.outline-unit').text().split('\n'), $.trim))).toEqual(
['Unit 100', 'Unit 50']
);
expect($modalWindow.find('.outline-subsection')).not.toExist();
});
});
// Note: most tests for units can be found in Bok Choy
describe('Unit', function() {
var getUnitStatus = function(options) {
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
return getItemsOfType('unit').find('.unit-status .status-message');
};
it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
getItemHeaders('unit').find('.delete-button').click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit');
AjaxHelpers.respondWithJson(requests, {});
// Note: verification of the server response and the UI's handling of it
// is handled in the acceptance tests.
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('has a link to the unit page', function() {
var unitAnchor;
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
unitAnchor = getItemsOfType('unit').find('.unit-title a');
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
it('shows partition group information', function() {
var messages = getUnitStatus({has_partition_group_components: true});
expect(messages.length).toBe(1);
expect(messages).toContainText(
'Access to some content in this unit is restricted to specific groups of learners'
);
});
it('shows partition group information with group_access set', function() {
var partitions = [
{
scheme: 'cohort',
id: 1,
groups: [
{
deleted: false,
selected: true,
id: 2,
name: 'Group 2'
},
{
deleted: false,
selected: true,
id: 3,
name: 'Group 3'
}
],
name: 'Content Group Configuration'
}
];
var messages = getUnitStatus({
has_partition_group_components: true,
user_partitions: partitions,
group_access: {1: [2, 3]},
user_partition_info: {
selected_partition_index: 1,
selected_groups_label: '1, 2',
selectable_partitions: partitions
}
});
expect(messages.length).toBe(1);
expect(messages).toContainText(
'Access to this unit is restricted to'
);
});
it('does not show partition group information if visible to all', function() {
var messages = getUnitStatus({});
expect(messages.length).toBe(0);
});
it('does not show partition group information if staff locked', function() {
var messages = getUnitStatus(
{has_partition_group_components: true, staff_only_message: true}
);
expect(messages.length).toBe(1);
expect(messages).toContainText('Contains staff only content');
});
verifyTypePublishable('unit', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
});
});
describe('Date and Time picker', function() {
// Two datetime formats can came from server: '%Y-%m-%dT%H:%M:%SZ' and %Y-%m-%dT%H:%M:%S+TZ:TZ'
it('can parse dates in both formats that can come from server', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('');
expect($('#start_time').val()).toBe('');
DateUtils.setDate($('#start_date'), ('#start_time'), '2015-08-10T05:10:00Z');
expect($('#start_date').val()).toBe('8/10/2015');
expect($('#start_time').val()).toBe('05:10');
DateUtils.setDate($('#start_date'), ('#start_time'), '2014-07-09T00:00:00+00:00');
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#start_time').val()).toBe('00:00');
});
});
});

View File

@@ -31,7 +31,10 @@ define(["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
});
});
afterEach(() => delete window.course);
afterEach(() => {
delete window.course;
jasmine.stealth.clearSpies();
});
describe("Basic", function() {
it("should render properly", function() {

View File

@@ -1,111 +1,114 @@
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers',
'js/views/xblock_editor', 'js/models/xblock_info'],
function($, _, AjaxHelpers, EditHelpers, XBlockEditorView, XBlockInfo) {
describe('XBlockEditorView', function() {
var model, editor, testDisplayName, mockSaveResponse;
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 XBlockEditorView from 'js/views/xblock_editor';
import XBlockInfo from 'js/models/xblock_info';
testDisplayName = 'Test Display Name';
mockSaveResponse = {
data: '<p>Some HTML</p>',
metadata: {
display_name: testDisplayName
}
};
describe('XBlockEditorView', function() {
var model, editor, testDisplayName, mockSaveResponse;
beforeEach(function() {
EditHelpers.installEditTemplates();
model = new XBlockInfo({
id: 'testCourse/branch/draft/block/verticalFFF',
display_name: 'Test Unit',
category: 'vertical'
});
editor = new XBlockEditorView({
model: model
});
});
testDisplayName = 'Test Display Name';
mockSaveResponse = {
data: '<p>Some HTML</p>',
metadata: {
display_name: testDisplayName
}
};
describe('Editing an xblock', function() {
var mockXBlockEditorHtml;
beforeEach(function() {
EditHelpers.installMockXBlock();
});
afterEach(function() {
EditHelpers.uninstallMockXBlock();
});
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('settings');
});
});
describe('Editing an xmodule', function() {
var mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore');
beforeEach(function() {
EditHelpers.installMockXModule(mockSaveResponse);
});
afterEach(function() {
EditHelpers.uninstallMockXModule();
});
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('editor');
});
it('saves any custom metadata', function() {
var requests = AjaxHelpers.requests(this),
request, response;
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
// Give the mock xblock a save method...
editor.xblock.save = window.MockDescriptor.save;
editor.model.save(editor.getXBlockFieldData());
request = AjaxHelpers.currentRequest(requests);
response = JSON.parse(request.requestBody);
expect(response.metadata.display_name).toBe(testDisplayName);
expect(response.metadata.custom_field).toBe('Custom Value');
});
it('can render a module with only settings', function() {
var requests = AjaxHelpers.requests(this),
mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore');
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('settings');
});
});
beforeEach(function() {
EditHelpers.installEditTemplates();
model = new XBlockInfo({
id: 'testCourse/branch/draft/block/verticalFFF',
display_name: 'Test Unit',
category: 'vertical'
});
editor = new XBlockEditorView({
model: model
});
});
describe('Editing an xblock', function() {
var mockXBlockEditorHtml;
beforeEach(function() {
EditHelpers.installMockXBlock();
});
afterEach(function() {
EditHelpers.uninstallMockXBlock();
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('settings');
});
});
describe('Editing an xmodule', function() {
var mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-editor.underscore');
beforeEach(function() {
EditHelpers.installMockXModule(mockSaveResponse);
});
afterEach(function() {
EditHelpers.uninstallMockXModule();
});
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('editor');
});
it('saves any custom metadata', function() {
var requests = AjaxHelpers.requests(this),
request, response;
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
// Give the mock xblock a save method...
editor.xblock.save = window.MockDescriptor.save;
editor.model.save(editor.getXBlockFieldData());
request = AjaxHelpers.currentRequest(requests);
response = JSON.parse(request.requestBody);
expect(response.metadata.display_name).toBe(testDisplayName);
expect(response.metadata.custom_field).toBe('Custom Value');
});
it('can render a module with only settings', function() {
var requests = AjaxHelpers.requests(this),
mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-settings-only-editor.underscore');
editor.render();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditorHtml,
resources: []
});
expect(editor.$el.select('.xblock-header')).toBeTruthy();
expect(editor.getMode()).toEqual('settings');
});
});
});

View File

@@ -1,156 +1,159 @@
define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers',
'js/spec_helpers/edit_helpers', 'js/models/xblock_info', 'js/views/xblock_string_field_editor'],
function($, AjaxHelpers, TemplateHelpers, EditHelpers, XBlockInfo, XBlockStringFieldEditor) {
describe('XBlockStringFieldEditorView', function() {
var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView;
import $ from 'jquery';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import EditHelpers from 'js/spec_helpers/edit_helpers';
import XBlockInfo from 'js/models/xblock_info';
import XBlockStringFieldEditor from 'js/views/xblock_string_field_editor';
getXBlockInfo = function(displayName) {
return new XBlockInfo(
{
display_name: displayName,
id: 'my_xblock'
},
{parse: true}
);
};
describe('XBlockStringFieldEditorView', function() {
var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView;
getFieldEditorView = function(xblockInfo) {
if (xblockInfo === undefined) {
xblockInfo = getXBlockInfo(initialDisplayName);
}
return new XBlockStringFieldEditor({
model: xblockInfo,
el: $('.wrapper-xblock-field')
});
};
getXBlockInfo = function(displayName) {
return new XBlockInfo(
{
display_name: displayName,
id: 'my_xblock'
},
{parse: true}
);
};
beforeEach(function() {
initialDisplayName = 'Default Display Name';
updatedDisplayName = 'Updated Display Name';
TemplateHelpers.installTemplate('xblock-string-field-editor');
appendSetFixtures(
'<div class="wrapper-xblock-field incontext-editor is-editable"' +
'data-field="display_name" data-field-display-name="Display Name">' +
'<h1 class="page-header-title xblock-field-value incontext-editor-value">' +
'<span class="title-value">' + initialDisplayName + '</span>' +
'</h1>' +
'</div>'
);
});
getFieldEditorView = function(xblockInfo) {
if (xblockInfo === undefined) {
xblockInfo = getXBlockInfo(initialDisplayName);
}
return new XBlockStringFieldEditor({
model: xblockInfo,
el: $('.wrapper-xblock-field')
});
};
describe('Editing', function() {
var expectPostedNewDisplayName, expectEditCanceled;
beforeEach(function() {
initialDisplayName = 'Default Display Name';
updatedDisplayName = 'Updated Display Name';
TemplateHelpers.installTemplate('xblock-string-field-editor');
appendSetFixtures(
'<div class="wrapper-xblock-field incontext-editor is-editable"' +
'data-field="display_name" data-field-display-name="Display Name">' +
'<h1 class="page-header-title xblock-field-value incontext-editor-value">' +
'<span class="title-value">' + initialDisplayName + '</span>' +
'</h1>' +
'</div>'
);
});
expectPostedNewDisplayName = function(requests, displayName) {
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', {
metadata: {
display_name: displayName
}
});
};
describe('Editing', function() {
var expectPostedNewDisplayName, expectEditCanceled;
expectEditCanceled = function(test, fieldEditorView, options) {
var requests, initialRequests, displayNameInput;
requests = AjaxHelpers.requests(test);
displayNameInput = EditHelpers.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
AjaxHelpers.expectNoRequests(requests);
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName);
expect(fieldEditorView.model.get('display_name')).toBe(initialDisplayName);
};
expectPostedNewDisplayName = function(requests, displayName) {
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', {
metadata: {
display_name: displayName
}
});
};
it('can inline edit the display name', function() {
var requests, fieldEditorView;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName);
// 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(fieldEditorView.$el, updatedDisplayName);
});
expectEditCanceled = function(test, fieldEditorView, options) {
var requests, initialRequests, displayNameInput;
requests = AjaxHelpers.requests(test);
displayNameInput = EditHelpers.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
AjaxHelpers.expectNoRequests(requests);
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName);
expect(fieldEditorView.model.get('display_name')).toBe(initialDisplayName);
};
it('does not change the title when a display name update fails', function() {
var requests, fieldEditorView, initialRequests;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName);
AjaxHelpers.respondWithError(requests);
// No fetch operation should occur.
AjaxHelpers.expectNoRequests(requests);
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName);
});
it('can inline edit the display name', function() {
var requests, fieldEditorView;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName);
// 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(fieldEditorView.$el, updatedDisplayName);
});
it('trims whitespace from the display name', function() {
var requests, fieldEditorView;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
updatedDisplayName += ' ';
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName.trim());
// 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.trim()});
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim());
});
it('does not change the title when a display name update fails', function() {
var requests, fieldEditorView, initialRequests;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName);
AjaxHelpers.respondWithError(requests);
// No fetch operation should occur.
AjaxHelpers.expectNoRequests(requests);
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName);
});
it('does not change the title when input is the empty string', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: ''});
});
it('trims whitespace from the display name', function() {
var requests, fieldEditorView;
requests = AjaxHelpers.requests(this);
fieldEditorView = getFieldEditorView().render();
updatedDisplayName += ' ';
EditHelpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
fieldEditorView.$('button[name=submit]').click();
expectPostedNewDisplayName(requests, updatedDisplayName.trim());
// 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.trim()});
EditHelpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim());
});
it('does not change the title when input is whitespace-only', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: ' '});
});
it('does not change the title when input is the empty string', 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('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 clicking cancel', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true});
});
});
it('can cancel an inline edit by pressing escape', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, pressEscape: true});
});
describe('Rendering', function() {
var expectInputMatchesModelDisplayName = function(displayName) {
var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render();
expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName);
};
it('can cancel an inline edit by clicking cancel', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true});
});
});
it('renders single quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated \'Display Name\'');
});
describe('Rendering', function() {
var expectInputMatchesModelDisplayName = function(displayName) {
var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render();
expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName);
};
it('renders double quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated "Display Name"');
});
it('renders single quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated \'Display Name\'');
});
it('renders open angle bracket in input field', function() {
expectInputMatchesModelDisplayName(updatedDisplayName + '<');
});
it('renders double quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated "Display Name"');
});
it('renders close angle bracket in input field', function() {
expectInputMatchesModelDisplayName('>' + updatedDisplayName);
});
});
});
});
it('renders open angle bracket in input field', function() {
expectInputMatchesModelDisplayName(updatedDisplayName + '<');
});
it('renders close angle bracket in input field', function() {
expectInputMatchesModelDisplayName('>' + updatedDisplayName);
});
});
});

View File

@@ -1,121 +1,140 @@
/**
* Provides helper methods for invoking Studio editors in Jasmine tests.
*/
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'js/spec_helpers/modal_helpers', 'js/views/modals/edit_xblock',
'js/collections/component_template', 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'],
function($, _, AjaxHelpers, TemplateHelpers, modal_helpers, EditXBlockModal, ComponentTemplates) {
var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule,
mockComponentTemplates, installEditTemplates, showEditModal, verifyXBlockRequest;
import $ from 'jquery';
import _ from 'underscore';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
import modal_helpers from 'js/spec_helpers/modal_helpers';
import EditXBlockModal from 'js/views/modals/edit_xblock';
import ComponentTemplates from 'js/collections/component_template';
import XModule from 'xmodule/js/src/xmodule';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
installMockXBlock = function(mockResult) {
window.MockXBlock = function(runtime, element) {
var block = {
runtime: runtime
};
if (mockResult) {
block.save = function() {
return mockResult;
};
}
return block;
var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule,
mockComponentTemplates, installEditTemplates, showEditModal, verifyXBlockRequest;
installMockXBlock = function(mockResult) {
window.MockXBlock = function(runtime, element) {
var block = {
runtime: runtime
};
if (mockResult) {
block.save = function() {
return mockResult;
};
};
}
return block;
};
};
uninstallMockXBlock = function() {
window.MockXBlock = null;
};
uninstallMockXBlock = function() {
window.MockXBlock = null;
};
installMockXModule = function(mockResult) {
window.MockDescriptor = _.extend(XModule.Descriptor, {
save: function() {
return mockResult;
}
});
};
uninstallMockXModule = function() {
window.MockDescriptor = null;
};
mockComponentTemplates = new ComponentTemplates([
{
templates: [
{
category: 'discussion',
display_name: 'Discussion'
}],
type: 'discussion',
support_legend: {show_legend: false}
}, {
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {show_legend: false}
}],
{
parse: true
});
installEditTemplates = function(append) {
modal_helpers.installModalTemplates(append);
// Add templates needed by the add XBlock menu
TemplateHelpers.installTemplate('add-xblock-component');
TemplateHelpers.installTemplate('add-xblock-component-button');
TemplateHelpers.installTemplate('add-xblock-component-menu');
TemplateHelpers.installTemplate('add-xblock-component-menu-problem');
TemplateHelpers.installTemplate('add-xblock-component-support-legend');
TemplateHelpers.installTemplate('add-xblock-component-support-level');
// Add templates needed by the edit XBlock modal
TemplateHelpers.installTemplate('edit-xblock-modal');
TemplateHelpers.installTemplate('editor-mode-button');
// Add templates needed by the settings editor
TemplateHelpers.installTemplate('metadata-editor');
TemplateHelpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry');
TemplateHelpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry');
};
showEditModal = function(requests, xblockElement, model, mockHtml, options) {
var modal = new EditXBlockModal({});
modal.edit(xblockElement, model, options);
AjaxHelpers.respondWithJson(requests, {
html: mockHtml,
resources: []
});
return modal;
};
verifyXBlockRequest = function(requests, expectedJson) {
var request = AjaxHelpers.currentRequest(requests),
actualJson = JSON.parse(request.requestBody);
expect(request.url).toEqual('/xblock/');
expect(request.method).toEqual('POST');
expect(actualJson).toEqual(expectedJson);
};
return $.extend(modal_helpers, {
installMockXBlock: installMockXBlock,
uninstallMockXBlock: uninstallMockXBlock,
installMockXModule: installMockXModule,
uninstallMockXModule: uninstallMockXModule,
mockComponentTemplates: mockComponentTemplates,
installEditTemplates: installEditTemplates,
showEditModal: showEditModal,
verifyXBlockRequest: verifyXBlockRequest
});
installMockXModule = function(mockResult) {
window.MockDescriptor = _.extend(XModule.Descriptor, {
save: function() {
return mockResult;
}
});
};
uninstallMockXModule = function() {
window.MockDescriptor = null;
};
mockComponentTemplates = new ComponentTemplates([
{
templates: [
{
category: 'discussion',
display_name: 'Discussion'
}],
type: 'discussion',
support_legend: {show_legend: false}
}, {
templates: [
{
category: 'html',
boilerplate_name: null,
display_name: 'Text'
}, {
category: 'html',
boilerplate_name: 'announcement.yaml',
display_name: 'Announcement'
}, {
category: 'html',
boilerplate_name: 'raw.yaml',
display_name: 'Raw HTML'
}],
type: 'html',
support_legend: {show_legend: false}
}],
{
parse: true
});
installEditTemplates = function(append) {
modal_helpers.installModalTemplates(append);
// Add templates needed by the add XBlock menu
TemplateHelpers.installTemplate('add-xblock-component');
TemplateHelpers.installTemplate('add-xblock-component-button');
TemplateHelpers.installTemplate('add-xblock-component-menu');
TemplateHelpers.installTemplate('add-xblock-component-menu-problem');
TemplateHelpers.installTemplate('add-xblock-component-support-legend');
TemplateHelpers.installTemplate('add-xblock-component-support-level');
// Add templates needed by the edit XBlock modal
TemplateHelpers.installTemplate('edit-xblock-modal');
TemplateHelpers.installTemplate('editor-mode-button');
// Add templates needed by the settings editor
TemplateHelpers.installTemplate('metadata-editor');
TemplateHelpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry');
TemplateHelpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry');
};
showEditModal = function(requests, xblockElement, model, mockHtml, options) {
var modal = new EditXBlockModal({});
modal.edit(xblockElement, model, options);
AjaxHelpers.respondWithJson(requests, {
html: mockHtml,
resources: []
});
return modal;
};
verifyXBlockRequest = function(requests, expectedJson) {
var request = AjaxHelpers.currentRequest(requests),
actualJson = JSON.parse(request.requestBody);
expect(request.url).toEqual('/xblock/');
expect(request.method).toEqual('POST');
expect(actualJson).toEqual(expectedJson);
};
var editHelpers = $.extend(modal_helpers, {
installMockXBlock: installMockXBlock,
uninstallMockXBlock: uninstallMockXBlock,
installMockXModule: installMockXModule,
uninstallMockXModule: uninstallMockXModule,
mockComponentTemplates: mockComponentTemplates,
installEditTemplates: installEditTemplates,
showEditModal: showEditModal,
verifyXBlockRequest: verifyXBlockRequest
});
export default editHelpers;
export {
installMockXBlock,
uninstallMockXBlock,
installMockXModule,
uninstallMockXModule,
mockComponentTemplates,
installEditTemplates,
showEditModal,
verifyXBlockRequest,
}

View File

@@ -76,7 +76,7 @@ function($, date, TriggerChangeEventOnEnter) {
var setDate = function(datepickerInput, timepickerInput, datetime) {
// given a pair of inputs (datepicker and timepicker) and the date as an
// ISO-formatted date string.
datetime = date.parse(datetime);
datetime = Date.parse(datetime);
if (datetime) {
$(datepickerInput).datepicker('setDate', datetime);
if (timepickerInput.length > 0) {

View File

@@ -9,7 +9,8 @@ define([
'common/js/components/views/feedback_alert',
'js/views/utils/xblock_utils',
'js/views/utils/move_xblock_utils',
'edx-ui-toolkit/js/utils/string-utils'
'edx-ui-toolkit/js/utils/string-utils',
'jquery.smoothScroll'
],
function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, StringUtils) {
'use strict';

View File

@@ -33,14 +33,23 @@ var options = {
fixtureFiles: [
{pattern: '../templates/js/**/*.underscore'},
{pattern: 'templates/**/*.underscore'}
{pattern: 'templates/**/*.underscore'},
],
runFiles: [
{pattern: 'cms/js/spec/main.js', included: true}
]
{pattern: 'cms/js/spec/main.js', included: true},
{pattern: 'jasmine.cms.conf.js', included: true}
],
preprocessors: {}
};
(options.sourceFiles.concat(options.specFiles))
.filter(function(file) { return file.webpack; })
.forEach(function(file) {
options.preprocessors[file.pattern] = ['webpack'];
});
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -62,24 +62,24 @@ webpackConfig.plugins.splice(commonsChunkPluginIndex, 1);
// Files which are needed by all lms/cms suites.
var commonFiles = {
libraryFiles: [
{pattern: 'common/js/vendor/**/*.js'},
{pattern: 'edx-pattern-library/js/**/*.js'},
{pattern: 'edx-ui-toolkit/js/**/*.js'},
{pattern: 'xmodule_js/common_static/common/js/**/!(*spec).js'},
{pattern: 'xmodule_js/common_static/js/**/!(*spec).js'},
{pattern: 'xmodule_js/src/**/*.js'}
{ pattern: 'common/js/vendor/**/*.js' },
{ pattern: 'edx-pattern-library/js/**/*.js' },
{ pattern: 'edx-ui-toolkit/js/**/*.js' },
{ pattern: 'xmodule_js/common_static/common/js/**/!(*spec).js' },
{ pattern: 'xmodule_js/common_static/js/**/!(*spec).js' },
{ pattern: 'xmodule_js/src/**/*.js' }
],
sourceFiles: [
{pattern: 'common/js/!(spec_helpers)/**/!(*spec).js'}
{ pattern: 'common/js/!(spec_helpers)/**/!(*spec).js' }
],
specFiles: [
{pattern: 'common/js/spec_helpers/**/*.js'}
{ pattern: 'common/js/spec_helpers/**/*.js' }
],
fixtureFiles: [
{pattern: 'common/templates/**/*.underscore'}
{ pattern: 'common/templates/**/*.underscore' }
]
};
@@ -154,8 +154,8 @@ function coverageSettings(config) {
subdir: '.',
includeAllSources: true,
reporters: [
{type: 'cobertura', file: path.file},
{type: 'text-summary'}
{ type: 'cobertura', file: path.file },
{ type: 'text-summary' }
]
};
}
@@ -200,7 +200,7 @@ function normalizePathsForCoverage(files, normalizeFunc, preprocessors) {
normalizedFile,
filesForCoverage = {};
files.forEach(function(file) {
files.forEach(function (file) {
if (!file.ignoreCoverage) {
normalizedFile = normalizeFn(appRoot, file.pattern);
if (preprocessors && preprocessors.hasOwnProperty(normalizedFile)) {
@@ -222,8 +222,8 @@ function normalizePathsForCoverage(files, normalizeFunc, preprocessors) {
* @return {Object}
*/
function setDefaults(files) {
return files.map(function(f) {
var file = _.isObject(f) ? f : {pattern: f};
return files.map(function (f) {
var file = _.isObject(f) ? f : { pattern: f };
if (!file.included && !file.webpack) {
f.included = false;
}
@@ -232,7 +232,7 @@ function setDefaults(files) {
}
function getBaseConfig(config, useRequireJs) {
var getFrameworkFiles = function() {
var getFrameworkFiles = function () {
var files = [
'common/static/common/js/vendor/jquery.js',
'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
@@ -263,8 +263,8 @@ function getBaseConfig(config, useRequireJs) {
// which isn't a karma plugin. Though a karma framework for jasmine-jquery is available
// but it's not actively maintained. In future we also wanna add jQuery at the top when
// we upgrade to jQuery 2
var initFrameworks = function(files) {
getFrameworkFiles().reverse().forEach(function(f) {
var initFrameworks = function (files) {
getFrameworkFiles().reverse().forEach(function (f) {
files.unshift({
pattern: path.join(appRoot, f),
included: true,
@@ -423,7 +423,7 @@ function configure(config, options) {
baseConfig = getBaseConfig(config, useRequireJs);
if (options.includeCommonFiles) {
_.forEach(['libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles'], function(collectionName) {
_.forEach(['libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles'], function (collectionName) {
options[collectionName] = _.flatten([commonFiles[collectionName], options[collectionName]]);
});
}
@@ -431,16 +431,16 @@ function configure(config, options) {
var files = _.flatten(
_.map(
['libraryFilesToInclude', 'libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles', 'runFiles'],
function(collectionName) { return options[collectionName] || []; }
function (collectionName) { return options[collectionName] || []; }
)
);
files.unshift(
{pattern: path.join(appRoot, 'common/static/common/js/jasmine.common.conf.js'), included: true}
{ pattern: path.join(appRoot, 'common/static/common/js/jasmine.common.conf.js'), included: true }
);
if (useRequireJs) {
files.unshift({pattern: 'common/js/utils/require-serial.js', included: true});
files.unshift({ pattern: 'common/js/utils/require-serial.js', included: true });
}
// Karma sets included=true by default.
@@ -450,7 +450,7 @@ function configure(config, options) {
var filesForCoverage = _.flatten(
_.map(
['sourceFiles', 'specFiles'],
function(collectionName) { return options[collectionName]; }
function (collectionName) { return options[collectionName]; }
)
);

View File

@@ -1,11 +1,10 @@
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
(function(define) {
'use strict';
define(['jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
define(['jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
function($, NotificationView, Prompt, AjaxHelpers) {
'use strict';
var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing,
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange,
@@ -148,5 +147,5 @@
submitAndVerifyFormSuccess: submitAndVerifyFormSuccess,
submitAndVerifyFormError: submitAndVerifyFormError
};
});
}).call(this, define || RequireJS.define);
}
);

View File

@@ -54,6 +54,6 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<%block name="page_bundle">
<%static:webpack entry="js/factories/login">
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
</%static:webpack>
</%block>

View File

@@ -120,7 +120,7 @@ class TestPaverJavaScriptTestTasks(PaverTestCase):
for suite in suites:
# Karma test command
karma_config_file = Env.KARMA_CONFIG_FILES[Env.JS_TEST_ID_KEYS.index(suite)]
expected_test_tool_command = u'karma start {options}'.format(
expected_test_tool_command = u'nodejs --max_old_space_size=4096 node_modules/.bin/karma start {options}'.format(
options=self.EXPECTED_KARMA_OPTIONS.format(
config_file=karma_config_file,
single_run='false' if dev_mode else 'true',

View File

@@ -181,6 +181,7 @@ class Env(object):
KARMA_CONFIG_FILES = [
REPO_ROOT / 'cms/static/karma_cms.conf.js',
REPO_ROOT / 'cms/static/karma_cms_squire.conf.js',
REPO_ROOT / 'cms/static/karma_cms_webpack.conf.js',
REPO_ROOT / 'lms/static/karma_lms.conf.js',
REPO_ROOT / 'common/lib/xmodule/xmodule/js/karma_xmodule.conf.js',
REPO_ROOT / 'common/lib/xmodule/xmodule/js/karma_xmodule_webpack.conf.js',
@@ -191,6 +192,7 @@ class Env(object):
JS_TEST_ID_KEYS = [
'cms',
'cms-squire',
'cms-webpack',
'lms',
'xmodule',
'xmodule-webpack',

View File

@@ -76,7 +76,9 @@ class JsTestSubSuite(TestSuite):
Run the tests using karma runner.
"""
cmd = [
"karma",
"nodejs",
"--max_old_space_size=4096",
"node_modules/.bin/karma",
"start",
self.test_conf_file,
"--single-run={}".format('false' if self.mode == 'dev' else 'true'),

View File

@@ -101,6 +101,7 @@ module.exports = {
'window.jQuery': 'jquery',
Popper: 'popper.js', // used by bootstrap
CodeMirror: 'codemirror',
'edx.HtmlUtils': 'edx-ui-toolkit/js/utils/html-utils',
}),
// Note: Until karma-webpack releases v3, it doesn't play well with
@@ -321,6 +322,7 @@ module.exports = {
'cms/djangoapps/pipeline_js/js',
'cms/static',
'cms/static/cms/js',
'cms/templates/js',
'lms/static',
'common/lib/xmodule',
'common/lib/xmodule/xmodule/js/src',
@@ -357,5 +359,9 @@ module.exports = {
watchOptions: {
poll: true
},
node: {
fs: 'empty'
}
};