mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
f865a75c94
Fixes #14722
1496 lines
52 KiB
JavaScript
1496 lines
52 KiB
JavaScript
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')
|
|
const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
|
|
|
|
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", async () => {
|
|
pane = null
|
|
const pendingSpy = jasmine.createSpy('onItemDidTerminatePendingState')
|
|
const destroySpy = jasmine.createSpy('onWillDestroyItem')
|
|
|
|
await atom.workspace.open('sample.txt', {pending: true}).then(() => {
|
|
pane = atom.workspace.getActivePane()
|
|
})
|
|
|
|
pane.onItemDidTerminatePendingState(pendingSpy)
|
|
pane.onWillDestroyItem(destroySpy)
|
|
|
|
await atom.workspace.open('sample.js', {pending: true})
|
|
|
|
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', async () => {
|
|
itemURI = 'test'
|
|
confirm.andReturn(0)
|
|
|
|
await pane.destroyItem(item1)
|
|
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', async () => {
|
|
itemURI = null
|
|
|
|
showSaveDialog.andReturn('/selected/path')
|
|
confirm.andReturn(0)
|
|
|
|
await pane.destroyItem(item1)
|
|
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', async () => {
|
|
confirm.andReturn(2)
|
|
|
|
await pane.destroyItem(item1)
|
|
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', async () => {
|
|
confirm.andReturn(1)
|
|
|
|
await 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', async () => {
|
|
await 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', async () => {
|
|
const pane = new Pane(paneParams({items: [new Item('A'), new Item('B'), new Item('C')]}))
|
|
const [item1, item2, item3] = pane.getItems()
|
|
|
|
await pane.destroyItems()
|
|
expect(item1.isDestroyed()).toBe(true)
|
|
expect(item2.isDestroyed()).toBe(true)
|
|
expect(item3.isDestroyed()).toBe(true)
|
|
expect(pane.getItems()).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('::observeItems()', () => {
|
|
it('invokes the observer with all current and future items', () => {
|
|
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', async () => {
|
|
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
|
|
await pane.saveActiveItem()
|
|
expect(showSaveDialog).toHaveBeenCalled()
|
|
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
|
|
})
|
|
})
|
|
|
|
describe('when the current item has no saveAs method', () => {
|
|
it('does nothing', async () => {
|
|
expect(pane.getActiveItem().saveAs).toBeUndefined()
|
|
await pane.saveActiveItem()
|
|
expect(showSaveDialog).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('does nothing if the user cancels choosing a path', async () => {
|
|
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
|
|
showSaveDialog.andReturn(undefined)
|
|
await pane.saveActiveItem()
|
|
expect(pane.getActiveItem().saveAs).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("when the item's saveAs 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'
|
|
throw 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', async () => {
|
|
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)
|
|
await pane.close()
|
|
expect(confirm).toHaveBeenCalled()
|
|
expect(item1.save).toHaveBeenCalled()
|
|
expect(pane.isDestroyed()).toBe(true)
|
|
})
|
|
|
|
it('does not destroy the pane if the user clicks cancel', async () => {
|
|
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)
|
|
|
|
await pane.close()
|
|
expect(confirm).toHaveBeenCalled()
|
|
expect(item1.save).not.toHaveBeenCalled()
|
|
expect(pane.isDestroyed()).toBe(false)
|
|
})
|
|
|
|
it('does not destroy the pane if the user starts to save but then does not choose a path', async () => {
|
|
const pane = new Pane(paneParams({items: [new Item('A'), new Item('B')]}))
|
|
const [item1] = pane.getItems()
|
|
|
|
item1.shouldPromptToSave = () => true
|
|
item1.saveAs = jasmine.createSpy('saveAs')
|
|
|
|
confirm.andReturn(0)
|
|
showSaveDialog.andReturn(undefined)
|
|
|
|
await pane.close()
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
|
|
expect(confirm.callCount).toBe(1)
|
|
expect(item1.saveAs).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', async () => {
|
|
let confirmations = 0
|
|
confirm.andCallFake(() => {
|
|
confirmations++
|
|
if (confirmations === 1) {
|
|
return 0 // click save
|
|
} else {
|
|
return 1
|
|
}
|
|
}) // click cancel
|
|
|
|
await pane.close()
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
|
|
expect(confirmations).toBe(2)
|
|
expect(item1.save).toHaveBeenCalled()
|
|
expect(pane.isDestroyed()).toBe(false)
|
|
})
|
|
|
|
it('does destroy the pane if the user saves the file under a new name', async () => {
|
|
item1.saveAs = jasmine.createSpy('saveAs').andReturn(true)
|
|
|
|
let confirmations = 0
|
|
confirm.andCallFake(() => {
|
|
confirmations++
|
|
return 0
|
|
}) // save and then save as
|
|
|
|
showSaveDialog.andReturn('new/path')
|
|
|
|
await pane.close()
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
|
|
expect(confirmations).toBe(2)
|
|
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
|
|
expect(item1.save).toHaveBeenCalled()
|
|
expect(item1.saveAs).toHaveBeenCalled()
|
|
expect(pane.isDestroyed()).toBe(true)
|
|
})
|
|
|
|
it('asks again if the saveAs also fails', async () => {
|
|
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')
|
|
|
|
await pane.close()
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
|
|
expect(confirmations).toBe(3)
|
|
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
|
|
expect(item1.save).toHaveBeenCalled()
|
|
expect(item1.saveAs).toHaveBeenCalled()
|
|
expect(pane.isDestroyed()).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('::destroy()', () => {
|
|
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, pane, eventCount
|
|
|
|
beforeEach(async () => {
|
|
editor1 = await atom.workspace.open('sample.txt', {pending: true})
|
|
pane = atom.workspace.getActivePane()
|
|
eventCount = 0
|
|
editor1.onDidTerminatePendingState(() => eventCount++)
|
|
})
|
|
|
|
it('does not open file in pending state by default', async () => {
|
|
await atom.workspace.open('sample.js')
|
|
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', async () => {
|
|
const originalText = editor1.getText()
|
|
editor1.insertText('Some text')
|
|
advanceClock(editor1.getBuffer().stoppedChangingDelay)
|
|
|
|
await editor1.save()
|
|
|
|
editor1.insertText('More text')
|
|
advanceClock(editor1.getBuffer().stoppedChangingDelay)
|
|
|
|
expect(pane.getPendingItem()).toBeNull()
|
|
expect(eventCount).toBe(1)
|
|
|
|
// Reset fixture back to original state
|
|
editor1.setText(originalText)
|
|
await 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])
|
|
})
|
|
})
|
|
})
|