pulsar/spec/pane-spec.coffee
2016-11-29 16:55:14 -08:00

1180 lines
44 KiB
CoffeeScript

{extend} = require 'underscore-plus'
{Emitter} = require 'event-kit'
Grim = require 'grim'
Pane = require '../src/pane'
PaneContainer = require '../src/pane-container'
describe "Pane", ->
[confirm, showSaveDialog, deserializerDisposable] = []
class Item
@deserialize: ({name, uri}) -> new this(name, uri)
constructor: (@name, @uri) -> @emitter = new Emitter
destroyed: false
getURI: -> @uri
getPath: -> @path
serialize: -> {deserializer: 'Item', @name, @uri}
isEqual: (other) -> @name is other?.name
onDidDestroy: (fn) -> @emitter.on('did-destroy', fn)
destroy: -> @destroyed = true; @emitter.emit('did-destroy')
isDestroyed: -> @destroyed
onDidTerminatePendingState: (callback) -> @emitter.on 'terminate-pending-state', callback
terminatePendingState: -> @emitter.emit 'terminate-pending-state'
beforeEach ->
confirm = spyOn(atom.applicationDelegate, 'confirm')
showSaveDialog = spyOn(atom.applicationDelegate, 'showSaveDialog')
deserializerDisposable = atom.deserializers.add(Item)
afterEach ->
deserializerDisposable.dispose()
paneParams = (params) ->
extend({
applicationDelegate: atom.applicationDelegate,
config: atom.config,
deserializerManager: atom.deserializers,
notificationManager: atom.notifications
}, params)
describe "construction", ->
it "sets the active item to the first item", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
expect(pane.getActiveItem()).toBe pane.itemAtIndex(0)
it "compacts the items array", ->
pane = new Pane(paneParams(items: [undefined, new Item("A"), null, new Item("B")]))
expect(pane.getItems().length).toBe 2
expect(pane.getActiveItem()).toBe pane.itemAtIndex(0)
describe "::activate()", ->
[container, pane1, pane2] = []
beforeEach ->
container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate)
container.getActivePane().splitRight()
[pane1, pane2] = container.getPanes()
it "changes the active pane on the container", ->
expect(container.getActivePane()).toBe pane2
pane1.activate()
expect(container.getActivePane()).toBe pane1
pane2.activate()
expect(container.getActivePane()).toBe pane2
it "invokes ::onDidChangeActivePane observers on the container", ->
observed = []
container.onDidChangeActivePane (activePane) -> observed.push(activePane)
pane1.activate()
pane1.activate()
pane2.activate()
pane1.activate()
expect(observed).toEqual [pane1, pane2, pane1]
it "invokes ::onDidChangeActive observers on the relevant panes", ->
observed = []
pane1.onDidChangeActive (active) -> observed.push(active)
pane1.activate()
pane2.activate()
expect(observed).toEqual [true, false]
it "invokes ::onDidActivate() observers", ->
eventCount = 0
pane1.onDidActivate -> eventCount++
pane1.activate()
pane1.activate()
pane2.activate()
expect(eventCount).toBe 2
describe "::addItem(item, index)", ->
it "adds the item at the given index", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item3 = new Item("C")
pane.addItem(item3, index: 1)
expect(pane.getItems()).toEqual [item1, item3, item2]
it "adds the item after the active item if no index is provided", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.activateItem(item2)
item4 = new Item("D")
pane.addItem(item4)
expect(pane.getItems()).toEqual [item1, item2, item4, item3]
it "sets the active item after adding the first item", ->
pane = new Pane(paneParams())
item = new Item("A")
pane.addItem(item)
expect(pane.getActiveItem()).toBe item
it "invokes ::onDidAddItem() observers", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
events = []
pane.onDidAddItem (event) -> events.push(event)
item = new Item("C")
pane.addItem(item, index: 1)
expect(events).toEqual [{item, index: 1, moved: false}]
it "throws an exception if the item is already present on a pane", ->
item = new Item("A")
container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate)
pane1 = container.getActivePane()
pane1.addItem(item)
pane2 = pane1.splitRight()
expect(-> pane2.addItem(item)).toThrow()
it "throws an exception if the item isn't an object", ->
pane = new Pane(paneParams(items: []))
expect(-> pane.addItem(null)).toThrow()
expect(-> pane.addItem('foo')).toThrow()
expect(-> pane.addItem(1)).toThrow()
it "destroys any existing pending item", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
itemC = new Item("C")
pane.addItem(itemA, pending: false)
pane.addItem(itemB, pending: true)
pane.addItem(itemC, pending: false)
expect(itemB.isDestroyed()).toBe true
it "adds the new item before destroying any existing pending item", ->
eventOrder = []
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
pane.addItem(itemA, pending: true)
pane.onDidAddItem ({item}) ->
eventOrder.push("add") if item is itemB
pane.onDidRemoveItem ({item}) ->
eventOrder.push("remove") if item is itemA
pane.addItem(itemB)
waitsFor ->
eventOrder.length is 2
runs ->
expect(eventOrder).toEqual ["add", "remove"]
describe "when using the old API of ::addItem(item, index)", ->
beforeEach ->
spyOn Grim, "deprecate"
it "supports the older public API", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
itemC = new Item("C")
pane.addItem(itemA, 0)
pane.addItem(itemB, 0)
pane.addItem(itemC, 0)
expect(pane.getItems()).toEqual [itemC, itemB, itemA]
it "shows a deprecation warning", ->
pane = new Pane(paneParams(items: []))
pane.addItem(new Item(), 2)
expect(Grim.deprecate).toHaveBeenCalledWith "Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})"
describe "::activateItem(item)", ->
pane = null
beforeEach ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
it "changes the active item to the current item", ->
expect(pane.getActiveItem()).toBe pane.itemAtIndex(0)
pane.activateItem(pane.itemAtIndex(1))
expect(pane.getActiveItem()).toBe pane.itemAtIndex(1)
it "adds the given item if it isn't present in ::items", ->
item = new Item("C")
pane.activateItem(item)
expect(item in pane.getItems()).toBe true
expect(pane.getActiveItem()).toBe item
it "invokes ::onDidChangeActiveItem() observers", ->
observed = []
pane.onDidChangeActiveItem (item) -> observed.push(item)
pane.activateItem(pane.itemAtIndex(1))
expect(observed).toEqual [pane.itemAtIndex(1)]
describe "when the item being activated is pending", ->
itemC = null
itemD = null
beforeEach ->
itemC = new Item("C")
itemD = new Item("D")
it "replaces the active item if it is pending", ->
pane.activateItem(itemC, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'C', 'B']
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'D', 'B']
it "adds the item after the active item if it is not pending", ->
pane.activateItem(itemC, pending: true)
pane.activateItemAtIndex(2)
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'B', 'D']
describe "::setPendingItem", ->
pane = null
beforeEach ->
pane = atom.workspace.getActivePane()
it "changes the pending item", ->
expect(pane.getPendingItem()).toBeNull()
pane.setPendingItem("fake item")
expect(pane.getPendingItem()).toEqual "fake item"
describe "::onItemDidTerminatePendingState callback", ->
pane = null
callbackCalled = false
beforeEach ->
pane = atom.workspace.getActivePane()
callbackCalled = false
it "is called when the pending item changes", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(item).toEqual "fake item one"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
it "has access to the new pending item via ::getPendingItem", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(pane.getPendingItem()).toEqual "fake item two"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
it "isn't called when a pending item is replaced with a new one", ->
pane = null
pendingSpy = jasmine.createSpy("onItemDidTerminatePendingState")
destroySpy = jasmine.createSpy("onWillDestroyItem")
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then ->
pane = atom.workspace.getActivePane()
runs ->
pane.onItemDidTerminatePendingState pendingSpy
pane.onWillDestroyItem destroySpy
waitsForPromise ->
atom.workspace.open('sample.js', pending: true)
runs ->
expect(destroySpy).toHaveBeenCalled()
expect(pendingSpy).not.toHaveBeenCalled()
describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", ->
it "sets the active item to the next/previous item in the itemStack, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")]))
[item1, item2, item3, item4, item5] = pane.getItems()
pane.itemStack = [item3, item1, item2, item5, item4]
pane.activateItem(item4)
expect(pane.getActiveItem()).toBe item4
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item5
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item2
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item5
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item4
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item3
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item1
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item3
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item4
pane.activateNextRecentlyUsedItem()
pane.moveActiveItemToTopOfStack()
expect(pane.getActiveItem()).toBe item5
expect(pane.itemStack[4]).toBe item5
describe "::activateNextItem() and ::activatePreviousItem()", ->
it "sets the active item to the next/previous item, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
expect(pane.getActiveItem()).toBe item1
pane.activatePreviousItem()
expect(pane.getActiveItem()).toBe item3
pane.activatePreviousItem()
expect(pane.getActiveItem()).toBe item2
pane.activateNextItem()
expect(pane.getActiveItem()).toBe item3
pane.activateNextItem()
expect(pane.getActiveItem()).toBe item1
describe "::activateLastItem()", ->
it "sets the active item to the last item", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
expect(pane.getActiveItem()).toBe item1
pane.activateLastItem()
expect(pane.getActiveItem()).toBe item3
describe "::moveItemRight() and ::moveItemLeft()", ->
it "moves the active item to the right and left, without looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.activateItemAtIndex(0)
expect(pane.getActiveItem()).toBe item1
pane.moveItemLeft()
expect(pane.getItems()).toEqual [item1, item2, item3]
pane.moveItemRight()
expect(pane.getItems()).toEqual [item2, item1, item3]
pane.moveItemLeft()
expect(pane.getItems()).toEqual [item1, item2, item3]
pane.activateItemAtIndex(2)
expect(pane.getActiveItem()).toBe item3
pane.moveItemRight()
expect(pane.getItems()).toEqual [item1, item2, item3]
describe "::activateItemAtIndex(index)", ->
it "activates the item at the given index", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.activateItemAtIndex(2)
expect(pane.getActiveItem()).toBe item3
pane.activateItemAtIndex(1)
expect(pane.getActiveItem()).toBe item2
pane.activateItemAtIndex(0)
expect(pane.getActiveItem()).toBe item1
# Doesn't fail with out-of-bounds indices
pane.activateItemAtIndex(100)
expect(pane.getActiveItem()).toBe item1
pane.activateItemAtIndex(-1)
expect(pane.getActiveItem()).toBe item1
describe "::destroyItem(item)", ->
[pane, item1, item2, item3] = []
beforeEach ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
it "removes the item from the items list and destroys it", ->
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item2)
expect(item2 in pane.getItems()).toBe false
expect(item2.isDestroyed()).toBe true
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item1)
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
it "removes the item from the itemStack", ->
pane.itemStack = [item2, item3, item1]
pane.activateItem(item1)
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item3)
expect(pane.itemStack).toEqual [item2, item1]
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item1)
expect(pane.itemStack).toEqual [item2]
expect(pane.getActiveItem()).toBe item2
pane.destroyItem(item2)
expect(pane.itemStack).toEqual []
expect(pane.getActiveItem()).toBeUndefined()
it "invokes ::onWillDestroyItem() observers before destroying the item", ->
events = []
pane.onWillDestroyItem (event) ->
expect(item2.isDestroyed()).toBe false
events.push(event)
pane.destroyItem(item2)
expect(item2.isDestroyed()).toBe true
expect(events).toEqual [{item: item2, index: 1}]
it "invokes ::onWillRemoveItem() observers", ->
events = []
pane.onWillRemoveItem (event) -> events.push(event)
pane.destroyItem(item2)
expect(events).toEqual [{item: item2, index: 1, moved: false, destroyed: true}]
it "invokes ::onDidRemoveItem() observers", ->
events = []
pane.onDidRemoveItem (event) -> events.push(event)
pane.destroyItem(item2)
expect(events).toEqual [{item: item2, index: 1, moved: false, destroyed: true}]
describe "when the destroyed item is the active item and is the first item", ->
it "activates the next item", ->
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item1)
expect(pane.getActiveItem()).toBe item2
describe "when the destroyed item is the active item and is not the first item", ->
beforeEach ->
pane.activateItem(item2)
it "activates the previous item", ->
expect(pane.getActiveItem()).toBe item2
pane.destroyItem(item2)
expect(pane.getActiveItem()).toBe item1
describe "if the item is modified", ->
itemURI = null
beforeEach ->
item1.shouldPromptToSave = -> true
item1.save = jasmine.createSpy("save")
item1.saveAs = jasmine.createSpy("saveAs")
item1.getURI = -> itemURI
describe "if the [Save] option is selected", ->
describe "when the item has a uri", ->
it "saves the item before destroying it", ->
itemURI = "test"
confirm.andReturn(0)
pane.destroyItem(item1)
expect(item1.save).toHaveBeenCalled()
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
describe "when the item has no uri", ->
it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", ->
itemURI = null
showSaveDialog.andReturn("/selected/path")
confirm.andReturn(0)
pane.destroyItem(item1)
expect(showSaveDialog).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalledWith("/selected/path")
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
describe "if the [Don't Save] option is selected", ->
it "removes and destroys the item without saving it", ->
confirm.andReturn(2)
pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
describe "if the [Cancel] option is selected", ->
it "does not save, remove, or destroy the item", ->
confirm.andReturn(1)
pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
expect(item1 in pane.getItems()).toBe true
expect(item1.isDestroyed()).toBe false
describe "when the last item is destroyed", ->
describe "when the 'core.destroyEmptyPanes' config option is false (the default)", ->
it "does not destroy the pane, but leaves it in place with empty items", ->
expect(atom.config.get('core.destroyEmptyPanes')).toBe false
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe false
expect(pane.getActiveItem()).toBeUndefined()
expect(-> pane.saveActiveItem()).not.toThrow()
expect(-> pane.saveActiveItemAs()).not.toThrow()
describe "when the 'core.destroyEmptyPanes' config option is true", ->
it "destroys the pane", ->
atom.config.set('core.destroyEmptyPanes', true)
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe true
describe "::destroyActiveItem()", ->
it "destroys the active item", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
activeItem = pane.getActiveItem()
pane.destroyActiveItem()
expect(activeItem.isDestroyed()).toBe true
expect(activeItem in pane.getItems()).toBe false
it "does not throw an exception if there are no more items", ->
pane = new Pane(paneParams())
pane.destroyActiveItem()
describe "::destroyItems()", ->
it "destroys all items", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.destroyItems()
expect(item1.isDestroyed()).toBe true
expect(item2.isDestroyed()).toBe true
expect(item3.isDestroyed()).toBe true
expect(pane.getItems()).toEqual []
describe "::observeItems()", ->
it "invokes the observer with all current and future items", ->
pane = new Pane(paneParams(items: [new Item, new Item]))
[item1, item2] = pane.getItems()
observed = []
pane.observeItems (item) -> observed.push(item)
item3 = new Item
pane.addItem(item3)
expect(observed).toEqual [item1, item2, item3]
describe "when an item emits a destroyed event", ->
it "removes it from the list of items", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.itemAtIndex(1).destroy()
expect(pane.getItems()).toEqual [item1, item3]
describe "::destroyInactiveItems()", ->
it "destroys all items but the active item", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
pane.activateItem(item2)
pane.destroyInactiveItems()
expect(pane.getItems()).toEqual [item2]
describe "::saveActiveItem()", ->
pane = null
beforeEach ->
pane = new Pane(paneParams(items: [new Item("A")]))
showSaveDialog.andReturn('/selected/path')
describe "when the active item has a uri", ->
beforeEach ->
pane.getActiveItem().uri = "test"
describe "when the active item has a save method", ->
it "saves the current item", ->
pane.getActiveItem().save = jasmine.createSpy("save")
pane.saveActiveItem()
expect(pane.getActiveItem().save).toHaveBeenCalled()
describe "when the current item has no save method", ->
it "does nothing", ->
expect(pane.getActiveItem().save).toBeUndefined()
pane.saveActiveItem()
describe "when the current item has no uri", ->
describe "when the current item has a saveAs method", ->
it "opens a save dialog and saves the current item as the selected path", ->
pane.getActiveItem().saveAs = jasmine.createSpy("saveAs")
pane.saveActiveItem()
expect(showSaveDialog).toHaveBeenCalled()
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item has no saveAs method", ->
it "does nothing", ->
expect(pane.getActiveItem().saveAs).toBeUndefined()
pane.saveActiveItem()
expect(showSaveDialog).not.toHaveBeenCalled()
describe "when the item's saveAs method throws a well-known IO error", ->
notificationSpy = null
beforeEach ->
atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy()
it "creates a notification", ->
pane.getActiveItem().saveAs = ->
error = new Error("EACCES, permission denied '/foo'")
error.path = '/foo'
error.code = 'EACCES'
throw error
pane.saveActiveItem()
expect(notificationSpy).toHaveBeenCalled()
notification = notificationSpy.mostRecentCall.args[0]
expect(notification.getType()).toBe 'warning'
expect(notification.getMessage()).toContain 'Permission denied'
expect(notification.getMessage()).toContain '/foo'
describe "::saveActiveItemAs()", ->
pane = null
beforeEach ->
pane = new Pane(paneParams(items: [new Item("A")]))
showSaveDialog.andReturn('/selected/path')
describe "when the current item has a saveAs method", ->
it "opens the save dialog and calls saveAs on the item with the selected path", ->
pane.getActiveItem().path = __filename
pane.getActiveItem().saveAs = jasmine.createSpy("saveAs")
pane.saveActiveItemAs()
expect(showSaveDialog).toHaveBeenCalledWith(defaultPath: __filename)
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item does not have a saveAs method", ->
it "does nothing", ->
expect(pane.getActiveItem().saveAs).toBeUndefined()
pane.saveActiveItemAs()
expect(showSaveDialog).not.toHaveBeenCalled()
describe "when the item's saveAs method throws a well-known IO error", ->
notificationSpy = null
beforeEach ->
atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy()
it "creates a notification", ->
pane.getActiveItem().saveAs = ->
error = new Error("EACCES, permission denied '/foo'")
error.path = '/foo'
error.code = 'EACCES'
throw error
pane.saveActiveItemAs()
expect(notificationSpy).toHaveBeenCalled()
notification = notificationSpy.mostRecentCall.args[0]
expect(notification.getType()).toBe 'warning'
expect(notification.getMessage()).toContain 'Permission denied'
expect(notification.getMessage()).toContain '/foo'
describe "::itemForURI(uri)", ->
it "returns the item for which a call to .getURI() returns the given uri", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]))
[item1, item2, item3] = pane.getItems()
item1.uri = "a"
item2.uri = "b"
expect(pane.itemForURI("a")).toBe item1
expect(pane.itemForURI("b")).toBe item2
expect(pane.itemForURI("bogus")).toBeUndefined()
describe "::moveItem(item, index)", ->
[pane, item1, item2, item3, item4] = []
beforeEach ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]))
[item1, item2, item3, item4] = pane.getItems()
it "moves the item to the given index and invokes ::onDidMoveItem observers", ->
pane.moveItem(item1, 2)
expect(pane.getItems()).toEqual [item2, item3, item1, item4]
pane.moveItem(item2, 3)
expect(pane.getItems()).toEqual [item3, item1, item4, item2]
pane.moveItem(item2, 1)
expect(pane.getItems()).toEqual [item3, item2, item1, item4]
it "invokes ::onDidMoveItem() observers", ->
events = []
pane.onDidMoveItem (event) -> events.push(event)
pane.moveItem(item1, 2)
pane.moveItem(item2, 3)
expect(events).toEqual [
{item: item1, oldIndex: 0, newIndex: 2}
{item: item2, oldIndex: 0, newIndex: 3}
]
describe "::moveItemToPane(item, pane, index)", ->
[container, pane1, pane2] = []
[item1, item2, item3, item4, item5] = []
beforeEach ->
container = new PaneContainer(config: atom.config, confirm: confirm)
pane1 = container.getActivePane()
pane1.addItems([new Item("A"), new Item("B"), new Item("C")])
pane2 = pane1.splitRight(items: [new Item("D"), new Item("E")])
[item1, item2, item3] = pane1.getItems()
[item4, item5] = pane2.getItems()
it "moves the item to the given pane at the given index", ->
pane1.moveItemToPane(item2, pane2, 1)
expect(pane1.getItems()).toEqual [item1, item3]
expect(pane2.getItems()).toEqual [item4, item2, item5]
it "invokes ::onWillRemoveItem() observers", ->
events = []
pane1.onWillRemoveItem (event) -> events.push(event)
pane1.moveItemToPane(item2, pane2, 1)
expect(events).toEqual [{item: item2, index: 1, moved: true, destroyed: false}]
it "invokes ::onDidRemoveItem() observers", ->
events = []
pane1.onDidRemoveItem (event) -> events.push(event)
pane1.moveItemToPane(item2, pane2, 1)
expect(events).toEqual [{item: item2, index: 1, moved: true, destroyed: false}]
it "does not invoke ::onDidAddPaneItem observers on the container", ->
addedItems = []
container.onDidAddPaneItem (item) -> addedItems.push(item)
pane1.moveItemToPane(item2, pane2, 1)
expect(addedItems).toEqual []
describe "when the moved item the last item in the source pane", ->
beforeEach ->
item5.destroy()
describe "when the 'core.destroyEmptyPanes' config option is false (the default)", ->
it "does not destroy the pane or the item", ->
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe false
expect(item4.isDestroyed()).toBe false
describe "when the 'core.destroyEmptyPanes' config option is true", ->
it "destroys the pane, but not the item", ->
atom.config.set('core.destroyEmptyPanes', true)
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "when the item being moved is pending", ->
it "is made permanent in the new pane", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane1.moveItemToPane(item6, pane2, 0)
expect(pane2.getPendingItem()).not.toEqual item6
describe "when the target pane has a pending item", ->
it "does not destroy the pending item", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane2.moveItemToPane(item5, pane1, 0)
expect(pane1.getPendingItem()).toEqual item6
describe "split methods", ->
[pane1, item1, container] = []
beforeEach ->
container = new PaneContainer(config: atom.config, confirm: confirm, deserializerManager: atom.deserializers)
pane1 = container.getActivePane()
item1 = new Item("A")
pane1.addItem(item1)
describe "::splitLeft(params)", ->
describe "when the parent is the container root", ->
it "replaces itself with a row and inserts a new pane to the left of itself", ->
pane2 = pane1.splitLeft(items: [new Item("B")])
pane3 = pane1.splitLeft(items: [new Item("C")])
expect(container.root.orientation).toBe 'horizontal'
expect(container.root.children).toEqual [pane2, pane3, pane1]
describe "when `moveActiveItem: true` is passed in the params", ->
it "moves the active item", ->
pane2 = pane1.splitLeft(moveActiveItem: true)
expect(pane2.getActiveItem()).toBe item1
describe "when `copyActiveItem: true` is passed in the params", ->
it "duplicates the active item", ->
pane2 = pane1.splitLeft(copyActiveItem: true)
expect(pane2.getActiveItem()).toEqual pane1.getActiveItem()
describe "when the parent is a column", ->
it "replaces itself with a row and inserts a new pane to the left of itself", ->
pane1.splitDown()
pane2 = pane1.splitLeft(items: [new Item("B")])
pane3 = pane1.splitLeft(items: [new Item("C")])
row = container.root.children[0]
expect(row.orientation).toBe 'horizontal'
expect(row.children).toEqual [pane2, pane3, pane1]
describe "::splitRight(params)", ->
describe "when the parent is the container root", ->
it "replaces itself with a row and inserts a new pane to the right of itself", ->
pane2 = pane1.splitRight(items: [new Item("B")])
pane3 = pane1.splitRight(items: [new Item("C")])
expect(container.root.orientation).toBe 'horizontal'
expect(container.root.children).toEqual [pane1, pane3, pane2]
describe "when `moveActiveItem: true` is passed in the params", ->
it "moves the active item", ->
pane2 = pane1.splitLeft(moveActiveItem: true)
expect(pane2.getActiveItem()).toBe item1
describe "when `copyActiveItem: true` is passed in the params", ->
it "duplicates the active item", ->
pane2 = pane1.splitRight(copyActiveItem: true)
expect(pane2.getActiveItem()).toEqual pane1.getActiveItem()
describe "when the parent is a column", ->
it "replaces itself with a row and inserts a new pane to the right of itself", ->
pane1.splitDown()
pane2 = pane1.splitRight(items: [new Item("B")])
pane3 = pane1.splitRight(items: [new Item("C")])
row = container.root.children[0]
expect(row.orientation).toBe 'horizontal'
expect(row.children).toEqual [pane1, pane3, pane2]
describe "::splitUp(params)", ->
describe "when the parent is the container root", ->
it "replaces itself with a column and inserts a new pane above itself", ->
pane2 = pane1.splitUp(items: [new Item("B")])
pane3 = pane1.splitUp(items: [new Item("C")])
expect(container.root.orientation).toBe 'vertical'
expect(container.root.children).toEqual [pane2, pane3, pane1]
describe "when `moveActiveItem: true` is passed in the params", ->
it "moves the active item", ->
pane2 = pane1.splitLeft(moveActiveItem: true)
expect(pane2.getActiveItem()).toBe item1
describe "when `copyActiveItem: true` is passed in the params", ->
it "duplicates the active item", ->
pane2 = pane1.splitUp(copyActiveItem: true)
expect(pane2.getActiveItem()).toEqual pane1.getActiveItem()
describe "when the parent is a row", ->
it "replaces itself with a column and inserts a new pane above itself", ->
pane1.splitRight()
pane2 = pane1.splitUp(items: [new Item("B")])
pane3 = pane1.splitUp(items: [new Item("C")])
column = container.root.children[0]
expect(column.orientation).toBe 'vertical'
expect(column.children).toEqual [pane2, pane3, pane1]
describe "::splitDown(params)", ->
describe "when the parent is the container root", ->
it "replaces itself with a column and inserts a new pane below itself", ->
pane2 = pane1.splitDown(items: [new Item("B")])
pane3 = pane1.splitDown(items: [new Item("C")])
expect(container.root.orientation).toBe 'vertical'
expect(container.root.children).toEqual [pane1, pane3, pane2]
describe "when `moveActiveItem: true` is passed in the params", ->
it "moves the active item", ->
pane2 = pane1.splitLeft(moveActiveItem: true)
expect(pane2.getActiveItem()).toBe item1
describe "when `copyActiveItem: true` is passed in the params", ->
it "duplicates the active item", ->
pane2 = pane1.splitDown(copyActiveItem: true)
expect(pane2.getActiveItem()).toEqual pane1.getActiveItem()
describe "when the parent is a row", ->
it "replaces itself with a column and inserts a new pane below itself", ->
pane1.splitRight()
pane2 = pane1.splitDown(items: [new Item("B")])
pane3 = pane1.splitDown(items: [new Item("C")])
column = container.root.children[0]
expect(column.orientation).toBe 'vertical'
expect(column.children).toEqual [pane1, pane3, pane2]
it "activates the new pane", ->
expect(pane1.isActive()).toBe true
pane2 = pane1.splitRight()
expect(pane1.isActive()).toBe false
expect(pane2.isActive()).toBe true
describe "::close()", ->
it "prompts to save unsaved items before destroying the pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item1.shouldPromptToSave = -> true
item1.getURI = -> "/test/path"
item1.save = jasmine.createSpy("save")
confirm.andReturn(0)
pane.close()
expect(confirm).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
it "does not destroy the pane if cancel is called", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item1.shouldPromptToSave = -> true
item1.getURI = -> "/test/path"
item1.save = jasmine.createSpy("save")
confirm.andReturn(1)
pane.close()
expect(confirm).toHaveBeenCalled()
expect(item1.save).not.toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
describe "when item fails to save", ->
[pane, item1, item2] = []
beforeEach ->
pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config})
[item1, item2] = pane.getItems()
item1.shouldPromptToSave = -> true
item1.getURI = -> "/test/path"
item1.save = jasmine.createSpy("save").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
it "does not destroy the pane if save fails and user clicks cancel", ->
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations is 1
return 0 # click save
else
return 1 # click cancel
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(item1.save).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
it "does destroy the pane if the user saves the file under a new name", ->
item1.saveAs = jasmine.createSpy("saveAs").andReturn(true)
confirmations = 0
confirm.andCallFake ->
confirmations++
return 0 # save and then save as
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
it "asks again if the saveAs also fails", ->
item1.saveAs = jasmine.createSpy("saveAs").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations < 3
return 0 # save, save as, save as
return 2 # don't save
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(3)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
describe "::destroy()", ->
[container, pane1, pane2] = []
beforeEach ->
container = new PaneContainer(config: atom.config, confirm: confirm)
pane1 = container.root
pane1.addItems([new Item("A"), new Item("B")])
pane2 = pane1.splitRight()
it "invokes ::onWillDestroy observers before destroying items", ->
itemsDestroyed = null
pane1.onWillDestroy ->
itemsDestroyed = (item.isDestroyed() for item in pane1.getItems())
pane1.destroy()
expect(itemsDestroyed).toEqual([false, false])
it "destroys the pane's destroyable items", ->
[item1, item2] = pane1.getItems()
pane1.destroy()
expect(item1.isDestroyed()).toBe true
expect(item2.isDestroyed()).toBe true
describe "if the pane is active", ->
it "makes the next pane active", ->
expect(pane2.isActive()).toBe true
pane2.destroy()
expect(pane1.isActive()).to
describe "if the pane's parent has more than two children", ->
it "removes the pane from its parent", ->
pane3 = pane2.splitRight()
expect(container.root.children).toEqual [pane1, pane2, pane3]
pane2.destroy()
expect(container.root.children).toEqual [pane1, pane3]
describe "if the pane's parent has two children", ->
it "replaces the parent with its last remaining child", ->
pane3 = pane2.splitDown()
expect(container.root.children[0]).toBe pane1
expect(container.root.children[1].children).toEqual [pane2, pane3]
pane3.destroy()
expect(container.root.children).toEqual [pane1, pane2]
pane2.destroy()
expect(container.root).toBe pane1
describe "pending state", ->
editor1 = null
pane = null
eventCount = null
beforeEach ->
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then (o) ->
editor1 = o
pane = atom.workspace.getActivePane()
runs ->
eventCount = 0
editor1.onDidTerminatePendingState -> eventCount++
it "does not open file in pending state by default", ->
waitsForPromise ->
atom.workspace.open('sample.js').then (o) ->
editor1 = o
pane = atom.workspace.getActivePane()
runs ->
expect(pane.getPendingItem()).toBeNull()
it "opens file in pending state if 'pending' option is true", ->
expect(pane.getPendingItem()).toEqual editor1
it "terminates pending state if ::terminatePendingState is invoked", ->
editor1.terminatePendingState()
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
it "terminates pending state when buffer is changed", ->
editor1.insertText('I\'ll be back!')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
it "only calls terminate handler once when text is modified twice", ->
originalText = editor1.getText()
editor1.insertText('Some text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
editor1.save()
editor1.insertText('More text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
# Reset fixture back to original state
editor1.setText(originalText)
editor1.save()
it "only calls clearPendingItem if there is a pending item to clear", ->
spyOn(pane, "clearPendingItem").andCallThrough()
editor1.terminatePendingState()
editor1.terminatePendingState()
expect(pane.getPendingItem()).toBeNull()
expect(pane.clearPendingItem.callCount).toBe 1
describe "serialization", ->
pane = null
beforeEach ->
pane = new Pane(paneParams(
items: [new Item("A", "a"), new Item("B", "b"), new Item("C", "c")]
flexScale: 2
))
it "can serialize and deserialize the pane and all its items", ->
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getItems()).toEqual pane.getItems()
it "restores the active item on deserialization", ->
pane.activateItemAtIndex(1)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1)
it "restores the active item when it doesn't implement getURI()", ->
pane.items[1].getURI = null
pane.activateItemAtIndex(1)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1)
it "restores the correct item when it doesn't implement getURI() and some items weren't deserialized", ->
unserializable = {}
pane.addItem(unserializable, {index: 0})
pane.items[2].getURI = null
pane.activateItemAtIndex(2)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1)
it "does not include items that cannot be deserialized", ->
spyOn(console, 'warn')
unserializable = {}
pane.activateItem(unserializable)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getActiveItem()).toEqual pane.itemAtIndex(0)
expect(newPane.getItems().length).toBe pane.getItems().length - 1
it "includes the pane's focus state in the serialized state", ->
pane.focus()
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.focused).toBe true
it "can serialize and deserialize the order of the items in the itemStack", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item3, item1, item2]
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.itemStack).toEqual pane.itemStack
expect(newPane.itemStack[2]).toEqual item2
it "builds the itemStack if the itemStack is not serialized", ->
[item1, item2, item3] = pane.getItems()
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getItems()).toEqual newPane.itemStack
it "rebuilds the itemStack if items.length does not match itemStack.length", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item2, item3]
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getItems()).toEqual newPane.itemStack
it "does not serialize the reference to the items in the itemStack for pane items that will not be serialized", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item2, item1, item3]
unserializable = {}
pane.activateItem(unserializable)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.itemStack).toEqual [item2, item1, item3]