Commit 3ac3a315 by Andy Armstrong

Merge pull request #4913 from edx/andya/handle-component-errors

Fix Studio to gracefully handle xblock JavaScript errors
parents 30fc0441 d451c3e9
...@@ -22,12 +22,12 @@ ...@@ -22,12 +22,12 @@
"nonbsp" : true, // Warns about "non-breaking whitespace" characters. "nonbsp" : true, // Warns about "non-breaking whitespace" characters.
"nonew" : true, // Prohibits the use of constructor functions for side-effects. "nonew" : true, // Prohibits the use of constructor functions for side-effects.
"plusplus" : false, // Prohibits the use of unary increment and decrement operators. "plusplus" : false, // Prohibits the use of unary increment and decrement operators.
"quotmark" : "single", // Enforces the consistency of quotation marks used throughout your code. It accepts three values: true, "single", and "double". "quotmark" : false, // Enforces the consistency of quotation marks used throughout your code. It accepts three values: true, "single", and "double".
"undef" : true, // Prohibits the use of explicitly undeclared variables. "undef" : true, // Prohibits the use of explicitly undeclared variables.
"unused" : true, // Warns when you define and never use your variables. "unused" : true, // Warns when you define and never use your variables.
"strict" : true, // Requires all functions to run in ECMAScript 5's strict mode. "strict" : true, // Requires all functions to run in ECMAScript 5's strict mode.
"trailing" : true, // Makes it an error to leave a trailing whitespace in your code. "trailing" : true, // Makes it an error to leave a trailing whitespace in your code.
"maxlen" : 80, // Lets you set the maximum length of a line. "maxlen" : 120, // Lets you set the maximum length of a line.
//"maxparams" : 4, // Lets you set the max number of formal parameters allowed per function. //"maxparams" : 4, // Lets you set the max number of formal parameters allowed per function.
//"maxdepth" : 4, // Lets you control how nested do you want your blocks to be. //"maxdepth" : 4, // Lets you control how nested do you want your blocks to be.
//"maxstatements" : 4, // Lets you set the max number of statements allowed per function. //"maxstatements" : 4, // Lets you set the max number of statements allowed per function.
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
"shadow" : false, // Suppresses warnings about variable shadowing i.e. declaring a variable that had been already declared somewhere in the outer scope. "shadow" : false, // Suppresses warnings about variable shadowing i.e. declaring a variable that had been already declared somewhere in the outer scope.
"sub" : false, // Suppresses warnings about using [] notation when it can be expressed in dot notation. "sub" : false, // Suppresses warnings about using [] notation when it can be expressed in dot notation.
"supernew" : false, // Suppresses warnings about "weird" constructions like new function () { ... } and new Object;. "supernew" : false, // Suppresses warnings about "weird" constructions like new function () { ... } and new Object;.
"validthis" : true, // Suppresses warnings about possible strict violations when the code is running in strict mode and you use this in a non-constructor function. "validthis" : true, // Suppresses warnings about possible strict violations when the code is running in strict mode and you use this in a non-constructor function.
"noyield" : false, // Suppresses warnings about generator functions with no yield statement in them. "noyield" : false, // Suppresses warnings about generator functions with no yield statement in them.
...@@ -73,11 +73,11 @@ ...@@ -73,11 +73,11 @@
// The rest should remain `false`. Please see explanation for the "predef" parameter below. // The rest should remain `false`. Please see explanation for the "predef" parameter below.
"couch" : false, // Defines globals exposed by CouchDB. "couch" : false, // Defines globals exposed by CouchDB.
"dojo" : false, // Defines globals exposed by the Dojo Toolkit "dojo" : false, // Defines globals exposed by the Dojo Toolkit
"jquery" : false, // Defines globals exposed by the jQuery JavaScript library. "jquery" : false, // Defines globals exposed by the jQuery JavaScript library.
"mootools" : false, // Defines globals exposed by the MooTools JavaScript framework. "mootools" : false, // Defines globals exposed by the MooTools JavaScript framework.
"node" : false, // Defines globals available when your code is running inside of the Node runtime environment. "node" : false, // Defines globals available when your code is running inside of the Node runtime environment.
"nonstandard" : false, // Defines non-standard but widely adopted globals such as escape and unescape. "nonstandard" : false, // Defines non-standard but widely adopted globals such as escape and unescape.
"phantom" : false, // Defines globals available when your core is running inside of the PhantomJS runtime environment. "phantom" : false, // Defines globals available when your core is running inside of the PhantomJS runtime environment.
"prototypejs" : false, // Defines globals exposed by the Prototype JavaScript framework. "prototypejs" : false, // Defines globals exposed by the Prototype JavaScript framework.
"rhino" : false, // Defines globals available when your code is running inside of the Rhino runtime environment. "rhino" : false, // Defines globals available when your code is running inside of the Rhino runtime environment.
"worker" : false, // Defines globals available when your code is running inside of a Web Worker. "worker" : false, // Defines globals available when your code is running inside of a Web Worker.
......
...@@ -71,8 +71,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers ...@@ -71,8 +71,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
refreshed = true; refreshed = true;
}; };
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
modal.runtime.notify('save', { state: 'start' }); modal.editorView.notifyRuntime('save', { state: 'start' });
modal.runtime.notify('save', { state: 'end' }); modal.editorView.notifyRuntime('save', { state: 'end' });
expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
expect(refreshed).toBeTruthy(); expect(refreshed).toBeTruthy();
}); });
...@@ -84,7 +84,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers ...@@ -84,7 +84,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
refreshed = true; refreshed = true;
}; };
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
modal.runtime.notify('cancel'); modal.editorView.notifyRuntime('cancel');
expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
expect(refreshed).toBeFalsy(); expect(refreshed).toBeFalsy();
}); });
......
...@@ -7,6 +7,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -7,6 +7,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
model, containerPage, requests, initialDisplayName, model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'), mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'), mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
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'), mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
...@@ -15,6 +17,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -15,6 +17,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
edit_helpers.installEditTemplates(); edit_helpers.installEditTemplates();
edit_helpers.installTemplate('xblock-string-field-editor'); edit_helpers.installTemplate('xblock-string-field-editor');
edit_helpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage); appendSetFixtures(mockContainerPage);
edit_helpers.installMockXBlock({ edit_helpers.installMockXBlock({
...@@ -83,6 +86,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -83,6 +86,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden'); 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() { it('inline edits the display name when performing a new action', function() {
renderContainerPage(this, mockContainerXBlockHtml, { renderContainerPage(this, mockContainerXBlockHtml, {
action: 'new' action: 'new'
...@@ -138,6 +153,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -138,6 +153,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
resources: [] resources: []
}); });
// Respond to the subsequent xblock info fetch request.
create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName});
// Expect the title to have been updated // Expect the title to have been updated
expect(displayNameElement.text().trim()).toBe(updatedDisplayName); expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
}); });
...@@ -177,6 +195,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -177,6 +195,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
}); });
expect(edit_helpers.isShowingModal()).toBeTruthy(); expect(edit_helpers.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();
create_sinon.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(edit_helpers.isShowingModal()).toBeTruthy();
});
}); });
describe("Editing an xmodule", function() { describe("Editing an xmodule", function() {
...@@ -268,10 +298,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -268,10 +298,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
clickDelete(componentIndex); clickDelete(componentIndex);
create_sinon.respondWithJson(requests, {}); create_sinon.respondWithJson(requests, {});
// first request contains given component's id (to delete the component) // second to last request contains given component's id (to delete the component)
expect(requests[requests.length - 2].url).toMatch( create_sinon.expectJsonRequest(requests, 'DELETE',
new RegExp("locator-component-" + GROUP_TO_TEST + (componentIndex + 1)) '/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
); null, requests.length - 2);
// final request to refresh the xblock info // final request to refresh the xblock info
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
...@@ -302,6 +332,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -302,6 +332,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
}); });
it("can delete an xblock with broken JavaScript", function() {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.delete-button').first().click();
edit_helpers.confirmPrompt(promptSpy);
create_sinon.respondWithJson(requests, {});
// expect the second to last request to be a delete of the xblock
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript',
null, requests.length - 2);
// expect the last request to be a fetch of the xblock info for the parent container
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
});
it('does not delete when clicking No in prompt', function () { it('does not delete when clicking No in prompt', function () {
var numRequests; var numRequests;
...@@ -387,6 +429,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -387,6 +429,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
}); });
it("can duplicate an xblock with broken JavaScript", function() {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.duplicate-button').first().click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'duplicate_source_locator': 'locator-broken-javascript',
'parent_locator': 'locator-container'
});
});
it('shows a notification when duplicating', function () { it('shows a notification when duplicating', function () {
var notificationSpy = edit_helpers.createNotificationSpy(); var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
......
...@@ -65,15 +65,10 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie ...@@ -65,15 +65,10 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
onDisplayXBlock: function() { onDisplayXBlock: function() {
var editorView = this.editorView, var editorView = this.editorView,
title = this.getTitle(), title = this.getTitle();
xblock = editorView.xblock,
runtime = xblock.runtime;
// Notify the runtime that the modal has been shown // Notify the runtime that the modal has been shown
if (runtime) { editorView.notifyRuntime('modal-shown', this);
this.runtime = runtime;
runtime.notify('modal-shown', this);
}
// Update the modal's header // Update the modal's header
if (editorView.hasCustomTabs()) { if (editorView.hasCustomTabs()) {
...@@ -93,7 +88,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie ...@@ -93,7 +88,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
// If the xblock is not using custom buttons then choose which buttons to show // If the xblock is not using custom buttons then choose which buttons to show
if (!editorView.hasCustomButtons()) { if (!editorView.hasCustomButtons()) {
// If the xblock does not support save then disable the save button // If the xblock does not support save then disable the save button
if (!xblock.save) { if (!editorView.xblock.save) {
this.disableSave(); this.disableSave();
} }
this.getActionBar().show(); this.getActionBar().show();
...@@ -175,9 +170,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie ...@@ -175,9 +170,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
BaseModal.prototype.hide.call(this); BaseModal.prototype.hide.call(this);
// Notify the runtime that the modal has been hidden // Notify the runtime that the modal has been hidden
if (this.runtime) { this.editorView.notifyRuntime('modal-hidden');
this.runtime.notify('modal-hidden');
}
}, },
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) { findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
......
...@@ -9,6 +9,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -9,6 +9,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent, function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView, EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView,
XBlockUtils) { XBlockUtils) {
'use strict';
var XBlockContainerPage = BasePage.extend({ var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
...@@ -88,14 +89,22 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -88,14 +89,22 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Render the xblock // Render the xblock
xblockView.render({ xblockView.render({
success: function() { done: function() {
xblockView.xblock.runtime.notify("page-shown", self); // Show the xblock and hide the loading indicator
xblockView.$el.removeClass(hiddenCss); xblockView.$el.removeClass(hiddenCss);
loadingElement.addClass(hiddenCss);
// Notify the runtime that the page has been successfully shown
xblockView.notifyRuntime('page-shown', self);
// Render the add buttons
self.renderAddXBlockComponents(); self.renderAddXBlockComponents();
// Refresh the views now that the xblock is visible
self.onXBlockRefresh(xblockView); self.onXBlockRefresh(xblockView);
self.refreshDisplayName();
loadingElement.addClass(hiddenCss);
unitLocationTree.removeClass(hiddenCss); unitLocationTree.removeClass(hiddenCss);
// Re-enable Backbone events for any updated DOM elements
self.delegateEvents(); self.delegateEvents();
} }
}); });
...@@ -109,11 +118,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -109,11 +118,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
return this.xblockView.model.urlRoot; return this.xblockView.model.urlRoot;
}, },
refreshDisplayName: function() {
var displayName = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
this.model.set('display_name', displayName);
},
onXBlockRefresh: function(xblockView) { onXBlockRefresh: function(xblockView) {
this.addButtonActions(xblockView.$el); this.addButtonActions(xblockView.$el);
this.xblockView.refresh(); this.xblockView.refresh();
...@@ -159,6 +163,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -159,6 +163,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}); });
}, },
createPlaceholderElement: function() {
return $("<div/>", { class: "studio-xblock-wrapper" });
},
createComponent: function(template, target) { createComponent: function(template, target) {
// A placeholder element is created in the correct location for the new xblock // A placeholder element is created in the correct location for the new xblock
// and then onNewXBlock will replace it with a rendering of the xblock. Note that // and then onNewXBlock will replace it with a rendering of the xblock. Note that
...@@ -168,7 +176,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -168,7 +176,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
buttonPanel = target.closest('.add-xblock-component'), buttonPanel = target.closest('.add-xblock-component'),
listPanel = buttonPanel.prev(), listPanel = buttonPanel.prev(),
scrollOffset = ViewUtils.getScrollOffset(buttonPanel), scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').appendTo(listPanel), placeholderElement = this.createPlaceholderElement().appendTo(listPanel),
requestData = _.extend(template, { requestData = _.extend(template, {
parent_locator: parentLocator parent_locator: parentLocator
}); });
...@@ -189,7 +197,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -189,7 +197,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
ViewUtils.runOperationShowingMessage(gettext('Duplicating&hellip;'), ViewUtils.runOperationShowingMessage(gettext('Duplicating&hellip;'),
function() { function() {
var scrollOffset = ViewUtils.getScrollOffset(xblockElement), var scrollOffset = ViewUtils.getScrollOffset(xblockElement),
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').insertAfter(xblockElement), placeholderElement = self.createPlaceholderElement().insertAfter(xblockElement),
parentElement = self.findXBlockElement(parent), parentElement = self.findXBlockElement(parent),
requestData = { requestData = {
duplicate_source_locator: xblockElement.data('locator'), duplicate_source_locator: xblockElement.data('locator'),
...@@ -217,10 +225,13 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -217,10 +225,13 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
onDelete: function(xblockElement) { onDelete: function(xblockElement) {
// get the parent so we can remove this component from its parent. // get the parent so we can remove this component from its parent.
var xblockView = this.xblockView, var xblockView = this.xblockView,
xblock = xblockView.xblock,
parent = this.findXBlockElement(xblockElement.parent()); parent = this.findXBlockElement(xblockElement.parent());
xblockElement.remove(); xblockElement.remove();
xblock.runtime.notify('deleted-child', parent.data('locator'));
// Inform the runtime that the child has been deleted in case
// other views are listening to deletion events.
xblockView.notifyRuntime('deleted-child', parent.data('locator'));
// Update publish and last modified information from the server. // Update publish and last modified information from the server.
this.model.fetch(); this.model.fetch();
}, },
......
...@@ -29,34 +29,58 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -29,34 +29,58 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
var self = this, var self = this,
wrapper = this.$el, wrapper = this.$el,
xblockElement, xblockElement,
success = options ? options.success : null, successCallback = options ? options.success || options.done : null,
errorCallback = options ? options.error || options.done : null,
xblock, xblock,
fragmentsRendered; fragmentsRendered;
fragmentsRendered = this.renderXBlockFragment(fragment, wrapper); fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
fragmentsRendered.done(function() { fragmentsRendered.always(function() {
xblockElement = self.$('.xblock').first(); xblockElement = self.$('.xblock').first();
xblock = XBlock.initializeBlock(xblockElement); try {
self.xblock = xblock; xblock = XBlock.initializeBlock(xblockElement);
self.xblockReady(xblock); self.xblock = xblock;
if (success) { self.xblockReady(xblock);
success(xblock); if (successCallback) {
successCallback(xblock);
}
} catch (e) {
console.error(e.stack);
// Add 'xblock-initialization-failed' class to every xblock
self.$('.xblock').addClass('xblock-initialization-failed');
// If the xblock was rendered but failed then still call xblockReady to allow
// drag-and-drop to be initialized.
if (xblockElement) {
self.xblockReady(null);
}
if (errorCallback) {
errorCallback();
}
} }
}); });
}, },
/** /**
* This method is called upon successful rendering of an xblock. * Sends a notification event to the runtime, if one is available. Note that the runtime
* is only available once the xblock has been rendered and successfully initialized.
* @param eventName The name of the event to be fired.
* @param data The data to be passed to any listener's of the event.
*/ */
xblockReady: function(xblock) { notifyRuntime: function(eventName, data) {
// Do nothing var runtime = this.xblock && this.xblock.runtime;
if (runtime) {
runtime.notify(eventName, data);
}
}, },
/** /**
* Returns true if the specified xblock has children. * This method is called upon successful rendering of an xblock. Note that the xblock
* may have thrown JavaScript errors after rendering in which case the xblock parameter
* will be null.
*/ */
hasChildXBlocks: function() { xblockReady: function(xblock) {
return this.$('.wrapper-xblock').length > 0; // Do nothing
}, },
/** /**
...@@ -77,9 +101,16 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -77,9 +101,16 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
} }
// Render the HTML first as the scripts might depend upon it, and then // Render the HTML first as the scripts might depend upon it, and then
// asynchronously add the resources to the page. // asynchronously add the resources to the page. Any errors that are thrown
this.updateHtml(element, html); // by included scripts are logged to the console but are then ignored assuming
return this.addXBlockFragmentResources(resources); // that at least the rendered HTML will be in place.
try {
this.updateHtml(element, html);
return this.addXBlockFragmentResources(resources);
} catch(e) {
console.error(e.stack);
return $.Deferred().resolve();
}
}, },
/** /**
...@@ -106,7 +137,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -106,7 +137,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
numResources = resources.length; numResources = resources.length;
deferred = $.Deferred(); deferred = $.Deferred();
applyResource = function(index) { applyResource = function(index) {
var hash, resource, head, value, promise; var hash, resource, value, promise;
if (index >= numResources) { if (index >= numResources) {
deferred.resolve(); deferred.resolve();
return; return;
......
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span class="xblock-display-name">Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-locator="locator-container"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-broken-javascript">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"
data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim"
data-request-token="5efb4488272611e48053080027880ca6" data-runtime-version="1"
data-usage-id="i4x:;_;_edX;_mock"
data-type="HTMLModule" data-block-type="html">
<script type="text/javascript">
noSuchVariable.noSuchFunction();
</script>
</div>
</article>
</section>
</li>
</ol>
</div>
</article>
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span class="xblock-display-name">Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-locator="locator-container"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-broken-javascript">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"
data-runtime-class="InvalidRuntime" data-init="XBlockToXModuleShim"
data-request-token="5efb4488272611e48053080027880ca6" data-runtime-version="1"
data-usage-id="i4x:;_;_edX;_mock"
data-type="HTMLModule" data-block-type="html">
</div>
</article>
</section>
</li>
</ol>
</div>
</article>
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1"> data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A"> <section class="wrapper-xblock level-nesting">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-details"> <div class="header-details">
...@@ -38,8 +38,7 @@ ...@@ -38,8 +38,7 @@
<div class="xblock" data-request-token="page-render-token"> <div class="xblock" data-request-token="page-render-token">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-A1">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-actions"> <div class="header-actions">
...@@ -64,9 +63,7 @@ ...@@ -64,9 +63,7 @@
</section> </section>
</li> </li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-A2">
<header class="xblock-header"> <header class="xblock-header">
<div class="header-actions"> <div class="header-actions">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
...@@ -91,8 +88,7 @@ ...@@ -91,8 +88,7 @@
</section> </section>
</li> </li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-A3">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-actions"> <div class="header-actions">
...@@ -123,7 +119,7 @@ ...@@ -123,7 +119,7 @@
</section> </section>
</li> </li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-B"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-B">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-B"> <section class="wrapper-xblock level-nesting">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-details"> <div class="header-details">
...@@ -147,9 +143,7 @@ ...@@ -147,9 +143,7 @@
<div class="xblock" data-request-token="page-render-token"> <div class="xblock" data-request-token="page-render-token">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-B1">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-actions"> <div class="header-actions">
...@@ -174,9 +168,7 @@ ...@@ -174,9 +168,7 @@
</section> </section>
</li> </li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-B2">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-actions"> <div class="header-actions">
...@@ -201,9 +193,7 @@ ...@@ -201,9 +193,7 @@
</section> </section>
</li> </li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element">
data-locator="locator-component-B3">
<header class="xblock-header"> <header class="xblock-header">
<div class="xblock-header-primary"> <div class="xblock-header-primary">
<div class="header-actions"> <div class="header-actions">
......
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" tabindex="0"> <div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime"
data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
data-type="VerticalDescriptor" tabindex="0">
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/"> <div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
<section class="editor-with-tabs"> <section class="editor-with-tabs">
<div class="edit-header"> <div class="edit-header">
<span class="component-name"></span> <span class="component-name"></span>
<ul class="editor-tabs"> <ul class="editor-tabs">
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0" class="tab current">Basic</a></li> <li class="inner_tab_wrap">
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a></li> <a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0" class="tab current">Basic</a>
</li>
<li class="inner_tab_wrap">
<a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a>
</li>
</ul> </ul>
</div> </div>
<div class="tabs-wrapper"> <div class="tabs-wrapper">
......
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" tabindex="0"> <div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime"
data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
data-type="MockDescriptor" tabindex="0">
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/"> <div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
</div> </div>
<section class="sequence-edit"> <section class="sequence-edit">
...@@ -24,7 +26,8 @@ ...@@ -24,7 +26,8 @@
</script> </script>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/> <div class="wrapper-comp-settings metadata_edit" id="settings-tab"
data-metadata='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/>
<textarea data-metadata-name="custom_field">Custom Value</textarea> <textarea data-metadata-name="custom_field">Custom Value</textarea>
</section> </section>
</div> </div>
...@@ -37,6 +37,10 @@ class ContainerPage(PageObject): ...@@ -37,6 +37,10 @@ class ContainerPage(PageObject):
return None return None
def is_browser_on_page(self): def is_browser_on_page(self):
def _xblock_count(class_name, request_token):
return len(self.q(css='{body_selector} .xblock.{class_name}[data-request-token="{request_token}"]'.format(
body_selector=XBlockWrapper.BODY_SELECTOR, class_name=class_name, request_token=request_token
)).results)
def _is_finished_loading(): def _is_finished_loading():
is_done = False is_done = False
...@@ -46,11 +50,15 @@ class ContainerPage(PageObject): ...@@ -46,11 +50,15 @@ class ContainerPage(PageObject):
request_token = data_request_elements.first.attrs('data-request-token')[0] request_token = data_request_elements.first.attrs('data-request-token')[0]
# Then find the number of Studio xblock wrappers on the page with that request token. # Then find the number of Studio xblock wrappers on the page with that request token.
num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results) num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results)
# Wait until all components have been loaded. # Wait until all components have been loaded and marked as either initialized or failed.
# See common/static/coffee/src/xblock/core.coffee which adds the # See:
# class "xblock-initialized" at the end of initializeBlock # - common/static/coffee/src/xblock/core.coffee which adds the class "xblock-initialized"
num_xblocks_init = len(self.q(css='{} .xblock.xblock-initialized[data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results) # at the end of initializeBlock.
is_done = num_wrappers == num_xblocks_init # - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed"
# if the xblock threw an error while initializing.
num_initialized_xblocks = _xblock_count('xblock-initialized', request_token)
num_failed_xblocks = _xblock_count('xblock-initialization-failed', request_token)
is_done = num_wrappers == (num_initialized_xblocks + num_failed_xblocks)
return (is_done, is_done) return (is_done, is_done)
# First make sure that an element with the view-container class is present on the page, # First make sure that an element with the view-container class is present on the page,
......
...@@ -95,8 +95,7 @@ class JSErrorBadContentTest(BadComponentTest): ...@@ -95,8 +95,7 @@ class JSErrorBadContentTest(BadComponentTest):
""" """
Tests that components that throw JS errors do not break the Unit page. Tests that components that throw JS errors do not break the Unit page.
""" """
# TODO: ENABLE TEST WITH ANDY'S PR __test__ = True
__test__ = False
def get_bad_html_content(self): def get_bad_html_content(self):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment