Files
edx-platform/cms/static/js/spec/views/pages/container_spec.js
Andy Armstrong b2a0b2f728 Improve response handling in AjaxHelpers
I've changed the logic so that AjaxHelpers keeps
track of which requests have not yet had mock
responses sent. This ensures that every response
is handled before moving on to the next one,
rather than always handling the last request.
My intention is that this won't allow bugs to creep
in where a request isn't fired and instead the test
responds to an old request. It also should ensure
that extra events aren't accidentally fired.
2015-09-23 20:23:09 -04:00

686 lines
36 KiB
JavaScript

define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/ajax_helpers",
"common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers",
"js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, XBlockInfo) {
'use strict';
function parameterized_suite(label, globalPageOptions) {
describe(label + " ContainerPage", function () {
var getContainerPage, renderContainerPage, handleContainerPageRefresh, expectComponents,
respondWithHtml, model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures(globalPageOptions.initial),
mockXBlockHtml = readFixtures(globalPageOptions.addResponse),
mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'),
mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'),
mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'),
PageClass = globalPageOptions.page,
pagedSpecificTests = globalPageOptions.pagedSpecificTests,
hasVisibilityEditor = globalPageOptions.hasVisibilityEditor;
beforeEach(function () {
var newDisplayName = 'New Display Name';
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
EditHelpers.installMockXBlock({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
initialDisplayName = 'Test Container';
model = new XBlockInfo({
id: 'locator-container',
display_name: initialDisplayName,
category: 'vertical'
});
});
afterEach(function () {
EditHelpers.uninstallMockXBlock();
});
respondWithHtml = function (html) {
AjaxHelpers.respondWithJson(
requests,
{ html: html, "resources": [] }
);
};
getContainerPage = function (options) {
var default_options = {
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content')
};
return new PageClass(_.extend(options || {}, globalPageOptions, default_options));
};
renderContainerPage = function (test, html, options) {
requests = AjaxHelpers.requests(test);
containerPage = getContainerPage(options);
containerPage.render();
respondWithHtml(html);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, options);
};
handleContainerPageRefresh = function(requests) {
var request = AjaxHelpers.currentRequest(requests);
expect(str.startsWith(request.url,
'/xblock/locator-container/container_preview')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
};
expectComponents = function (container, locators) {
// verify expected components (in expected order) by their locators
var components = $(container).find('.studio-xblock-wrapper');
expect(components.length).toBe(locators.length);
_.each(locators, function (locator, locator_index) {
expect($(components[locator_index]).data('locator')).toBe(locator);
});
};
describe("Initial display", function () {
it('can render itself', function () {
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
});
it('shows a loading indicator', function () {
requests = AjaxHelpers.requests(this);
containerPage = getContainerPage();
containerPage.render();
expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
respondWithHtml(mockContainerXBlockHtml);
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('can show an xblock with broken JavaScript', function () {
renderContainerPage(this, mockBadContainerXBlockHtml);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('can show an xblock with an invalid XBlock', function () {
renderContainerPage(this, mockBadXBlockContainerXBlockHtml);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('inline edits the display name when performing a new action', function () {
renderContainerPage(this, mockContainerXBlockHtml, {
action: 'new'
});
expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.xblock-field-input')).not.toHaveClass('is-hidden');
});
});
describe("Editing the container", function () {
var updatedDisplayName = 'Updated Test Container',
getDisplayNameWrapper;
afterEach(function () {
EditHelpers.cancelModalIfShowing();
});
getDisplayNameWrapper = function () {
return containerPage.$('.wrapper-xblock-field');
};
it('can edit itself', function () {
var editButtons, displayNameElement, request;
renderContainerPage(this, mockContainerXBlockHtml);
displayNameElement = containerPage.$('.page-header-title');
// Click the root edit button
editButtons = containerPage.$('.nav-actions .edit-button');
editButtons.first().click();
// Expect a request to be made to show the studio view for the container
request = AjaxHelpers.currentRequest(requests);
expect(str.startsWith(request.url, '/xblock/locator-container/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockContainerXBlockHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
// Expect the correct title to be shown
expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container');
// Press the save button and respond with a success message to the save
EditHelpers.pressModalButton('.action-save');
AjaxHelpers.respondWithJson(requests, { });
expect(EditHelpers.isShowingModal()).toBeFalsy();
// Expect the last request be to refresh the container page
handleContainerPageRefresh(requests);
// Respond to the subsequent xblock info fetch request.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
// Expect the title to have been updated
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
});
it('can inline edit the display name', function () {
var displayNameInput, displayNameWrapper;
renderContainerPage(this, mockContainerXBlockHtml);
displayNameWrapper = getDisplayNameWrapper();
displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName);
displayNameInput.change();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, { });
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
});
});
describe("Editing an xblock", function () {
afterEach(function () {
EditHelpers.cancelModalIfShowing();
});
it('can show an edit modal for a child xblock', function () {
var editButtons, request;
renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons[0].click();
// Make sure that the correct xblock is requested to be edited
request = AjaxHelpers.currentRequest(requests);
expect(str.startsWith(request.url, '/xblock/locator-component-A1/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
});
it('can show an edit modal for a child xblock with broken JavaScript', function () {
var editButtons;
renderContainerPage(this, mockBadContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
editButtons[0].click();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
});
it('can show a visibility modal for a child xblock if supported for the page', function() {
var visibilityButtons, request;
renderContainerPage(this, mockContainerXBlockHtml);
visibilityButtons = containerPage.$('.wrapper-xblock .visibility-button');
if (hasVisibilityEditor) {
expect(visibilityButtons.length).toBe(6);
visibilityButtons[0].click();
request = AjaxHelpers.currentRequest(requests);
expect(str.startsWith(request.url, '/xblock/locator-component-A1/visibility_view'))
.toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockVisibilityEditorHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
}
else {
expect(visibilityButtons.length).toBe(0);
}
});
});
describe("Editing an xmodule", function () {
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
beforeEach(function () {
EditHelpers.installMockXModule({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
});
afterEach(function () {
EditHelpers.uninstallMockXModule();
EditHelpers.cancelModalIfShowing();
});
it('can save changes to settings', function () {
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons[0].click();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditor,
resources: []
});
modal = $('.edit-xblock-modal');
expect(modal.length).toBe(1);
// Click on the settings tab
modal.find('.settings-button').click();
// Change the display name's text
modal.find('.setting-input').text("Mock Update");
// Press the save button
modal.find('.action-save').click();
// Respond to the save
AjaxHelpers.respondWithJson(requests, {
id: model.id
});
// Respond to the request to refresh
respondWithHtml(mockUpdatedXBlockHtml);
// Verify that the xblock was updated
expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update');
});
});
describe("xblock operations", function () {
var getGroupElement,
NUM_COMPONENTS_PER_GROUP = 3, GROUP_TO_TEST = "A",
allComponentsInGroup = _.map(
_.range(NUM_COMPONENTS_PER_GROUP),
function (index) {
return 'locator-component-' + GROUP_TO_TEST + (index + 1);
}
);
getGroupElement = function () {
return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']");
};
describe("Deleting an xblock", function () {
var clickDelete, deleteComponent, deleteComponentWithSuccess,
promptSpy;
beforeEach(function () {
promptSpy = EditHelpers.createPromptSpy();
});
clickDelete = function (componentIndex, clickNo) {
// find all delete buttons for the given group
var deleteButtons = getGroupElement().find(".delete-button");
expect(deleteButtons.length).toBe(NUM_COMPONENTS_PER_GROUP);
// click the requested delete button
deleteButtons[componentIndex].click();
// click the 'yes' or 'no' button in the prompt
EditHelpers.confirmPrompt(promptSpy, clickNo);
};
deleteComponent = function (componentIndex) {
clickDelete(componentIndex);
// first request to delete the component
AjaxHelpers.expectJsonRequest(requests, 'DELETE',
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
null);
AjaxHelpers.respondWithNoContent(requests);
// then handle the request to refresh the preview
if (globalPageOptions.requiresPageRefresh) {
handleContainerPageRefresh(requests);
}
// final request to refresh the xblock info
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
AjaxHelpers.respondWithJson(requests, {});
};
deleteComponentWithSuccess = function (componentIndex) {
deleteComponent(componentIndex);
// verify the new list of components within the group (unless reloading)
if (!globalPageOptions.requiresPageRefresh) {
expectComponents(
getGroupElement(),
_.without(allComponentsInGroup, allComponentsInGroup[componentIndex])
);
}
};
it("can delete the first xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(0);
});
it("can delete a middle xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(1);
});
it("can delete the last xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can delete an xblock with broken JavaScript", function () {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.delete-button').first().click();
EditHelpers.confirmPrompt(promptSpy);
// expect the second to last request to be a delete of the xblock
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript');
AjaxHelpers.respondWithNoContent(requests);
// handle the refresh request for pages that require a full refresh on delete
if (globalPageOptions.requiresPageRefresh) {
handleContainerPageRefresh(requests);
}
// expect the last request to be a fetch of the xblock info for the parent container
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
});
it('does not delete when clicking No in prompt', function () {
renderContainerPage(this, mockContainerXBlockHtml);
// click delete on the first component but press no
clickDelete(0, true);
// all components should still exist
expectComponents(getGroupElement(), allComponentsInGroup);
// no requests should have been sent to the server
AjaxHelpers.expectNoRequests(requests);
});
it('shows a notification during the delete operation', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickDelete(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
AjaxHelpers.respondWithJson(requests, {});
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not delete an xblock upon failure', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickDelete(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
AjaxHelpers.respondWithError(requests);
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
expectComponents(getGroupElement(), allComponentsInGroup);
});
});
describe("Duplicating an xblock", function () {
var clickDuplicate, duplicateComponentWithSuccess,
refreshXBlockSpies;
clickDuplicate = function (componentIndex) {
// find all duplicate buttons for the given group
var duplicateButtons = getGroupElement().find(".duplicate-button");
expect(duplicateButtons.length).toBe(NUM_COMPONENTS_PER_GROUP);
// click the requested duplicate button
duplicateButtons[componentIndex].click();
};
duplicateComponentWithSuccess = function (componentIndex) {
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
clickDuplicate(componentIndex);
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
'duplicate_source_locator': 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
'parent_locator': 'locator-group-' + GROUP_TO_TEST
});
// send the response
AjaxHelpers.respondWithJson(requests, {
'locator': 'locator-duplicated-component'
});
// expect parent container to be refreshed
expect(refreshXBlockSpies).toHaveBeenCalled();
};
it("can duplicate the first xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(0);
});
it("can duplicate a middle xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(1);
});
it("can duplicate the last xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can duplicate an xblock with broken JavaScript", function () {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.duplicate-button').first().click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
'duplicate_source_locator': 'locator-broken-javascript',
'parent_locator': 'locator-container'
});
});
it('shows a notification when duplicating', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickDuplicate(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithJson(requests, {"locator": "new_item"});
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not duplicate an xblock upon failure', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
clickDuplicate(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithError(requests);
expectComponents(getGroupElement(), allComponentsInGroup);
expect(refreshXBlockSpies).not.toHaveBeenCalled();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
});
});
describe("Previews", function () {
var getButtonIcon, getButtonText;
getButtonIcon = function (containerPage) {
return containerPage.$('.action-toggle-preview i');
};
getButtonText = function (containerPage) {
return containerPage.$('.action-toggle-preview .preview-text').text().trim();
};
if (pagedSpecificTests) {
it('has no text on the preview button to start with', function () {
containerPage = getContainerPage();
expect(getButtonIcon(containerPage)).toHaveClass('fa-refresh');
expect(getButtonIcon(containerPage).parent()).toHaveClass('is-hidden');
expect(getButtonText(containerPage)).toBe("");
});
var updatePreviewButtonTest = function(show_previews, expected_text) {
it('can set preview button to "' + expected_text + '"', function () {
containerPage = getContainerPage();
containerPage.updatePreviewButton(show_previews);
expect(getButtonText(containerPage)).toBe(expected_text);
});
};
updatePreviewButtonTest(true, 'Hide Previews');
updatePreviewButtonTest(false, 'Show Previews');
it('triggers underlying view togglePreviews when preview button clicked', function () {
containerPage = getContainerPage();
containerPage.render();
spyOn(containerPage.xblockView, 'togglePreviews');
containerPage.$('.toggle-preview-button').click();
expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled();
});
}
});
describe('createNewComponent ', function () {
var clickNewComponent;
clickNewComponent = function (index) {
containerPage.$(".new-component .new-component-type button.single-template")[index].click();
};
it('Attaches a handler to new component button', function() {
containerPage = getContainerPage();
containerPage.render();
// Stub jQuery.scrollTo module.
$.scrollTo = jasmine.createSpy('jQuery.scrollTo');
containerPage.$('.new-component-button').click();
expect($.scrollTo).toHaveBeenCalled();
});
it('sends the correct JSON to the server', function () {
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
EditHelpers.verifyXBlockRequest(requests, {
"category": "discussion",
"type": "discussion",
"parent_locator": "locator-group-A"
});
});
it('also works for older-style add component links', function () {
// Some third party xblocks (problem-builder in particular) expect add
// event handlers on custom <a> add buttons which is what the platform
// used to use instead of <button>s.
// This can be removed once there is a proper API that XBlocks can use
// to add children or allow authors to add children.
renderContainerPage(this, mockContainerXBlockHtml);
$(".add-xblock-component-button").each(function() {
var htmlAsLink = $($(this).prop('outerHTML').replace(/(<\/?)button/g, "$1a"));
$(this).replaceWith(htmlAsLink);
});
$(".add-xblock-component-button").first().click();
EditHelpers.verifyXBlockRequest(requests, {
"category": "discussion",
"type": "discussion",
"parent_locator": "locator-group-A"
});
});
it('shows a notification while creating', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/);
AjaxHelpers.respondWithJson(requests, { });
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not insert component upon failure', function () {
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
AjaxHelpers.expectNoRequests(requests);
expectComponents(getGroupElement(), allComponentsInGroup);
});
describe('Template Picker', function () {
var showTemplatePicker, verifyCreateHtmlComponent;
showTemplatePicker = function () {
containerPage.$('.new-component .new-component-type button.multiple-templates')[0].click();
};
verifyCreateHtmlComponent = function (test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html button')[templateIndex].click();
EditHelpers.verifyXBlockRequest(requests, expectedRequest);
AjaxHelpers.respondWithJson(requests, {"locator": "new_item"});
respondWithHtml(mockXBlockHtml);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
};
it('can add an HTML component without a template', function () {
verifyCreateHtmlComponent(this, 0, {
"category": "html",
"parent_locator": "locator-group-A"
});
});
it('can add an HTML component with a template', function () {
verifyCreateHtmlComponent(this, 1, {
"category": "html",
"boilerplate": "announcement.yaml",
"parent_locator": "locator-group-A"
});
});
});
});
});
});
}
// Create a suite for a non-paged container that includes 'edit visibility' buttons
parameterized_suite("Non paged",
{
page: ContainerPage,
requiresPageRefresh: false,
initial: 'mock/mock-container-xblock.underscore',
addResponse: 'mock/mock-xblock.underscore',
hasVisibilityEditor: true,
pagedSpecificTests: false
}
);
// Create a suite for a paged container that does not include 'edit visibility' buttons
parameterized_suite("Paged",
{
page: PagedContainerPage,
page_size: 42,
requiresPageRefresh: true,
initial: 'mock/mock-container-paged-xblock.underscore',
addResponse: 'mock/mock-xblock-paged.underscore',
hasVisibilityEditor: false,
pagedSpecificTests: true
}
);
});