define(["js/utils/drag_and_drop", "common/js/components/views/feedback_notification", "common/js/spec_helpers/ajax_helpers", "jquery", "underscore"], function (ContentDragger, Notification, AjaxHelpers, $, _) { describe("Overview drag and drop functionality", function () { beforeEach(function () { setFixtures(readFixtures('mock/mock-outline.underscore')); _.each( $('.unit'), function (element) { ContentDragger.makeDraggable(element, { type: '.unit', handleClass: '.unit-drag-handle', droppableClass: 'ol.sortable-unit-list', parentLocationSelector: 'li.courseware-subsection', refresh: jasmine.createSpy('Spy on Unit'), ensureChildrenRendered: jasmine.createSpy('Spy on Unit') }); } ); _.each( $('.courseware-subsection'), function (element) { ContentDragger.makeDraggable(element, { type: '.courseware-subsection', handleClass: '.subsection-drag-handle', droppableClass: '.sortable-subsection-list', parentLocationSelector: 'section', refresh: jasmine.createSpy('Spy on Subsection'), ensureChildrenRendered: jasmine.createSpy('Spy on Subsection') }); } ); }); describe("findDestination", function () { it("correctly finds the drop target of a drag", function () { var $ele, destination; $ele = $('#unit-1'); $ele.offset({ top: $ele.offset().top + 10, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination.ele).toBe($('#unit-2')); expect(destination.attachMethod).toBe('before'); }); it("can drag and drop across section boundaries, with special handling for single sibling", function () { var $ele, $unit0, $unit4, destination; $ele = $('#unit-1'); $unit4 = $('#unit-4'); $ele.offset({ top: $unit4.offset().top + 8, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination.ele).toBe($unit4); expect(destination.attachMethod).toBe('after'); destination = ContentDragger.findDestination($ele, -1); expect(destination.ele).toBe($unit4); expect(destination.attachMethod).toBe('before'); $ele.offset({ top: $unit4.offset().top + $unit4.height() + 1, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 0); expect(destination.ele).toBe($unit4); expect(destination.attachMethod).toBe('after'); $unit0 = $('#unit-0'); $ele.offset({ top: $unit0.offset().top - 16, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 0); expect(destination.ele).toBe($unit0); expect(destination.attachMethod).toBe('before'); }); it("can drop before the first element, even if element being dragged is\nslightly before the first element", function () { var $ele, destination; $ele = $('#subsection-2'); $ele.offset({ top: $('#subsection-0').offset().top - 5, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, -1); expect(destination.ele).toBe($('#subsection-0')); expect(destination.attachMethod).toBe('before'); }); it("can drag and drop across section boundaries, with special handling for last element", function () { var $ele, destination; $ele = $('#unit-4'); $ele.offset({ top: $('#unit-3').offset().bottom + 4, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, -1); expect(destination.ele).toBe($('#unit-3')); expect(destination.attachMethod).toBe('after'); $ele.offset({ top: $('#unit-3').offset().top + 4, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, -1); expect(destination.ele).toBe($('#unit-3')); expect(destination.attachMethod).toBe('before'); }); it("can drop past the last element, even if element being dragged is\nslightly before/taller then the last element", function () { var $ele, destination; $ele = $('#subsection-2'); $ele.offset({ top: $('#subsection-4').offset().top - 1, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination.ele).toBe($('#subsection-4')); expect(destination.attachMethod).toBe('after'); }); it("can drag into an empty list", function () { var $ele, destination; $ele = $('#unit-1'); $ele.offset({ top: $('#subsection-3').offset().top + 10, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination.ele).toBe($('#subsection-list-3')); expect(destination.attachMethod).toBe('prepend'); }); it("reports a null destination on a failed drag", function () { var $ele, destination; $ele = $('#unit-1'); $ele.offset({ top: $ele.offset().top + 200, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination).toEqual({ ele: null, attachMethod: "" }); }); it("can drag into a collapsed list", function () { var $ele, destination; $('#subsection-2').addClass('is-collapsed'); $ele = $('#unit-2'); $ele.offset({ top: $('#subsection-2').offset().top + 3, left: $ele.offset().left }); destination = ContentDragger.findDestination($ele, 1); expect(destination.ele).toBe($('#subsection-list-2')); expect(destination.parentList).toBe($('#subsection-2')); expect(destination.attachMethod).toBe('prepend'); }); }); describe("onDragStart", function () { it("sets the dragState to its default values", function () { expect(ContentDragger.dragState).toEqual({}); ContentDragger.onDragStart({ element: $('#unit-1') }, null, null); expect(ContentDragger.dragState).toEqual({ dropDestination: null, attachMethod: '', parentList: null, lastY: 0, dragDirection: 0 }); }); it("collapses expanded elements", function () { expect($('#subsection-1')).not.toHaveClass('is-collapsed'); ContentDragger.onDragStart({ element: $('#subsection-1') }, null, null); expect($('#subsection-1')).toHaveClass('is-collapsed'); expect($('#subsection-1')).toHaveClass('expand-on-drop'); }); }); describe("onDragMove", function () { beforeEach(function () { this.redirectSpy = spyOn(window, 'scrollBy').andCallThrough(); }); it("adds the correct CSS class to the drop destination", function () { var $ele, dragX, dragY; $ele = $('#unit-1'); dragY = $ele.offset().top + 10; dragX = $ele.offset().left; $ele.offset({ top: dragY, left: dragX }); ContentDragger.onDragMove({ element: $ele, dragPoint: { y: dragY } }, '', { clientX: dragX }); expect($('#unit-2')).toHaveClass('drop-target drop-target-before'); expect($ele).toHaveClass('valid-drop'); }); it("does not add CSS class to the drop destination if out of bounds", function () { var $ele, dragY; $ele = $('#unit-1'); dragY = $ele.offset().top + 10; $ele.offset({ top: dragY, left: $ele.offset().left }); ContentDragger.onDragMove({ element: $ele, dragPoint: { y: dragY } }, '', { clientX: $ele.offset().left - 3 }); expect($('#unit-2')).not.toHaveClass('drop-target drop-target-before'); expect($ele).not.toHaveClass('valid-drop'); }); it("scrolls up if necessary", function () { ContentDragger.onDragMove({ element: $('#unit-1') }, '', { clientY: 2 }); expect(this.redirectSpy).toHaveBeenCalledWith(0, -10); }); it("scrolls down if necessary", function () { ContentDragger.onDragMove({ element: $('#unit-1') }, '', { clientY: window.innerHeight - 5 }); expect(this.redirectSpy).toHaveBeenCalledWith(0, 10); }); }); describe("onDragEnd", function () { beforeEach(function () { this.reorderSpy = spyOn(ContentDragger, 'handleReorder'); }); afterEach(function () { this.reorderSpy.reset(); }); it("calls handleReorder on a successful drag", function () { ContentDragger.dragState.dropDestination = $('#unit-2'); ContentDragger.dragState.attachMethod = "after"; ContentDragger.dragState.parentList = $('#subsection-1'); $('#unit-1').offset({ top: $('#unit-1').offset().top + 10, left: $('#unit-1').offset().left }); ContentDragger.onDragEnd({ element: $('#unit-1') }, null, { clientX: $('#unit-1').offset().left }); expect(this.reorderSpy).toHaveBeenCalled(); }); it("clears out the drag state", function () { ContentDragger.onDragEnd({ element: $('#unit-1') }, null, null); expect(ContentDragger.dragState).toEqual({}); }); it("sets the element to the correct position", function () { ContentDragger.onDragEnd({ element: $('#unit-1') }, null, null); expect(['0px', 'auto']).toContain($('#unit-1').css('top')); expect(['0px', 'auto']).toContain($('#unit-1').css('left')); }); it("expands an element if it was collapsed on drag start", function () { $('#subsection-1').addClass('is-collapsed'); $('#subsection-1').addClass('expand-on-drop'); ContentDragger.onDragEnd({ element: $('#subsection-1') }, null, null); expect($('#subsection-1')).not.toHaveClass('is-collapsed'); expect($('#subsection-1')).not.toHaveClass('expand-on-drop'); }); it("expands a collapsed element when something is dropped in it", function () { expandElementSpy = spyOn(ContentDragger, 'expandElement').andCallThrough(); expect(expandElementSpy).not.toHaveBeenCalled(); expect($('#subsection-2').data('ensureChildrenRendered')).not.toHaveBeenCalled(); $('#subsection-2').addClass('is-collapsed'); ContentDragger.dragState.dropDestination = $('#list-2'); ContentDragger.dragState.attachMethod = "prepend"; ContentDragger.dragState.parentList = $('#subsection-2'); ContentDragger.onDragEnd({ element: $('#unit-1') }, null, { clientX: $('#unit-1').offset().left }); // verify collapsed element expands while ensuring its children are properly rendered expect(expandElementSpy).toHaveBeenCalled(); expect($('#subsection-2').data('ensureChildrenRendered')).toHaveBeenCalled(); expect($('#subsection-2')).not.toHaveClass('is-collapsed'); }); }); describe("AJAX", function () { beforeEach(function () { this.savingSpies = spyOnConstructor(Notification, "Mini", ["show", "hide"]); this.savingSpies.show.andReturn(this.savingSpies); this.clock = sinon.useFakeTimers(); }); afterEach(function () { this.clock.restore(); }); it("should send an update on reorder from one parent to another", function () { var requests, request, savingOptions; requests = AjaxHelpers["requests"](this); ContentDragger.dragState.dropDestination = $('#unit-4'); ContentDragger.dragState.attachMethod = "after"; ContentDragger.dragState.parentList = $('#subsection-2'); $('#unit-1').offset({ top: $('#unit-4').offset().top + 10, left: $('#unit-4').offset().left }); ContentDragger.onDragEnd({ element: $('#unit-1') }, null, { clientX: $('#unit-1').offset().left }); request = AjaxHelpers.currentRequest(requests); expect(this.savingSpies.constructor).toHaveBeenCalled(); expect(this.savingSpies.show).toHaveBeenCalled(); expect(this.savingSpies.hide).not.toHaveBeenCalled(); savingOptions = this.savingSpies.constructor.mostRecentCall.args[0]; expect(savingOptions.title).toMatch(/Saving/); expect($('#unit-1')).toHaveClass('was-dropped'); expect(request.requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}'); request.respond(200); expect(this.savingSpies.hide).toHaveBeenCalled(); this.clock.tick(1001); expect($('#unit-1')).not.toHaveClass('was-dropped'); // source expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); // target expect($('#subsection-2').data('refresh')).toHaveBeenCalled(); }); it("should send an update on reorder within the same parent", function () { var requests = AjaxHelpers["requests"](this), request; ContentDragger.dragState.dropDestination = $('#unit-2'); ContentDragger.dragState.attachMethod = "after"; ContentDragger.dragState.parentList = $('#subsection-1'); $('#unit-1').offset({ top: $('#unit-1').offset().top + 10, left: $('#unit-1').offset().left }); ContentDragger.onDragEnd({ element: $('#unit-1') }, null, { clientX: $('#unit-1').offset().left }); request = AjaxHelpers.currentRequest(requests); expect($('#unit-1')).toHaveClass('was-dropped'); expect(request.requestBody).toEqual( '{"children":["second-unit-id","first-unit-id","third-unit-id"]}' ); request.respond(200); this.clock.tick(1001); expect($('#unit-1')).not.toHaveClass('was-dropped'); // parent expect($('#subsection-1').data('refresh')).toHaveBeenCalled(); }); }); }); });