Merge pull request #18385 from edx/release-mergeback-to-master
Merge release back to master
This commit is contained in:
@@ -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='': []
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([]);
|
||||
}
|
||||
);
|
||||
45
cms/djangoapps/pipeline_js/templates/xmodule.js
Normal file
45
cms/djangoapps/pipeline_js/templates/xmodule.js
Normal 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});
|
||||
});
|
||||
10
cms/djangoapps/pipeline_js/urls.py
Normal file
10
cms/djangoapps/pipeline_js/urls.py
Normal 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'),
|
||||
]
|
||||
@@ -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]
|
||||
44
cms/djangoapps/pipeline_js/views.py
Normal file
44
cms/djangoapps/pipeline_js/views.py
Normal 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",
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import * as ContextCourse from 'js/models/course';
|
||||
|
||||
export {ContextCourse}
|
||||
@@ -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}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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?');
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
6
cms/static/js/pages/course.js
Normal file
6
cms/static/js/pages/course.js
Normal file
@@ -0,0 +1,6 @@
|
||||
define(
|
||||
['js/models/course'],
|
||||
function(ContextCourse) {
|
||||
window.course = new ContextCourse(window.pageFactoryArguments.ContextCourse);
|
||||
}
|
||||
);
|
||||
8
cms/static/js/pages/login.js
Normal file
8
cms/static/js/pages/login.js
Normal 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);
|
||||
}
|
||||
);
|
||||
|
||||
7
cms/static/js/pages/textbooks.js
Normal file
7
cms/static/js/pages/textbooks.js
Normal 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);
|
||||
}
|
||||
);
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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='': []
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
extends: 'eslint-config-edx',
|
||||
root: true,
|
||||
settings: {
|
||||
'import/resolver': 'webpack',
|
||||
},
|
||||
overrides: {
|
||||
excludedFiles: 'public/js/*',
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -0,0 +1,5 @@
|
||||
window.WordCloud = function(el) {
|
||||
RequireJS.require(['WordCloudMain'], function(WordCloudMain) {
|
||||
new WordCloudMain(el);
|
||||
});
|
||||
};
|
||||
@@ -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) {
|
||||
@@ -1,3 +0,0 @@
|
||||
const WordCloudMain = require('xmodule/assets/word_cloud/src/js/word_cloud_main.js');
|
||||
|
||||
window.WordCloud = WordCloudMain.default;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: '_',
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -78,4 +78,4 @@ function(AsyncProcess) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').and.returnValue(null);
|
||||
|
||||
state = jasmine.initializePlayer('video_html5.html');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
(function(require) {
|
||||
'use strict';
|
||||
|
||||
require(
|
||||
['video/00_iterator.js'],
|
||||
function(Iterator) {
|
||||
@@ -102,4 +100,4 @@ function(Iterator) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -64,4 +64,4 @@ function(Sjson) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
}(RequireJS.require));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import '../helper.js'
|
||||
|
||||
(function(undefined) {
|
||||
'use strict';
|
||||
var describeInfo, state, oldOTBD;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user