Merge pull request #4913 from edx/andya/handle-component-errors
Fix Studio to gracefully handle xblock JavaScript errors
This commit is contained in:
10
.jshintrc
10
.jshintrc
@@ -22,12 +22,12 @@
|
||||
"nonbsp" : true, // Warns about "non-breaking whitespace" characters.
|
||||
"nonew" : true, // Prohibits the use of constructor functions for side-effects.
|
||||
"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.
|
||||
"unused" : true, // Warns when you define and never use your variables.
|
||||
"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.
|
||||
"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.
|
||||
//"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.
|
||||
@@ -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.
|
||||
"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;.
|
||||
"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.
|
||||
|
||||
|
||||
@@ -73,11 +73,11 @@
|
||||
// The rest should remain `false`. Please see explanation for the "predef" parameter below.
|
||||
"couch" : false, // Defines globals exposed by CouchDB.
|
||||
"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.
|
||||
"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.
|
||||
"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.
|
||||
"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.
|
||||
|
||||
@@ -71,8 +71,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
refreshed = true;
|
||||
};
|
||||
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
|
||||
modal.runtime.notify('save', { state: 'start' });
|
||||
modal.runtime.notify('save', { state: 'end' });
|
||||
modal.editorView.notifyRuntime('save', { state: 'start' });
|
||||
modal.editorView.notifyRuntime('save', { state: 'end' });
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeTruthy();
|
||||
});
|
||||
@@ -84,7 +84,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
refreshed = true;
|
||||
};
|
||||
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
|
||||
modal.runtime.notify('cancel');
|
||||
modal.editorView.notifyRuntime('cancel');
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
model, containerPage, requests, initialDisplayName,
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.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'),
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
@@ -15,6 +17,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
|
||||
edit_helpers.installEditTemplates();
|
||||
edit_helpers.installTemplate('xblock-string-field-editor');
|
||||
edit_helpers.installTemplate('container-message');
|
||||
appendSetFixtures(mockContainerPage);
|
||||
|
||||
edit_helpers.installMockXBlock({
|
||||
@@ -83,6 +86,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
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'
|
||||
@@ -138,6 +153,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
resources: []
|
||||
});
|
||||
|
||||
// Respond to the subsequent xblock info fetch request.
|
||||
create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName});
|
||||
|
||||
// Expect the title to have been updated
|
||||
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
|
||||
});
|
||||
@@ -177,6 +195,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
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() {
|
||||
@@ -268,10 +298,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
clickDelete(componentIndex);
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
|
||||
// first request contains given component's id (to delete the component)
|
||||
expect(requests[requests.length - 2].url).toMatch(
|
||||
new RegExp("locator-component-" + GROUP_TO_TEST + (componentIndex + 1))
|
||||
);
|
||||
// second to last request contains given component's id (to delete the component)
|
||||
create_sinon.expectJsonRequest(requests, 'DELETE',
|
||||
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
|
||||
null, requests.length - 2);
|
||||
|
||||
// final request to refresh the xblock info
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
|
||||
@@ -302,6 +332,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
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 () {
|
||||
var numRequests;
|
||||
|
||||
@@ -387,6 +429,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
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 () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
|
||||
@@ -65,15 +65,10 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
|
||||
onDisplayXBlock: function() {
|
||||
var editorView = this.editorView,
|
||||
title = this.getTitle(),
|
||||
xblock = editorView.xblock,
|
||||
runtime = xblock.runtime;
|
||||
title = this.getTitle();
|
||||
|
||||
// Notify the runtime that the modal has been shown
|
||||
if (runtime) {
|
||||
this.runtime = runtime;
|
||||
runtime.notify('modal-shown', this);
|
||||
}
|
||||
editorView.notifyRuntime('modal-shown', this);
|
||||
|
||||
// Update the modal's header
|
||||
if (editorView.hasCustomTabs()) {
|
||||
@@ -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 (!editorView.hasCustomButtons()) {
|
||||
// If the xblock does not support save then disable the save button
|
||||
if (!xblock.save) {
|
||||
if (!editorView.xblock.save) {
|
||||
this.disableSave();
|
||||
}
|
||||
this.getActionBar().show();
|
||||
@@ -175,9 +170,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
BaseModal.prototype.hide.call(this);
|
||||
|
||||
// Notify the runtime that the modal has been hidden
|
||||
if (this.runtime) {
|
||||
this.runtime.notify('modal-hidden');
|
||||
}
|
||||
this.editorView.notifyRuntime('modal-hidden');
|
||||
},
|
||||
|
||||
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
|
||||
|
||||
@@ -9,6 +9,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
|
||||
EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView,
|
||||
XBlockUtils) {
|
||||
'use strict';
|
||||
var XBlockContainerPage = BasePage.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
@@ -88,14 +89,22 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
|
||||
// Render the xblock
|
||||
xblockView.render({
|
||||
success: function() {
|
||||
xblockView.xblock.runtime.notify("page-shown", self);
|
||||
done: function() {
|
||||
// Show the xblock and hide the loading indicator
|
||||
xblockView.$el.removeClass(hiddenCss);
|
||||
self.renderAddXBlockComponents();
|
||||
self.onXBlockRefresh(xblockView);
|
||||
self.refreshDisplayName();
|
||||
loadingElement.addClass(hiddenCss);
|
||||
|
||||
// Notify the runtime that the page has been successfully shown
|
||||
xblockView.notifyRuntime('page-shown', self);
|
||||
|
||||
// Render the add buttons
|
||||
self.renderAddXBlockComponents();
|
||||
|
||||
// Refresh the views now that the xblock is visible
|
||||
self.onXBlockRefresh(xblockView);
|
||||
unitLocationTree.removeClass(hiddenCss);
|
||||
|
||||
// Re-enable Backbone events for any updated DOM elements
|
||||
self.delegateEvents();
|
||||
}
|
||||
});
|
||||
@@ -109,11 +118,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
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) {
|
||||
this.addButtonActions(xblockView.$el);
|
||||
this.xblockView.refresh();
|
||||
@@ -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) {
|
||||
// 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
|
||||
@@ -168,7 +176,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
buttonPanel = target.closest('.add-xblock-component'),
|
||||
listPanel = buttonPanel.prev(),
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
|
||||
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').appendTo(listPanel),
|
||||
placeholderElement = this.createPlaceholderElement().appendTo(listPanel),
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
});
|
||||
@@ -189,7 +197,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
ViewUtils.runOperationShowingMessage(gettext('Duplicating…'),
|
||||
function() {
|
||||
var scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
||||
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').insertAfter(xblockElement),
|
||||
placeholderElement = self.createPlaceholderElement().insertAfter(xblockElement),
|
||||
parentElement = self.findXBlockElement(parent),
|
||||
requestData = {
|
||||
duplicate_source_locator: xblockElement.data('locator'),
|
||||
@@ -217,10 +225,13 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
onDelete: function(xblockElement) {
|
||||
// get the parent so we can remove this component from its parent.
|
||||
var xblockView = this.xblockView,
|
||||
xblock = xblockView.xblock,
|
||||
parent = this.findXBlockElement(xblockElement.parent());
|
||||
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.
|
||||
this.model.fetch();
|
||||
},
|
||||
|
||||
@@ -29,34 +29,58 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
var self = this,
|
||||
wrapper = this.$el,
|
||||
xblockElement,
|
||||
success = options ? options.success : null,
|
||||
successCallback = options ? options.success || options.done : null,
|
||||
errorCallback = options ? options.error || options.done : null,
|
||||
xblock,
|
||||
fragmentsRendered;
|
||||
|
||||
fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
|
||||
fragmentsRendered.done(function() {
|
||||
fragmentsRendered.always(function() {
|
||||
xblockElement = self.$('.xblock').first();
|
||||
xblock = XBlock.initializeBlock(xblockElement);
|
||||
self.xblock = xblock;
|
||||
self.xblockReady(xblock);
|
||||
if (success) {
|
||||
success(xblock);
|
||||
try {
|
||||
xblock = XBlock.initializeBlock(xblockElement);
|
||||
self.xblock = xblock;
|
||||
self.xblockReady(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) {
|
||||
// Do nothing
|
||||
notifyRuntime: function(eventName, data) {
|
||||
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() {
|
||||
return this.$('.wrapper-xblock').length > 0;
|
||||
xblockReady: function(xblock) {
|
||||
// Do nothing
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
// asynchronously add the resources to the page.
|
||||
this.updateHtml(element, html);
|
||||
return this.addXBlockFragmentResources(resources);
|
||||
// asynchronously add the resources to the page. Any errors that are thrown
|
||||
// by included scripts are logged to the console but are then ignored assuming
|
||||
// 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"],
|
||||
numResources = resources.length;
|
||||
deferred = $.Deferred();
|
||||
applyResource = function(index) {
|
||||
var hash, resource, head, value, promise;
|
||||
var hash, resource, value, promise;
|
||||
if (index >= numResources) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<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>
|
||||
@@ -0,0 +1,51 @@
|
||||
<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 @@
|
||||
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-group-A">
|
||||
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A">
|
||||
<section class="wrapper-xblock level-nesting">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
@@ -38,8 +38,7 @@
|
||||
<div class="xblock" data-request-token="page-render-token">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A1">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -64,9 +63,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A2">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<div class="xblock-header-primary">
|
||||
@@ -91,8 +88,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A3">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -123,7 +119,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<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">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
@@ -147,9 +143,7 @@
|
||||
<div class="xblock" data-request-token="page-render-token">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B1">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -174,9 +168,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B2">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -201,9 +193,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B3">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<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/">
|
||||
<section class="editor-with-tabs">
|
||||
<div class="edit-header">
|
||||
<span class="component-name"></span>
|
||||
<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"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a></li>
|
||||
<li class="inner_tab_wrap">
|
||||
<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>
|
||||
</div>
|
||||
<div class="tabs-wrapper">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<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>
|
||||
<section class="sequence-edit">
|
||||
@@ -24,7 +26,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab"
|
||||
data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
<textarea data-metadata-name="custom_field">Custom Value</textarea>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,10 @@ class ContainerPage(PageObject):
|
||||
return None
|
||||
|
||||
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():
|
||||
is_done = False
|
||||
@@ -46,11 +50,15 @@ class ContainerPage(PageObject):
|
||||
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.
|
||||
num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results)
|
||||
# Wait until all components have been loaded.
|
||||
# See common/static/coffee/src/xblock/core.coffee which adds the
|
||||
# class "xblock-initialized" at the end of initializeBlock
|
||||
num_xblocks_init = len(self.q(css='{} .xblock.xblock-initialized[data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results)
|
||||
is_done = num_wrappers == num_xblocks_init
|
||||
# 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 class "xblock-initialized"
|
||||
# at the end of initializeBlock.
|
||||
# - 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)
|
||||
|
||||
# First make sure that an element with the view-container class is present on the page,
|
||||
|
||||
@@ -95,8 +95,7 @@ class JSErrorBadContentTest(BadComponentTest):
|
||||
"""
|
||||
Tests that components that throw JS errors do not break the Unit page.
|
||||
"""
|
||||
# TODO: ENABLE TEST WITH ANDY'S PR
|
||||
__test__ = False
|
||||
__test__ = True
|
||||
|
||||
def get_bad_html_content(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user