Extract a new webpack-based suite of cms javascript tests
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
35
cms/static/cms/js/spec/main_webpack.js
Normal file
35
cms/static/cms/js/spec/main_webpack.js
Normal 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
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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]; }
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user