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 @@
"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);
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);
self.refreshDisplayName();
loadingElement.addClass(hiddenCss);
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&hellip;'),
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;
......
<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 @@
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">
......
<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">
......
<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='{&#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>
</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):
"""
......
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