Merge pull request #1415 from atom/ns-pane-model-specs

Move relevant pane specs from PaneView to Pane model
This commit is contained in:
Nathan Sobo
2014-01-15 15:06:43 -08:00
3 changed files with 444 additions and 499 deletions

View File

@@ -4,6 +4,288 @@ PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
describe "Pane", ->
class Item extends Model
@deserialize: ({name, uri}) -> new this(name, uri)
constructor: (@name, @uri) ->
getUri: -> @uri
getPath: -> @path
serialize: -> {deserializer: 'Item', @name, @uri}
isEqual: (other) -> @name is other?.name
beforeEach ->
atom.deserializers.add(Item)
afterEach ->
atom.deserializers.remove(Item)
describe "construction", ->
it "sets the active item to the first item", ->
pane = new Pane(items: [new Item("A"), new Item("B")])
expect(pane.activeItem).toBe pane.items[0]
describe "::activateItem(item)", ->
pane = null
beforeEach ->
pane = new Pane(items: [new Item("A"), new Item("B")])
it "changes the active item to the current item", ->
expect(pane.activeItem).toBe pane.items[0]
pane.activateItem(pane.items[1])
expect(pane.activeItem).toBe pane.items[1]
it "adds the given item if it isn't present in ::items", ->
item = new Item("C")
pane.activateItem(item)
expect(item in pane.items).toBe true
expect(pane.activeItem).toBe item
describe "::activateNextItem() and ::activatePreviousItem()", ->
it "sets the active item to the next/previous item, looping around at either end", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
expect(pane.activeItem).toBe item1
pane.activatePreviousItem()
expect(pane.activeItem).toBe item3
pane.activatePreviousItem()
expect(pane.activeItem).toBe item2
pane.activateNextItem()
expect(pane.activeItem).toBe item3
pane.activateNextItem()
expect(pane.activeItem).toBe item1
describe "::activateItemAtIndex(index)", ->
it "activates the item at the given index", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.activateItemAtIndex(2)
expect(pane.activeItem).toBe item3
pane.activateItemAtIndex(1)
expect(pane.activeItem).toBe item2
pane.activateItemAtIndex(0)
expect(pane.activeItem).toBe item1
# Doesn't fail with out-of-bounds indices
pane.activateItemAtIndex(100)
expect(pane.activeItem).toBe item1
pane.activateItemAtIndex(-1)
expect(pane.activeItem).toBe item1
describe "::destroyItem(item)", ->
[pane, item1, item2, item3] = []
beforeEach ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
it "removes the item from the items list and activates the next item if it was the active item", ->
expect(pane.activeItem).toBe item1
pane.destroyItem(item2)
expect(item2 in pane.items).toBe false
expect(pane.activeItem).toBe item1
pane.destroyItem(item1)
expect(item1 in pane.items).toBe false
expect(pane.activeItem).toBe item3
it "emits 'item-removed' with the item, its index, and true indicating the item is being destroyed", ->
pane.on 'item-removed', itemRemovedHandler = jasmine.createSpy("itemRemovedHandler")
pane.destroyItem(item2)
expect(itemRemovedHandler).toHaveBeenCalledWith(item2, 1, true)
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"
spyOn(atom, 'confirm').andReturn(0)
pane.destroyItem(item1)
expect(item1.save).toHaveBeenCalled()
expect(item1 in pane.items).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
spyOn(atom, 'showSaveDialogSync').andReturn("/selected/path")
spyOn(atom, 'confirm').andReturn(0)
pane.destroyItem(item1)
expect(atom.showSaveDialogSync).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalledWith("/selected/path")
expect(item1 in pane.items).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", ->
spyOn(atom, 'confirm').andReturn(2)
pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
expect(item1 in pane.items).toBe false
expect(item1.isDestroyed()).toBe true
describe "if the [Cancel] option is selected", ->
it "does not save, remove, or destroy the item", ->
spyOn(atom, 'confirm').andReturn(1)
pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
expect(item1 in pane.items).toBe true
expect(item1.isDestroyed()).toBe false
describe "when the last item is destroyed", ->
it "destroys the pane", ->
pane.destroyItem(item) for item in pane.getItems()
expect(pane.isDestroyed()).toBe true
describe "::destroyItems()", ->
it "destroys all items and the pane", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.destroyItems()
expect(item1.isDestroyed()).toBe true
expect(item2.isDestroyed()).toBe true
expect(item3.isDestroyed()).toBe true
expect(pane.isDestroyed()).toBe true
expect(pane.items).toEqual []
describe "when an item emits a destroyed event", ->
it "removes it from the list of items", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.items[1].destroy()
expect(pane.items).toEqual [item1, item3]
describe "::destroyInactiveItems()", ->
it "destroys all items but the active item", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
[item1, item2, item3] = pane.items
pane.activateItem(item2)
pane.destroyInactiveItems()
expect(pane.items).toEqual [item2]
describe "::saveActiveItem()", ->
pane = null
beforeEach ->
pane = new Pane(items: [new Item("A")])
spyOn(atom, 'showSaveDialogSync').andReturn('/selected/path')
describe "when the active item has a uri", ->
beforeEach ->
pane.activeItem.uri = "test"
describe "when the active item has a save method", ->
it "saves the current item", ->
pane.activeItem.save = jasmine.createSpy("save")
pane.saveActiveItem()
expect(pane.activeItem.save).toHaveBeenCalled()
describe "when the current item has no save method", ->
it "does nothing", ->
expect(pane.activeItem.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.activeItem.saveAs = jasmine.createSpy("saveAs")
pane.saveActiveItem()
expect(atom.showSaveDialogSync).toHaveBeenCalled()
expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item has no saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.saveActiveItem()
expect(atom.showSaveDialogSync).not.toHaveBeenCalled()
describe "::saveActiveItemAs()", ->
pane = null
beforeEach ->
pane = new Pane(items: [new Item("A")])
spyOn(atom, 'showSaveDialogSync').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.activeItem.path = __filename
pane.activeItem.saveAs = jasmine.createSpy("saveAs")
pane.saveActiveItemAs()
expect(atom.showSaveDialogSync).toHaveBeenCalledWith(__dirname)
expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item does not have a saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.saveActiveItemAs()
expect(atom.showSaveDialogSync).not.toHaveBeenCalled()
describe "::itemForUri(uri)", ->
it "returns the item for which a call to .getUri() returns the given uri", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")])
[item1, item2, item3] = pane.items
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)", ->
it "moves the item to the given index and emits an 'item-moved' event with the item and its new index", ->
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")])
[item1, item2, item3, item4] = pane.items
pane.on 'item-moved', itemMovedHandler = jasmine.createSpy("itemMovedHandler")
pane.moveItem(item1, 2)
expect(pane.getItems()).toEqual [item2, item3, item1, item4]
expect(itemMovedHandler).toHaveBeenCalledWith(item1, 2)
itemMovedHandler.reset()
pane.moveItem(item2, 3)
expect(pane.getItems()).toEqual [item3, item1, item4, item2]
expect(itemMovedHandler).toHaveBeenCalledWith(item2, 3)
itemMovedHandler.reset()
pane.moveItem(item2, 1)
expect(pane.getItems()).toEqual [item3, item2, item1, item4]
expect(itemMovedHandler).toHaveBeenCalledWith(item2, 1)
describe "::moveItemToPane(item, pane, index)", ->
[container, pane1, pane2] = []
[item1, item2, item3, item4, item5] = []
beforeEach ->
pane1 = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
container = new PaneContainer(root: pane1)
pane2 = pane1.splitRight(items: [new Item("D"), new Item("E")])
[item1, item2, item3] = pane1.items
[item4, item5] = pane2.items
it "moves the item to the given pane at the given index", ->
pane1.moveItemToPane(item2, pane2, 1)
expect(pane1.items).toEqual [item1, item3]
expect(pane2.items).toEqual [item4, item2, item5]
describe "when the moved item the last item in the source pane", ->
it "destroys the pane, but not the item", ->
item5.destroy()
pane2.moveItemToPane(item4, pane1, 0)
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "split methods", ->
[pane1, container] = []
@@ -84,21 +366,6 @@ describe "Pane", ->
pane2 = pane1.splitRight()
expect(pane2.focused).toBe true
describe "::destroyItem(item)", ->
describe "when the last item is destroyed", ->
it "destroys the pane", ->
pane = new Pane(items: ["A", "B"])
pane.destroyItem("A")
pane.destroyItem("B")
expect(pane.isDestroyed()).toBe true
describe "when an item emits a destroyed event", ->
it "removes it from the list of items", ->
pane = new Pane(items: [new Model, new Model, new Model])
[item1, item2, item3] = pane.items
pane.items[1].destroy()
expect(pane.items).toEqual [item1, item3]
describe "::destroy()", ->
[pane1, container] = []
@@ -112,6 +379,13 @@ describe "Pane", ->
expect(item1.isDestroyed()).toBe true
expect(item2.isDestroyed()).toBe true
describe "if the pane is active", ->
it "makes the next pane active", ->
pane2 = pane1.splitRight()
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", ->
pane2 = pane1.splitRight()
@@ -132,3 +406,32 @@ describe "Pane", ->
expect(container.root.children).toEqual [pane1, pane2]
pane2.destroy()
expect(container.root).toBe pane1
describe "serialization", ->
pane = null
beforeEach ->
pane = new Pane(items: [new Item("A", "a"), new Item("B", "b"), new Item("C", "c")])
it "can serialize and deserialize the pane and all its items", ->
newPane = pane.testSerialization()
expect(newPane.items).toEqual pane.items
it "restores the active item on deserialization", ->
pane.activateItemAtIndex(1)
newPane = pane.testSerialization()
expect(newPane.activeItem).toEqual newPane.items[1]
it "does not include items that cannot be deserialized", ->
spyOn(console, 'warn')
unserializable = {}
pane.activateItem(unserializable)
newPane = pane.testSerialization()
expect(newPane.activeItem).toEqual pane.items[0]
expect(newPane.items.length).toBe pane.items.length - 1
it "includes the pane's focus state in the serialized state", ->
pane.focus()
newPane = pane.testSerialization()
expect(newPane.focused).toBe true

