diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee deleted file mode 100644 index 610f51fc8..000000000 --- a/spec/pane-spec.coffee +++ /dev/null @@ -1,1213 +0,0 @@ -{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} - copy: -> new 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' - isPermanentDockItem: -> false - - 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 - location: 'center' - 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) - - waitsForPromise -> - pane.destroyItem(item1).then -> - 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) - - waitsForPromise -> - pane.destroyItem(item1).then -> - 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) - - waitsForPromise -> - pane.destroyItem(item1).then -> - 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 force=true", -> - it "destroys the item immediately", -> - pane.destroyItem(item1, true) - - expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.getItems()).toBe false - expect(item1.isDestroyed()).toBe true - - 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 "when passed a permanent dock item", -> - it "doesn't destroy the item", -> - spyOn(item1, 'isPermanentDockItem').andReturn true - pane.destroyItem(item1) - expect(item1 in pane.getItems()).toBe true - expect(item1.isDestroyed()).toBe false - - it "destroy the item if force=true", -> - spyOn(item1, 'isPermanentDockItem').andReturn true - pane.destroyItem(item1, true) - expect(item1 in pane.getItems()).toBe false - expect(item1.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() - - waitsForPromise -> pane.destroyItems() - - runs -> - 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 rejects with a well-known IO error", -> - it "creates a notification", -> - pane.getActiveItem().saveAs = -> - error = new Error("EACCES, permission denied '/foo'") - error.path = '/foo' - error.code = 'EACCES' - Promise.reject(error) - - waitsFor (done) -> - subscription = atom.notifications.onDidAddNotification (notification) -> - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain '/foo' - subscription.dispose() - done() - pane.saveActiveItem() - - 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", -> - it "creates a notification", -> - pane.getActiveItem().saveAs = -> - error = new Error("EACCES, permission denied '/foo'") - error.path = '/foo' - error.code = 'EACCES' - Promise.reject(error) - - waitsFor (done) -> - subscription = atom.notifications.onDidAddNotification (notification) -> - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain '/foo' - subscription.dispose() - done() - pane.saveActiveItemAs() - - 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() - - it "does nothing if the active item doesn't implement .copy()", -> - item1.copy = null - pane2 = pane1.splitLeft(copyActiveItem: true) - expect(pane2.getActiveItem()).toBeUndefined() - - 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().then -> - 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) - - waitsForPromise -> - pane.close().then -> - 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 - - waitsForPromise -> - pane.close().then -> - 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") - - waitsForPromise -> - pane.close().then -> - 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") - - waitsForPromise -> - pane.close().then -> - 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) - - waitsForPromise -> editor1.save() - - runs -> - editor1.insertText('More text') - advanceClock(editor1.getBuffer().stoppedChangingDelay) - - expect(pane.getPendingItem()).toBeNull() - expect(eventCount).toBe 1 - - # Reset fixture back to original state - waitsForPromise -> - 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] diff --git a/spec/pane-spec.js b/spec/pane-spec.js new file mode 100644 index 000000000..314e8dab2 --- /dev/null +++ b/spec/pane-spec.js @@ -0,0 +1,1500 @@ +const {extend} = require('underscore-plus') +const {Emitter} = require('event-kit') +const Grim = require('grim') +const Pane = require('../src/pane') +const PaneContainer = require('../src/pane-container') + +describe('Pane', () => { + let confirm, showSaveDialog, deserializerDisposable + + class Item { + static deserialize ({name, uri}) { + return new Item(name, uri) + } + + constructor (name, uri) { + this.name = name + this.uri = uri + this.emitter = new Emitter() + this.destroyed = false + } + + getURI () { return this.uri } + getPath () { return this.path } + isEqual (other) { return this.name === (other && other.name) } + isPermanentDockItem () { return false } + isDestroyed () { return this.destroyed } + + serialize () { + return {deserializer: 'Item', name: this.name, uri: this.uri} + } + + copy () { + return new Item(this.name, this.uri) + } + + destroy () { + this.destroyed = true + return this.emitter.emit('did-destroy') + } + + onDidDestroy (fn) { + return this.emitter.on('did-destroy', fn) + } + + onDidTerminatePendingState (callback) { + return this.emitter.on('terminate-pending-state', callback) + } + + terminatePendingState () { + return this.emitter.emit('terminate-pending-state') + } + } + + beforeEach(() => { + confirm = spyOn(atom.applicationDelegate, 'confirm') + showSaveDialog = spyOn(atom.applicationDelegate, 'showSaveDialog') + deserializerDisposable = atom.deserializers.add(Item) + }) + + afterEach(() => { + deserializerDisposable.dispose() + }) + + function paneParams (params) { + return 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0)) + }) + + it('compacts the items array', () => { + const 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()', () => { + let container, pane1, pane2 + + beforeEach(() => { + container = new PaneContainer({ + location: 'center', + 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', () => { + const 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', () => { + const observed = [] + pane1.onDidChangeActive(active => observed.push(active)) + pane1.activate() + pane2.activate() + expect(observed).toEqual([true, false]) + }) + + it('invokes ::onDidActivate() observers', () => { + let 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + const [item1, item2] = pane.getItems() + const 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [item1, item2, item3] = pane.getItems() + pane.activateItem(item2) + const 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', () => { + const pane = new Pane(paneParams()) + const item = new Item('A') + pane.addItem(item) + expect(pane.getActiveItem()).toBe(item) + }) + + it('invokes ::onDidAddItem() observers', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + const events = [] + pane.onDidAddItem(event => events.push(event)) + + const 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', () => { + const item = new Item('A') + const container = new PaneContainer({config: atom.config, applicationDelegate: atom.applicationDelegate}) + const pane1 = container.getActivePane() + pane1.addItem(item) + const pane2 = pane1.splitRight() + expect(() => pane2.addItem(item)).toThrow() + }) + + it("throws an exception if the item isn't an object", () => { + const 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', () => { + const pane = new Pane(paneParams({items: []})) + const itemA = new Item('A') + const itemB = new Item('B') + const 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', () => { + const eventOrder = [] + + const pane = new Pane(paneParams({items: []})) + const itemA = new Item('A') + const itemB = new Item('B') + pane.addItem(itemA, {pending: true}) + + pane.onDidAddItem(function ({item}) { + if (item === itemB) eventOrder.push('add') + }) + + pane.onDidRemoveItem(function ({item}) { + if (item === itemA) eventOrder.push('remove') + }) + + pane.addItem(itemB) + + waitsFor(() => eventOrder.length === 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', () => { + const pane = new Pane(paneParams({items: []})) + const itemA = new Item('A') + const itemB = new Item('B') + const 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', () => { + const 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)', () => { + let 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", () => { + const item = new Item('C') + pane.activateItem(item) + expect(pane.getItems().includes(item)).toBe(true) + expect(pane.getActiveItem()).toBe(item) + }) + + it('invokes ::onDidChangeActiveItem() observers', () => { + const 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', () => { + let itemC = null + let 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', () => { + let 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', () => { + let pane = null + let callbackCalled = false + + beforeEach(() => { + pane = atom.workspace.getActivePane() + callbackCalled = false + }) + + it('is called when the pending item changes', () => { + pane.setPendingItem('fake item one') + pane.onItemDidTerminatePendingState(function (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(function (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 + const pendingSpy = jasmine.createSpy('onItemDidTerminatePendingState') + const 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C'), new Item('D'), new Item('E')]})) + const [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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [item1,, 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [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)', () => { + let 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(pane.getItems().includes(item2)).toBe(false) + expect(item2.isDestroyed()).toBe(true) + expect(pane.getActiveItem()).toBe(item1) + + pane.destroyItem(item1) + expect(pane.getItems().includes(item1)).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', () => { + const events = [] + pane.onWillDestroyItem(function (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', () => { + const 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', () => { + const 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', () => { + let 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) + + waitsForPromise(() => + pane.destroyItem(item1).then(() => { + expect(item1.save).toHaveBeenCalled() + expect(pane.getItems().includes(item1)).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) + + waitsForPromise(() => + pane.destroyItem(item1).then(() => { + expect(showSaveDialog).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalledWith('/selected/path') + expect(pane.getItems().includes(item1)).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) + + waitsForPromise(() => + pane.destroyItem(item1).then(() => { + expect(item1.save).not.toHaveBeenCalled() + expect(pane.getItems().includes(item1)).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(pane.getItems().includes(item1)).toBe(true) + expect(item1.isDestroyed()).toBe(false) + }) + }) + + describe('when force=true', () => { + it('destroys the item immediately', () => { + pane.destroyItem(item1, true) + + expect(item1.save).not.toHaveBeenCalled() + expect(pane.getItems().includes(item1)).toBe(false) + expect(item1.isDestroyed()).toBe(true) + }) + }) + }) + + 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) + for (let item of pane.getItems()) { pane.destroyItem(item) } + 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) + for (let item of pane.getItems()) { + pane.destroyItem(item) + } + expect(pane.isDestroyed()).toBe(true) + }) + }) + }) + + describe('when passed a permanent dock item', () => { + it("doesn't destroy the item", () => { + spyOn(item1, 'isPermanentDockItem').andReturn(true) + pane.destroyItem(item1) + expect(pane.getItems().includes(item1)).toBe(true) + expect(item1.isDestroyed()).toBe(false) + }) + + it('destroy the item if force=true', () => { + spyOn(item1, 'isPermanentDockItem').andReturn(true) + pane.destroyItem(item1, true) + expect(pane.getItems().includes(item1)).toBe(false) + expect(item1.isDestroyed()).toBe(true) + }) + }) + }) + + describe('::destroyActiveItem()', () => { + it('destroys the active item', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + const activeItem = pane.getActiveItem() + pane.destroyActiveItem() + expect(activeItem.isDestroyed()).toBe(true) + expect(pane.getItems().includes(activeItem)).toBe(false) + }) + + it('does not throw an exception if there are no more items', () => { + const pane = new Pane(paneParams()) + pane.destroyActiveItem() + }) + }) + + describe('::destroyItems()', () => { + it('destroys all items', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [item1, item2, item3] = pane.getItems() + + waitsForPromise(() => pane.destroyItems()) + + runs(() => { + 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', () => { + const pane = new Pane(paneParams({items: [new Item(), new Item()]})) + const [item1, item2] = pane.getItems() + + const observed = [] + pane.observeItems(item => observed.push(item)) + + const 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [item1,, item3] = pane.getItems() + pane.itemAtIndex(1).destroy() + expect(pane.getItems()).toEqual([item1, item3]) + }) + }) + + describe('::destroyInactiveItems()', () => { + it('destroys all items but the active item', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]})) + const [, item2] = pane.getItems() + pane.activateItem(item2) + pane.destroyInactiveItems() + expect(pane.getItems()).toEqual([item2]) + }) + }) + + describe('::saveActiveItem()', () => { + let pane + + 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 rejects with a well-known IO error", () => { + it('creates a notification', () => { + pane.getActiveItem().saveAs = () => { + const error = new Error("EACCES, permission denied '/foo'") + error.path = '/foo' + error.code = 'EACCES' + return Promise.reject(error) + } + + waitsFor((done) => { + const subscription = atom.notifications.onDidAddNotification(function (notification) { + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Permission denied') + expect(notification.getMessage()).toContain('/foo') + subscription.dispose() + done() + }) + pane.saveActiveItem() + }) + }) + }) + }) + + describe('::saveActiveItemAs()', () => { + let 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", () => { + it('creates a notification', () => { + pane.getActiveItem().saveAs = () => { + const error = new Error("EACCES, permission denied '/foo'") + error.path = '/foo' + error.code = 'EACCES' + return Promise.reject(error) + } + + waitsFor((done) => { + const subscription = atom.notifications.onDidAddNotification(function (notification) { + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Permission denied') + expect(notification.getMessage()).toContain('/foo') + subscription.dispose() + done() + }) + pane.saveActiveItemAs() + }) + }) + }) + }) + + describe('::itemForURI(uri)', () => { + it('returns the item for which a call to .getURI() returns the given uri', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C'), new Item('D')]})) + const [item1, item2] = 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)', () => { + let 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', () => { + const 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)', () => { + let container, pane1, pane2 + let item1, item2, item3, item4, item5 + + beforeEach(() => { + container = new PaneContainer({config: atom.config, 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', () => { + const 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', () => { + const 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', () => { + const 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', () => { + const 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', () => { + const 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', () => { + let pane1, item1, container + + beforeEach(() => { + container = new PaneContainer({config: atom.config, 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', () => { + const pane2 = pane1.splitLeft({items: [new Item('B')]}) + const 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', () => { + const pane2 = pane1.splitLeft({moveActiveItem: true}) + expect(pane2.getActiveItem()).toBe(item1) + }) + }) + + describe('when `copyActiveItem: true` is passed in the params', () => { + it('duplicates the active item', () => { + const pane2 = pane1.splitLeft({copyActiveItem: true}) + expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem()) + }) + + it("does nothing if the active item doesn't implement .copy()", () => { + item1.copy = null + const pane2 = pane1.splitLeft({copyActiveItem: true}) + expect(pane2.getActiveItem()).toBeUndefined() + }) + }) + + 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() + const pane2 = pane1.splitLeft({items: [new Item('B')]}) + const pane3 = pane1.splitLeft({items: [new Item('C')]}) + const 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', () => { + const pane2 = pane1.splitRight({items: [new Item('B')]}) + const 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', () => { + const pane2 = pane1.splitLeft({moveActiveItem: true}) + expect(pane2.getActiveItem()).toBe(item1) + }) + }) + + describe('when `copyActiveItem: true` is passed in the params', () => { + it('duplicates the active item', () => { + const 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() + const pane2 = pane1.splitRight({items: [new Item('B')]}) + const pane3 = pane1.splitRight({items: [new Item('C')]}) + const 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', () => { + const pane2 = pane1.splitUp({items: [new Item('B')]}) + const 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', () => { + const pane2 = pane1.splitLeft({moveActiveItem: true}) + expect(pane2.getActiveItem()).toBe(item1) + }) + }) + + describe('when `copyActiveItem: true` is passed in the params', () => { + it('duplicates the active item', () => { + const 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() + const pane2 = pane1.splitUp({items: [new Item('B')]}) + const pane3 = pane1.splitUp({items: [new Item('C')]}) + const 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', () => { + const pane2 = pane1.splitDown({items: [new Item('B')]}) + const 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', () => { + const pane2 = pane1.splitLeft({moveActiveItem: true}) + expect(pane2.getActiveItem()).toBe(item1) + }) + }) + + describe('when `copyActiveItem: true` is passed in the params', () => { + it('duplicates the active item', () => { + const 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() + const pane2 = pane1.splitDown({items: [new Item('B')]}) + const pane3 = pane1.splitDown({items: [new Item('C')]}) + const 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) + const 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', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + const [item1] = pane.getItems() + + item1.shouldPromptToSave = () => true + item1.getURI = () => '/test/path' + item1.save = jasmine.createSpy('save') + + confirm.andReturn(0) + + waitsForPromise(() => + pane.close().then(() => { + expect(confirm).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe(true) + }) + ) + }) + + it('does not destroy the pane if cancel is called', () => { + const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]})) + const [item1] = pane.getItems() + + item1.shouldPromptToSave = () => true + item1.getURI = () => '/test/path' + item1.save = jasmine.createSpy('save') + + confirm.andReturn(1) + + waitsForPromise(() => + pane.close().then(() => { + expect(confirm).toHaveBeenCalled() + expect(item1.save).not.toHaveBeenCalled() + expect(pane.isDestroyed()).toBe(false) + }) + ) + }) + + describe('when item fails to save', () => { + let pane, item1 + + beforeEach(() => { + pane = new Pane({items: [new Item('A'), new Item('B')], applicationDelegate: atom.applicationDelegate, config: atom.config}); + [item1] = pane.getItems() + + item1.shouldPromptToSave = () => true + item1.getURI = () => '/test/path' + + item1.save = jasmine.createSpy('save').andCallFake(() => { + const 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', () => { + let confirmations = 0 + confirm.andCallFake(() => { + confirmations++ + if (confirmations === 1) { + return 0 // click save + } else { + return 1 + } + }) // click cancel + + waitsForPromise(() => + pane.close().then(() => { + 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) + + let confirmations = 0 + confirm.andCallFake(() => { + confirmations++ + return 0 + }) // save and then save as + + showSaveDialog.andReturn('new/path') + + waitsForPromise(() => + pane.close().then(() => { + 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(() => { + const error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + }) + + let confirmations = 0 + confirm.andCallFake(() => { + confirmations++ + if (confirmations < 3) { + return 0 // save, save as, save as + } + return 2 + }) // don't save + + showSaveDialog.andReturn('new/path') + + waitsForPromise(() => + pane.close().then(() => { + 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()', () => { + let container, pane1, pane2 + + beforeEach(() => { + container = new PaneContainer({config: atom.config, confirm}) + pane1 = container.root + pane1.addItems([new Item('A'), new Item('B')]) + pane2 = pane1.splitRight() + }) + + it('invokes ::onWillDestroy observers before destroying items', () => { + let itemsDestroyed = null + pane1.onWillDestroy(() => { + itemsDestroyed = (pane1.getItems().map((item) => item.isDestroyed())) + }) + pane1.destroy() + expect(itemsDestroyed).toEqual([false, false]) + }) + + it("destroys the pane's destroyable items", () => { + const [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()).toBe(true) + }) + }) + + describe("if the pane's parent has more than two children", () => { + it('removes the pane from its parent', () => { + const 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', () => { + const 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', () => { + let editor1 = null + let pane = null + let eventCount = null + + beforeEach(() => { + waitsForPromise(() => + atom.workspace.open('sample.txt', {pending: true}).then(function (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(function (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', () => { + const originalText = editor1.getText() + editor1.insertText('Some text') + advanceClock(editor1.getBuffer().stoppedChangingDelay) + + waitsForPromise(() => editor1.save()) + + runs(() => { + editor1.insertText('More text') + advanceClock(editor1.getBuffer().stoppedChangingDelay) + + expect(pane.getPendingItem()).toBeNull() + expect(eventCount).toBe(1) + }) + + // Reset fixture back to original state + waitsForPromise(() => { + editor1.setText(originalText) + return 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', () => { + let 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', () => { + const newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.getItems()).toEqual(pane.getItems()) + }) + + it('restores the active item on deserialization', () => { + pane.activateItemAtIndex(1) + const 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) + const 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", () => { + const unserializable = {} + pane.addItem(unserializable, {index: 0}) + pane.items[2].getURI = null + pane.activateItemAtIndex(2) + const 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') + const unserializable = {} + pane.activateItem(unserializable) + + const 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() + const newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.focused).toBe(true) + }) + + it('can serialize and deserialize the order of the items in the itemStack', () => { + const [item1, item2, item3] = pane.getItems() + pane.itemStack = [item3, item1, item2] + const 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', () => { + const newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.getItems()).toEqual(newPane.itemStack) + }) + + it('rebuilds the itemStack if items.length does not match itemStack.length', () => { + const [, item2, item3] = pane.getItems() + pane.itemStack = [item2, item3] + const 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', () => { + const [item1, item2, item3] = pane.getItems() + pane.itemStack = [item2, item1, item3] + const unserializable = {} + pane.activateItem(unserializable) + + const newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.itemStack).toEqual([item2, item1, item3]) + }) + }) +})