Paging for LibraryView added with JS tests.

This commit is contained in:
E. Kolpakov
2014-11-03 20:20:29 +07:00
parent 058176144e
commit ff1a08cbd5
22 changed files with 1987 additions and 630 deletions

View File

@@ -1,22 +1,20 @@
define([
'jquery', 'js/models/xblock_info', 'js/views/pages/container',
'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1'
],
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
function($, _, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict';
return function (componentTemplates, XBlockInfoJson, action, isUnitPage) {
var templates = new ComponentTemplates(componentTemplates, {parse: true}),
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
return function (componentTemplates, XBlockInfoJson, action, options) {
var main_options = {
el: $('#content'),
model: new XBlockInfo(XBlockInfoJson, {parse: true}),
action: action,
templates: new ComponentTemplates(componentTemplates, {parse: true})
};
xmoduleLoader.done(function () {
var view = new ContainerPage({
el: $('#content'),
model: mainXBlockInfo,
action: action,
templates: templates,
isUnitPage: isUnitPage
});
var view = new ContainerPage(_.extend(main_options, options));
view.render();
});
};

View File

@@ -1,22 +1,20 @@
define([
'jquery', 'js/models/xblock_info', 'js/views/pages/container',
'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1'
],
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
function($, _, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict';
return function (componentTemplates, XBlockInfoJson) {
var templates = new ComponentTemplates(componentTemplates, {parse: true}),
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
return function (componentTemplates, XBlockInfoJson, options) {
var main_options = {
el: $('#content'),
model: new XBlockInfo(XBlockInfoJson, {parse: true}),
templates: new ComponentTemplates(componentTemplates, {parse: true}),
action: 'view'
};
xmoduleLoader.done(function () {
var view = new ContainerPage({
el: $('#content'),
model: mainXBlockInfo,
action: "view",
templates: templates,
isUnitPage: false
});
var view = new ContainerPage(_.extend(main_options, options));
view.render();
});
};

View File

@@ -0,0 +1,489 @@
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info",
"js/views/library_container", "js/views/paging_header", "js/views/paging_footer"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingContainer, PagingFooter) {
var htmlResponseTpl = _.template('' +
'<div class="xblock-container-paging-parameters" data-start="<%= start %>" data-displayed="<%= displayed %>" data-total="<%= total %>"/>'
);
function getResponseHtml(options){
return '<div class="xblock" data-request-token="request_token">' +
'<div class="container-paging-header"></div>' +
htmlResponseTpl(options) +
'<div class="container-paging-footer"></div>' +
'</div>'
}
var PAGE_SIZE = 3;
var mockFirstPage = {
resources: [],
html: getResponseHtml({
start: 0,
displayed: PAGE_SIZE,
total: PAGE_SIZE + 1
})
};
var mockSecondPage = {
resources: [],
html: getResponseHtml({
start: PAGE_SIZE,
displayed: 1,
total: PAGE_SIZE + 1
})
};
var mockEmptyPage = {
resources: [],
html: getResponseHtml({
start: 0,
displayed: 0,
total: 0
})
};
var respondWithMockPage = function(requests) {
var requestIndex = requests.length - 1;
var request = requests[requestIndex];
var url = new URI(request.url);
var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
var page = queryParameters.page_number;
var response = page === "0" ? mockFirstPage : mockSecondPage;
AjaxHelpers.respondWithJson(requests, response, requestIndex);
};
var MockPagingView = PagedContainer.extend({
view: 'container_preview',
el: $("<div><div class='xblock' data-request-token='test_request_token'/></div>"),
model: new XBlockInfo({}, {parse: true})
});
describe("Paging Container", function() {
var pagingContainer;
beforeEach(function () {
var feedbackTpl = readFixtures('system-feedback.underscore');
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTpl));
pagingContainer = new MockPagingView({ page_size: PAGE_SIZE });
});
describe("Container", function () {
describe("setPage", function () {
it('can set the current page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should not change page after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.setPage(1);
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
});
describe("nextPage", function () {
it('does not move forward after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.nextPage();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.nextPage();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can not move forward from the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.nextPage();
expect(requests.length).toBe(1);
});
});
describe("previousPage", function () {
it('can move back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.previousPage();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can not move back from the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.previousPage();
expect(requests.length).toBe(1);
});
it('does not move back after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.previousPage();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
});
});
describe("PagingHeader", function () {
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-header.underscore');
appendSetFixtures($("<script>", { id: "paging-header-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
pagingContainer.render();
});
it('does not move forward if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.next-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.next-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should be enabled when there is at least one more page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.next-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled on the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
});
it('should be disabled on an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
});
});
describe("Previous page button", function () {
beforeEach(function () {
pagingContainer.render();
});
it('does not move back if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.previous-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can go back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.previous-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('should be disabled on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
});
it('should be enabled on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled for an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
});
});
describe("Page metadata section", function() {
it('shows the correct metadata for the current page', function () {
var requests = AjaxHelpers.requests(this),
message;
pagingContainer.setPage(0);
respondWithMockPage(requests);
message = pagingContainer.pagingHeader.$('.meta').html().trim();
expect(message).toBe('<p>Showing <span class="count-current-shown">1-3</span>' +
' out of <span class="count-total">4 total</span>, ' +
'sorted by <span class="sort-order">Date added</span> descending</p>');
});
});
describe("Children count label", function () {
it('should show correct count on first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('1-3');
});
it('should show correct count on second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('4-4');
});
it('should show correct count for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('0-0');
});
});
describe("Children total label", function () {
it('should show correct total on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('4 total');
});
it('should show correct total on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('4 total');
});
it('should show zero total for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('0 total');
});
});
});
describe("PagingFooter", function () {
var pagingFooter;
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-footer.underscore');
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
// Render the page and header so that they can react to events
pagingContainer.render();
});
it('does not move forward if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.next-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.next-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should be enabled when there is at least one more page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.next-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled on the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
});
it('should be disabled on an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
});
});
describe("Previous page button", function () {
beforeEach(function () {
// Render the page and header so that they can react to events
pagingContainer.render();
});
it('does not move back if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.previous-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can go back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.previous-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('should be disabled on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
});
it('should be enabled on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled for an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
});
});
describe("Current page label", function () {
it('should show 1 on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('1');
});
it('should show 2 on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('2');
});
it('should show 1 for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('1');
});
});
describe("Page total label", function () {
it('should show the correct value with more than one page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.total-pages')).toHaveText('2');
});
it('should show page 1 when there are no assets', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.total-pages')).toHaveText('1');
});
});
describe("Page input field", function () {
var input;
it('should initially have a blank page input', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should handle invalid page requests', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('abc');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
expect(pagingContainer.collection.currentPage).toBe(0);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should switch pages via the input field', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('2');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
AjaxHelpers.respondWithJson(requests, mockSecondPage);
expect(pagingContainer.collection.currentPage).toBe(1);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should handle AJAX failures when switching pages via the input field', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('2');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
});
});
});
});

