mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 18:24:09 +03:00
516 lines
20 KiB
CoffeeScript
516 lines
20 KiB
CoffeeScript
{Model} = require 'theorist'
|
|
Pane = require '../src/pane'
|
|
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]
|
|
|
|
it "compacts the items array", ->
|
|
pane = new Pane(items: [undefined, new Item("A"), null, new Item("B")])
|
|
expect(pane.items.length).toBe 2
|
|
expect(pane.activeItem).toBe pane.items[0]
|
|
|
|
describe "::addItem(item, index)", ->
|
|
it "adds the item at the given index", ->
|
|
pane = new Pane(items: [new Item("A"), new Item("B")])
|
|
[item1, item2] = pane.items
|
|
item3 = new Item("C")
|
|
pane.addItem(item3, 1)
|
|
expect(pane.items).toEqual [item1, item3, item2]
|
|
|
|
it "adds the item after the active item ", ->
|
|
pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")])
|
|
[item1, item2, item3] = pane.items
|
|
pane.activateItem(item2)
|
|
item4 = new Item("D")
|
|
pane.addItem(item4)
|
|
expect(pane.items).toEqual [item1, item2, item4, item3]
|
|
|
|
it "sets the active item after adding the first item", ->
|
|
pane = new Pane
|
|
item = new Item("A")
|
|
events = []
|
|
pane.on 'item-added', -> events.push('item-added')
|
|
pane.$activeItem.changes.onValue -> events.push('active-item-changed')
|
|
|
|
pane.addItem(item)
|
|
expect(pane.activeItem).toBe item
|
|
expect(events).toEqual ['item-added', 'active-item-changed']
|
|
|
|
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", ->
|
|
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
|
|
|
|
describe "when the destroyed item is the active item and is the first item", ->
|
|
it "activates the next item", ->
|
|
expect(pane.activeItem).toBe item1
|
|
pane.destroyItem(item1)
|
|
expect(pane.activeItem).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.activeItem).toBe item2
|
|
pane.destroyItem(item2)
|
|
expect(pane.activeItem).toBe item1
|
|
|
|
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", ->
|
|
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.activeItem).toBeUndefined()
|
|
expect(-> pane.saveActiveItem()).not.toThrow()
|
|
expect(-> pane.saveActiveItemAs()).not.toThrow()
|
|
|
|
describe "when the 'core.destroyEmptyPanes' config option is true", ->
|
|
it "destroys the pane", ->
|
|
atom.config.set('core.destroyEmptyPanes', true)
|
|
pane.destroyItem(item) for item in pane.getItems()
|
|
expect(pane.isDestroyed()).toBe true
|
|
|
|
describe "::destroyActiveItem()", ->
|
|
it "destroys the active item", ->
|
|
pane = new Pane(items: [new Item("A"), new Item("B")])
|
|
activeItem = pane.activeItem
|
|
pane.destroyActiveItem()
|
|
expect(activeItem.isDestroyed()).toBe true
|
|
expect(activeItem in pane.items).toBe false
|
|
|
|
it "does not throw an exception if there are no more items", ->
|
|
pane = new Pane
|
|
pane.destroyActiveItem()
|
|
|
|
describe "::destroyItems()", ->
|
|
it "destroys all items", ->
|
|
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.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(__filename)
|
|
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", ->
|
|
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 "split methods", ->
|
|
[pane1, container] = []
|
|
|
|
beforeEach ->
|
|
pane1 = new Pane(items: ["A"])
|
|
container = new PaneContainer(root: pane1)
|
|
|
|
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: ["B"])
|
|
pane3 = pane1.splitLeft(items: ["C"])
|
|
expect(container.root.orientation).toBe 'horizontal'
|
|
expect(container.root.children).toEqual [pane2, pane3, pane1]
|
|
|
|
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: ["B"])
|
|
pane3 = pane1.splitLeft(items: ["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: ["B"])
|
|
pane3 = pane1.splitRight(items: ["C"])
|
|
expect(container.root.orientation).toBe 'horizontal'
|
|
expect(container.root.children).toEqual [pane1, pane3, pane2]
|
|
|
|
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: ["B"])
|
|
pane3 = pane1.splitRight(items: ["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: ["B"])
|
|
pane3 = pane1.splitUp(items: ["C"])
|
|
expect(container.root.orientation).toBe 'vertical'
|
|
expect(container.root.children).toEqual [pane2, pane3, pane1]
|
|
|
|
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: ["B"])
|
|
pane3 = pane1.splitUp(items: ["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: ["B"])
|
|
pane3 = pane1.splitDown(items: ["C"])
|
|
expect(container.root.orientation).toBe 'vertical'
|
|
expect(container.root.children).toEqual [pane1, pane3, pane2]
|
|
|
|
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: ["B"])
|
|
pane3 = pane1.splitDown(items: ["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 "::destroy()", ->
|
|
[container, pane1, pane2] = []
|
|
|
|
beforeEach ->
|
|
container = new PaneContainer
|
|
pane1 = container.root
|
|
pane1.addItems([new Item("A"), new Item("B")])
|
|
pane2 = pane1.splitRight()
|
|
|
|
it "destroys the pane's destroyable items", ->
|
|
[item1, item2] = pane1.items
|
|
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 "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
|