View File

@@ -5,7 +5,7 @@ path = require 'path'
temp = require 'temp'
describe "PaneView", ->
[container, view1, view2, editor1, editor2, pane] = []
[container, view1, view2, editor1, editor2, pane, paneModel] = []
class TestView extends View
@deserialize: ({id, text}) -> new TestView({id, text})
@@ -23,377 +23,120 @@ describe "PaneView", ->
editor1 = atom.project.openSync('sample.js')
editor2 = atom.project.openSync('sample.txt')
pane = new PaneView(view1, editor1, view2, editor2)
paneModel = pane.model
container.setRoot(pane)
afterEach ->
atom.deserializers.remove(TestView)
describe "::initialize(items...)", ->
it "displays the first item in the pane", ->
expect(pane.itemViews.find('#view-1')).toExist()
describe "::activateItem(item)", ->
describe "when the active pane item changes", ->
it "hides all item views except the one being shown and sets the activeItem", ->
expect(pane.activeItem).toBe view1
expect(view1.css('display')).not.toBe 'none'
pane.activateItem(view2)
expect(view1.css('display')).toBe 'none'
expect(view2.css('display')).not.toBe 'none'
expect(pane.activeItem).toBe view2
it "triggers 'pane:active-item-changed' if the item isn't already the activeItem", ->
pane.activate()
it "triggers 'pane:active-item-changed'", ->
itemChangedHandler = jasmine.createSpy("itemChangedHandler")
container.on 'pane:active-item-changed', itemChangedHandler
expect(pane.activeItem).toBe view1
pane.activateItem(view2)
pane.activateItem(view2)
paneModel.activateItem(view2)
paneModel.activateItem(view2)
expect(itemChangedHandler.callCount).toBe 1
expect(itemChangedHandler.argsForCall[0][1]).toBe view2
itemChangedHandler.reset()
pane.activateItem(editor1)
paneModel.activateItem(editor1)
expect(itemChangedHandler).toHaveBeenCalled()
expect(itemChangedHandler.argsForCall[0][1]).toBe editor1
itemChangedHandler.reset()
describe "if the pane's active view is focused before calling activateItem", ->
it "focuses the new active view", ->
container.attachToDom()
pane.focus()
expect(pane.activeView).not.toBe view2
expect(pane.activeView).toMatchSelector ':focus'
pane.activateItem(view2)
expect(view2).toMatchSelector ':focus'
it "transfers focus to the new active view if the previous view was focused", ->
container.attachToDom()
pane.focus()
expect(pane.activeView).not.toBe view2
expect(pane.activeView).toMatchSelector ':focus'
paneModel.activateItem(view2)
expect(view2).toMatchSelector ':focus'
describe "when the given item isn't yet in the items list on the pane", ->
view3 = null
beforeEach ->
view3 = new TestView(id: 'view-3', text: "View 3")
pane.activateItem(editor1)
expect(pane.getActiveItemIndex()).toBe 1
describe "when the new activeItem is a model", ->
it "shows the item's view or creates and shows a new view for the item if none exists", ->
initialViewCount = pane.itemViews.find('.test-view').length
it "adds it to the items list after the active item", ->
pane.activateItem(view3)
expect(pane.getItems()).toEqual [view1, editor1, view3, view2, editor2]
expect(pane.activeItem).toBe view3
expect(pane.getActiveItemIndex()).toBe 2
model1 =
id: 'test-model-1'
text: 'Test Model 1'
serialize: -> {@id, @text}
getViewClass: -> TestView
it "triggers the 'item-added' event with the item and its index before the 'active-item-changed' event", ->
events = []
container.on 'pane:item-added', (e, item, index) -> events.push(['pane:item-added', item, index])
container.on 'pane:active-item-changed', (e, item) -> events.push(['pane:active-item-changed', item])
pane.activateItem(view3)
expect(events).toEqual [['pane:item-added', view3, 2], ['pane:active-item-changed', view3]]
model2 =
id: 'test-model-2'
text: 'Test Model 2'
serialize: -> {@id, @text}
getViewClass: -> TestView
describe "when showing a model item", ->
describe "when no view has yet been appended for that item", ->
it "appends and shows a view to display the item based on its `.getViewClass` method", ->
pane.activateItem(editor1)
editorView = pane.activeView
expect(editorView.css('display')).not.toBe 'none'
expect(editorView.editor).toBe editor1
paneModel.activateItem(model1)
paneModel.activateItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
describe "when a valid view has already been appended for another item", ->
it "multiple views are created for multiple items", ->
pane.activateItem(editor1)
pane.activateItem(editor2)
expect(pane.itemViews.find('.editor').length).toBe 2
editorView = pane.activeView
expect(editorView.css('display')).not.toBe 'none'
expect(editorView.editor).toBe editor2
paneModel.activatePreviousItem()
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
it "creates a new view with the item", ->
initialViewCount = pane.itemViews.find('.test-view').length
paneModel.destroyItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 1
model1 =
id: 'test-model-1'
text: 'Test Model 1'
serialize: -> {@id, @text}
getViewClass: -> TestView
paneModel.destroyItem(model1)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount
model2 =
id: 'test-model-2'
text: 'Test Model 2'
serialize: -> {@id, @text}
getViewClass: -> TestView
pane.activateItem(model1)
pane.activateItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.activatePreviousItem()
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.destroyItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 1
pane.destroyItem(model1)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount
describe "when showing a view item", ->
describe "when the new activeItem is a view", ->
it "appends it to the itemViews div if it hasn't already been appended and shows it", ->
expect(pane.itemViews.find('#view-2')).not.toExist()
pane.activateItem(view2)
paneModel.activateItem(view2)
expect(pane.itemViews.find('#view-2')).toExist()
expect(pane.activeView).toBe view2
paneModel.activateItem(view1)
paneModel.activateItem(view2)
expect(pane.itemViews.find('#view-2').length).toBe 1
describe "::destroyItem(item)", ->
describe "if the item is not modified", ->
it "removes the item and tries to call destroy on it", ->
pane.destroyItem(editor2)
expect(pane.getItems().indexOf(editor2)).toBe -1
expect(editor2.isDestroyed()).toBe true
describe "if the item is modified", ->
beforeEach ->
jasmine.unspy(editor2, 'shouldPromptToSave')
spyOn(editor2, 'save')
spyOn(editor2, 'saveAs')
editor2.insertText('a')
expect(editor2.isModified()).toBeTruthy()
describe "if the [Save] option is selected", ->
describe "when the item has a uri", ->
it "saves the item before removing and destroying it", ->
spyOn(atom, 'confirm').andReturn(0)
pane.destroyItem(editor2)
expect(editor2.save).toHaveBeenCalled()
expect(pane.getItems().indexOf(editor2)).toBe -1
expect(editor2.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", ->
editor2.buffer.setPath(undefined)
spyOn(atom, 'showSaveDialogSync').andReturn("/selected/path")
spyOn(atom, 'confirm').andReturn(0)
pane.destroyItem(editor2)
expect(atom.showSaveDialogSync).toHaveBeenCalled()
expect(editor2.saveAs).toHaveBeenCalledWith("/selected/path")
expect(pane.getItems().indexOf(editor2)).toBe -1
expect(editor2.isDestroyed()).toBe true
describe "if the [Don't Save] option is selected", ->
it "removes and destroys the item without saving it", ->
spyOn(atom, 'confirm').andReturn(2)
pane.destroyItem(editor2)
expect(editor2.save).not.toHaveBeenCalled()
expect(pane.getItems().indexOf(editor2)).toBe -1
expect(editor2.isDestroyed()).toBe true
describe "if the [Cancel] option is selected", ->
it "does not save, remove, or destroy the item", ->
spyOn(atom, 'confirm').andReturn(1)
pane.destroyItem(editor2)
expect(editor2.save).not.toHaveBeenCalled()
expect(pane.getItems().indexOf(editor2)).not.toBe -1
expect(editor2.isDestroyed()).toBe false
it "removes the item's associated view", ->
view1.remove = (selector, keepData) -> @wasRemoved = not keepData
pane.destroyItem(view1)
expect(view1.wasRemoved).toBe true
it "removes the item from the items list and shows the next item if it was showing", ->
pane.destroyItem(view1)
expect(pane.getItems()).toEqual [editor1, view2, editor2]
expect(pane.activeItem).toBe editor1
pane.activateItem(editor2)
pane.destroyItem(editor2)
expect(pane.getItems()).toEqual [editor1, view2]
expect(pane.activeItem).toBe editor1
it "triggers 'pane:item-removed' with the item and its former index", ->
describe "when an item is destroyed", ->
it "triggers the 'pane:item-removed' event with the item and its former index", ->
itemRemovedHandler = jasmine.createSpy("itemRemovedHandler")
pane.on 'pane:item-removed', itemRemovedHandler
pane.destroyItem(editor1)
paneModel.destroyItem(editor1)
expect(itemRemovedHandler).toHaveBeenCalled()
expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editor1, 1]
describe "when removing the last item", ->
it "removes the pane", ->
pane.destroyItem(item) for item in pane.getItems()
expect(pane.hasParent()).toBeFalsy()
describe "when the pane is focused", ->
it "shifts focus to the next pane", ->
expect(container.getRoot()).toBe pane
container.attachToDom()
pane2 = pane.splitRight(new TestView(id: 'view-3', text: 'View 3'))
pane.focus()
expect(pane).toMatchSelector(':has(:focus)')
pane.destroyItem(item) for item in pane.getItems()
expect(pane2).toMatchSelector ':has(:focus)'
describe "when the item is a view", ->
describe "when the destroyed item is a view", ->
it "removes the item from the 'item-views' div", ->
expect(view1.parent()).toMatchSelector pane.itemViews
pane.destroyItem(view1)
paneModel.destroyItem(view1)
expect(view1.parent()).not.toMatchSelector pane.itemViews
describe "when the item is a model", ->
it "removes the associated view only when all items that require it have been removed", ->
pane.activateItem(editor1)
pane.activateItem(editor2)
pane.destroyItem(editor2)
expect(pane.itemViews.find('.editor')).toExist()
describe "when the destroyed item is a model", ->
it "removes the associated view", ->
paneModel.activateItem(editor1)
expect(pane.itemViews.find('.editor').length).toBe 1
pane.destroyItem(editor1)
expect(pane.itemViews.find('.editor')).not.toExist()
expect(pane.itemViews.find('.editor').length).toBe 0
describe "::moveItem(item, index)", ->
it "moves the item to the given index and emits a 'pane:item-moved' event with the item and the new index", ->
itemMovedHandler = jasmine.createSpy("itemMovedHandler")
pane.on 'pane:item-moved', itemMovedHandler
pane.moveItem(view1, 2)
expect(pane.getItems()).toEqual [editor1, view2, view1, editor2]
describe "when an item is moved within the same pane", ->
it "emits a 'pane:item-moved' event with the item and the new index", ->
pane.on 'pane:item-moved', itemMovedHandler = jasmine.createSpy("itemMovedHandler")
paneModel.moveItem(view1, 2)
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [view1, 2]
itemMovedHandler.reset()
pane.moveItem(editor1, 3)
expect(pane.getItems()).toEqual [view2, view1, editor2, editor1]
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editor1, 3]
itemMovedHandler.reset()
pane.moveItem(editor1, 1)
expect(pane.getItems()).toEqual [view2, editor1, view1, editor2]
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editor1, 1]
itemMovedHandler.reset()
describe "::moveItemToPane(item, pane, index)", ->
[pane2, view3] = []
beforeEach ->
view3 = new TestView(id: 'view-3', text: "View 3")
pane2 = pane.splitRight(view3)
it "moves the item to the given pane at the given index", ->
pane.moveItemToPane(view1, pane2, 1)
expect(pane.getItems()).toEqual [editor1, view2, editor2]
expect(pane2.getItems()).toEqual [view3, view1]
describe "when it is the last item on the source pane", ->
it "removes the source pane, but does not destroy the item", ->
pane.destroyItem(view1)
pane.destroyItem(view2)
pane.destroyItem(editor2)
expect(pane.getItems()).toEqual [editor1]
pane.moveItemToPane(editor1, pane2, 1)
expect(pane.hasParent()).toBeFalsy()
expect(pane2.getItems()).toEqual [view3, editor1]
expect(editor1.isDestroyed()).toBe false
describe "when the item is a jQuery object", ->
it "preserves data by detaching instead of removing", ->
view1.data('preservative', 1234)
pane.moveItemToPane(view1, pane2, 1)
pane2.activateItemAtIndex(1)
expect(pane2.activeView.data('preservative')).toBe 1234
describe "pane:close", ->
it "destroys all items and removes the pane", ->
pane.activateItem(editor1)
pane.trigger 'pane:close'
expect(pane.hasParent()).toBeFalsy()
expect(editor2.isDestroyed()).toBe true
expect(editor1.isDestroyed()).toBe true
describe "pane:close-other-items", ->
it "destroys all items except the current", ->
pane.activateItem(editor1)
pane.trigger 'pane:close-other-items'
expect(editor2.isDestroyed()).toBe true
expect(pane.getItems()).toEqual [editor1]
describe "::saveActiveItem()", ->
describe "when the current item has a uri", ->
describe "when the current item has a save method", ->
it "saves the current item", ->
spyOn(editor2, 'save')
pane.activateItem(editor2)
pane.saveActiveItem()
expect(editor2.save).toHaveBeenCalled()
describe "when the current item has no save method", ->
it "does nothing", ->
pane.activeItem.getUri = -> 'you are eye'
expect(pane.activeItem.save).toBeUndefined()
pane.saveActiveItem()
describe "when the current item has no uri", ->
beforeEach ->
spyOn(atom, 'showSaveDialogSync').andReturn('/selected/path')
describe "when the current item has a saveAs method", ->
it "opens a save dialog and saves the current item as the selected path", ->
newEditor = atom.project.openSync()
spyOn(newEditor, 'saveAs')
pane.activateItem(newEditor)
pane.saveActiveItem()
expect(atom.showSaveDialogSync).toHaveBeenCalled()
expect(newEditor.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item has no saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.saveActiveItem()
expect(atom.showSaveDialogSync).not.toHaveBeenCalled()
describe "::saveActiveItemAs()", ->
beforeEach ->
spyOn(atom, 'showSaveDialogSync').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", ->
spyOn(editor2, 'saveAs')
pane.activateItem(editor2)
pane.saveActiveItemAs()
expect(atom.showSaveDialogSync).toHaveBeenCalledWith(path.dirname(editor2.getPath()))
expect(editor2.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item does not have a saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.saveActiveItemAs()
expect(atom.showSaveDialogSync).not.toHaveBeenCalled()
describe "pane:show-next-item and pane:show-previous-item", ->
it "advances forward/backward through the pane's items, looping around at either end", ->
expect(pane.activeItem).toBe view1
pane.trigger 'pane:show-previous-item'
expect(pane.activeItem).toBe editor2
pane.trigger 'pane:show-previous-item'
expect(pane.activeItem).toBe view2
pane.trigger 'pane:show-next-item'
expect(pane.activeItem).toBe editor2
pane.trigger 'pane:show-next-item'
expect(pane.activeItem).toBe view1
describe "pane:show-item-N events", ->
it "shows the (n-1)th item if it exists", ->
pane.trigger 'pane:show-item-2'
expect(pane.activeItem).toBe pane.itemAtIndex(1)
pane.trigger 'pane:show-item-1'
expect(pane.activeItem).toBe pane.itemAtIndex(0)
pane.trigger 'pane:show-item-9' # don't fail on out-of-bounds indices
expect(pane.activeItem).toBe pane.itemAtIndex(0)
describe "when an item is moved to another pane", ->
it "detaches the item's view rather than removing it", ->
paneModel2 = paneModel.splitRight()
view1.data('preservative', 1234)
paneModel.moveItemToPane(view1, paneModel2, 1)
expect(view1.data('preservative')).toBe 1234
paneModel2.activateItemAtIndex(1)
expect(view1.data('preservative')).toBe 1234
describe "when the title of the active item changes", ->
it "emits pane:active-item-title-changed", ->
@@ -424,12 +167,7 @@ describe "PaneView", ->
waitsFor ->
pane.items.length == 4
describe "::remove()", ->
it "destroys all the pane's items", ->
pane.remove()
expect(editor1.isDestroyed()).toBe true
expect(editor2.isDestroyed()).toBe true
describe "when a pane is destroyed", ->
it "triggers a 'pane:removed' event with the pane", ->
removedHandler = jasmine.createSpy("removedHandler")
container.on 'pane:removed', removedHandler
@@ -437,52 +175,28 @@ describe "PaneView", ->
expect(removedHandler).toHaveBeenCalled()
expect(removedHandler.argsForCall[0][1]).toBe pane
describe "when there are other panes", ->
describe "if the destroyed pane has focus", ->
[paneToLeft, paneToRight] = []
beforeEach ->
pane.activateItem(editor1)
paneToLeft = pane.splitLeft(pane.copyActiveItem())
paneToRight = pane.splitRight(pane.copyActiveItem())
container.attachToDom()
describe "if it is not the last pane in the container", ->
it "focuses the next pane", ->
paneModel.activateItem(editor1)
pane2Model = paneModel.splitRight(items: [paneModel.copyActiveItem()])
pane2 = pane2Model._view
container.attachToDom()
expect(pane.hasFocus()).toBe false
pane2Model.destroy()
expect(pane.hasFocus()).toBe true
describe "when the removed pane is active", ->
it "makes the next the next pane active and focuses it", ->
pane.activate()
pane.remove()
expect(paneToLeft.isActive()).toBeFalsy()
expect(paneToRight.isActive()).toBeTruthy()
expect(paneToRight).toMatchSelector ':has(:focus)'
describe "when the removed pane is not active", ->
it "does not affect the active pane or the focus", ->
paneToLeft.focus()
expect(paneToLeft.isActive()).toBeTruthy()
expect(paneToRight.isActive()).toBeFalsy()
pane.remove()
expect(paneToLeft.isActive()).toBeTruthy()
expect(paneToRight.isActive()).toBeFalsy()
expect(paneToLeft).toMatchSelector ':has(:focus)'
describe "when it is the last pane", ->
beforeEach ->
expect(container.getPanes().length).toBe 1
atom.workspaceView = focus: jasmine.createSpy("workspaceView.focus")
describe "when the removed pane is focused", ->
it "calls focus on workspaceView so we don't lose focus", ->
describe "if it is the last pane in the container", ->
it "shifts focus to the workspace view", ->
atom.workspaceView = {focus: jasmine.createSpy("atom.workspaceView.focus")}
container.attachToDom()
pane.focus()
pane.remove()
expect(container.hasFocus()).toBe true
paneModel.destroy()
expect(atom.workspaceView.focus).toHaveBeenCalled()
describe "when the removed pane is not focused", ->
it "does not call focus on root view", ->
expect(pane).not.toMatchSelector ':has(:focus)'
pane.remove()
expect(atom.workspaceView.focus).not.toHaveBeenCalled()
describe "::getNextPane()", ->
it "returns the next pane if one exists, wrapping around from the last pane to the first", ->
pane.activateItem(editor1)
@@ -491,137 +205,67 @@ describe "PaneView", ->
expect(pane.getNextPane()).toBe pane2
expect(pane2.getNextPane()).toBe pane
describe "when the pane's active status changes", ->
[pane2, pane2Model] = []
beforeEach ->
pane2Model = paneModel.splitRight(items: [pane.copyActiveItem()])
pane2 = pane2Model._view
expect(pane2Model.isActive()).toBe true
it "adds or removes the .active class as appropriate", ->
expect(pane).not.toHaveClass('active')
paneModel.activate()
expect(pane).toHaveClass('active')
pane2Model.activate()
expect(pane).not.toHaveClass('active')
it "triggers 'pane:became-active' or 'pane:became-inactive' according to the current status", ->
pane.on 'pane:became-active', becameActiveHandler = jasmine.createSpy("becameActiveHandler")
pane.on 'pane:became-inactive', becameInactiveHandler = jasmine.createSpy("becameInactiveHandler")
paneModel.activate()
expect(becameActiveHandler.callCount).toBe 1
expect(becameInactiveHandler.callCount).toBe 0
pane2Model.activate()
expect(becameActiveHandler.callCount).toBe 1
expect(becameInactiveHandler.callCount).toBe 1
describe "when the pane is focused", ->
beforeEach ->
container.attachToDom()
it "focuses the active item view", ->
it "transfers focus to the active view", ->
focusHandler = jasmine.createSpy("focusHandler")
pane.activeItem.on 'focus', focusHandler
pane.focus()
expect(focusHandler).toHaveBeenCalled()
it "triggers 'pane:became-active' if it was not previously active", ->
pane2 = pane.splitRight(view2) # Make pane inactive
it "makes the pane active", ->
paneModel.splitRight(items: [pane.copyActiveItem()])
expect(paneModel.isActive()).toBe false
pane.focus()
expect(paneModel.isActive()).toBe true
becameActiveHandler = jasmine.createSpy("becameActiveHandler")
pane.on 'pane:became-active', becameActiveHandler
expect(pane.isActive()).toBeFalsy()
pane.focusin()
expect(pane.isActive()).toBeTruthy()
pane.focusin()
expect(becameActiveHandler.callCount).toBe 1
it "triggers 'pane:became-inactive' when it was previously active", ->
pane2 = pane.splitRight(view2) # Make pane inactive
becameInactiveHandler = jasmine.createSpy("becameInactiveHandler")
pane.on 'pane:became-inactive', becameInactiveHandler
expect(pane.isActive()).toBeFalsy()
pane.focusin()
expect(pane.isActive()).toBeTruthy()
pane.splitRight(pane.copyActiveItem())
expect(pane.isActive()).toBeFalsy()
expect(becameInactiveHandler.callCount).toBe 1
describe "split methods", ->
[pane1, view3, view4] = []
beforeEach ->
describe "when a pane is split", ->
it "builds the appropriate pane-row and pane-column views", ->
pane1 = pane
pane1Model = pane.model
pane.activateItem(editor1)
view3 = new TestView(id: 'view-3', text: 'View 3')
view4 = new TestView(id: 'view-4', text: 'View 4')
describe "splitRight(items...)", ->
it "builds a row if needed, then appends a new pane after itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane1.splitRight(pane1.copyActiveItem())
expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]]
expect(pane2.items).toEqual [editor1]
expect(pane2.activeItem).not.toBe editor1 # it's a copy
pane2Model = pane1Model.splitRight(items: [pane1Model.copyActiveItem()])
pane3Model = pane2Model.splitDown(items: [pane2Model.copyActiveItem()])
pane2 = pane2Model._view
pane3 = pane3Model._view
pane3 = pane2.splitRight(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.pane-row .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]]
expect(container.find('> .pane-row > .pane').toArray()).toEqual [pane1[0]]
expect(container.find('> .pane-row > .pane-column > .pane').toArray()).toEqual [pane2[0], pane3[0]]
it "builds a row if needed, then appends a new pane after itself ", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane1.splitRight()
expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0]]
expect(pane2.items).toEqual []
expect(pane2.activeItem).toBeUndefined()
pane3 = pane2.splitRight()
expect(container.find('.pane-row .pane').toArray()).toEqual [pane1[0], pane2[0], pane3[0]]
expect(pane3.items).toEqual []
expect(pane3.activeItem).toBeUndefined()
describe "splitLeft(items...)", ->
it "builds a row if needed, then appends a new pane before itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitLeft(pane1.copyActiveItem())
expect(container.find('.pane-row .pane').toArray()).toEqual [pane2[0], pane[0]]
expect(pane2.items).toEqual [editor1]
expect(pane2.activeItem).not.toBe editor1 # it's a copy
pane3 = pane2.splitLeft(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.pane-row .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]]
describe "splitDown(items...)", ->
it "builds a column if needed, then appends a new pane after itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitDown(pane1.copyActiveItem())
expect(container.find('.pane-column .pane').toArray()).toEqual [pane[0], pane2[0]]
expect(pane2.items).toEqual [editor1]
expect(pane2.activeItem).not.toBe editor1 # it's a copy
pane3 = pane2.splitDown(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.pane-column .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]]
describe "splitUp(items...)", ->
it "builds a column if needed, then appends a new pane before itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitUp(pane1.copyActiveItem())
expect(container.find('.pane-column .pane').toArray()).toEqual [pane2[0], pane[0]]
expect(pane2.items).toEqual [editor1]
expect(pane2.activeItem).not.toBe editor1 # it's a copy
pane3 = pane2.splitUp(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.pane-column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]]
describe "::itemForUri(uri)", ->
it "returns the item for which a call to .getUri() returns the given uri", ->
expect(pane.itemForUri(editor1.getUri())).toBe editor1
expect(pane.itemForUri(editor2.getUri())).toBe editor2
pane1Model.destroy()
expect(container.find('> .pane-column > .pane').toArray()).toEqual [pane2[0], pane3[0]]
describe "serialization", ->
it "can serialize and deserialize the pane and all its items", ->
newPane = new PaneView(pane.model.testSerialization())
expect(newPane.getItems()).toEqual [view1, editor1, view2, editor2]
it "restores the active item on deserialization", ->
pane.activateItem(editor2)
newPane = new PaneView(pane.model.testSerialization())
expect(newPane.activeItem).toEqual editor2
it "does not show items that cannot be deserialized", ->
spyOn(console, 'warn')
class Unserializable
getViewClass: -> TestView
pane.activateItem(new Unserializable)
newPane = new PaneView(pane.model.testSerialization())
expect(newPane.activeItem).toEqual pane.items[0]
expect(newPane.items.length).toBe pane.items.length - 1
it "focuses the pane after attach only if had focus when serialized", ->
container.attachToDom()
pane.focus()

View File

@@ -44,8 +44,6 @@ class PaneView extends View
@handleEvents()
handleEvents: ->
@subscribe @model, 'destroyed', => @remove()
@subscribe @model.$activeItem, @onActiveItemChanged
@subscribe @model, 'item-added', @onItemAdded
@subscribe @model, 'item-removed', @onItemRemoved