Merge pull request #18385 from edx/release-mergeback-to-master

Merge release back to master
This commit is contained in:
Calen Pennington
2018-06-15 16:30:26 -04:00
committed by GitHub
162 changed files with 6950 additions and 7573 deletions

View File

@@ -47,19 +47,3 @@ def _django_clear_site_cache():
with what has been working for us so far.
"""
pass
@pytest.fixture(autouse=True)
def no_webpack_loader(monkeypatch):
"""
Monkeypatch webpack_loader to make sure that webpack assets don't need to be
compiled before unit tests are run.
"""
monkeypatch.setattr(
"webpack_loader.templatetags.webpack_loader.render_bundle",
lambda entry, extension=None, config='DEFAULT', attrs='': ''
)
monkeypatch.setattr(
"webpack_loader.utils.get_as_tags",
lambda entry, extension=None, config='DEFAULT', attrs='': []
)

View File

@@ -17,26 +17,17 @@ DEPRECATED_SETTINGS = ["CSS Class for Course Reruns", "Hide Progress Tab", "XQA
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
world.wait_for_js_to_load() # pylint: disable=no-member
world.wait_for_js_variable_truthy('window.studioNavMenuActive') # pylint: disable=no-member
for _ in range(5):
world.click_course_settings() # pylint: disable=no-member
# The click handlers are set up so that if you click <body>
# the menu disappears. This means that if we're even a *little*
# bit off on the last item ('Advanced Settings'), the menu
# will close and the test will fail.
# For this reason, we retrieve the link and visit it directly
# This is what the browser *should* be doing, since it's just a native
# link with no JavaScript involved.
link_css = 'li.nav-course-settings-advanced a'
try:
world.wait_for_visible(link_css) # pylint: disable=no-member
break
except AssertionError:
continue
world.click_course_settings()
# The click handlers are set up so that if you click <body>
# the menu disappears. This means that if we're even a *little*
# bit off on the last item ('Advanced Settings'), the menu
# will close and the test will fail.
# For this reason, we retrieve the link and visit it directly
# This is what the browser *should* be doing, since it's just a native
# link with no JavaScript involved.
link_css = 'li.nav-course-settings-advanced a'
world.wait_for_visible(link_css)
link = world.css_find(link_css).first['href']
world.visit(link)

View File

@@ -247,6 +247,7 @@ def create_unit_from_course_outline():
world.css_click(selector)
world.wait_for_mathjax()
world.wait_for_xmodule()
world.wait_for_loading()
assert world.is_css_present('ul.new-component-type')

View File

@@ -15,6 +15,11 @@ Feature: CMS.HTML Editor
Then I can modify the display name
And my display name change is persisted on save
Scenario: Edit High Level source is available for LaTeX html
Given I have created an E-text Written in LaTeX
When I edit and select Settings
Then Edit High Level Source is visible
Scenario: TinyMCE image plugin sets urls correctly
Given I have created a Blank HTML Page
When I edit the page

View File

@@ -82,7 +82,22 @@ Feature: CMS.Problem Editor
And I can modify the display name
Then If I press Cancel my changes are not persisted
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
When I edit and select Settings
Then Edit High Level Source is visible
Scenario: Cheat sheet visible on toggle
Given I have created a Blank Common Problem
And I can edit the problem
Then I can see cheatsheet
Scenario: Reply on Annotation and Return to Annotation link works for Annotation problem
Given I have created a unit with advanced module "annotatable"
And I have created an advanced component "Annotation" of type "annotatable"
And I have created an advanced problem of type "Blank Advanced Problem"
And I edit first blank advanced problem for annotation response
When I mouseover on "annotatable-span"
Then I can see Reply to Annotation link
And I see that page has scrolled "down" when I click on "annotatable-reply" link
And I see that page has scrolled "up" when I click on "annotation-return" link

View File

@@ -39,8 +39,7 @@ from xmodule.modulestore.django import ModuleI18nService, modulestore
from xmodule.partitions.partitions_service import PartitionService
from xmodule.services import SettingsService
from xmodule.studio_editable import has_author_view
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem, XModule, XModuleDescriptor
import webpack_loader.utils
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem
from .helpers import render_from_lms
from .session_kv_store import SessionKeyValueStore
@@ -299,15 +298,6 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'language': getattr(course, 'language', None)
}
if isinstance(xblock, (XModule, XModuleDescriptor)):
# Add the webpackified asset tags
class_name = getattr(xblock.__class__, 'unmixed_class', xblock.__class__).__name__
for tag in webpack_loader.utils.get_as_tags(class_name):
frag.add_resource(tag, mimetype='text/html', placement='head')
for tag in webpack_loader.utils.get_as_tags("js/factories/xblock_validation"):
frag.add_resource(tag, mimetype='text/html', placement='head')
html = render_to_string('studio_xblock_wrapper.html', template_context)
frag = wrap_fragment(frag, html)
return frag

View File

@@ -1,60 +0,0 @@
// This file is designed to load all the XModule Javascript files in one wad
// using requirejs. It is passed through the Mako template system, which
// populates the `urls` variable with a list of paths to XModule JS files.
// These files assume that several libraries are available and bound to
// variables in the global context, so we load those libraries with requirejs
// and attach them to the global context manually.
define(
[
'jquery', 'underscore', 'codemirror', 'tinymce', 'scriptjs',
'jquery.tinymce', 'jquery.qtip', 'jquery.scrollTo', 'jquery.flot',
'jquery.cookie',
'utility'
],
function($, _, CodeMirror, tinymce, $script) {
'use strict';
window.$ = $;
window._ = _;
$script(
'//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js' +
'?config=TeX-MML-AM_SVG&delayStartupUntil=configured',
'mathjax'
);
window.CodeMirror = CodeMirror;
window.RequireJS = {
requirejs: {}, // This is never used by current xmodules
require: $script, // $script([deps], callback) acts approximately like the require function
define: define
};
/**
* Loads all modules one-by-one in exact order.
* The module should be used until we'll use RequireJS for XModules.
* @param {Array} modules A list of urls.
* @return {jQuery Promise}
**/
function requireQueue(modules) {
var deferred = $.Deferred();
function loadScript(queue) {
$script.ready('mathjax', function() {
// Loads the next script if queue is not empty.
if (queue.length) {
$script([queue.shift()], function() {
loadScript(queue);
});
} else {
deferred.resolve();
}
});
}
loadScript(modules.concat());
return deferred.promise();
}
// if (!window.xmoduleUrls) {
// throw Error('window.xmoduleUrls must be defined');
// }
return requireQueue([]);
}
);

View File

@@ -0,0 +1,45 @@
## This file is designed to load all the XModule Javascript files in one wad
## using requirejs. It is passed through the Mako template system, which
## populates the `urls` variable with a list of paths to XModule JS files.
## These files assume that several libraries are available and bound to
## variables in the global context, so we load those libraries with requirejs
## and attach them to the global context manually.
define(["jquery", "underscore", "codemirror", "tinymce",
"jquery.tinymce", "jquery.qtip", "jquery.scrollTo", "jquery.flot",
"jquery.cookie",
"utility"],
function($, _, CodeMirror, tinymce) {
window.$ = $;
window._ = _;
require(['mathjax']);
window.CodeMirror = CodeMirror;
window.RequireJS = {
'requirejs': requirejs,
'require': require,
'define': define
};
/**
* Loads all modules one-by-one in exact order.
* The module should be used until we'll use RequireJS for XModules.
* @param {Array} modules A list of urls.
* @return {jQuery Promise}
**/
var requireQueue = function(modules) {
var deferred = $.Deferred();
var loadScript = function (queue) {
// Loads the next script if queue is not empty.
if (queue.length) {
require([queue.shift()], function() {
loadScript(queue);
});
} else {
deferred.resolve();
}
};
loadScript(modules.concat());
return deferred.promise();
};
return requireQueue(${urls});
});

View File

@@ -0,0 +1,10 @@
"""
URL patterns for Javascript files used to load all of the XModule JS in one wad.
"""
from django.conf.urls import url
from pipeline_js.views import xmodule_js_files, requirejs_xmodule
urlpatterns = [
url(r'^files\.json$', xmodule_js_files, name='xmodule_js_files'),
url(r'^xmodule\.js$', requirejs_xmodule, name='requirejs_xmodule'),
]

View File

@@ -1,18 +0,0 @@
"""
Utilities for returning XModule JS (used by requirejs)
"""
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
def get_xmodule_urls():
"""
Returns a list of the URLs to hit to grab all the XModule JS
"""
pipeline_js_settings = settings.PIPELINE_JS["module-js"]
if settings.DEBUG:
paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]]
else:
paths = [pipeline_js_settings["output_filename"]]
return [staticfiles_storage.url(path) for path in paths]

View File

@@ -0,0 +1,44 @@
"""
Views for returning XModule JS (used by requirejs)
"""
import json
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import HttpResponse
from edxmako.shortcuts import render_to_response
def get_xmodule_urls():
"""
Returns a list of the URLs to hit to grab all the XModule JS
"""
pipeline_js_settings = settings.PIPELINE_JS["module-js"]
if settings.DEBUG:
paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]]
else:
paths = [pipeline_js_settings["output_filename"]]
return [staticfiles_storage.url(path) for path in paths]
def xmodule_js_files(request): # pylint: disable=unused-argument
"""
View function that returns XModule URLs as a JSON list; meant to be used
as an API
"""
urls = get_xmodule_urls()
return HttpResponse(json.dumps(urls), content_type="application/json")
def requirejs_xmodule(request): # pylint: disable=unused-argument
"""
View function that returns a requirejs-wrapped Javascript file that
loads all the XModule URLs; meant to be loaded via requireJS
"""
return render_to_response(
"xmodule.js",
{"urls": get_xmodule_urls()},
content_type="text/javascript",
)

View File

@@ -110,6 +110,10 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
USE_I18N = True
# Override the test stub webpack_loader that is installed in test.py.
INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'openedx.tests.util.webpack_loader']
INSTALLED_APPS.append('webpack_loader')
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
# django.contrib.staticfiles used to be loaded by lettuce, now we must add it ourselves
# django.contrib.staticfiles is not added to lms as there is a ^/static$ route built in to the app

View File

@@ -49,9 +49,6 @@ update_module_store_settings(
# Needed to enable licensing on video modules
XBLOCK_SETTINGS.update({'VideoDescriptor': {'licensing_enabled': True}})
# Capture the console log via template includes, until webdriver supports log capture again
CAPTURE_CONSOLE_LOG = True
############################ STATIC FILES #############################
# Enable debug so that static assets are served by Django

View File

@@ -16,9 +16,3 @@ LOGGING['handlers']['local'] = LOGGING['handlers']['tracking'] = {
}
LOGGING['loggers']['tracking']['handlers'] = ['console']
# Point the URL used to test YouTube availability to our stub YouTube server
BOK_CHOY_HOST = os.environ['BOK_CHOY_HOSTNAME']
YOUTUBE['API'] = "http://{}:{}/get_youtube_api/".format(BOK_CHOY_HOST, YOUTUBE_PORT)
YOUTUBE['METADATA_URL'] = "http://{}:{}/test_youtube/".format(BOK_CHOY_HOST, YOUTUBE_PORT)
YOUTUBE['TEXT_API']['url'] = "{}:{}/test_transcripts_youtube/".format(BOK_CHOY_HOST, YOUTUBE_PORT)

View File

@@ -54,6 +54,8 @@ TEST_ROOT = path('test_root')
# Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles"
INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'webpack_loader']
INSTALLED_APPS.append('openedx.tests.util.webpack_loader')
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json"
GITHUB_REPO_ROOT = TEST_ROOT / "data"

View File

@@ -19,19 +19,24 @@
modules: getModulesList([
'js/factories/asset_index',
'js/factories/base',
'js/factories/container',
'js/factories/course_create_rerun',
'js/factories/course_info',
'js/factories/edit_tabs',
'js/factories/export',
'js/factories/group_configurations',
'js/certificates/factories/certificates_page_factory',
'js/factories/index',
'js/factories/library',
'js/factories/manage_users',
'js/factories/outline',
'js/factories/register',
'js/factories/settings',
'js/factories/settings_advanced',
'js/factories/settings_graders',
'js/factories/videos_index'
'js/factories/textbooks',
'js/factories/videos_index',
'js/factories/xblock_validation'
]),
/**
* By default all the configuration for optimization happens from the command

View File

@@ -1,87 +1,86 @@
/* globals AjaxPrefix */
define([
'domReady',
'jquery',
'underscore',
'underscore.string',
'backbone',
'gettext',
'../../common/js/components/views/feedback_notification',
'jquery.cookie'
], function(domReady, $, _, str, Backbone, gettext, NotificationView) {
(function(AjaxPrefix) {
'use strict';
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
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');
});
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',
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',
data: JSON.stringify(data),
success: callback,
global: data ? data.global : true // Trigger global AJAX error handler or not
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');
}
});
};
$.postJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign
return sendJSON(url, data, callback, 'POST');
};
$.patchJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign
return sendJSON(url, data, callback, 'PATCH');
};
return domReady(function() {
if (window.onTouchBasedDevice()) {
return $('body').addClass('touch-based-device');
}
return null;
});
};
main();
return main;
});
main();
return main;
});
}).call(this, AjaxPrefix);

View File

@@ -4,7 +4,6 @@
(function(requirejs, requireSerial) {
'use strict';
var i, specHelpers, testFiles;
if (window) {
define('add-a11y-deps',
[
@@ -21,6 +20,8 @@
});
}
var i, specHelpers, testFiles;
requirejs.config({
baseUrl: '/base/',
paths: {
@@ -229,6 +230,7 @@
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',
@@ -261,21 +263,32 @@
'js/spec/views/previous_video_upload_list_spec',
'js/spec/views/assets_spec',
'js/spec/views/baseview_spec',
'js/spec/views/container_spec',
'js/spec/views/module_edit_spec',
'js/spec/views/paged_container_spec',
'js/spec/views/group_configuration_spec',
'js/spec/views/unit_outline_spec',
'js/spec/views/xblock_spec',
'js/spec/views/xblock_editor_spec',
'js/spec/views/xblock_string_field_editor_spec',
'js/spec/views/xblock_validation_spec',
'js/spec/views/license_spec',
'js/spec/views/paging_spec',
'js/spec/views/login_studio_spec',
'js/spec/views/pages/container_spec',
'js/spec/views/pages/container_subviews_spec',
'js/spec/views/pages/group_configurations_spec',
'js/spec/views/pages/course_outline_spec',
'js/spec/views/pages/course_rerun_spec',
'js/spec/views/pages/index_spec',
'js/spec/views/pages/library_users_spec',
'js/spec/views/modals/base_modal_spec',
'js/spec/views/modals/edit_xblock_spec',
'js/spec/views/modals/move_xblock_modal_spec',
'js/spec/views/modals/validation_error_modal_spec',
'js/spec/views/move_xblock_spec',
'js/spec/views/settings/main_spec',
'js/spec/factories/xblock_validation_spec',
'js/certificates/spec/models/certificate_spec',
'js/certificates/spec/views/certificate_details_spec',
'js/certificates/spec/views/certificate_editor_spec',

View File

@@ -1,35 +0,0 @@
jasmine.getFixtures().fixturesPath = '/base/templates';
import 'common/js/spec_helpers/jasmine-extensions';
import 'common/js/spec_helpers/jasmine-stealth';
import 'common/js/spec_helpers/jasmine-waituntil';
// These libraries are used by the tests (and the code under test)
// but not explicitly imported
import 'jquery.ui';
import _ from 'underscore';
import str from 'underscore.string';
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
window._ = _;
window._.str = str;
window.edx = window.edx || {};
window.edx.HtmlUtils = HtmlUtils;
window.edx.StringUtils = StringUtils;
// These are the tests that will be run
import './xblock/cms.runtime.v1_spec.js';
import '../../../js/spec/factories/xblock_validation_spec.js';
import '../../../js/spec/views/container_spec.js';
import '../../../js/spec/views/login_studio_spec.js';
import '../../../js/spec/views/modals/edit_xblock_spec.js';
import '../../../js/spec/views/module_edit_spec.js';
import '../../../js/spec/views/move_xblock_spec.js';
import '../../../js/spec/views/pages/container_spec.js';
import '../../../js/spec/views/pages/container_subviews_spec.js';
import '../../../js/spec/views/pages/course_outline_spec.js';
import '../../../js/spec/views/xblock_editor_spec.js';
import '../../../js/spec/views/xblock_string_field_editor_spec.js';
window.__karma__.start(); // eslint-disable-line no-underscore-dangle

View File

@@ -1,82 +1,81 @@
import EditHelpers from 'js/spec_helpers/edit_helpers';
import BaseModal from 'js/views/modals/base_modal';
import 'xblock/cms.runtime.v1';
define(['js/spec_helpers/edit_helpers', 'js/views/modals/base_modal', 'xblock/cms.runtime.v1'],
function(EditHelpers, BaseModal) {
'use strict';
describe('Studio Runtime v1', function() {
'use strict';
describe('Studio Runtime v1', function() {
var runtime;
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'
beforeEach(function() {
EditHelpers.installEditTemplates();
runtime = new window.StudioRuntime.v1();
});
modal.show();
};
beforeEach(function() {
EditHelpers.installEditTemplates();
});
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();
});
afterEach(function() {
EditHelpers.hideModalIfShowing(modal);
});
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('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('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();
});
});
});
});
});

View File

@@ -26,35 +26,8 @@ define([
IframeUtils,
DropdownMenuView
) {
'use strict';
var $body;
function smoothScrollLink(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $(this).attr('href')
});
}
function hideNotification(e) {
(e).preventDefault();
$(this)
.closest('.wrapper-notification')
.removeClass('is-shown')
.addClass('is-hiding')
.attr('aria-hidden', 'true');
}
function hideAlert(e) {
(e).preventDefault();
$(this).closest('.wrapper-alert').removeClass('is-shown');
}
domReady(function() {
var dropdownMenuView;
@@ -71,14 +44,14 @@ define([
$('.action-notification-close').bind('click', hideNotification);
// nav - dropdown related
$body.click(function() {
$body.click(function(e) {
$('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dd .nav-item .title').removeClass('is-selected');
});
$('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) {
var $subnav = $(this).find('.wrapper-nav-sub'),
$title = $(this).find('.title');
$subnav = $(this).find('.wrapper-nav-sub');
$title = $(this).find('.title');
if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown');
@@ -95,8 +68,7 @@ define([
});
// general link management - new window/tab
$('a[rel="external"]:not([title])')
.attr('title', gettext('This link will open in a new browser window/tab'));
$('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab'));
$('a[rel="external"]').attr('target', '_blank');
// general link management - lean modal window
@@ -125,7 +97,39 @@ define([
});
dropdownMenuView.postRender();
}
window.studioNavMenuActive = true;
});
function smoothScrollLink(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $(this).attr('href')
});
}
function smoothScrollTop(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $('#view-top')
});
}
function hideNotification(e) {
(e).preventDefault();
$(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true');
}
function hideAlert(e) {
(e).preventDefault();
$(this).closest('.wrapper-alert').removeClass('is-shown');
}
}); // end require()

View File

@@ -1,5 +1,3 @@
// We can't convert this to an es6 module until all factories that use it have been converted out
// of RequireJS
define(['js/base', 'cms/js/main', 'js/src/logger', 'datepair', 'accessibility',
'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course'],
function() {

View File

@@ -1,26 +1,21 @@
import * as $ from 'jquery';
import * as _ from 'underscore';
import * as XBlockContainerInfo from 'js/models/xblock_container_info';
import * as ContainerPage from 'js/views/pages/container';
import * as ComponentTemplates from 'js/collections/component_template';
import * as xmoduleLoader from 'xmodule';
import './base';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
define([
'jquery', 'underscore', 'js/models/xblock_container_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'cms/js/main',
'xblock/cms.runtime.v1'
],
function($, _, XBlockContainerInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict';
return function(componentTemplates, XBlockInfoJson, action, options) {
var main_options = {
el: $('#content'),
model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}),
action: action,
templates: new ComponentTemplates(componentTemplates, {parse: true})
};
'use strict';
export default function ContainerFactory(componentTemplates, XBlockInfoJson, action, options) {
var main_options = {
el: $('#content'),
model: new XBlockContainerInfo(XBlockInfoJson, {parse: true}),
action: action,
templates: new ComponentTemplates(componentTemplates, {parse: true})
xmoduleLoader.done(function() {
var view = new ContainerPage(_.extend(main_options, options));
view.render();
});
};
xmoduleLoader.done(function() {
var view = new ContainerPage(_.extend(main_options, options));
view.render();
});
};
export {ContainerFactory}
});

View File

@@ -1,3 +0,0 @@
import * as ContextCourse from 'js/models/course';
export {ContextCourse}

View File

@@ -1,25 +1,20 @@
import * as TabsModel from 'js/models/explicit_url';
import * as TabsEditView from 'js/views/tabs';
import * as xmoduleLoader from 'xmodule';
import './base';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
define([
'js/models/explicit_url', 'js/views/tabs', 'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'
], function(TabsModel, TabsEditView, xmoduleLoader) {
'use strict';
return function(courseLocation, explicitUrl) {
xmoduleLoader.done(function() {
var model = new TabsModel({
id: courseLocation,
explicit_url: explicitUrl
}),
editView;
'use strict';
export default function EditTabsFactory(courseLocation, explicitUrl) {
xmoduleLoader.done(function() {
var model = new TabsModel({
id: courseLocation,
explicit_url: explicitUrl
}),
editView;
editView = new TabsEditView({
el: $('.tab-list'),
model: model,
mast: $('.wrapper-mast')
editView = new TabsEditView({
el: $('.tab-list'),
model: model,
mast: $('.wrapper-mast')
});
});
});
};
export {EditTabsFactory}
};
});

View File

@@ -1,28 +1,23 @@
import * as $ from 'jquery';
import * as _ from 'underscore';
import * as XBlockInfo from 'js/models/xblock_info';
import * as PagedContainerPage from 'js/views/pages/paged_container';
import * as LibraryContainerView from 'js/views/library_container';
import * as ComponentTemplates from 'js/collections/component_template';
import * as xmoduleLoader from 'xmodule';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
define([
'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/paged_container',
'js/views/library_container', 'js/collections/component_template', 'xmodule', 'cms/js/main',
'xblock/cms.runtime.v1'
],
function($, _, XBlockInfo, PagedContainerPage, LibraryContainerView, ComponentTemplates, xmoduleLoader) {
'use strict';
return function(componentTemplates, XBlockInfoJson, options) {
var main_options = {
el: $('#content'),
model: new XBlockInfo(XBlockInfoJson, {parse: true}),
templates: new ComponentTemplates(componentTemplates, {parse: true}),
action: 'view',
viewClass: LibraryContainerView,
canEdit: true
};
'use strict';
export default function LibraryFactory(componentTemplates, XBlockInfoJson, options) {
var main_options = {
el: $('#content'),
model: new XBlockInfo(XBlockInfoJson, {parse: true}),
templates: new ComponentTemplates(componentTemplates, {parse: true}),
action: 'view',
viewClass: LibraryContainerView,
canEdit: true
xmoduleLoader.done(function() {
var view = new PagedContainerPage(_.extend(main_options, options));
view.render();
});
};
xmoduleLoader.done(function() {
var view = new PagedContainerPage(_.extend(main_options, options));
view.render();
});
};
export {LibraryFactory}
});

View File

@@ -1,63 +1,57 @@
define(['jquery.cookie', 'utility', 'common/js/components/utils/view_utils'], function(cookie, utility, ViewUtils) {
'use strict';
return function LoginFactory(homepageURL) {
function postJSON(url, data, callback) {
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: data,
success: callback
});
}
'use strict';
import cookie from 'jquery.cookie';
import utility from 'utility';
import ViewUtils from 'common/js/components/utils/view_utils';
export default function LoginFactory(homepageURL) {
function postJSON(url, data, callback) {
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: data,
success: callback
// Clear the login error message when credentials are edited
$('input#email').on('input', function() {
$('#login_error').removeClass('is-shown');
});
}
// Clear the login error message when credentials are edited
$('input#email').on('input', function () {
$('#login_error').removeClass('is-shown');
});
$('input#password').on('input', function() {
$('#login_error').removeClass('is-shown');
});
$('input#password').on('input', function () {
$('#login_error').removeClass('is-shown');
});
$('form#login_form').submit(function(event) {
event.preventDefault();
var $submitButton = $('#submit'),
deferred = new $.Deferred(),
promise = deferred.promise();
ViewUtils.disableElementWhileRunning($submitButton, function() { return promise; });
var submit_data = $('#login_form').serialize();
$('form#login_form').submit(function (event) {
event.preventDefault();
var $submitButton = $('#submit'),
deferred = new $.Deferred(),
promise = deferred.promise();
ViewUtils.disableElementWhileRunning($submitButton, function () { return promise; });
var submit_data = $('#login_form').serialize();
postJSON('/login_post', submit_data, function (json) {
if (json.success) {
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1 && !isExternal(next[1])) {
ViewUtils.redirect(next[1]);
postJSON('/login_post', submit_data, function(json) {
if (json.success) {
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1 && !isExternal(next[1])) {
ViewUtils.redirect(next[1]);
} else {
ViewUtils.redirect(homepageURL);
}
} else if ($('#login_error').length === 0) {
$('#login_form').prepend(
'<div id="login_error" class="message message-status error">' +
json.value +
'</span></div>'
);
$('#login_error').addClass('is-shown');
deferred.resolve();
} else {
ViewUtils.redirect(homepageURL);
$('#login_error')
.stop()
.addClass('is-shown')
.html(json.value);
deferred.resolve();
}
} else if ($('#login_error').length === 0) {
$('#login_form').prepend(
'<div id="login_error" class="message message-status error">' +
json.value +
'</span></div>'
);
$('#login_error').addClass('is-shown');
deferred.resolve();
} else {
$('#login_error')
.stop()
.addClass('is-shown')
.html(json.value);
deferred.resolve();
}
});
});
});
};
export { LoginFactory }
};
});

View File

@@ -1,23 +1,20 @@
import * as gettext from 'gettext';
import * as Section from 'js/models/section';
import * as TextbookCollection from 'js/collections/textbook';
import * as ListTextbooksView from 'js/views/list_textbooks';
define([
'gettext', 'js/models/section', 'js/collections/textbook', 'js/views/list_textbooks'
], function(gettext, Section, TextbookCollection, ListTextbooksView) {
'use strict';
return function(textbooksJson) {
var textbooks = new TextbookCollection(textbooksJson, {parse: true}),
tbView = new ListTextbooksView({collection: textbooks});
'use strict';
export default function TextbooksFactory(textbooksJson) {
var textbooks = new TextbookCollection(textbooksJson, {parse: true}),
tbView = new ListTextbooksView({collection: textbooks});
$('.content-primary').append(tbView.render().el);
$('.nav-actions .new-button').click(function(event) {
tbView.addOne(event);
});
$(window).on('beforeunload', function() {
var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); });
if (dirty) {
return gettext('You have unsaved changes. Do you really want to leave this page?');
}
});
};
export {TextbooksFactory}
$('.content-primary').append(tbView.render().el);
$('.nav-actions .new-button').click(function(event) {
tbView.addOne(event);
});
$(window).on('beforeunload', function() {
var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); });
if (dirty) {
return gettext('You have unsaved changes. Do you really want to leave this page?');
}
});
};
});

View File

@@ -1,22 +1,19 @@
define(['js/views/xblock_validation', 'js/models/xblock_validation'],
function(XBlockValidationView, XBlockValidationModel) {
'use strict';
return function(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) {
var model, response;
import * as XBlockValidationView from 'js/views/xblock_validation';
import * as XBlockValidationModel from 'js/models/xblock_validation';
if (hasEditingUrl && !isRoot) {
validationMessages.showSummaryOnly = true;
}
response = validationMessages;
response.isUnit = isUnit;
'use strict';
export default function XBlockValidationFactory(validationMessages, hasEditingUrl, isRoot, isUnit, validationEle) {
var model, response;
model = new XBlockValidationModel(response, {parse: true});
if (hasEditingUrl && !isRoot) {
validationMessages.showSummaryOnly = true;
}
response = validationMessages;
response.isUnit = isUnit;
model = new XBlockValidationModel(response, {parse: true});
if (!model.get('empty')) {
new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render();
}
};
export {XBlockValidationFactory}
if (!model.get('empty')) {
new XBlockValidationView({el: validationEle, model: model, root: isRoot}).render();
}
};
});

View File

@@ -0,0 +1,6 @@
define(
['js/models/course'],
function(ContextCourse) {
window.course = new ContextCourse(window.pageFactoryArguments.ContextCourse);
}
);

View File

@@ -0,0 +1,8 @@
define(
['js/factories/login', 'common/js/utils/page_factory', 'js/factories/base'],
function(LoginFactory, invokePageFactory) {
'use strict';
invokePageFactory('LoginFactory', LoginFactory);
}
);

View File

@@ -0,0 +1,7 @@
define(
['js/factories/textbooks', 'common/js/utils/page_factory', 'js/factories/base', 'js/pages/course'],
function(TextbooksFactory, invokePageFactory) {
'use strict';
invokePageFactory('TextbooksFactory', TextbooksFactory);
}
);

View File

@@ -1,41 +1,39 @@
import * as domReady from 'domReady';
import * as $ from 'jquery';
import 'jquery.smoothScroll';
define(['domReady', 'jquery', 'jquery.smoothScroll'],
function(domReady, $) {
'use strict';
'use strict';
var toggleSock = function(e) {
e.preventDefault();
var toggleSock = function (e) {
e.preventDefault();
var $btnShowSockLabel = $(this).find('.copy-show');
var $btnHideSockLabel = $(this).find('.copy-hide');
var $sock = $('.wrapper-sock');
var $sockContent = $sock.find('.wrapper-inner');
var $btnShowSockLabel = $(this).find('.copy-show');
var $btnHideSockLabel = $(this).find('.copy-hide');
var $sock = $('.wrapper-sock');
var $sockContent = $sock.find('.wrapper-inner');
if ($sock.hasClass('is-shown')) {
$sock.removeClass('is-shown');
$sockContent.hide('fast');
$btnHideSockLabel.removeClass('is-shown').addClass('is-hidden');
$btnShowSockLabel.removeClass('is-hidden').addClass('is-shown');
} else {
$sock.addClass('is-shown');
$sockContent.show('fast');
$btnHideSockLabel.removeClass('is-hidden').addClass('is-shown');
$btnShowSockLabel.removeClass('is-shown').addClass('is-hidden');
}
if ($sock.hasClass('is-shown')) {
$sock.removeClass('is-shown');
$sockContent.hide('fast');
$btnHideSockLabel.removeClass('is-shown').addClass('is-hidden');
$btnShowSockLabel.removeClass('is-hidden').addClass('is-shown');
} else {
$sock.addClass('is-shown');
$sockContent.show('fast');
$btnHideSockLabel.removeClass('is-hidden').addClass('is-shown');
$btnShowSockLabel.removeClass('is-shown').addClass('is-hidden');
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $sock
});
};
domReady(function() {
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
});
}
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $sock
});
};
domReady(function () {
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
});
export { toggleSock }
);

View File

@@ -1,77 +1,77 @@
import $ from 'jquery';
import XBlockValidationFactory from 'js/factories/xblock_validation';
import TemplateHelpers from 'common/js/spec_helpers/template_helpers';
define(['jquery', 'js/factories/xblock_validation', 'common/js/spec_helpers/template_helpers'],
function($, XBlockValidationFactory, TemplateHelpers) {
describe('XBlockValidationFactory', function() {
var $messageDiv;
describe('XBlockValidationFactory', () => {
var $messageDiv;
beforeEach(function() {
TemplateHelpers.installTemplate('xblock-validation-messages');
appendSetFixtures($('<div class="messages"></div>'));
$messageDiv = $('.messages');
});
beforeEach(function() {
TemplateHelpers.installTemplate('xblock-validation-messages');
appendSetFixtures($('<div class="messages"></div>'));
$messageDiv = $('.messages');
});
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('Does not attach a view if messages is empty', function() {
XBlockValidationFactory({empty: true}, false, false, false, $messageDiv);
expect($messageDiv.children().length).toEqual(0);
});
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 attach a view if messages are not empty', function() {
XBlockValidationFactory({empty: false}, false, false, false, $messageDiv);
expect($messageDiv.children().length).toEqual(1);
});
it('Passes through the root property to the view.', function() {
var noContainerContent = 'no-container-content';
it('Passes through the root property to the view.', function() {
var noContainerContent = 'no-container-content';
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);
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);
// Root is true, will add noContainerContent.
XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv);
expect($messageDiv.find('.validation')).toHaveClass(noContainerContent);
});
// Root is true, will add noContainerContent.
XBlockValidationFactory(notConfiguredMessages, true, true, false, $messageDiv);
expect($messageDiv.find('.validation')).toHaveClass(noContainerContent);
});
describe('Controls display of detailed messages based on url and root property', function() {
var messagesWithSummary, checkDetailedMessages;
describe('Controls display of detailed messages based on url and root property', function() {
var messagesWithSummary, checkDetailedMessages;
beforeEach(function() {
messagesWithSummary = {
empty: false,
summary: {text: 'my summary'},
messages: [{text: 'one', type: 'warning'}, {text: 'two', type: 'error'}],
xblock_id: 'id'
};
});
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);
});
});
});
checkDetailedMessages = function(expectedDetailedMessages) {
expect($messageDiv.children().length).toEqual(1);
expect($messageDiv.find('.xblock-message-item').length).toBe(expectedDetailedMessages);
};
it('Does not show details if xblock has an editing URL and it is not rendered as root', function() {
XBlockValidationFactory(messagesWithSummary, true, false, false, $messageDiv);
checkDetailedMessages(0);
});
it('Shows details if xblock does not have its own editing URL, regardless of root value', function() {
XBlockValidationFactory(messagesWithSummary, false, false, false, $messageDiv);
checkDetailedMessages(2);
XBlockValidationFactory(messagesWithSummary, false, true, false, $messageDiv);
checkDetailedMessages(2);
});
it('Shows details if xblock has its own editing URL and is rendered as root', function() {
XBlockValidationFactory(messagesWithSummary, true, true, false, $messageDiv);
checkDetailedMessages(2);
});
});
});
}
);

View File

@@ -309,7 +309,6 @@ define(['sinon', 'js/utils/drag_and_drop', 'common/js/components/views/feedback_
});
afterEach(function() {
this.clock.restore();
jasmine.stealth.clearSpies();
});
it('should send an update on reorder from one parent to another', function() {
var requests, request, savingOptions;

View File

@@ -1,205 +1,198 @@
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';
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,
describe('Container View', () => {
describe('Supports reordering components', () => {
var model, containerView, mockContainerHTML, init, getComponent,
getDragHandle, dragComponentVertically, dragComponentAbove,
verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy,
rootLocator = 'locator-container',
containerTestUrl = '/xblock/' + rootLocator,
rootLocator = 'locator-container',
containerTestUrl = '/xblock/' + rootLocator,
groupAUrl = '/xblock/locator-group-A',
groupA = 'locator-group-A',
groupAComponent1 = 'locator-component-A1',
groupAComponent2 = 'locator-component-A2',
groupAComponent3 = 'locator-component-A3',
groupAUrl = '/xblock/locator-group-A',
groupA = 'locator-group-A',
groupAComponent1 = 'locator-component-A1',
groupAComponent2 = 'locator-component-A2',
groupAComponent3 = 'locator-component-A3',
groupBUrl = '/xblock/locator-group-B',
groupB = 'locator-group-B',
groupBComponent1 = 'locator-component-B1',
groupBComponent2 = 'locator-component-B2',
groupBComponent3 = 'locator-component-B3';
groupBUrl = '/xblock/locator-group-B',
groupB = 'locator-group-B',
groupBComponent1 = 'locator-component-B1',
groupBComponent2 = 'locator-component-B2',
groupBComponent3 = 'locator-component-B3';
mockContainerHTML = readFixtures('mock/mock-container-xblock.underscore');
mockContainerHTML = readFixtures('templates/mock/mock-container-xblock.underscore');
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'
});
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'
});
containerView = new ContainerView({
model: model,
view: 'container_preview',
el: $('.wrapper-xblock')
});
});
containerView = new ContainerView({
model: model,
view: 'container_preview',
el: $('.wrapper-xblock')
});
});
afterEach(function() {
EditHelpers.uninstallMockXBlock();
containerView.remove();
});
afterEach(() => {
EditHelpers.uninstallMockXBlock();
containerView.remove();
});
init = function(caller) {
var requests = AjaxHelpers.requests(caller);
containerView.render();
init = function(caller) {
var requests = AjaxHelpers.requests(caller);
containerView.render();
AjaxHelpers.respondWithJson(requests, {
html: mockContainerHTML,
resources: []
});
AjaxHelpers.respondWithJson(requests, {
html: mockContainerHTML,
resources: []
});
$('body').append(containerView.$el);
$('body').append(containerView.$el);
// Give the whole container enough height to contain everything.
$('.xblock[data-locator=locator-container]').css('height', 2000);
// 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 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);
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);
};
// Now process the actual request
AjaxHelpers.respond(requests, {statusCode: status});
};
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]);
});
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]);
});
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 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);
// 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);
// 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);
verifyRequest(requests, 0, groupAUrl,
[groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]);
verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]);
});
verifyRequest(requests, 0, groupAUrl,
[groupBComponent1, groupAComponent1, groupAComponent2, groupAComponent3]);
verifyRequest(requests, 1, groupBUrl, [groupBComponent2, groupBComponent3]);
});
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);
});
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);
});
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('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]);
});
describe('Shows a saving message', function() {
it('hides saving message upon success', function() {
var requests, savingOptions;
requests = init(this);
describe('Shows a saving message', () => {
it('hides saving message upon success', () => {
var requests, savingOptions;
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, 200);
EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving');
respondToRequest(requests, 1, 200);
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not hide saving message if failure', function() {
var requests = init(this);
it('does not hide saving message if failure', () => {
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, 500);
EditHelpers.verifyNotificationShowing(notificationSpy, 'Saving');
// 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);
// Since the first reorder call failed, the removal will not be called.
verifyNumReorderCalls(requests, 1);
});
});
});
});
});
});

View File

@@ -1,35 +1,32 @@
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;
'use strict';
beforeEach(function() {
loadFixtures('mock/login.underscore');
var login_factory = new LoginFactory('/home/');
$submitButton = $('#submit');
});
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('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');
});
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');
it('It will not disable the submit button if there are errors in ajax request', function() {
var requests = AjaxHelpers.requests(this);
expect($submitButton).not.toHaveClass('is-disabled');
$submitButton.click();
expect($submitButton).toHaveClass('is-disabled');
AjaxHelpers.respondWithError(requests, {});
expect($submitButton).not.toHaveClass('is-disabled');
});
});
});

View File

@@ -1,215 +1,211 @@
'use strict';
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;
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';
showModal = function(requests, mockHtml, options) {
var $xblockElement = $('.xblock');
return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options);
};
describe('EditXBlockModal', function() {
var model, modal, showModal;
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'
});
});
showModal = function(requests, mockHtml, options) {
var $xblockElement = $('.xblock');
return EditHelpers.showEditModal(requests, $xblockElement, model, mockHtml, options);
};
afterEach(function() {
EditHelpers.cancelModalIfShowing();
});
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('XBlock Editor', function() {
var mockXBlockEditorHtml;
afterEach(function() {
EditHelpers.cancelModalIfShowing();
});
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
describe('XBlock Editor', function() {
var mockXBlockEditorHtml;
beforeEach(function() {
EditHelpers.installMockXBlock();
spyOn(Backbone, 'trigger').and.callThrough();
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
afterEach(function() {
EditHelpers.uninstallMockXBlock();
});
beforeEach(function() {
EditHelpers.installMockXBlock();
spyOn(Backbone, 'trigger').and.callThrough();
});
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();
});
afterEach(function() {
EditHelpers.uninstallMockXBlock();
});
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('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('shows the correct title', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockXBlockEditorHtml);
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
});
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('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('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 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('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('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 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');
});
describe('Custom Buttons', function() {
var mockCustomButtonsHtml;
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');
});
mockCustomButtonsHtml = readFixtures('mock/mock-xblock-editor-with-custom-buttons.underscore');
describe('Custom Buttons', function() {
var mockCustomButtonsHtml;
it('hides the modal\'s button bar', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockCustomButtonsHtml);
expect(modal.$('.modal-actions')).toBeHidden();
});
});
});
mockCustomButtonsHtml = readFixtures('templates/mock/mock-xblock-editor-with-custom-buttons.underscore');
describe('XModule Editor', function() {
var mockXModuleEditorHtml;
it('hides the modal\'s button bar', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockCustomButtonsHtml);
expect(modal.$('.modal-actions')).toBeHidden();
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);
});
});
});
});
describe('XModule Editor', function() {
var mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-editor.underscore');
beforeEach(function() {
EditHelpers.installMockXModule();
});
afterEach(function() {
EditHelpers.uninstallMockXModule();
});
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockXModuleEditorHtml);
expect(EditHelpers.isShowingModal(modal)).toBeTruthy();
EditHelpers.cancelModal(modal);
expect(EditHelpers.isShowingModal(modal)).toBeFalsy();
});
it('shows the correct title', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockXModuleEditorHtml);
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
});
it('shows the correct default buttons', function() {
var requests = AjaxHelpers.requests(this),
editorButton,
settingsButton;
modal = showModal(requests, mockXModuleEditorHtml);
expect(modal.$('.editor-modes a').length).toBe(2);
editorButton = modal.$('.editor-button');
settingsButton = modal.$('.settings-button');
expect(editorButton.length).toBe(1);
expect(editorButton).toHaveClass('is-set');
expect(settingsButton.length).toBe(1);
expect(settingsButton).not.toHaveClass('is-set');
});
it('can switch tabs', function() {
var requests = AjaxHelpers.requests(this),
editorButton,
settingsButton;
modal = showModal(requests, mockXModuleEditorHtml);
expect(modal.$('.editor-modes a').length).toBe(2);
editorButton = modal.$('.editor-button');
settingsButton = modal.$('.settings-button');
expect(modal.$('.metadata_edit')).toHaveClass('is-inactive');
settingsButton.click();
expect(modal.$('.metadata_edit')).toHaveClass('is-active');
editorButton.click();
expect(modal.$('.metadata_edit')).toHaveClass('is-inactive');
});
describe('Custom Tabs', function() {
var mockCustomTabsHtml;
mockCustomTabsHtml = readFixtures('templates/mock/mock-xmodule-editor-with-custom-tabs.underscore');
it('hides the modal\'s header', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockCustomTabsHtml);
expect(modal.$('.modal-header')).toBeHidden();
});
it('shows the correct title', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockCustomTabsHtml);
expect(modal.$('.component-name').text()).toBe('Editing: Component');
});
});
});
describe('XModule Editor (settings only)', function() {
var mockXModuleEditorHtml;
mockXModuleEditorHtml = readFixtures('templates/mock/mock-xmodule-settings-only-editor.underscore');
beforeEach(function() {
EditHelpers.installMockXModule();
});
afterEach(function() {
EditHelpers.uninstallMockXModule();
});
it('can render itself', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockXModuleEditorHtml);
expect(EditHelpers.isShowingModal(modal)).toBeTruthy();
EditHelpers.cancelModal(modal);
expect(EditHelpers.isShowingModal(modal)).toBeFalsy();
});
it('does not show any mode buttons', function() {
var requests = AjaxHelpers.requests(this);
modal = showModal(requests, mockXModuleEditorHtml);
expect(modal.$('.editor-modes li').length).toBe(0);
});
});
});

View File

@@ -1,58 +1,37 @@
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() {
(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() {
beforeEach(function() {
spyOn(ModuleEdit.prototype, 'render');
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,
@@ -60,206 +39,227 @@ describe('ModuleEdit', function() {
});
return this.moduleEdit;
});
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('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');
});
});
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('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();
});
});
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
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(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)
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));
});
});
this.moduleEdit.clickEditButton({
preventDefault: jasmine.createSpy('event.preventDefault')
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
$.ajax.calls.mostRecent().args[0].success({
html: mockXBlockEditorHtml,
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads inline css from fragments', function() {
var args = "<style type='text/css'>inline-css</style>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads css urls from fragments', function() {
var args = "<link rel='stylesheet' href='css-url' type='text/css'>";
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads inline js from fragments', function() {
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
});
it('loads js urls from fragments', function() {
return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url');
});
it('loads head html', function() {
return expect($('head').append).toHaveBeenCalledWith('head-html');
});
it("doesn't load body html", function() {
return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html');
});
it("doesn't reload resources", function() {
var count;
count = $('head').append.calls.count();
$.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html 2</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
]
]
});
return expect($('head').append.calls.count()).toBe(count);
});
});
describe('loadDisplay', function() {
beforeEach(function() {
spyOn(XBlock, 'initializeBlock');
return this.moduleEdit.loadDisplay();
});
it('loads the .xmodule-display inside the module editor', function() {
expect(XBlock.initializeBlock).toHaveBeenCalled();
var sel = '.xblock-student_view';
return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0));
});
});
});
});
}).call(this);

View File

@@ -1,785 +1,766 @@
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';
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';
describe('MoveXBlock', function() {
parentChildMap = {
course: 'section',
section: 'subsection',
subsection: 'unit',
unit: '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';
categoryMap = {
section: 'chapter',
subsection: 'sequential',
unit: 'vertical',
component: 'component'
};
parentChildMap = {
course: 'section',
section: 'subsection',
subsection: 'unit',
unit: 'component'
};
courseOutlineOptions = {
section: 2,
subsection: 2,
unit: 2,
component: 2
};
categoryMap = {
section: 'chapter',
subsection: 'sequential',
unit: 'vertical',
component: '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'
}
]
};
courseOutlineOptions = {
section: 2,
subsection: 2,
unit: 2,
component: 2
};
xblockAncestorInfo = {
ancestors: [
{
category: 'vertical',
sourceParentXBlockInfo = new XBlockInfo({
id: sourceParentLocator,
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'
}
]
};
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
category: 'vertical'
});
}
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData);
createContainerPage = function() {
containerPage = new ContainerPage({
model: sourceParentXBlockInfo,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
};
// send the response
AjaxHelpers.respondWithJson(requests, _.extend(responseData, {
source_index: sourceIndex
}));
};
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();
});
/**
* 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();
};
afterEach(function() {
modal.hide();
courseOutline = null;
containerPage.remove();
});
/**
* 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
})
);
};
showModal = function() {
modal = new MoveXBlockModal({
sourceXBlockInfo: new XBlockInfo({
id: sourceLocator,
display_name: sourceDisplayName,
category: 'component'
}),
sourceParentXBlockInfo: sourceParentXBlockInfo,
XBlockUrlRoot: '/xblock'
});
modal.show();
};
/**
* 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
/**
* 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 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');
};
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 undo move text.
*/
verifyConfirmationFeedbackUndoMoveActionText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move');
};
/**
* 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);
};
/**
* Get sent requests.
*
* @returns {Object}
*/
getSentRequests = function() {
return jasmine.Ajax.requests.filter(function(request) {
return request.readyState > 0;
});
};
/**
* 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);
};
it('renders views with correct information', function() {
var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1},
outline = createCourseOutline(outlineOptions);
/**
* 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
};
};
renderViews(outline, xblockAncestorInfo);
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true);
});
/**
* 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);
}
};
it('shows correct behavior on breadcrumb navigation', function() {
var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1});
/**
* 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));
renderViews(outline);
_.each(_.range(3), function() {
clickForwardButton();
});
expect(displayedBreadcrumbs).toEqual(
_.map(visitedCategories, function(visitedCategory) {
return visitedCategory === 'course' ?
'Course Outline' : visitedCategory + '_display_name_' + xblockIndex;
})
);
};
_.each(['component', 'unit', 'subsection', 'section'], function(category) {
verifyListViewInfo(category, 1);
if (category !== 'section') {
/**
* 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();
}
});
});
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'
/**
* 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];
};
renderViews(nonParentableXBlockInfo);
// move is disabled on non-parent xblocks.
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
/**
* 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");
});
});
});
});
describe('Move an xblock', function() {
it('can not move in a disabled state', function() {
verifyMoveEnabled(false);
modal.$el.find('.modal-actions .action-move').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('move an xblock when move button is clicked', function() {
var requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
});
it('do not move an xblock when cancel button is clicked', function() {
modal.$el.find('.modal-actions .action-cancel').click();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});
it('undo move an xblock when undo move link is clicked', function() {
var sourceIndex = 0,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
verifyUndoConfirmationFeedbackTitleText(sourceDisplayName);
});
});
describe('shows a notification', function() {
it('mini operation message when moving an xblock', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy();
// navigate to a target parent and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
verifyNotificationStatus(requests, notificationSpy, 'Moving');
});
it('mini operation message when undo moving an xblock', function() {
var notificationSpy,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
notificationSpy = ViewHelpers.createNotificationSpy();
getMovedAlertNotification().find('.action-save').click();
verifyNotificationStatus(requests, notificationSpy, 'Undo moving');
});
it('error message when move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
// select a target item and click
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(1);
});
modal.$el.find('.modal-actions .action-move').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
it('error message when undo move request fails', function() {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
moveXBlockWithSuccess(requests);
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
});
});

View File

@@ -1,855 +1,839 @@
'use strict';
'use strict';
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';
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';
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;
beforeEach(function() {
var newDisplayName = 'New Display Name';
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
EditHelpers.installMockXBlock({
data: '<p>Some HTML</p>',
metadata: {
display_name: newDisplayName
}
});
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');
});
});
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();
// 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('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;
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;
beforeEach(function() {
promptSpy = EditHelpers.createPromptSpy();
var newDisplayName = 'New Display Name';
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
EditHelpers.installMockXBlock({
data: '<p>Some HTML</p>',
metadata: {
display_name: newDisplayName
}
});
initialDisplayName = 'Test Container';
model = new XBlockInfo({
id: 'locator-container',
display_name: initialDisplayName,
category: 'vertical'
});
});
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);
afterEach(function() {
EditHelpers.uninstallMockXBlock();
if (containerPage !== undefined) {
containerPage.remove();
}
});
// final request to refresh the xblock info
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, {});
respondWithHtml = function(html) {
AjaxHelpers.respondWithJson(
requests,
{html: html, resources: []}
);
};
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
});
// 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);
});
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));
};
updatePreviewButtonTest(true, 'Hide Previews');
updatePreviewButtonTest(false, 'Show Previews');
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 || {});
};
it('triggers underlying view togglePreviews when preview button clicked', function() {
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();
spyOn(containerPage.xblockView, 'togglePreviews');
containerPage.$('.toggle-preview-button').click();
expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled();
expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
respondWithHtml(mockContainerXBlockHtml);
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
}
});
describe('createNewComponent ', function() {
var clickNewComponent;
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');
});
clickNewComponent = function(index) {
containerPage.$('.new-component .new-component-type button.single-template')[index].click();
};
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('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('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');
});
});
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);
describe('Editing the container', function() {
var updatedDisplayName = 'Updated Test Container',
getDisplayNameWrapper;
afterEach(function() {
EditHelpers.cancelModalIfShowing();
});
$('.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();
getDisplayNameWrapper = function() {
return containerPage.$('.wrapper-xblock-field');
};
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.
it('can edit itself', function() {
var editButtons, displayNameElement, request;
renderContainerPage(this, mockContainerXBlockHtml);
showTemplatePicker();
expect(containerPage.$('.support-documentation').length).toBe(0);
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('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);
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);
});
});
// 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);
describe('Editing an xblock', function() {
afterEach(function() {
EditHelpers.cancelModalIfShowing();
});
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('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('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();
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();
});
supportLevelIndicators = $(containerPage.$('.new-component-template')[0])
.find('.support-level');
expect(supportLevelIndicators.length).toBe(3);
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);
}
});
getScreenReaderText = function(index) {
return $($(supportLevelIndicators[index]).siblings()[0]).text().trim();
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 + "']");
};
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);
};
// 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');
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);
});
});
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/);
});
});
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();
});
}
});
describe('createNewComponent ', function() {
var clickNewComponent;
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'
});
});
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');
});
});
});
});
});
});
}
// Create a suite for a non-paged container that includes 'edit visibility' buttons
parameterized_suite('Non paged',
{
page: ContainerPage,
requiresPageRefresh: false,
initial: 'mock/mock-container-xblock.underscore',
addResponse: 'mock/mock-xblock.underscore',
hasVisibilityEditor: true,
pagedSpecificTests: false,
hasMoveModal: true
}
);
// Create a suite for a paged container that does not include 'edit visibility' buttons
parameterized_suite('Paged',
{
page: PagedContainerPage,
page_size: 42,
requiresPageRefresh: true,
initial: 'mock/mock-container-paged-xblock.underscore',
addResponse: 'mock/mock-xblock-paged.underscore',
hasVisibilityEditor: false,
pagedSpecificTests: true,
hasMoveModal: false
}
);
});
}
// Create a suite for a non-paged container that includes 'edit visibility' buttons
parameterized_suite('Non paged',
{
page: ContainerPage,
requiresPageRefresh: false,
initial: 'templates/mock/mock-container-xblock.underscore',
addResponse: 'templates/mock/mock-xblock.underscore',
hasVisibilityEditor: true,
pagedSpecificTests: false,
hasMoveModal: true
}
);
// Create a suite for a paged container that does not include 'edit visibility' buttons
parameterized_suite('Paged',
{
page: PagedContainerPage,
page_size: 42,
requiresPageRefresh: true,
initial: 'templates/mock/mock-container-paged-xblock.underscore',
addResponse: 'templates/mock/mock-xblock-paged.underscore',
hasVisibilityEditor: false,
pagedSpecificTests: true,
hasMoveModal: false
}
);

View File

@@ -1,627 +1,620 @@
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';
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;
var VisibilityState = XBlockUtils.VisibilityState;
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');
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"'
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
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"');
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);
});
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);
afterEach(function() {
delete window.course;
if (containerPage !== undefined) {
containerPage.remove();
}
});
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
}
});
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');
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'
}));
AjaxHelpers.respondWithJson(requests, createXBlockInfo(options));
};
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');
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);
verifyExplicitStaffOnly(false);
verifyImplicitStaffOnly(false);
}
};
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass);
expect(containerPage.$(bitPublishingCss)).toHaveClass(unscheduledClass);
};
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
beforeEach(function() {
promptSpies = jasmine.stealth.spyOnConstructor(Prompt, 'Warning', ['show', 'hide']);
promptSpies.show.and.returnValue(this.promptSpies);
});
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
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();
});
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('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);
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'
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();
});
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'
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();
});
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
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();
});
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'
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();
});
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
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);
});
});
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);
});
});
});
});
describe('PublishHistory', function() {
var lastPublishCss = '.wrapper-last-publish';
it('renders never published when the block is unpublished', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
published: false, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Never published');
});
it('renders the last published date and user when the block is published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'
});
expect(containerPage.$(lastPublishCss).text()).
toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
});
it('renders correctly when the block is published without publish info', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
published: true, published_on: null, published_by: null
});
expect(containerPage.$(lastPublishCss).text()).toContain('Previously published');
});
});
describe('Message Area', function() {
var messageSelector = '.container-message .warning',
warningMessage = 'Caution: The last published version of this unit is live. ' +
'By publishing changes you will change the student experience.';
it('is empty for a unit that is not currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message for a unit that is currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
it('hides the message when the unit is hidden from students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
fetch({currently_visible_to_students: false});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message when a unit is made visible', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
fetch({currently_visible_to_students: true});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
});
});

View File

@@ -1,2057 +1,2053 @@
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';
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');
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: '',
prereq_min_completion: '',
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');
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}});
};
expectHighlightsEnabledToBe = function(expectedEnabled) {
if (expectedEnabled) {
expect('.status-highlights-enabled-value.button').not.toExist();
expect('.status-highlights-enabled-value.text').toExist();
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: '',
prereq_min_completion: '',
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 {
expect('.status-highlights-enabled-value.button').toExist();
expect('.status-highlights-enabled-value.text').not.toExist();
return JSON.stringify({
developer_message: 'Could not reindex course.',
user_message: 'Could not reindex course.'
});
}
};
expectServerHandshake = function() {
// POST to update course
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-course', {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true
}
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')
});
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));
if (!createOnly) {
outlinePage.render();
}
return highlights;
return outlinePage;
};
highlightsLink = function() {
return outlinePage.$('.section-status >> .highlights-button');
};
verifyTypePublishable = function(type, getMockCourseJSON) {
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
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
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');
});
AjaxHelpers.respondWithJson(requests, {});
// GET updated section
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, createMockSectionJSON({highlights: highlights}));
};
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);
});
expectHighlightsToUpdate = function(originalHighlights, updatedHighlights) {
createCourseWithHighlights(originalHighlights);
it('should show publish button if it is published and changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: true
});
verifyPublishButton(this, mockCourseJSON);
});
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({
it('should show publish button if it is not published, but changed', function() {
var mockCourseJSON = getMockCourseJSON({
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, minCompletion) {
$('#prereq option:last').prop('selected', true).trigger('change');
$('#prereq_min_score').val(minScore).trigger('keyup');
$('#prereq_min_completion').val(minCompletion).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;
delete mockSubsectionJSON.prereq_min_completion;
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
}, [
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', '0');
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');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('0');
$('.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',
prereq_min_completion: '50'
}, [])
])
]);
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');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('50');
});
it('can show a saved prerequisite subsection with empty min score 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: '',
prereq_min_completion: '50'
}, [])
])
]);
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('100');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('50');
});
it('can display validation error on non-integer or empty 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('', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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('', '');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_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('abc', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', 'abc');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '5.5');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '-5');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '105');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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('80', '50');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('0', '0');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('100', '100');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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();
});
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();
});
};
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);
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('Show correctness editor works as expected.', function() {
beforeEach(function() {
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, minCompletion) {
this.$('#prereq option:last').prop('selected', true).trigger('change');
this.$('#prereq_min_score').val(minScore).trigger('keyup');
this.$('#prereq_min_completion').val(minCompletion).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;
delete mockSubsectionJSON.prereq_min_completion;
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('show_correctness="always" (default, unchanged metadata)', function() {
setShowCorrectness('always');
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);
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', defaultModalSettings);
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
});
$.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;
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, {});
setShowCorrectness(showCorrectness);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
expectedSettings);
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', '0');
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');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('0');
$('.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',
prereq_min_completion: '50'
}, [])
])
]);
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');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('50');
});
it('can show a saved prerequisite subsection with empty min score 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: '',
prereq_min_completion: '50'
}, [])
])
]);
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('100');
expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
expect($('#prereq_min_completion').val()).toBe('50');
});
it('can display validation error on non-integer or empty 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('', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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('', '');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_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('abc', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', 'abc');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '5.5');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '-5');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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', '50');
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
selectLastPrerequisiteSubsection('50', '105');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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('80', '50');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
expect($('#prereq_min_score_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('0', '0');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_error').css('display')).toBe('none');
selectLastPrerequisiteSubsection('100', '100');
expect($('#prereq_min_score_error').css('display')).toBe('none');
expect($('#prereq_min_completion_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();
});
});
});
verifyTypePublishable('subsection', function(options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON(options, [
createMockVerticalJSON()
])
])
]);
});
// 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 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;
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');
});
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();
});
});
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');
});
// 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('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('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: [
it('shows partition group information with group_access set', function() {
var partitions = [
{
deleted: false,
selected: true,
id: 2,
name: 'Group 2'
},
{
deleted: false,
selected: true,
id: 3,
name: 'Group 3'
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'
}
],
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
}
];
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)
])
])
]);
});
});
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');
});
});
});
});
describe('Date and Time picker', function() {
// Two datetime formats can came from server: '%Y-%m-%dT%H:%M:%SZ' and %Y-%m-%dT%H:%M:%S+TZ:TZ'
it('can parse dates in both formats that can come from server', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#start_date').val()).toBe('');
expect($('#start_time').val()).toBe('');
DateUtils.setDate($('#start_date'), ('#start_time'), '2015-08-10T05:10:00Z');
expect($('#start_date').val()).toBe('8/10/2015');
expect($('#start_time').val()).toBe('05:10');
DateUtils.setDate($('#start_date'), ('#start_time'), '2014-07-09T00:00:00+00:00');
expect($('#start_date').val()).toBe('7/9/2014');
expect($('#start_time').val()).toBe('00:00');
});
});
});

View File

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

View File

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

View File

@@ -1,159 +1,156 @@
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';
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;
describe('XBlockStringFieldEditorView', function() {
var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView;
getXBlockInfo = function(displayName) {
return new XBlockInfo(
{
display_name: displayName,
id: 'my_xblock'
},
{parse: true}
);
};
getXBlockInfo = function(displayName) {
return new XBlockInfo(
{
display_name: displayName,
id: 'my_xblock'
},
{parse: true}
);
};
getFieldEditorView = function(xblockInfo) {
if (xblockInfo === undefined) {
xblockInfo = getXBlockInfo(initialDisplayName);
}
return new XBlockStringFieldEditor({
model: xblockInfo,
el: $('.wrapper-xblock-field')
});
};
getFieldEditorView = function(xblockInfo) {
if (xblockInfo === undefined) {
xblockInfo = getXBlockInfo(initialDisplayName);
}
return new XBlockStringFieldEditor({
model: xblockInfo,
el: $('.wrapper-xblock-field')
});
};
beforeEach(function() {
initialDisplayName = 'Default Display Name';
updatedDisplayName = 'Updated Display Name';
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>'
);
});
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>'
);
});
describe('Editing', function() {
var expectPostedNewDisplayName, expectEditCanceled;
describe('Editing', function() {
var expectPostedNewDisplayName, expectEditCanceled;
expectPostedNewDisplayName = function(requests, displayName) {
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', {
metadata: {
display_name: displayName
}
});
};
expectPostedNewDisplayName = function(requests, displayName) {
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', {
metadata: {
display_name: displayName
}
});
};
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);
};
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('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('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('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 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('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('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 the empty string', 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('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 whitespace-only', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: ' '});
});
it('can cancel an inline edit by pressing escape', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, pressEscape: true});
});
it('can cancel an inline edit by pressing escape', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, pressEscape: true});
});
it('can cancel an inline edit by clicking cancel', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true});
});
});
it('can cancel an inline edit by clicking cancel', function() {
var fieldEditorView = getFieldEditorView().render();
expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true});
});
});
describe('Rendering', function() {
var expectInputMatchesModelDisplayName = function(displayName) {
var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render();
expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName);
};
describe('Rendering', function() {
var expectInputMatchesModelDisplayName = function(displayName) {
var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render();
expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName);
};
it('renders single quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated \'Display Name\'');
});
it('renders single quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated \'Display Name\'');
});
it('renders double quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated "Display Name"');
});
it('renders double quotes in input field', function() {
expectInputMatchesModelDisplayName('Updated "Display Name"');
});
it('renders open angle bracket in input field', function() {
expectInputMatchesModelDisplayName(updatedDisplayName + '<');
});
it('renders open angle bracket in input field', function() {
expectInputMatchesModelDisplayName(updatedDisplayName + '<');
});
it('renders close angle bracket in input field', function() {
expectInputMatchesModelDisplayName('>' + updatedDisplayName);
});
});
});
it('renders close angle bracket in input field', function() {
expectInputMatchesModelDisplayName('>' + updatedDisplayName);
});
});
});
});

View File

@@ -1,140 +1,121 @@
/**
* Provides helper methods for invoking Studio editors in Jasmine tests.
*/
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';
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;
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;
installMockXBlock = function(mockResult) {
window.MockXBlock = function(runtime, element) {
var block = {
runtime: runtime
};
if (mockResult) {
block.save = function() {
return mockResult;
};
}
return block;
};
}
return block;
};
};
};
uninstallMockXBlock = function() {
window.MockXBlock = null;
};
uninstallMockXBlock = function() {
window.MockXBlock = null;
};
installMockXModule = function(mockResult) {
window.MockDescriptor = _.extend(XModule.Descriptor, {
save: function() {
return mockResult;
}
});
};
installMockXModule = function(mockResult) {
window.MockDescriptor = _.extend(XModule.Descriptor, {
save: function() {
return mockResult;
}
});
};
uninstallMockXModule = function() {
window.MockDescriptor = null;
};
uninstallMockXModule = function() {
window.MockDescriptor = null;
};
mockComponentTemplates = new ComponentTemplates([
{
templates: [
mockComponentTemplates = new ComponentTemplates([
{
category: 'discussion',
display_name: 'Discussion'
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}
}],
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
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
});
});
installEditTemplates = function(append) {
modal_helpers.installModalTemplates(append);
// Add templates needed by the add XBlock menu
TemplateHelpers.installTemplate('add-xblock-component');
TemplateHelpers.installTemplate('add-xblock-component-button');
TemplateHelpers.installTemplate('add-xblock-component-menu');
TemplateHelpers.installTemplate('add-xblock-component-menu-problem');
TemplateHelpers.installTemplate('add-xblock-component-support-legend');
TemplateHelpers.installTemplate('add-xblock-component-support-level');
// Add templates needed by the edit XBlock modal
TemplateHelpers.installTemplate('edit-xblock-modal');
TemplateHelpers.installTemplate('editor-mode-button');
// Add templates needed by the settings editor
TemplateHelpers.installTemplate('metadata-editor');
TemplateHelpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry');
TemplateHelpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry');
};
showEditModal = function(requests, xblockElement, model, mockHtml, options) {
var modal = new EditXBlockModal({});
modal.edit(xblockElement, model, options);
AjaxHelpers.respondWithJson(requests, {
html: mockHtml,
resources: []
});
return modal;
};
verifyXBlockRequest = function(requests, expectedJson) {
var request = AjaxHelpers.currentRequest(requests),
actualJson = JSON.parse(request.requestBody);
expect(request.url).toEqual('/xblock/');
expect(request.method).toEqual('POST');
expect(actualJson).toEqual(expectedJson);
};
var editHelpers = $.extend(modal_helpers, {
installMockXBlock: installMockXBlock,
uninstallMockXBlock: uninstallMockXBlock,
installMockXModule: installMockXModule,
uninstallMockXModule: uninstallMockXModule,
mockComponentTemplates: mockComponentTemplates,
installEditTemplates: installEditTemplates,
showEditModal: showEditModal,
verifyXBlockRequest: verifyXBlockRequest
});
export default editHelpers;
export {
installMockXBlock,
uninstallMockXBlock,
installMockXModule,
uninstallMockXModule,
mockComponentTemplates,
installEditTemplates,
showEditModal,
verifyXBlockRequest,
}

View File

@@ -1,80 +1,9 @@
define(['jquery', 'date', 'js/utils/change_on_enter', 'jquery.ui', 'jquery.timepicker'],
function($, date, TriggerChangeEventOnEnter) {
'use strict';
function getDate(datepickerInput, timepickerInput) {
// given a pair of inputs (datepicker and timepicker), return a JS Date
// object that corresponds to the datetime.js that they represent. Assume
// UTC timezone, NOT the timezone of the user's browser.
var selectedDate = null,
selectedTime = null;
if (datepickerInput.length > 0) {
selectedDate = $(datepickerInput).datepicker('getDate');
}
if (timepickerInput.length > 0) {
selectedTime = $(timepickerInput).timepicker('getTime');
}
if (selectedDate && selectedTime) {
return new Date(Date.UTC(
selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(),
selectedTime.getHours(), selectedTime.getMinutes()
));
} else if (selectedDate) {
return new Date(Date.UTC(
selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()));
} else {
return null;
}
}
function setDate(datepickerInput, timepickerInput, datetime) {
// given a pair of inputs (datepicker and timepicker) and the date as an
// ISO-formatted date string.
var parsedDatetime = Date.parse(datetime);
if (parsedDatetime) {
$(datepickerInput).datepicker('setDate', parsedDatetime);
if (timepickerInput.length > 0) {
$(timepickerInput).timepicker('setTime', parsedDatetime);
}
}
}
function renderDate(dateArg) {
// Render a localized date from an argument that can be passed to
// the Date constructor (e.g. another Date or an ISO 8601 string)
var dateObj = new Date(dateArg);
return dateObj.toLocaleString(
[],
{timeZone: 'UTC', timeZoneName: 'short'}
);
}
function parseDateFromString(stringDate) {
if (stringDate && typeof stringDate === 'string') {
return new Date(stringDate);
} else {
return stringDate;
}
}
function convertDateStringsToObjects(obj, dateFields) {
var i;
for (i = 0; i < dateFields.length; i++) {
if (obj[dateFields[i]]) {
obj[dateFields[i]] = parseDateFromString(obj[dateFields[i]]);
}
}
return obj;
}
function setupDatePicker(fieldName, view, index) {
var setupDatePicker = function(fieldName, view, index) {
var cacheModel;
var div;
var datefield;
var timefield;
var cacheview;
var setfield;
var currentDate;
if (typeof index !== 'undefined' && view.hasOwnProperty('collection')) {
cacheModel = view.collection.models[index];
div = view.$el.find('#' + view.collectionSelector(cacheModel.cid));
@@ -82,10 +11,10 @@ function($, date, TriggerChangeEventOnEnter) {
cacheModel = view.model;
div = view.$el.find('#' + view.fieldToSelectorMap[fieldName]);
}
datefield = $(div).find('input.date');
timefield = $(div).find('input.time');
cacheview = view;
setfield = function(event) {
var datefield = $(div).find('input.date');
var timefield = $(div).find('input.time');
var cacheview = view;
var setfield = function(event) {
var newVal = getDate(datefield, timefield);
// Setting to null clears the time as well, as date and time are linked.
@@ -105,19 +34,83 @@ function($, date, TriggerChangeEventOnEnter) {
timefield.on('changeTime', setfield);
timefield.on('input', setfield);
currentDate = null;
var current_date = null;
if (cacheModel) {
currentDate = cacheModel.get(fieldName);
current_date = cacheModel.get(fieldName);
}
// timepicker doesn't let us set null, so check that we have a time
if (currentDate) {
setDate(datefield, timefield, currentDate);
} else {
// but reset fields either way
if (current_date) {
setDate(datefield, timefield, current_date);
} // but reset fields either way
else {
timefield.val('');
datefield.val('');
}
}
};
var getDate = function(datepickerInput, timepickerInput) {
// given a pair of inputs (datepicker and timepicker), return a JS Date
// object that corresponds to the datetime.js that they represent. Assume
// UTC timezone, NOT the timezone of the user's browser.
var date = null,
time = null;
if (datepickerInput.length > 0) {
date = $(datepickerInput).datepicker('getDate');
}
if (timepickerInput.length > 0) {
time = $(timepickerInput).timepicker('getTime');
}
if (date && time) {
return new Date(Date.UTC(
date.getFullYear(), date.getMonth(), date.getDate(),
time.getHours(), time.getMinutes()
));
} else if (date) {
return new Date(Date.UTC(
date.getFullYear(), date.getMonth(), date.getDate()));
} else {
return null;
}
};
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);
if (datetime) {
$(datepickerInput).datepicker('setDate', datetime);
if (timepickerInput.length > 0) {
$(timepickerInput).timepicker('setTime', datetime);
}
}
};
var renderDate = function(dateArg) {
// Render a localized date from an argument that can be passed to
// the Date constructor (e.g. another Date or an ISO 8601 string)
var date = new Date(dateArg);
return date.toLocaleString(
[],
{timeZone: 'UTC', timeZoneName: 'short'}
);
};
var parseDateFromString = function(stringDate) {
if (stringDate && typeof stringDate === 'string') {
return new Date(stringDate);
} else {
return stringDate;
}
};
var convertDateStringsToObjects = function(obj, dateFields) {
for (var i = 0; i < dateFields.length; i++) {
if (obj[dateFields[i]]) {
obj[dateFields[i]] = parseDateFromString(obj[dateFields[i]]);
}
}
return obj;
};
return {
getDate: getDate,

View File

@@ -1,11 +1,6 @@
define([
'jquery', 'underscore', 'js/views/xblock', 'js/utils/module',
'gettext', 'common/js/components/views/feedback_notification',
'jquery.ui'
], // The container view uses sortable, which is provided by jquery.ui.
define(['jquery', 'underscore', 'js/views/xblock', 'js/utils/module', 'gettext', 'common/js/components/views/feedback_notification',
'jquery.ui'], // The container view uses sortable, which is provided by jquery.ui.
function($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
'use strict';
var studioXBlockWrapperClass = '.studio-xblock-wrapper';
var ContainerView = XBlockView.extend({
@@ -17,10 +12,10 @@ define([
new_child_view: 'reorderable_container_child_preview',
xblockReady: function() {
XBlockView.prototype.xblockReady.call(this);
var reorderableClass, reorderableContainer,
newParent, oldParent,
self = this;
XBlockView.prototype.xblockReady.call(this);
this.requestToken = this.$('div.xblock').first().data('request-token');
reorderableClass = this.makeRequestSpecificSelector('.reorderable-container');
@@ -29,13 +24,13 @@ define([
reorderableContainer.sortable({
handle: '.drag-handle',
start: function() {
start: function(event, ui) {
// Necessary because of an open bug in JQuery sortable.
// http://bugs.jqueryui.com/ticket/4990
reorderableContainer.sortable('refreshPositions');
},
stop: function() {
stop: function(event, ui) {
var saving, hideSaving, removeFromParent;
if (_.isUndefined(oldParent)) {

View File

@@ -9,8 +9,7 @@ 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',
'jquery.smoothScroll'
'edx-ui-toolkit/js/utils/string-utils'
],
function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, StringUtils) {
'use strict';

View File

@@ -44,8 +44,7 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
successCallback = options ? options.success || options.done : null,
errorCallback = options ? options.error || options.done : null,
xblock,
fragmentsRendered,
aside;
fragmentsRendered;
fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
fragmentsRendered.always(function() {
@@ -56,7 +55,7 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
self.xblockReady(self.xblock);
self.$('.xblock_asides-v1').each(function() {
if (!$(this).hasClass('xblock-initialized')) {
aside = XBlock.initializeBlock($(this));
var aside = XBlock.initializeBlock($(this));
self.initRuntimeData(aside, options);
}
});
@@ -64,7 +63,7 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
successCallback(xblock);
}
} catch (e) {
console.error(e, e.stack);
console.error(e.stack);
// Add 'xblock-initialization-failed' class to every xblock
self.$('.xblock').addClass('xblock-initialization-failed');
@@ -87,15 +86,13 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
* @param data The data to be passed to any listener's of the event.
*/
notifyRuntime: function(eventName, data) {
var runtime = this.xblock && this.xblock.runtime,
xblockChildren;
var runtime = this.xblock && this.xblock.runtime;
if (runtime) {
runtime.notify(eventName, data);
} else if (this.xblock) {
xblockChildren = this.xblock.element && $(this.xblock.element).prop('xblock_children');
if (xblockChildren) {
$(xblockChildren).each(function() {
var xblock_children = this.xblock.element && $(this.xblock.element).prop('xblock_children');
if (xblock_children) {
$(xblock_children).each(function() {
if (this.runtime) {
this.runtime.notify(eventName, data);
}
@@ -125,8 +122,7 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
*/
renderXBlockFragment: function(fragment, element) {
var html = fragment.html,
resources = fragment.resources,
blockView = this;
resources = fragment.resources;
if (!element) {
element = this.$el;
}
@@ -136,11 +132,10 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie
// by included scripts are logged to the console but are then ignored assuming
// that at least the rendered HTML will be in place.
try {
return this.addXBlockFragmentResources(resources).done(function() {
blockView.updateHtml(element, html);
});
this.updateHtml(element, html);
return this.addXBlockFragmentResources(resources);
} catch (e) {
console.error(e, e.stack);
console.error(e.stack);
return $.Deferred().resolve();
}
},

View File

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

View File

@@ -1,55 +0,0 @@
/* eslint-env node */
// Karma config for cms suite.
// Docs and troubleshooting tips in common/static/common/js/karma.common.conf.js
'use strict';
var path = require('path');
var configModule = require(path.join(__dirname, '../../common/static/common/js/karma.common.conf.js'));
var options = {
includeCommonFiles: true,
libraryFiles: [],
libraryFilesToInclude: [
],
// Make sure the patterns in sourceFiles and specFiles do not match the same file.
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [],
// {pattern: 'js/factories/login.js', webpack: true},
// {pattern: 'js/factories/xblock_validation.js', webpack: true},
// {pattern: 'js/factories/container.js', webpack: true},
// {pattern: 'js/factories/context_course.js', webpack: true},
// {pattern: 'js/factories/edit_tabs.js', webpack: true},
// {pattern: 'js/factories/library.js', webpack: true},
// {pattern: 'js/factories/textbooks.js', webpack: true},
// ],
// All spec files should be imported in main_webpack.js, rather than being listed here
specFiles: [],
fixtureFiles: [
{pattern: '../templates/js/**/*.underscore'},
{pattern: 'templates/**/*.underscore'}
],
runFiles: [
{pattern: 'cms/js/spec/main_webpack.js', webpack: true},
{pattern: 'jasmine.cms.conf.js', included: true}
],
preprocessors: {}
};
options.runFiles
.filter(function(file) { return file.webpack; })
.forEach(function(file) {
options.preprocessors[file.pattern] = ['webpack'];
});
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -41,23 +41,6 @@ from openedx.core.release import RELEASE_LINE
jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
%>
% if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
<script type="text/javascript">
var oldOnError = window.onerror;
window.localStorage.setItem('console_log_capture', JSON.stringify([]));
window.onerror = function (message, url, lineno, colno, error) {
if (oldOnError) {
oldOnError.apply(this, arguments);
}
var messages = JSON.parse(window.localStorage.getItem('console_log_capture'));
messages.push([message, url, lineno, colno, (error || {}).stack]);
window.localStorage.setItem('console_log_capture', JSON.stringify(messages));
}
</script>
% endif
<script type="text/javascript" src="${static.url(jsi18n_path)}"></script>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${EDX_ROOT_URL}">
@@ -142,9 +125,12 @@ from openedx.core.release import RELEASE_LINE
<%block name="jsextra"></%block>
% if context_course:
<%static:webpack entry="js/factories/context_course"/>
<script type="text/javascript">
window.course = new ContextCourse({
if (typeof window.pageFactoryArguments == "undefined") {
window.pageFactoryArguments = {};
}
window.pageFactoryArguments['ContextCourse'] = {
id: "${context_course.id | n, js_escaped_string}",
name: "${context_course.display_name_with_default | n, js_escaped_string}",
url_name: "${context_course.location.block_id | n, js_escaped_string}",
@@ -153,16 +139,21 @@ from openedx.core.release import RELEASE_LINE
display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}",
revision: "${context_course.location.branch | n, js_escaped_string}",
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json }
});
}
</script>
% endif
% if user.is_authenticated:
<%static:webpack entry='js/sock'/>
<%static:invoke_page_bundle page_name='js/sock'/>
% endif
<%block name='page_bundle'>
<script type="text/javascript">
require(['js/factories/base'], function () {
<%block name='requirejs'></%block>
require(['js/models/course'], function(Course) {
% if context_course:
window.course = new Course(window.pageFactoryArguments['ContextCourse']);
% endif
<%block name='requirejs'></%block>
});
});
</script>
</%block>

View File

@@ -42,8 +42,8 @@ from openedx.core.djangolib.markup import HTML, Text
% endif
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/container">
<%block name="requirejs">
require(["js/factories/container"], function(ContainerFactory) {
ContainerFactory(
${component_templates | n, dump_js_escaped_json},
${xblock_info | n, dump_js_escaped_json},
@@ -54,7 +54,7 @@ from openedx.core.djangolib.markup import HTML, Text
outlineURL: "${outline_url | n, js_escaped_string}"
}
);
</%static:webpack>
});
</%block>
<%block name="content">

View File

@@ -19,10 +19,10 @@
% endfor
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/edit_tabs">
<%block name="requirejs">
require(["js/factories/edit_tabs"], function (EditTabsFactory) {
EditTabsFactory("${context_course.location | n, js_escaped_string}", "${reverse('tabs_handler', kwargs={'course_key_string': context_course.id})}");
</%static:webpack>
});
</%block>
<%block name="content">

View File

@@ -25,8 +25,8 @@ from openedx.core.djangolib.markup import HTML, Text
</script>
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/library">
<%block name="requirejs">
require(["js/factories/library"], function(LibraryFactory) {
LibraryFactory(
${component_templates | n, dump_js_escaped_json},
${xblock_info | n, dump_js_escaped_json},
@@ -37,7 +37,7 @@ from openedx.core.djangolib.markup import HTML, Text
showChildrenPreviews: ${context_library.show_children_previews | n, dump_js_escaped_json}
}
);
</%static:webpack>
});
</%block>
<%block name="content">

View File

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

View File

@@ -26,14 +26,16 @@ block_is_unit = is_unit(xblock)
</script>
</%block>
<script type="text/javascript">
XBlockValidationFactory(
${messages | n, dump_js_escaped_json},
${bool(xblock_url) | n, dump_js_escaped_json}, // xblock_url will be None or a string
${bool(is_root) | n, dump_js_escaped_json}, // is_root will be None or a boolean
${bool(block_is_unit) | n, dump_js_escaped_json}, // block_is_unit will be None or a boolean
$('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]')
);
<script>
require(["jquery", "js/factories/xblock_validation"], function($, XBlockValidationFactory) {
XBlockValidationFactory(
${messages | n, dump_js_escaped_json},
${bool(xblock_url) | n, dump_js_escaped_json}, // xblock_url will be None or a string
${bool(is_root) | n, dump_js_escaped_json}, // is_root will be None or a boolean
${bool(block_is_unit) | n, dump_js_escaped_json}, // block_is_unit will be None or a boolean
$('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]')
);
});
</script>
% if not is_root:

View File

@@ -28,9 +28,9 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/textbooks">
TextbooksFactory(${textbooks | n, dump_js_escaped_json});
</%static:webpack>
<%static:invoke_page_bundle page_name="js/pages/textbooks" class_name="TextbooksFactory">
${textbooks | n, dump_js_escaped_json}
</%static:invoke_page_bundle>
</%block>
<%block name="content">

View File

@@ -52,6 +52,7 @@ urlpatterns = [
# noop to squelch ajax errors
url(r'^event$', contentstore.views.event, name='event'),
url(r'^xmodule/', include('pipeline_js.urls')),
url(r'^heartbeat', include('openedx.core.djangoapps.heartbeat.urls')),
url(r'^user_api/', include('openedx.core.djangoapps.user_api.legacy_urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),

View File

@@ -164,6 +164,31 @@ source, template_path = Loader(engine).load_template_source(path)
</script>
</%def>
<%def name="invoke_page_bundle(page_name, class_name=None)">
<%doc>
Loads Javascript onto your page synchronously.
Uses RequireJS in development and a plain script tag in production.
The body of the tag should be a comma-separated list of arguments
to be passed to the page factory specified by the class_name argument.
</%doc>
<%
body = capture(caller.body)
%>
% if class_name:
<script type="text/javascript">
if (typeof pageFactoryArguments == "undefined") {
var pageFactoryArguments = {};
}
% if body:
pageFactoryArguments['${class_name | n, js_escaped_string}'] = [${ body | n, decode.utf8 }];
% else:
pageFactoryArguments['${class_name | n, js_escaped_string}'] = [];
% endif
</script>
% endif
<%self:webpack entry="${page_name}"/>
</%def>
<%def name="require_module(module_name, class_name)">
<%doc>
Loads Javascript onto your page synchronously.

View File

@@ -36,6 +36,11 @@ REQUIREJS_WAIT = {
"jquery", "js/base", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
# Unit page
re.compile(r'^Unit \|'): [
"jquery", "js/base", "js/models/xblock_info", "js/views/pages/container",
"js/collections/component_template", "xmodule", "cms/js/main", "xblock/cms.runtime.v1"],
# Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up
re.compile(r'^Course Outline \|'): [
@@ -43,27 +48,16 @@ REQUIREJS_WAIT = {
# Dashboard
re.compile(r'^Studio Home \|'): [
"gettext", "js/base",
"js/sock", "gettext", "js/base",
"jquery.ui", "cms/js/main", "underscore"],
# Pages
re.compile(r'^Pages \|'): [
'js/models/explicit_url', 'js/views/tabs', 'cms/js/main', 'xblock/cms.runtime.v1'
'js/models/explicit_url', 'js/views/tabs',
'xmodule', 'cms/js/main', 'xblock/cms.runtime.v1'
],
}
TRUTHY_WAIT = {
# Pages
re.compile(r'^Pages \|'): [
'XBlock'
],
# Unit page
re.compile(r'Unit \|'): [
"jQuery", "XBlock", "ContainerFactory"
],
}
@world.absorb
def wait(seconds):
@@ -72,15 +66,12 @@ def wait(seconds):
@world.absorb
def wait_for_js_to_load():
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.title):
world.wait_for_requirejs(req)
requirements = req
break
for test, req in TRUTHY_WAIT.items():
if test.search(world.browser.title):
for var in req:
world.wait_for_js_variable_truthy(var)
world.wait_for_requirejs(requirements)
# Selenium's `execute_async_script` function pauses Selenium's execution
@@ -142,7 +133,7 @@ def wait_for_xmodule():
@world.absorb
def wait_for_mathjax():
"Wait until MathJax is loaded and set up on the page."
world.wait_for_js_variable_truthy("MathJax")
world.wait_for_js_variable_truthy("MathJax.isReady")
class RequireJSError(Exception):

View File

@@ -3,17 +3,3 @@
# Patch the xml libs before anything else.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
import pytest
@pytest.fixture(autouse=True)
def no_webpack_loader(monkeypatch):
monkeypatch.setattr(
"webpack_loader.templatetags.webpack_loader.render_bundle",
lambda entry, extension=None, config='DEFAULT', attrs='': ''
)
monkeypatch.setattr(
"webpack_loader.utils.get_as_tags",
lambda entry, extension=None, config='DEFAULT', attrs='': []
)

View File

@@ -1,6 +1,7 @@
/* JavaScript for Vertical Student View. */
/* global Set:false */ // false means do not assign to Set
/* global ViewedEventTracker:false */
// The vertical marks blocks complete if they are completable by viewing. The
// global variable SEEN_COMPLETABLES tracks blocks between separate loads of
@@ -8,61 +9,62 @@
// navigates back within a given sequential) to protect against duplicate calls
// to the server.
import BookmarkButton from 'course_bookmarks/js/views/bookmark_button';
import {ViewedEventTracker} from '../../../../../../../../lms/static/completion/js/ViewedEvent.js';
var SEEN_COMPLETABLES = new Set();
window.VerticalStudentView = function(runtime, element) {
'use strict';
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
return new BookmarkButton({
el: $bookmarkButtonElement,
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
RequireJS.require(['course_bookmarks/js/views/bookmark_button'], function(BookmarkButton) {
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
return new BookmarkButton({
el: $bookmarkButtonElement,
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
});
});
var tracker, vertical, viewedAfter;
var completableBlocks = [];
var vertModDivs = element.getElementsByClassName('vert-mod');
if (vertModDivs.length === 0) {
return;
}
vertical = vertModDivs[0];
$(element).find('.vert').each(function(idx, block) {
if (block.dataset.completableByViewing !== undefined) {
completableBlocks.push(block);
RequireJS.require(['bundles/ViewedEvent'], function() {
var tracker, vertical, viewedAfter;
var completableBlocks = [];
var vertModDivs = element.getElementsByClassName('vert-mod');
if (vertModDivs.length === 0) {
return;
}
});
if (completableBlocks.length > 0) {
viewedAfter = parseInt(vertical.dataset.completionDelayMs, 10);
if (!(viewedAfter >= 0)) {
// parseInt will return NaN if it fails to parse, which is not >= 0.
viewedAfter = 5000;
}
tracker = new ViewedEventTracker(completableBlocks, viewedAfter);
tracker.addHandler(function(block, event) {
var blockKey = block.dataset.id;
if (blockKey && !SEEN_COMPLETABLES.has(blockKey)) {
if (event.elementHasBeenViewed) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_completion'),
data: JSON.stringify({
block_key: blockKey,
completion: 1.0
})
}).then(
function() {
SEEN_COMPLETABLES.add(blockKey);
}
);
}
vertical = vertModDivs[0];
$(element).find('.vert').each(function(idx, block) {
if (block.dataset.completableByViewing !== undefined) {
completableBlocks.push(block);
}
});
}
if (completableBlocks.length > 0) {
viewedAfter = parseInt(vertical.dataset.completionDelayMs, 10);
if (!(viewedAfter >= 0)) {
// parseInt will return NaN if it fails to parse, which is not >= 0.
viewedAfter = 5000;
}
tracker = new ViewedEventTracker(completableBlocks, viewedAfter);
tracker.addHandler(function(block, event) {
var blockKey = block.dataset.id;
if (blockKey && !SEEN_COMPLETABLES.has(blockKey)) {
if (event.elementHasBeenViewed) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_completion'),
data: JSON.stringify({
block_key: blockKey,
completion: 1.0
})
}).then(
function() {
SEEN_COMPLETABLES.add(blockKey);
}
);
}
}
});
}
});
};

View File

@@ -1,10 +0,0 @@
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': 'webpack',
},
overrides: {
excludedFiles: 'public/js/*',
},
};

View File

@@ -1 +0,0 @@
public

View File

@@ -0,0 +1,5 @@
window.WordCloud = function(el) {
RequireJS.require(['WordCloudMain'], function(WordCloudMain) {
new WordCloudMain(el);
});
};

View File

@@ -0,0 +1,349 @@
/**
* @file The main module definition for Word Cloud XModule.
*
* Defines a constructor function which operates on a DOM element. Either show the user text inputs so
* he can enter words, or render his selected words along with the word cloud representing the top words.
*
* @module WordCloudMain
*
* @exports WordCloudMain
*
* @external d3, $, RequireJS
*/
(function(requirejs, require, define) {
'use strict';
define('WordCloudMain', [
'gettext',
'edx-ui-toolkit/js/utils/html-utils'
], function(gettext, HtmlUtils) {
function generateUniqueId(wordCloudId, counter) {
return '_wc_' + wordCloudId + '_' + counter;
}
/**
* @function WordCloudMain
*
* This function will process all the attributes from the DOM element passed, taking all of
* the configuration attributes. It will either then attach a callback handler for the click
* event on the button in the case when the user needs to enter words, or it will call the
* appropriate mehtod to generate and render a word cloud from user's enetered words along with
* all of the other words.
*
* @constructor
*
* @param {jQuery} el DOM element where the word cloud will be processed and created.
*/
var WordCloudMain = function(el) {
var _this = this;
this.wordCloudEl = $(el).find('.word_cloud');
// Get the URL to which we will post the users words.
this.ajax_url = this.wordCloudEl.data('ajax-url');
// Dimensions of the box where the word cloud will be drawn.
this.width = 635;
this.height = 635;
// Hide WordCloud container before Ajax request done
this.wordCloudEl.hide();
// Retriveing response from the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/get_state', null,
function(response) {
if (response.status !== 'success') {
return;
}
_this.configJson = response;
}
)
.done(function() {
// Show WordCloud container after Ajax request done
_this.wordCloudEl.show();
if (_this.configJson && _this.configJson.submitted) {
_this.showWordCloud(_this.configJson);
return;
}
});
$(el).find('.save').on('click', function() {
_this.submitAnswer();
});
}; // End-of: var WordCloudMain = function(el) {
/**
* @function submitAnswer
*
* Callback to be executed when the user eneter his words. It will send user entries to the
* server, and upon receiving correct response, will call the function to generate the
* word cloud.
*/
WordCloudMain.prototype.submitAnswer = function() {
var _this = this,
data = {student_words: []};
// Populate the data to be sent to the server with user's words.
this.wordCloudEl.find('input.input-cloud').each(function(index, value) {
data.student_words.push($(value).val());
});
// Send the data to the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/submit', $.param(data),
function(response) {
if (response.status !== 'success') {
return;
}
_this.showWordCloud(response);
}
);
}; // End-of: WordCloudMain.prototype.submitAnswer = function() {
/**
* @function showWordCloud
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* This function will set up everything for d3 and launch the draw method. Among other things,
* iw will determine maximum word size.
*/
WordCloudMain.prototype.showWordCloud = function(response) {
var words,
_this = this,
maxSize, minSize, scaleFactor, maxFontSize, minFontSize;
this.wordCloudEl.find('.input_cloud_section').hide();
words = response.top_words;
maxSize = 0;
minSize = 10000;
scaleFactor = 1;
maxFontSize = 200;
minFontSize = 16;
// Find the word with the maximum percentage. I.e. the most popular word.
$.each(words, function(index, word) {
if (word.size > maxSize) {
maxSize = word.size;
}
if (word.size < minSize) {
minSize = word.size;
}
});
// Find the longest word, and calculate the scale appropriately. This is
// required so that even long words fit into the drawing area.
//
// This is a fix for: if the word is very long and/or big, it is discarded by
// for unknown reason.
$.each(words, function(index, word) {
var tempScaleFactor = 1.0,
size = ((word.size / maxSize) * maxFontSize);
if (size * 0.7 * word.text.length > _this.width) {
tempScaleFactor = ((_this.width / word.text.length) / 0.7) / size;
}
if (scaleFactor > tempScaleFactor) {
scaleFactor = tempScaleFactor;
}
});
// Update the maximum font size based on the longest word.
maxFontSize *= scaleFactor;
// Generate the word cloud.
d3.layout.cloud().size([this.width, this.height])
.words(words)
.rotate(function() {
return Math.floor((Math.random() * 2)) * 90;
})
.font('Impact')
.fontSize(function(d) {
var size = (d.size / maxSize) * maxFontSize;
size = size >= minFontSize ? size : minFontSize;
return size;
})
.on('end', function(words, bounds) { // eslint-disable-line no-shadow
// Draw the word cloud.
_this.drawWordCloud(response, words, bounds);
})
.start();
}; // End-of: WordCloudMain.prototype.showWordCloud = function(response) {
/**
* @function drawWordCloud
*
* This function will be called when d3 has finished initing the state for our word cloud,
* and it is ready to hand off the process to the drawing routine. Basically set up everything
* necessary for the actual drwing of the words.
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* @param {array} words An array of objects. Each object must have two properties. One property
* is 'text' (the actual word), and the other property is 'size' which represents the number that the
* word was enetered by the students.
*
* @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding
* box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each
* coordinate object contains two properties: 'x', and 'y'.
*/
WordCloudMain.prototype.drawWordCloud = function(response, words, bounds) {
// Color words in different colors.
var fill = d3.scale.category20(),
// Will be populated by words the user enetered.
studentWordsKeys = [],
// Comma separated string of user enetered words.
studentWordsStr,
// By default we do not scale.
scale = 1,
// Caсhing of DOM element
cloudSectionEl = this.wordCloudEl.find('.result_cloud_section'),
// Needed for caсhing of d3 group elements
groupEl,
// Iterator for word cloud count for uniqueness
wcCount = 0;
// If bounding rectangle is given, scale based on the bounding box of all the words.
if (bounds) {
scale = 0.5 * Math.min(
this.width / Math.abs(bounds[1].x - this.width / 2),
this.width / Math.abs(bounds[0].x - this.width / 2),
this.height / Math.abs(bounds[1].y - this.height / 2),
this.height / Math.abs(bounds[0].y - this.height / 2)
);
}
$.each(response.student_words, function(word, stat) {
var percent = (response.display_student_percents) ? ' ' + (Math.round(100 * (stat / response.total_count))) + '%' : '';
studentWordsKeys.push(HtmlUtils.interpolateHtml(
'{listStart}{startTag}{word}{endTag}{percent}{listEnd}',
{
listStart: HtmlUtils.HTML('<li>'),
startTag: HtmlUtils.HTML('<strong>'),
word: word,
endTag: HtmlUtils.HTML('</strong>'),
percent: percent,
listEnd: HtmlUtils.HTML('</li>')
}
).toString());
});
studentWordsStr = '' + studentWordsKeys.join('');
cloudSectionEl
.addClass('active');
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words'),
HtmlUtils.HTML(studentWordsStr)
);
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words').end().find('.total_num_words'),
HtmlUtils.interpolateHtml(
gettext('{start_strong}{total}{end_strong} words submitted in total.'),
{
start_strong: HtmlUtils.HTML('<strong>'),
end_strong: HtmlUtils.HTML('</strong>'),
total: response.total_count
}
)
);
$(cloudSectionEl.attr('id') + ' .word_cloud').empty();
// Actual drawing of word cloud.
groupEl = d3.select('#' + cloudSectionEl.attr('id') + ' .word_cloud').append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', 'translate(' + (0.5 * this.width) + ',' + (0.5 * this.height) + ')')
.selectAll('text')
.data(words)
.enter()
.append('g')
.attr('data-id', function() {
wcCount = wcCount + 1;
return wcCount;
})
.attr('aria-describedby', function() {
return HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId} title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).data('id'))
}
);
});
groupEl
.append('title')
.attr('id', function() {
return HtmlUtils.interpolateHtml(
gettext('title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id'))
}
);
})
.text(function(d) {
var res = '';
$.each(response.top_words, function(index, value) {
if (value.text === d.text) {
res = value.percent + '%';
return;
}
});
return res;
});
groupEl
.append('text')
.attr('id', function() {
return HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id'))
}
);
})
.style('font-size', function(d) {
return d.size + 'px';
})
.style('font-family', 'Impact')
.style('fill', function(d, i) {
return fill(i);
})
.attr('text-anchor', 'middle')
.attr('transform', function(d) {
return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')scale(' + scale + ')';
})
.text(function(d) {
return d.text;
});
}; // End-of: WordCloudMain.prototype.drawWordCloud = function(words, bounds) {
return WordCloudMain;
}); // End-of: define('WordCloudMain', [], function() {
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function(requirejs, require, define) {

View File

@@ -1,3 +0,0 @@
const WordCloudMain = require('xmodule/assets/word_cloud/src/js/word_cloud_main.js');
window.WordCloud = WordCloudMain.default;

View File

@@ -1,315 +0,0 @@
/**
* @file The main module definition for Word Cloud XModule.
*
* Defines a constructor function which operates on a DOM element. Either
* show the user text inputs so he can enter words, or render his selected
* words along with the word cloud representing the top words.
*
* @module WordCloudMain
*
* @exports WordCloudMain
*
* @external $
*/
import * as HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
import d3 from 'd3.min';
import { cloud as d3Cloud } from 'd3.layout.cloud';
import gettext from 'gettext';
function generateUniqueId(wordCloudId, counter) {
return `_wc_${wordCloudId}_${counter}`;
}
/**
* @function WordCloudMain
*
* This function will process all the attributes from the DOM element passed, taking all of
* the configuration attributes. It will either then attach a callback handler for the click
* event on the button in the case when the user needs to enter words, or it will call the
* appropriate mehtod to generate and render a word cloud from user's enetered words along with
* all of the other words.
*
* @constructor
*
* @param {jQuery} el DOM element where the word cloud will be processed and created.
*/
export default class WordCloudMain {
constructor(el) {
this.wordCloudEl = $(el).find('.word_cloud');
// Get the URL to which we will post the users words.
this.ajax_url = this.wordCloudEl.data('ajax-url');
// Dimensions of the box where the word cloud will be drawn.
this.width = 635;
this.height = 635;
// Hide WordCloud container before Ajax request done
this.wordCloudEl.hide();
// Retriveing response from the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
`${this.ajax_url}/get_state`,
null,
(response) => {
if (response.status !== 'success') {
return;
}
this.configJson = response;
},
)
.done(() => {
// Show WordCloud container after Ajax request done
this.wordCloudEl.show();
if (this.configJson && this.configJson.submitted) {
this.showWordCloud(this.configJson);
}
});
$(el).find('.save').on('click', () => {
this.submitAnswer();
});
}
/**
* @function submitAnswer
*
* Callback to be executed when the user eneter his words. It will send user entries to the
* server, and upon receiving correct response, will call the function to generate the
* word cloud.
*/
submitAnswer() {
const data = { student_words: [] };
// Populate the data to be sent to the server with user's words.
this.wordCloudEl.find('input.input-cloud').each((index, value) => {
data.student_words.push($(value).val());
});
// Send the data to the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
`${this.ajax_url}/submit`, $.param(data),
(response) => {
if (response.status !== 'success') {
return;
}
this.showWordCloud(response);
},
);
}
/**
* @function showWordCloud
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* This function will set up everything for d3 and launch the draw method. Among other things,
* iw will determine maximum word size.
*/
showWordCloud(response) {
const words = response.top_words;
let maxSize = 0;
let minSize = 10000;
let scaleFactor = 1;
let maxFontSize = 200;
const minFontSize = 16;
this.wordCloudEl.find('.input_cloud_section').hide();
// Find the word with the maximum percentage. I.e. the most popular word.
$.each(words, (index, word) => {
if (word.size > maxSize) {
maxSize = word.size;
}
if (word.size < minSize) {
minSize = word.size;
}
});
// Find the longest word, and calculate the scale appropriately. This is
// required so that even long words fit into the drawing area.
//
// This is a fix for: if the word is very long and/or big, it is discarded by
// for unknown reason.
$.each(words, (index, word) => {
let tempScaleFactor = 1.0;
const size = ((word.size / maxSize) * maxFontSize);
if (size * 0.7 * word.text.length > this.width) {
tempScaleFactor = ((this.width / word.text.length) / 0.7) / size;
}
if (scaleFactor > tempScaleFactor) {
scaleFactor = tempScaleFactor;
}
});
// Update the maximum font size based on the longest word.
maxFontSize *= scaleFactor;
// Generate the word cloud.
d3Cloud().size([this.width, this.height])
.words(words)
.rotate(() => Math.floor((Math.random() * 2)) * 90)
.font('Impact')
.fontSize((d) => {
let size = (d.size / maxSize) * maxFontSize;
size = size >= minFontSize ? size : minFontSize;
return size;
})
// Draw the word cloud.
.on('end', (wds, bounds) => this.drawWordCloud(response, wds, bounds))
.start();
}
/**
* @function drawWordCloud
*
* This function will be called when d3 has finished initing the state for our word cloud,
* and it is ready to hand off the process to the drawing routine. Basically set up everything
* necessary for the actual drwing of the words.
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* @param {array} words An array of objects. Each object must have two properties. One property
* is 'text' (the actual word), and the other property is 'size' which represents the number that the
* word was enetered by the students.
*
* @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding
* box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each
* coordinate object contains two properties: 'x', and 'y'.
*/
drawWordCloud(response, words, bounds) {
// Color words in different colors.
const fill = d3.scale.category20();
// Will be populated by words the user enetered.
const studentWordsKeys = [];
// By default we do not scale.
let scale = 1;
// Caсhing of DOM element
const cloudSectionEl = this.wordCloudEl.find('.result_cloud_section');
// Iterator for word cloud count for uniqueness
let wcCount = 0;
// If bounding rectangle is given, scale based on the bounding box of all the words.
if (bounds) {
scale = 0.5 * Math.min(
this.width / Math.abs(bounds[1].x - (this.width / 2)),
this.width / Math.abs(bounds[0].x - (this.width / 2)),
this.height / Math.abs(bounds[1].y - (this.height / 2)),
this.height / Math.abs(bounds[0].y - (this.height / 2)),
);
}
$.each(response.student_words, (word, stat) => {
const percent = (response.display_student_percents) ? ` ${Math.round(100 * (stat / response.total_count))}%` : '';
studentWordsKeys.push(HtmlUtils.interpolateHtml(
'{listStart}{startTag}{word}{endTag}{percent}{listEnd}',
{
listStart: HtmlUtils.HTML('<li>'),
startTag: HtmlUtils.HTML('<strong>'),
word,
endTag: HtmlUtils.HTML('</strong>'),
percent,
listEnd: HtmlUtils.HTML('</li>'),
},
).toString());
});
// Comma separated string of user enetered words.
const studentWordsStr = studentWordsKeys.join('');
cloudSectionEl
.addClass('active');
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words'),
HtmlUtils.HTML(studentWordsStr),
);
HtmlUtils.setHtml(
cloudSectionEl.find('.your_words').end().find('.total_num_words'),
HtmlUtils.interpolateHtml(
gettext('{start_strong}{total}{end_strong} words submitted in total.'),
{
start_strong: HtmlUtils.HTML('<strong>'),
end_strong: HtmlUtils.HTML('</strong>'),
total: response.total_count,
},
),
);
$(`${cloudSectionEl.attr('id')} .word_cloud`).empty();
// Actual drawing of word cloud.
const groupEl = d3.select(`#${cloudSectionEl.attr('id')} .word_cloud`).append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', `translate(${0.5 * this.width},${0.5 * this.height})`)
.selectAll('text')
.data(words)
.enter()
.append('g')
.attr('data-id', () => {
wcCount += 1;
return wcCount;
})
.attr('aria-describedby', () => HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId} title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).data('id')),
},
));
groupEl
.append('title')
.attr('id', () => HtmlUtils.interpolateHtml(
gettext('title_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')),
},
))
.text((d) => {
let res = '';
$.each(response.top_words, (index, value) => {
if (value.text === d.text) {
res = `${value.percent}%`;
}
});
return res;
});
groupEl
.append('text')
.attr('id', () => HtmlUtils.interpolateHtml(
gettext('text_word_{uniqueId}'),
{
uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')),
},
))
.style('font-size', d => `${d.size}px`)
.style('font-family', 'Impact')
.style('fill', (d, i) => fill(i))
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${d.x}, ${d.y})rotate(${d.rotate$})scale(${scale})`)
.text(d => d.text);
}
}

View File

@@ -1,56 +0,0 @@
/* eslint-env node */
'use strict';
var path = require('path');
module.exports = {
entry: {
word_cloud: 'word_cloud',
},
output: {
path: path.resolve(__dirname, 'public/js'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: 'babel-loader',
},
{
test: /d3.min/,
use: [
'babel-loader',
{
loader: 'exports-loader',
options: {
d3: true,
},
},
],
},
],
},
resolve: {
modules: [
path.resolve(__dirname, 'src/js'),
path.resolve(__dirname, '../../../../../../node_modules'),
],
alias: {
'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths
},
extensions: ['.js', '.jsx', '.json'],
},
externals: {
gettext: 'gettext',
canvas: 'canvas',
jquery: 'jQuery',
$: 'jQuery',
underscore: '_',
},
};

View File

@@ -13,18 +13,17 @@
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<div class="video-player">
<section class="video-player">
<iframe id="id"></iframe>
</div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<div class="video-controls is-hidden">
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</div>
</section>
</article>
</div>
<div class="focus_grabber last"></div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -12,15 +12,12 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
<h4 class="hd hd-4 video-hls-error is-hidden">
Your browser does not support this video format. Try using a different browser.
</h4>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -12,12 +12,9 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -12,12 +12,9 @@
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden"></section>
</article>
</div>

View File

@@ -19,7 +19,6 @@
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -17,7 +17,6 @@
<iframe id="id1"></iframe>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -1,82 +0,0 @@
import '../../../../static/js/src/ajax_prefix.js';
import '../../../../static/common/js/vendor/underscore.js';
import '../../../../static/common/js/vendor/backbone.js';
import '../../../../static/js/vendor/CodeMirror/codemirror.js';
import '../../../../static/js/vendor/draggabilly.js';
import '../../../../static/common/js/vendor/jquery.js';
import '../../../../static/common/js/vendor/jquery-migrate.js';
import '../../../../static/js/vendor/jquery.cookie.js';
import '../../../../static/js/vendor/jquery.leanModal.js';
import '../../../../static/js/vendor/jquery.timeago.js';
import '../../../../static/js/vendor/jquery-ui.min.js';
import '../../../../static/js/vendor/jquery.ui.draggable.js';
import '../../../../static/js/vendor/json2.js';
// import '../../../../static/common/js/vendor/moment-with-locales.js';
import '../../../../static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js';
import '../../../../static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js';
import '../../../../static/js/src/accessibility_tools.js';
import '../../../../static/js/src/logger.js';
import '../../../../static/js/src/utility.js';
import '../../../../static/js/test/add_ajax_prefix.js';
import '../../../../static/js/test/i18n.js';
import '../../../../static/common/js/vendor/hls.js';
import '../assets/vertical/public/js/vertical_student_view.js';
import '../../../../static/js/vendor/jasmine-imagediff.js';
import '../../../../static/common/js/spec_helpers/jasmine-waituntil.js';
import '../../../../static/common/js/spec_helpers/jasmine-extensions.js';
import '../../../../static/common/js/vendor/sinon.js';
// These libraries are used by the tests (and the code under test)
// but not explicitly imported
import 'jquery.ui';
// These
import './src/video/10_main.js'
import './spec/helper.js'
import './spec/video_helper.js'
// These are the tests that will be run
import './spec/video/async_process_spec.js';
import './spec/video/completion_spec.js';
import './spec/video/events_spec.js';
import './spec/video/general_spec.js';
import './spec/video/html5_video_spec.js';
import './spec/video/initialize_spec.js';
import './spec/video/iterator_spec.js';
import './spec/video/resizer_spec.js';
import './spec/video/sjson_spec.js';
import './spec/video/video_autoadvance_spec.js';
import './spec/video/video_bumper_spec.js';
import './spec/video/video_caption_spec.js';
import './spec/video/video_context_menu_spec.js';
import './spec/video/video_control_spec.js';
import './spec/video/video_events_bumper_plugin_spec.js';
import './spec/video/video_events_plugin_spec.js';
import './spec/video/video_focus_grabber_spec.js';
import './spec/video/video_full_screen_spec.js';
import './spec/video/video_player_spec.js';
import './spec/video/video_play_pause_control_spec.js';
import './spec/video/video_play_placeholder_spec.js';
import './spec/video/video_play_skip_control_spec.js';
import './spec/video/video_poster_spec.js';
import './spec/video/video_progress_slider_spec.js';
import './spec/video/video_quality_control_spec.js';
import './spec/video/video_save_state_plugin_spec.js';
import './spec/video/video_skip_control_spec.js';
import './spec/video/video_speed_control_spec.js';
import './spec/video/video_storage_spec.js';
import './spec/video/video_volume_control_spec.js';
import './spec/time_spec.js';
// overwrite the loaded method and manually start the karma after a delay
// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
'use strict';
window.__karma__.loaded = function () {
setTimeout(function () {
window.__karma__.start();
}, 1000);
};

View File

@@ -41,6 +41,7 @@ var options = {
{pattern: 'common_static/js/test/i18n.js', included: true},
{pattern: 'common_static/common/js/vendor/hls.js', included: true},
{pattern: 'public/js/split_test_staff.js', included: true},
{pattern: 'public/js/vertical_student_view.js', included: true},
{pattern: 'src/word_cloud/d3.min.js', included: true},
// Load test utilities
@@ -68,18 +69,13 @@ var options = {
// Make sure the patterns in sourceFiles and specFiles do not match the same file.
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [
{ pattern: 'src/xmodule.js', included: true, ignoreCoverage: true }, // To prevent getting instrumented twice.
// Load these before the xmodules that use them
{ pattern: 'src/javascript_loader.js', included: true },
{ pattern: 'src/collapsible.js', included: true },
// Load everything else
{pattern: 'src/**/!(video)/!(poll|time).js', included: true}
{pattern: 'src/xmodule.js', included: true, ignoreCoverage: true}, // To prevent getting instrumented twice.
{pattern: 'src/**/*.js', included: true}
],
specFiles: [
{pattern: 'spec/helper.js', included: true, ignoreCoverage: true}, // Helper which depends on source files.
{ pattern: 'spec/**/!(video)/*.js', included: true },
{ pattern: 'spec/!(time_spec|video_helper).js', included: true }
{pattern: 'spec/**/*.js', included: true}
],
fixtureFiles: [
@@ -92,8 +88,6 @@ var options = {
]
};
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -1,45 +0,0 @@
/* eslint-env node */
// Karma config for xmodule suite.
// Docs and troubleshooting tips in common/static/common/js/karma.common.conf.js
'use strict';
var path = require('path');
var configModule = require(path.join(__dirname, 'common_static/common/js/karma.common.conf.js'));
var options = {
useRequireJs: false,
normalizePathsForCoverageFunc: function(appRoot, pattern) {
return pattern;
},
libraryFilesToInclude: [],
libraryFiles: [],
sourceFiles: [],
specFiles: [],
fixtureFiles: [
{pattern: 'fixtures/*.*'},
{pattern: 'fixtures/hls/**/*.*'}
],
runFiles: [
{pattern: 'karma_runner_webpack.js', webpack: true}
],
preprocessors: {}
};
options.runFiles
.filter(function(file) { return file.webpack; })
.forEach(function(file) {
options.preprocessors[file.pattern] = ['webpack'];
});
module.exports = function(config) {
configModule.configure(config, options);
};

View File

@@ -1,5 +1,3 @@
/* global _ */
(function() {
'use strict';
var origAjax = $.ajax;
@@ -176,13 +174,13 @@
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
) {
// Do nothing.
return {};
return;
} else if (settings.url === '/save_user_state') {
return {success: true};
} else if (settings.url.match(new RegExp(jasmine.getFixtures().fixturesPath + '.+', 'g'))) {
return origAjax(settings);
} else {
return $.ajax.and.callThrough();
$.ajax.and.callThrough();
}
});
};
@@ -196,8 +194,21 @@
// Stub jQuery.scrollTo module.
$.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo');
// Stub window.Video.loadYouTubeIFrameAPI()
window.Video.loadYouTubeIFrameAPI = jasmine.createSpy('window.Video.loadYouTubeIFrameAPI').and.returnValue(
function(scriptTag) {
var event = document.createEvent('Event');
if (fixture === 'video.html') {
event.initEvent('load', false, false);
} else {
event.initEvent('error', false, false);
}
scriptTag.dispatchEvent(event);
}
);
jasmine.initializePlayer = function(fixture, params) {
var state, metadata;
var state;
if (_.isString(fixture)) {
// `fixture` is a name of a fixture file.
@@ -217,7 +228,7 @@
// If `params` is an object, assign its properties as data attributes
// to the main video DIV element.
if (_.isObject(params)) {
metadata = _.extend($('#video_id').data('metadata'), params);
var metadata = _.extend($('#video_id').data('metadata'), params);
$('#video_id').data('metadata', metadata);
}

View File

@@ -1,57 +1,56 @@
(function(undefined) {
'use strict';
'use strict';
describe('Time', function() {
describe('format', function() {
describe('with NAN', function() {
it('return a correct time format', function() {
expect(Time.format('string')).toEqual('0:00');
expect(Time.format(void(0))).toEqual('0:00');
});
});
import * as Time from 'time.js';
describe('with duration more than or equal to 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(3600)).toEqual('1:00:00');
expect(Time.format(7272)).toEqual('2:01:12');
});
});
describe('Time', function() {
describe('format', function() {
describe('with NAN', function() {
it('return a correct time format', function() {
expect(Time.format('string')).toEqual('0:00');
expect(Time.format(void(0))).toEqual('0:00');
describe('with duration less than 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(1)).toEqual('0:01');
expect(Time.format(61)).toEqual('1:01');
expect(Time.format(3599)).toEqual('59:59');
});
});
});
describe('with duration more than or equal to 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(3600)).toEqual('1:00:00');
expect(Time.format(7272)).toEqual('2:01:12');
describe('formatFull', function() {
it('gives correct string for times', function() {
var testTimes = [
[0, '00:00:00'], [60, '00:01:00'],
[488, '00:08:08'], [2452, '00:40:52'],
[3600, '01:00:00'], [28800, '08:00:00'],
[144532, '40:08:52'], [190360, '52:52:40'],
[294008, '81:40:08'], [-5, '00:00:00']
];
$.each(testTimes, function(index, times) {
var timeInt = times[0],
timeStr = times[1];
expect(Time.formatFull(timeInt)).toBe(timeStr);
});
});
});
describe('with duration less than 1 hour', function() {
it('return a correct time format', function() {
expect(Time.format(1)).toEqual('0:01');
expect(Time.format(61)).toEqual('1:01');
expect(Time.format(3599)).toEqual('59:59');
describe('convert', function() {
it('return a correct time based on speed modifier', function() {
expect(Time.convert(0, 1, 1.5)).toEqual('0.000');
expect(Time.convert(100, 1, 1.5)).toEqual('66.667');
expect(Time.convert(100, 1.5, 1)).toEqual('150.000');
});
});
});
describe('formatFull', function() {
it('gives correct string for times', function() {
var testTimes = [
[0, '00:00:00'], [60, '00:01:00'],
[488, '00:08:08'], [2452, '00:40:52'],
[3600, '01:00:00'], [28800, '08:00:00'],
[144532, '40:08:52'], [190360, '52:52:40'],
[294008, '81:40:08'], [-5, '00:00:00']
];
$.each(testTimes, function(index, times) {
var timeInt = times[0],
timeStr = times[1];
expect(Time.formatFull(timeInt)).toBe(timeStr);
});
});
});
describe('convert', function() {
it('return a correct time based on speed modifier', function() {
expect(Time.convert(0, 1, 1.5)).toEqual('0.000');
expect(Time.convert(100, 1, 1.5)).toEqual('66.667');
expect(Time.convert(100, 1.5, 1)).toEqual('150.000');
});
});
});
}).call(this);

View File

@@ -78,4 +78,4 @@ function(AsyncProcess) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,5 +1,7 @@
(function(undefined) {
describe('Video', function() {
var oldOTBD, state;
afterEach(function() {
$('source').remove();
window.VideoState = {};
@@ -9,8 +11,6 @@
describe('constructor', function() {
describe('YT', function() {
var state;
beforeEach(function() {
loadFixtures('video.html');
$.cookie.and.returnValue('0.50');
@@ -18,24 +18,24 @@
describe('by default', function() {
beforeEach(function() {
state = jasmine.initializePlayerYouTube('video_html5.html');
this.state = jasmine.initializePlayerYouTube('video_html5.html');
});
afterEach(function() {
state.storage.clear();
state.videoPlayer.destroy();
this.state.storage.clear();
this.state.videoPlayer.destroy();
});
it('check videoType', function() {
expect(state.videoType).toEqual('youtube');
expect(this.state.videoType).toEqual('youtube');
});
it('set the elements', function() {
expect(state.el).toEqual($('#video_id'));
expect(this.state.el).toEqual($('#video_id'));
});
it('parse the videos', function() {
expect(state.videos).toEqual({
expect(this.state.videos).toEqual({
'0.50': '7tqY6eQzVhE',
'1.0': 'cogebirgzzM',
'1.50': 'abcdefghijkl'
@@ -43,11 +43,11 @@
});
it('parse available video speeds', function() {
expect(state.speeds).toEqual(['0.50', '1.0', '1.50']);
expect(this.state.speeds).toEqual(['0.50', '1.0', '1.50']);
});
it('set current video speed via cookie', function() {
expect(state.speed).toEqual('1.50');
expect(this.state.speed).toEqual('1.50');
});
});
});

View File

@@ -11,8 +11,6 @@
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').and.returnValue(null);
state = jasmine.initializePlayer('video_html5.html');
});
afterEach(function() {

View File

@@ -1,4 +1,4 @@
(function(require) {
(function(requirejs, require, define, undefined) {
'use strict';
require(
@@ -85,7 +85,8 @@ function(Initialize) {
};
});
it('returns duration for the 1.0 speed if speed is not 1.0', function() {
var msg = 'returns duration for the 1.0 speed if speed is not 1.0';
it(msg, function() {
var expected;
state.speed = '1.50';
@@ -104,7 +105,8 @@ function(Initialize) {
expect(expected).toEqual(100);
});
it('returns duration for the 1.0 speed as a fall-back', function() {
var msg = 'returns duration for the 1.0 speed as a fall-back';
it(msg, function() {
var expected;
state.isFlashMode.and.returnValue(true);
@@ -275,21 +277,21 @@ function(Initialize) {
describe('isFlashMode', function() {
it('returns `true` if player in `flash` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
},
isFlashMode = Initialize.prototype.isFlashMode,
actual = isFlashMode.call(testState);
actual = isFlashMode.call(state);
expect(actual).toBeTruthy();
});
it('returns `false` if player is not in `flash` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
},
isFlashMode = Initialize.prototype.isFlashMode,
actual = isFlashMode.call(testState);
actual = isFlashMode.call(state);
expect(actual).toBeFalsy();
});
@@ -297,25 +299,25 @@ function(Initialize) {
describe('isHtml5Mode', function() {
it('returns `true` if player in `html5` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
},
isHtml5Mode = Initialize.prototype.isHtml5Mode,
actual = isHtml5Mode.call(testState);
actual = isHtml5Mode.call(state);
expect(actual).toBeTruthy();
});
it('returns `false` if player is not in `html5` mode', function() {
var testState = {
var state = {
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
},
isHtml5Mode = Initialize.prototype.isHtml5Mode,
actual = isHtml5Mode.call(testState);
actual = isHtml5Mode.call(state);
expect(actual).toBeFalsy();
});
});
});
});
}(require));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -1,6 +1,4 @@
(function(require) {
'use strict';
require(
['video/00_iterator.js'],
function(Iterator) {
@@ -102,4 +100,4 @@ function(Iterator) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,8 +1,7 @@
(function(require) {
'use strict';
(function(requirejs, require, define, undefined) {
require(
['video/00_resizer.js', 'underscore'],
function(Resizer, _) {
['video/00_resizer.js'],
function(Resizer) {
describe('Resizer', function() {
var html = [
'<div ' +
@@ -59,8 +58,7 @@ function(Resizer, _) {
var resizer = new Resizer(config).align(),
expectedHeight = $container.height(),
realHeight = $element.height(),
expectedWidth = 50,
realWidth;
expectedWidth = 50;
// containerRatio >= elementRatio
expect(realHeight).toBe(expectedHeight);
@@ -77,8 +75,7 @@ function(Resizer, _) {
var resizer = new Resizer(config).setMode('height'),
expectedHeight = $container.height(),
realHeight = $element.height(),
expectedWidth = 50,
realWidth;
expectedWidth = 50;
// containerRatio >= elementRatio
expect(realHeight).toBe(expectedHeight);
@@ -92,15 +89,13 @@ function(Resizer, _) {
});
it('`setElement` works correctly', function() {
var $newElement,
expectedHeight;
$container.append('<div ' +
'id="Another-el" ' +
'style="width:100px; height: 150px;"' +
'>');
$newElement = $('#Another-el');
expectedHeight = $container.height();
var $newElement = $('#Another-el'),
expectedHeight = $container.height();
new Resizer(config).setElement($newElement).alignByHeightOnly();
expect($element.height()).not.toBe(expectedHeight);
@@ -266,4 +261,4 @@ function(Resizer, _) {
});
});
});
}(require));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -64,4 +64,4 @@ function(Sjson) {
});
});
});
}(require));
}(RequireJS.require));

View File

@@ -1,12 +1,8 @@
/* global _, WAIT_TIMEOUT */
(function() {
'use strict';
(function(undefined) {
describe('VideoCaption', function() {
var state, oldOTBD;
var parseIntAttribute = function(element, attrName) {
return parseInt(element.attr(attrName), 10);
return parseInt(element.attr(attrName));
};
beforeEach(function() {
@@ -59,9 +55,8 @@
});
it('add ARIA attributes to transcript control', function() {
var $captionControl;
state = jasmine.initializePlayer();
$captionControl = $('.toggle-transcript');
var $captionControl = $('.toggle-transcript');
expect($captionControl).toHaveAttrs({
'aria-disabled': 'false'
});
@@ -154,10 +149,9 @@
});
it('can destroy itself', function() {
var plugin;
spyOn($, 'ajaxWithPrefix');
state = jasmine.initializePlayer();
plugin = state.videoCaption;
var plugin = state.videoCaption;
spyOn($.fn, 'off').and.callThrough();
state.videoCaption.destroy();
@@ -230,17 +224,16 @@
};
it('if languages more than 1', function() {
var transcripts, langCodes, langLabels;
state = jasmine.initializePlayer();
transcripts = state.config.transcriptLanguages;
langCodes = _.keys(transcripts);
langLabels = _.values(transcripts);
var transcripts = state.config.transcriptLanguages,
langCodes = _.keys(transcripts),
langLabels = _.values(transcripts);
expect($('.langs-list')).toExist();
expect($('.langs-list')).toHandle('click');
$('.langs-list li').each(function() {
$('.langs-list li').each(function(index) {
var code = $(this).data('lang-code'),
link = $(this).find('.control'),
label = link.text();
@@ -251,10 +244,9 @@
});
it('when clicking on link with new language', function() {
var Caption, $link;
state = jasmine.initializePlayer();
Caption = state.videoCaption;
$link = $('.langs-list li[data-lang-code="de"] .control-lang');
var Caption = state.videoCaption,
$link = $('.langs-list li[data-lang-code="de"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
@@ -277,11 +269,9 @@
});
it('when clicking on link with current language', function() {
var Caption, $link;
state = jasmine.initializePlayer();
Caption = state.videoCaption;
$link = $('.langs-list li[data-lang-code="en"] .control-lang');
var Caption = state.videoCaption,
$link = $('.langs-list li[data-lang-code="en"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
@@ -310,11 +300,7 @@
$('.language-menu').focus();
$('.language-menu').trigger(keyPressEvent(KEY.UP));
expect($('.lang')).toHaveClass('is-opened');
expect($('.langs-list')
.find('li')
.last()
.find('.control-lang'))
.toBeFocused();
expect($('.langs-list').find('li').last().find('.control-lang')).toBeFocused();
});
it('closes the language menu on ESC', function() {
@@ -470,10 +456,9 @@
});
});
var originalClearTimeout;
describe('mouse movement', function() {
var originalClearTimeout;
beforeEach(function(done) {
jasmine.clock().install();
state = jasmine.initializePlayer();
@@ -602,7 +587,7 @@
'loaded yet';
it(msg, function() {
Caption.loaded = false;
state.hideCaptions = false;
state.hide_captions = false;
Caption.fetchCaption();
expect($.ajaxWithPrefix).toHaveBeenCalled();
@@ -610,7 +595,7 @@
Caption.loaded = false;
Caption.hideCaptions.calls.reset();
state.hideCaptions = true;
state.hide_captions = true;
Caption.fetchCaption();
expect($.ajaxWithPrefix).toHaveBeenCalled();
@@ -972,6 +957,7 @@
jasmine.waitUntil(function() {
return state.videoCaption.rendered;
}).then(function() {
videoControl = state.videoControl;
$('.subtitles li span[data-index=1]').addClass('current');
state.videoCaption.onResize();
}).always(done);

View File

@@ -1,5 +1,3 @@
import '../helper.js'
(function(undefined) {
'use strict';
var describeInfo, state, oldOTBD;

View File

@@ -1,11 +1,9 @@
/* global YT */
(function(require, define, undefined) {
(function(requirejs, require, define, undefined) {
'use strict';
require(
['video/03_video_player.js', 'hls', 'underscore'],
function(VideoPlayer, HLS, _) {
['video/03_video_player.js', 'hls'],
function(VideoPlayer, HLS) {
describe('VideoPlayer', function() {
var STATUS = window.STATUS,
state,
@@ -1067,4 +1065,4 @@ function(VideoPlayer, HLS, _) {
});
});
});
}(require, define));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -24,7 +24,7 @@
});
it('build the slider', function() {
expect($('.slider').toArray()).toContain(state.videoProgressSlider.slider);
expect($('.slider')).toContain(state.videoProgressSlider.slider);
expect($.fn.slider).toHaveBeenCalledWith({
range: 'min',
min: 0,
@@ -35,7 +35,7 @@
});
it('build the seek handle', function() {
expect($('.ui-slider-handle').toArray())
expect($('.ui-slider-handle'))
.toContain(state.videoProgressSlider.handle);
});

View File

@@ -1,8 +1,5 @@
import * as Time from 'time.js';
(function(undefined) {
'use strict';
describe('VideoPlayer Save State plugin', function() {
var state, oldOTBD;
@@ -45,6 +42,7 @@ import * as Time from 'time.js';
beforeEach(function() {
state.videoPlayer.currentTime = videoPlayerCurrentTime;
spyOn(window.Time, 'formatFull').and.callThrough();
});
it('data is not an object, async is true', function() {
@@ -149,7 +147,9 @@ import * as Time from 'time.js';
positionVal,
true
);
expect(ajaxData.saved_video_position).toBe(Time.formatFull(positionVal));
expect(Time.formatFull).toHaveBeenCalledWith(
positionVal
);
}
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,

View File

@@ -1,4 +1,4 @@
(function(require, define, undefined) {
(function(requirejs, require, define, undefined) {
require(
['video/00_video_storage.js'],
function(VideoStorage) {
@@ -80,4 +80,4 @@ function(VideoStorage) {
});
});
});
}(require, define));
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

Some files were not shown because too many files have changed in this diff Show More