Files
edx-platform/cms/static/coffee/spec/views/overview_spec.coffee
Christina Roberts 461b805951 Merge pull request #1951 from edx/christina/fix-xhr
Workaround for "xhr.restore" failures.
2013-12-17 12:23:10 -08:00

448 lines
23 KiB
CoffeeScript

define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_sinon", "js/base", "date", "jquery.timepicker"],
(Overview, Notification, create_sinon) ->
describe "Course Overview", ->
beforeEach ->
appendSetFixtures """
<div class="section-published-date">
<span class="published-status">
<strong>Release date:</strong> 06/12/2013 at 04:00 UTC
</span>
<a href="#" class="edit-release-date action " data-date="06/12/2013" data-time="04:00" data-locator="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2"><i class="icon-time"></i> <span class="sr">Edit section release date</span></a>
</div>
"""
appendSetFixtures """
<div class="wrapper wrapper-dialog wrapper-dialog-edit-sectionrelease edit-section-publish-settings" aria-describedby="dialog-edit-sectionrelease-description" aria-labelledby="dialog-edit-sectionrelease-title" aria-hidden="" role="dialog">
<div class="dialog confirm">
<form class="edit-sectionrelease-dialog" action="#">
<div class="form-content">
<h2 class="title dialog-edit-sectionrelease-title">Section Release Date</h2>
<p id="dialog-edit-sectionrelease-description" class="message">On the date set below, this section - <strong class="section-name"></strong> - will be released to students. Any units marked private will only be visible to admins.</p>
<ul class="list-input picker datepair">
<li class="field field-start-date">
<label for="start_date">Release Day</label>
<input class="start-date date" type="text" name="start_date" value="04/08/1990" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</li>
<li class="field field-start-time">
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input class="start-time time" type="text" name="start_time" value="12:00" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</li>
</ul>
</div>
<div class="actions">
<h3 class="sr">Form Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-save">Save</a>
</li>
<li class="action-item">
<a href="#" class="button action-secondary action-cancel">Cancel</a>
</li>
</ul>
</div>
</form>
</div>
"""
appendSetFixtures """
<section class="courseware-section is-collapsible is-draggable" data-parent="a-parent-locator-goes-here" data-locator="a-location-goes-here">
<li class="branch collapsed id-holder" data-locator="an-id-goes-here">
<a href="#" data-tooltip="Delete this section" class="delete-section-button"><i class="icon-trash"></i> <span class="sr">Delete section</span></a>
</li>
</section>
"""
appendSetFixtures """
<section>
<ol class="sortable-subsection-list">
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-0" data-locator="subsection-0-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-0">
<li class="courseware-unit unit is-draggable" id="unit-0" data-parent="subsection-0-id" data-locator="zero-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-1" data-locator="subsection-1-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="courseware-unit unit is-draggable" id="unit-1" data-parent="subsection-1-id" data-locator="first-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-2" data-parent="subsection-1-id" data-locator="second-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-2" data-locator="subsection-2-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="courseware-unit unit is-draggable" id="unit-4" data-parent="subsection-2" data-locator="fourth-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-3" data-locator="subsection-3-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-3"></ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-4" data-locator="subsection-4-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-4">
<li class="courseware-unit unit is-draggable" id="unit-5" data-parent="subsection-4-id" data-locator="fifth-unit-id"></li>
</ol>
</li>
</ol>
</section>
"""
spyOn(Overview, 'saveSetSectionScheduleDate').andCallThrough()
# Have to do this here, as it normally gets bound in document.ready()
$('a.action-save').click(Overview.saveSetSectionScheduleDate)
$('a.delete-section-button').click(deleteSection)
$(".edit-subsection-publish-settings .start-date").datepicker()
@notificationSpy = spyOn(Notification.Mini.prototype, 'show').andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
Overview.overviewDragger.makeDraggable(
'.unit',
'.unit-drag-handle',
'ol.sortable-unit-list',
'li.courseware-subsection, article.subsection-body'
)
Overview.overviewDragger.makeDraggable(
'.courseware-subsection',
'.subsection-drag-handle',
'.sortable-subsection-list',
'section'
)
afterEach ->
delete window.analytics
delete window.course_location_analytics
@notificationSpy.reset()
it "should save model when save is clicked", ->
$('a.edit-release-date').click()
$('a.action-save').click()
expect(Overview.saveSetSectionScheduleDate).toHaveBeenCalled()
it "should show a confirmation on save", ->
$('a.edit-release-date').click()
$('a.action-save').click()
expect(@notificationSpy).toHaveBeenCalled()
# Fails sporadically in Jenkins.
# it "should delete model when delete is clicked", ->
# $('a.delete-section-button').click()
# $('a.action-primary').click()
# expect(@requests[0].url).toEqual('/delete_item')
it "should not delete model when cancel is clicked", ->
requests = create_sinon["requests"](this)
$('a.delete-section-button').click()
$('a.action-secondary').click()
expect(requests.length).toEqual(0)
# Fails sporadically in Jenkins.
# it "should show a confirmation on delete", ->
# $('a.delete-section-button').click()
# $('a.action-primary').click()
# expect(@notificationSpy).toHaveBeenCalled()
describe "findDestination", ->
it "correctly finds the drop target of a drag", ->
$ele = $('#unit-1')
$ele.offset(
top: $ele.offset().top + 10, left: $ele.offset().left
)
destination = Overview.overviewDragger.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", ->
$ele = $('#unit-1')
$unit4 = $('#unit-4')
$ele.offset(
top: $unit4.offset().top + 8
left: $ele.offset().left
)
# Dragging down, we will insert after.
destination = Overview.overviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($unit4)
expect(destination.attachMethod).toBe('after')
# Dragging up, we will insert before.
destination = Overview.overviewDragger.findDestination($ele, -1)
expect(destination.ele).toBe($unit4)
expect(destination.attachMethod).toBe('before')
# If past the end the drop target, will attach after.
$ele.offset(
top: $unit4.offset().top + $unit4.height() + 1
left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, 0)
expect(destination.ele).toBe($unit4)
expect(destination.attachMethod).toBe('after')
$unit0 = $('#unit-0')
# If before the start the drop target, will attach before.
$ele.offset(
top: $unit0.offset().top - 16
left: $ele.offset().left
)
destination = Overview.overviewDragger.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
slightly before the first element""", ->
$ele = $('#subsection-2')
$ele.offset(
top: $('#subsection-0').offset().top - 5
left: $ele.offset().left
)
destination = Overview.overviewDragger.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", ->
$ele = $('#unit-4')
$ele.offset(
top: $('#unit-3').offset().bottom + 4
left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, -1)
expect(destination.ele).toBe($('#unit-3'))
# Dragging down up into last element, we have a fudge factor makes it easier to drag at beginning.
expect(destination.attachMethod).toBe('after')
# Now past the "fudge factor".
$ele.offset(
top: $('#unit-3').offset().top + 4
left: $ele.offset().left
)
destination = Overview.overviewDragger.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
slightly before/taller then the last element""", ->
$ele = $('#subsection-2')
$ele.offset(
# Make the top 1 before the top of the last element in the list.
# This mimics the problem when the element being dropped is taller then then
# the last element in the list.
top: $('#subsection-4').offset().top - 1
left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#subsection-4'))
expect(destination.attachMethod).toBe('after')
it "can drag into an empty list", ->
$ele = $('#unit-1')
$ele.offset(
top: $('#subsection-3').offset().top + 10
left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#subsection-list-3'))
expect(destination.attachMethod).toBe('prepend')
it "reports a null destination on a failed drag", ->
$ele = $('#unit-1')
$ele.offset(
top: $ele.offset().top + 200, left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, 1)
expect(destination).toEqual(
ele: null
attachMethod: ""
)
it "can drag into a collapsed list", ->
$('#subsection-2').addClass('collapsed')
$ele = $('#unit-2')
$ele.offset(
top: $('#subsection-2').offset().top + 3
left: $ele.offset().left
)
destination = Overview.overviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#subsection-list-2'))
expect(destination.parentList).toBe($('#subsection-2'))
expect(destination.attachMethod).toBe('prepend')
describe "onDragStart", ->
it "sets the dragState to its default values", ->
expect(Overview.overviewDragger.dragState).toEqual({})
# Call with some dummy data
Overview.overviewDragger.onDragStart(
{element: $('#unit-1')},
null,
null
)
expect(Overview.overviewDragger.dragState).toEqual(
dropDestination: null,
attachMethod: '',
parentList: null,
lastY: 0,
dragDirection: 0
)
it "collapses expanded elements", ->
expect($('#subsection-1')).not.toHaveClass('collapsed')
Overview.overviewDragger.onDragStart(
{element: $('#subsection-1')},
null,
null
)
expect($('#subsection-1')).toHaveClass('collapsed')
expect($('#subsection-1')).toHaveClass('expand-on-drop')
describe "onDragMove", ->
beforeEach ->
@scrollSpy = spyOn(window, 'scrollBy').andCallThrough()
it "adds the correct CSS class to the drop destination", ->
$ele = $('#unit-1')
dragY = $ele.offset().top + 10
dragX = $ele.offset().left
$ele.offset(
top: dragY, left: dragX
)
Overview.overviewDragger.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", ->
$ele = $('#unit-1')
dragY = $ele.offset().top + 10
$ele.offset(
top: dragY, left: $ele.offset().left
)
Overview.overviewDragger.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", ->
Overview.overviewDragger.onDragMove(
{element: $('#unit-1')}, '', {clientY: 2}
)
expect(@scrollSpy).toHaveBeenCalledWith(0, -10)
it "scrolls down if necessary", ->
Overview.overviewDragger.onDragMove(
{element: $('#unit-1')}, '', {clientY: (window.innerHeight - 5)}
)
expect(@scrollSpy).toHaveBeenCalledWith(0, 10)
describe "onDragEnd", ->
beforeEach ->
@reorderSpy = spyOn(Overview.overviewDragger, 'handleReorder')
afterEach ->
@reorderSpy.reset()
it "calls handleReorder on a successful drag", ->
Overview.overviewDragger.dragState.dropDestination = $('#unit-2')
Overview.overviewDragger.dragState.attachMethod = "before"
Overview.overviewDragger.dragState.parentList = $('#subsection-1')
$('#unit-1').offset(
top: $('#unit-1').offset().top + 10
left: $('#unit-1').offset().left
)
Overview.overviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect(@reorderSpy).toHaveBeenCalled()
it "clears out the drag state", ->
Overview.overviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
null
)
expect(Overview.overviewDragger.dragState).toEqual({})
it "sets the element to the correct position", ->
Overview.overviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
null
)
# Chrome sets the CSS to 'auto', but Firefox uses '0px'.
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", ->
$('#subsection-1').addClass('collapsed')
$('#subsection-1').addClass('expand-on-drop')
Overview.overviewDragger.onDragEnd(
{element: $('#subsection-1')},
null,
null
)
expect($('#subsection-1')).not.toHaveClass('collapsed')
expect($('#subsection-1')).not.toHaveClass('expand-on-drop')
it "expands a collapsed element when something is dropped in it", ->
$('#subsection-2').addClass('collapsed')
Overview.overviewDragger.dragState.dropDestination = $('#list-2')
Overview.overviewDragger.dragState.attachMethod = "prepend"
Overview.overviewDragger.dragState.parentList = $('#subsection-2')
Overview.overviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect($('#subsection-2')).not.toHaveClass('collapsed')
describe "AJAX", ->
beforeEach ->
@savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "should send an update on reorder", ->
requests = create_sinon["requests"](this)
Overview.overviewDragger.dragState.dropDestination = $('#unit-4')
Overview.overviewDragger.dragState.attachMethod = "after"
Overview.overviewDragger.dragState.parentList = $('#subsection-2')
# Drag Unit 1 from Subsection 1 to the end of Subsection 2.
$('#unit-1').offset(
top: $('#unit-4').offset().top + 10
left: $('#unit-4').offset().left
)
Overview.overviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect(requests.length).toEqual(2)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
expect(@savingSpies.hide).not.toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch(/Saving/)
expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect(requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}')
requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}')
requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout.
@clock.tick(1001)
expect($('#unit-1')).not.toHaveClass('was-dropped')