View File

@@ -3,537 +3,560 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
"js/views/pages/container", "js/models/xblock_info", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, XBlockInfo) {
describe("ContainerPage", function() {
var lastRequest, renderContainerPage, expectComponents, respondWithHtml,
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');
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();
});
lastRequest = function() { return requests[requests.length - 1]; };
respondWithHtml = function(html) {
var requestIndex = requests.length - 1;
AjaxHelpers.respondWithJson(
requests,
{ html: html, "resources": [] },
requestIndex
);
};
renderContainerPage = function(test, html, options) {
requests = AjaxHelpers.requests(test);
containerPage = new ContainerPage(_.extend(options || {}, {
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content')
}));
containerPage.render();
respondWithHtml(html);
};
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.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;
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
expect(str.startsWith(lastRequest().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
expect(str.startsWith(lastRequest().url,
'/xblock/locator-container/container_preview')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
// 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;
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
expect(str.startsWith(lastRequest().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();
});
});
describe("Editing an xmodule", function() {
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
function parameterized_suite(label, global_page_options, fixtures) {
describe(label + " ContainerPage", function () {
var lastRequest, getContainerPage, renderContainerPage, expectComponents, respondWithHtml,
model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures(fixtures.initial),
mockXBlockHtml = readFixtures(fixtures.add_response),
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');
beforeEach(function () {
EditHelpers.installMockXModule({
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
}
});
});
afterEach(function() {
EditHelpers.uninstallMockXModule();
EditHelpers.cancelModalIfShowing();
});
initialDisplayName = 'Test Container';
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: []
model = new XBlockInfo({
id: 'locator-container',
display_name: initialDisplayName,
category: 'vertical'
});
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); }
);
afterEach(function () {
EditHelpers.uninstallMockXBlock();
});
getGroupElement = function() {
return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']");
lastRequest = function () {
return requests[requests.length - 1];
};
describe("Deleting an xblock", function() {
var clickDelete, deleteComponent, deleteComponentWithSuccess,
promptSpy;
respondWithHtml = function (html) {
var requestIndex = requests.length - 1;
AjaxHelpers.respondWithJson(
requests,
{ html: html, "resources": [] },
requestIndex
);
};
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);
getContainerPage = function (options) {
var default_options = {
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content')
};
return new ContainerPage(_.extend(options || {}, global_page_options, default_options));
};
deleteComponent = function(componentIndex) {
clickDelete(componentIndex);
AjaxHelpers.respondWithJson(requests, {});
renderContainerPage = function (test, html, options) {
requests = AjaxHelpers.requests(test);
containerPage = getContainerPage(options);
containerPage.render();
respondWithHtml(html);
};
// second to last request contains given component's id (to delete the component)
AjaxHelpers.expectJsonRequest(requests, 'DELETE',
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
null, requests.length - 2);
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);
});
};
// final request to refresh the xblock info
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
};
deleteComponentWithSuccess = function(componentIndex) {
deleteComponent(componentIndex);
// verify the new list of components within the group
expectComponents(
getGroupElement(),
_.without(allComponentsInGroup, allComponentsInGroup[componentIndex])
);
};
it("can delete the first xblock", function() {
describe("Initial display", function () {
it('can render itself', function () {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(0);
expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
});
it("can delete a middle xblock", function() {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(1);
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 delete the last xblock", function() {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can delete an xblock with broken JavaScript", function() {
it('can show an xblock with broken JavaScript', function () {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.delete-button').first().click();
EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.respondWithJson(requests, {});
// expect the second to last request to be a delete of the xblock
AjaxHelpers.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
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('does not delete when clicking No in prompt', function () {
var numRequests;
renderContainerPage(this, mockContainerXBlockHtml);
numRequests = requests.length;
// 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
expect(requests.length).toBe(numRequests);
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('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);
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("Duplicating an xblock", function() {
var clickDuplicate, duplicateComponentWithSuccess,
refreshXBlockSpies;
describe("Editing the container", function () {
var updatedDisplayName = 'Updated Test Container',
getDisplayNameWrapper;
clickDuplicate = function(componentIndex) {
afterEach(function () {
EditHelpers.cancelModalIfShowing();
});
// 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();
getDisplayNameWrapper = function () {
return containerPage.$('.wrapper-xblock-field');
};
duplicateComponentWithSuccess = function(componentIndex) {
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
it('can edit itself', function () {
var editButtons, displayNameElement;
renderContainerPage(this, mockContainerXBlockHtml);
displayNameElement = containerPage.$('.page-header-title');
clickDuplicate(componentIndex);
// Click the root edit button
editButtons = containerPage.$('.nav-actions .edit-button');
editButtons.first().click();
// 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
// Expect a request to be made to show the studio view for the container
expect(str.startsWith(lastRequest().url, '/xblock/locator-container/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
'locator': 'locator-duplicated-component'
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
expect(str.startsWith(lastRequest().url,
'/xblock/locator-container/container_preview')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
// expect parent container to be refreshed
expect(refreshXBlockSpies).toHaveBeenCalled();
};
// Respond to the subsequent xblock info fetch request.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
it("can duplicate the first xblock", function() {
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(0);
// Expect the title to have been updated
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
});
it("can duplicate a middle xblock", function() {
it('can inline edit the display name', function () {
var displayNameInput, displayNameWrapper;
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/);
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('createNewComponent ', function () {
var clickNewComponent;
describe("Editing an xblock", function () {
afterEach(function () {
EditHelpers.cancelModalIfShowing();
});
clickNewComponent = function (index) {
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
};
it('sends the correct JSON to the server', function () {
it('can show an edit modal for a child xblock', function () {
var editButtons;
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
EditHelpers.verifyXBlockRequest(requests, {
"category": "discussion",
"type": "discussion",
"parent_locator": "locator-group-A"
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
expect(str.startsWith(lastRequest().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();
});
});
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
}
});
});
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);
afterEach(function () {
EditHelpers.uninstallMockXModule();
EditHelpers.cancelModalIfShowing();
});
it('does not insert component upon failure', function () {
var requestCount;
it('can save changes to settings', function () {
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
requestCount = requests.length;
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
expect(requests.length).toBe(requestCount);
expectComponents(getGroupElement(), allComponentsInGroup);
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('Template Picker', function() {
var showTemplatePicker, verifyCreateHtmlComponent,
mockXBlockHtml = readFixtures('mock/mock-xblock.underscore');
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);
}
);
showTemplatePicker = function() {
containerPage.$('.new-component .new-component-type a.multiple-templates')[0].click();
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);
};
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html a')[templateIndex].click();
EditHelpers.verifyXBlockRequest(requests, expectedRequest);
deleteComponent = function (componentIndex) {
clickDelete(componentIndex);
AjaxHelpers.respondWithJson(requests, {});
// second to last request contains given component's id (to delete the component)
AjaxHelpers.expectJsonRequest(requests, 'DELETE',
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
null, requests.length - 2);
// final request to refresh the xblock info
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
};
deleteComponentWithSuccess = function (componentIndex) {
deleteComponent(componentIndex);
// verify the new list of components within the group
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);
AjaxHelpers.respondWithJson(requests, {});
// expect the second to last request to be a delete of the xblock
AjaxHelpers.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
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
});
it('does not delete when clicking No in prompt', function () {
var numRequests;
renderContainerPage(this, mockContainerXBlockHtml);
numRequests = requests.length;
// 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
expect(requests.length).toBe(numRequests);
});
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"});
respondWithHtml(mockXBlockHtml);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
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('createNewComponent ', function () {
var clickNewComponent;
clickNewComponent = function (index) {
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
};
it('can add an HTML component without a template', function() {
verifyCreateHtmlComponent(this, 0, {
"category": "html",
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('can add an HTML component with a template', function() {
verifyCreateHtmlComponent(this, 1, {
"category": "html",
"boilerplate" : "announcement.yaml",
"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 () {
var requestCount;
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
requestCount = requests.length;
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
expect(requests.length).toBe(requestCount);
expectComponents(getGroupElement(), allComponentsInGroup);
});
describe('Template Picker', function () {
var showTemplatePicker, verifyCreateHtmlComponent;
showTemplatePicker = function () {
containerPage.$('.new-component .new-component-type a.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 a')[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"
});
});
});
});
});
});
});
}
parameterized_suite("Non paged",
{ enable_paging: false },
{ initial: 'mock/mock-container-xblock.underscore', add_response: 'mock/mock-xblock.underscore' }
);
parameterized_suite("Paged",
{ enable_paging: true, page_size: 42 },
{
initial: 'mock/mock-container-paged-xblock.underscore',
add_response: 'mock/mock-container-paged-after-add-xblock.underscore'
});
});

View File

@@ -123,6 +123,10 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
});
},
acknowledgeXBlockDeletion: function(locator){
this.notifyRuntime('deleted-child', locator);
},
refresh: function() {
var sortableInitializedClass = this.makeRequestSpecificSelector('.reorderable-container.ui-sortable');
this.$(sortableInitializedClass).sortable('refresh');

View File

@@ -0,0 +1,164 @@
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification",
"js/views/paging_header", "js/views/paging_footer"],
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView, PagingHeader, PagingFooter) {
var LibraryContainerView = XBlockView.extend({
// Store the request token of the first xblock on the page (which we know was rendered by Studio when
// the page was generated). Use that request token to filter out user-defined HTML in any
// child xblocks within the page.
requestToken: "",
initialize: function(options){
var self = this;
XBlockView.prototype.initialize.call(this);
this.page_size = this.options.page_size || 10;
if (options) {
this.page_reload_callback = options.page_reload_callback;
}
// emulating Backbone.paginator interface
this.collection = {
currentPage: 0,
totalPages: 0,
totalCount: 0,
sortDirection: "desc",
start: 0,
_size: 0,
bind: function() {}, // no-op
size: function() { return self.collection._size; }
};
},
render: function(options) {
var eff_options = options || {};
if (eff_options.block_added) {
this.collection.currentPage = this.getPageCount(this.collection.totalCount+1) - 1;
}
eff_options.page_number = typeof eff_options.page_number !== "undefined"
? eff_options.page_number
: this.collection.currentPage;
return this.renderPage(eff_options);
},
renderPage: function(options){
var self = this,
view = this.view,
xblockInfo = this.model,
xblockUrl = xblockInfo.url();
return $.ajax({
url: decodeURIComponent(xblockUrl) + "/" + view,
type: 'GET',
cache: false,
data: this.getRenderParameters(options.page_number),
headers: { Accept: 'application/json' },
success: function(fragment) {
self.handleXBlockFragment(fragment, options);
self.processPaging({ requested_page: options.page_number });
if (options.paging && self.page_reload_callback){
self.page_reload_callback(self.$el);
}
}
});
},
getRenderParameters: function(page_number) {
return {
enable_paging: true,
page_size: this.page_size,
page_number: page_number
};
},
getPageCount: function(total_count){
if (total_count==0) return 1;
return Math.ceil(total_count / this.page_size);
},
setPage: function(page_number) {
this.render({ page_number: page_number, paging: true });
},
nextPage: function() {
var collection = this.collection,
currentPage = collection.currentPage,
lastPage = collection.totalPages - 1;
if (currentPage < lastPage) {
this.setPage(currentPage + 1);
}
},
previousPage: function() {
var collection = this.collection,
currentPage = collection.currentPage;
if (currentPage > 0) {
this.setPage(currentPage - 1);
}
},
processPaging: function(options){
var $element = this.$el.find('.xblock-container-paging-parameters'),
total = $element.data('total'),
displayed = $element.data('displayed'),
start = $element.data('start');
this.collection.currentPage = options.requested_page;
this.collection.totalCount = total;
this.collection.totalPages = this.getPageCount(total);
this.collection.start = start;
this.collection._size = displayed;
this.processPagingHeaderAndFooter();
},
processPagingHeaderAndFooter: function(){
if (this.pagingHeader)
this.pagingHeader.undelegateEvents();
if (this.pagingFooter)
this.pagingFooter.undelegateEvents();
this.pagingHeader = new PagingHeader({
view: this,
el: this.$el.find('.container-paging-header')
});
this.pagingFooter = new PagingFooter({
view: this,
el: this.$el.find('.container-paging-footer')
});
this.pagingHeader.render();
this.pagingFooter.render();
},
xblockReady: function () {
XBlockView.prototype.xblockReady.call(this);
this.requestToken = this.$('div.xblock').first().data('request-token');
},
refresh: function() { },
acknowledgeXBlockDeletion: function (locator){
this.notifyRuntime('deleted-child', locator);
this.collection._size -= 1;
this.collection.totalCount -= 1;
// pages are counted from 0 - thus currentPage == 1 if we're on second page
if (this.collection._size == 0 && this.collection.currentPage >= 1) {
this.setPage(this.collection.currentPage - 1);
this.collection.totalPages -= 1;
}
else {
this.pagingHeader.render();
this.pagingFooter.render();
}
},
makeRequestSpecificSelector: function(selector) {
return 'div.xblock[data-request-token="' + this.requestToken + '"] > ' + selector;
},
sortDisplayName: function() {
return "Date added"; // TODO add support for sorting
}
});
return LibraryContainerView;
}); // end define();

View File

@@ -3,10 +3,10 @@
* This page allows the user to understand and manipulate the xblock and its children.
*/
define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/view_utils",
"js/views/container", "js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock",
"js/views/container", "js/views/library_container", "js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock",
"js/models/xblock_info", "js/views/xblock_string_field_editor", "js/views/pages/container_subviews",
"js/views/unit_outline", "js/views/utils/xblock_utils"],
function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
function ($, _, gettext, BasePage, ViewUtils, ContainerView, PagedContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView,
XBlockUtils) {
'use strict';
@@ -27,6 +27,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
initialize: function(options) {
BasePage.prototype.initialize.call(this, options);
this.enable_paging = options.enable_paging || false;
if (this.enable_paging) {
this.page_size = options.page_size || 10;
}
this.nameEditor = new XBlockStringFieldEditor({
el: this.$('.wrapper-xblock-field'),
model: this.model
@@ -35,11 +39,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
if (this.options.action === 'new') {
this.nameEditor.$('.xblock-field-value-edit').click();
}
this.xblockView = new ContainerView({
el: this.$('.wrapper-xblock'),
model: this.model,
view: this.view
});
this.xblockView = this.getXBlockView();
this.messageView = new ContainerSubviews.MessageView({
el: this.$('.container-message'),
model: this.model
@@ -75,6 +75,28 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}
},
getXBlockView: function(){
var self = this,
parameters = {
el: this.$('.wrapper-xblock'),
model: this.model,
view: this.view
};
if (this.enable_paging) {
parameters = _.extend(parameters, {
page_size: this.page_size,
page_reload_callback: function($element) {
self.renderAddXBlockComponents();
}
});
return new PagedContainerView(parameters);
}
else {
return new ContainerView(parameters);
}
},
render: function(options) {
var self = this,
xblockView = this.xblockView,
@@ -106,7 +128,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Re-enable Backbone events for any updated DOM elements
self.delegateEvents();
}
},
block_added: options && options.block_added
});
},
@@ -144,7 +167,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
modal.edit(xblockElement, this.model, {
refresh: function() {
self.refreshXBlock(xblockElement);
self.refreshXBlock(xblockElement, false);
}
});
},
@@ -226,7 +249,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// 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'));
xblockView.acknowledgeXBlockDeletion(parent.data('locator'));
// Update publish and last modified information from the server.
this.model.fetch();
@@ -235,7 +258,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
onNewXBlock: function(xblockElement, scrollOffset, data) {
ViewUtils.setScrollOffset(xblockElement, scrollOffset);
xblockElement.data('locator', data.locator);
return this.refreshXBlock(xblockElement);
return this.refreshXBlock(xblockElement, true);
},
/**
@@ -243,17 +266,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
* reorderable container then the element will be refreshed inline. If not, then the
* parent container will be refreshed instead.
* @param element An element representing the xblock to be refreshed.
* @param block_added Flag to indicate that new block has been just added.
*/
refreshXBlock: function(element) {
refreshXBlock: function(element, block_added) {
var xblockElement = this.findXBlockElement(element),
parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({refresh: true});
this.render({refresh: true, block_added: block_added});
} else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement);
} else {
this.refreshXBlock(this.findXBlockElement(parentElement));
this.refreshXBlock(this.findXBlockElement(parentElement), block_added);
}
},

View File

@@ -38,6 +38,12 @@ define(["underscore", "js/views/baseview"], function(_, BaseView) {
currentPage = collection.currentPage + 1,
pageInput = this.$("#page-number-input"),
pageNumber = parseInt(pageInput.val(), 10);
if (pageNumber > collection.totalPages) {
pageNumber = false;
}
if (pageNumber <= 0) {
pageNumber = false;
}
if (pageNumber && pageNumber !== currentPage) {
view.setPage(pageNumber - 1);
}