mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-11 19:17:09 +03:00
1732 lines
58 KiB
JavaScript
1732 lines
58 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 { conditionPromise, timeoutPromise } = 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']));
|
|
});
|
|
|
|
it('subscribes to be notified when item terminates its pending state', () => {
|
|
const fakeDisposable = { dispose: () => {} };
|
|
const spy = jasmine
|
|
.createSpy('onDidTerminatePendingState')
|
|
.andReturn(fakeDisposable);
|
|
|
|
const pane = new Pane(paneParams({ items: [] }));
|
|
const item = {
|
|
getTitle: () => '',
|
|
onDidTerminatePendingState: spy
|
|
};
|
|
pane.addItem(item);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('subscribes to be notified when item is destroyed', () => {
|
|
const fakeDisposable = { dispose: () => {} };
|
|
const spy = jasmine.createSpy('onDidDestroy').andReturn(fakeDisposable);
|
|
|
|
const pane = new Pane(paneParams({ items: [] }));
|
|
const item = {
|
|
getTitle: () => '',
|
|
onDidDestroy: spy
|
|
};
|
|
pane.addItem(item);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
|
|
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 });
|
|
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('does nothing if prevented', () => {
|
|
const container = new PaneContainer({
|
|
config: atom.config,
|
|
deserializerManager: atom.deserializers,
|
|
applicationDelegate: atom.applicationDelegate
|
|
});
|
|
|
|
pane.setContainer(container);
|
|
container.onWillDestroyPaneItem(e => e.prevent());
|
|
pane.itemStack = [item2, item3, item1];
|
|
|
|
pane.activateItem(item1);
|
|
expect(pane.getActiveItem()).toBe(item1);
|
|
pane.destroyItem(item3);
|
|
expect(pane.itemStack).toEqual([item2, item3, item1]);
|
|
expect(pane.getActiveItem()).toBe(item1);
|
|
|
|
pane.destroyItem(item1);
|
|
expect(pane.itemStack).toEqual([item2, item3, item1]);
|
|
expect(pane.getActiveItem()).toBe(item1);
|
|
|
|
pane.destroyItem(item2);
|
|
expect(pane.itemStack).toEqual([item2, item3, item1]);
|
|
expect(pane.getActiveItem()).toBe(item1);
|
|
});
|
|
|
|
it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => {
|
|
jasmine.useRealClock();
|
|
pane.container = new PaneContainer({ config: atom.config, confirm });
|
|
const events = [];
|
|
|
|
pane.onWillDestroyItem(async event => {
|
|
expect(item2.isDestroyed()).toBe(false);
|
|
await timeoutPromise(50);
|
|
expect(item2.isDestroyed()).toBe(false);
|
|
events.push(['will-destroy-item', event]);
|
|
});
|
|
|
|
pane.container.onWillDestroyPaneItem(async event => {
|
|
expect(item2.isDestroyed()).toBe(false);
|
|
await timeoutPromise(50);
|
|
expect(item2.isDestroyed()).toBe(false);
|
|
events.push(['will-destroy-pane-item', event]);
|
|
});
|
|
|
|
await pane.destroyItem(item2);
|
|
expect(item2.isDestroyed()).toBe(true);
|
|
|
|
expect(events[0][0]).toEqual('will-destroy-item');
|
|
expect(events[0][1].item).toEqual(item2);
|
|
expect(events[0][1].index).toEqual(1);
|
|
|
|
expect(events[1][0]).toEqual('will-destroy-pane-item');
|
|
expect(events[1][1].item).toEqual(item2);
|
|
expect(events[1][1].index).toEqual(1);
|
|
expect(typeof events[1][1].prevent).toEqual('function');
|
|
expect(events[1][1].pane).toEqual(pane);
|
|
});
|
|
|
|
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.andCallFake((options, callback) => callback(0));
|
|
|
|
const success = await pane.destroyItem(item1);
|
|
expect(item1.save).toHaveBeenCalled();
|
|
expect(pane.getItems().includes(item1)).toBe(false);
|
|
expect(item1.isDestroyed()).toBe(true);
|
|
expect(success).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 () => {
|
|
jasmine.useRealClock();
|
|
|
|
itemURI = null;
|
|
|
|
showSaveDialog.andCallFake((options, callback) =>
|
|
callback('/selected/path')
|
|
);
|
|
confirm.andCallFake((options, callback) => callback(0));
|
|
|
|
const success = await pane.destroyItem(item1);
|
|
expect(showSaveDialog.mostRecentCall.args[0]).toEqual({});
|
|
|
|
await conditionPromise(() => item1.saveAs.callCount === 1);
|
|
expect(item1.saveAs).toHaveBeenCalledWith('/selected/path');
|
|
expect(pane.getItems().includes(item1)).toBe(false);
|
|
expect(item1.isDestroyed()).toBe(true);
|
|
expect(success).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("if the [Don't Save] option is selected", () => {
|
|
it('removes and destroys the item without saving it', async () => {
|
|
confirm.andCallFake((options, callback) => callback(2));
|
|
|
|
const success = await pane.destroyItem(item1);
|
|
expect(item1.save).not.toHaveBeenCalled();
|
|
expect(pane.getItems().includes(item1)).toBe(false);
|
|
expect(item1.isDestroyed()).toBe(true);
|
|
expect(success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('if the [Cancel] option is selected', () => {
|
|
it('does not save, remove, or destroy the item', async () => {
|
|
confirm.andCallFake((options, callback) => callback(1));
|
|
|
|
const success = await pane.destroyItem(item1);
|
|
expect(item1.save).not.toHaveBeenCalled();
|
|
expect(pane.getItems().includes(item1)).toBe(true);
|
|
expect(item1.isDestroyed()).toBe(false);
|
|
expect(success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when force=true', () => {
|
|
it('destroys the item immediately', async () => {
|
|
const success = await pane.destroyItem(item1, true);
|
|
expect(item1.save).not.toHaveBeenCalled();
|
|
expect(pane.getItems().includes(item1)).toBe(false);
|
|
expect(item1.isDestroyed()).toBe(true);
|
|
expect(success).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", async () => {
|
|
spyOn(item1, 'isPermanentDockItem').andReturn(true);
|
|
const success = await pane.destroyItem(item1);
|
|
expect(pane.getItems().includes(item1)).toBe(true);
|
|
expect(item1.isDestroyed()).toBe(false);
|
|
expect(success).toBe(false);
|
|
});
|
|
|
|
it('destroy the item if force=true', async () => {
|
|
spyOn(item1, 'isPermanentDockItem').andReturn(true);
|
|
const success = await pane.destroyItem(item1, true);
|
|
expect(pane.getItems().includes(item1)).toBe(false);
|
|
expect(item1.isDestroyed()).toBe(true);
|
|
expect(success).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.andCallFake((options, callback) =>
|
|
callback('/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.mostRecentCall.args[0]).toEqual({});
|
|
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.andCallFake((options, callback) => callback(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.andCallFake((options, callback) =>
|
|
callback('/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', async () => {
|
|
jasmine.useRealClock();
|
|
|
|
pane.getActiveItem().path = __filename;
|
|
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs');
|
|
pane.saveActiveItemAs();
|
|
expect(showSaveDialog.mostRecentCall.args[0]).toEqual({
|
|
defaultPath: __filename
|
|
});
|
|
|
|
await conditionPromise(
|
|
() => pane.getActiveItem().saveAs.callCount === 1
|
|
);
|
|
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.splitRight({ 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.splitUp({ 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.splitDown({ 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]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the pane is empty', () => {
|
|
describe('when `moveActiveItem: true` is passed in the params', () => {
|
|
it('gracefully ignores the moveActiveItem parameter', () => {
|
|
pane1.destroyItem(item1);
|
|
expect(pane1.getActiveItem()).toBe(undefined);
|
|
|
|
const pane2 = pane1.split('horizontal', 'before', {
|
|
moveActiveItem: true
|
|
});
|
|
expect(container.root.children).toEqual([pane2, pane1]);
|
|
|
|
expect(pane2.getActiveItem()).toBe(undefined);
|
|
});
|
|
});
|
|
|
|
describe('when `copyActiveItem: true` is passed in the params', () => {
|
|
it('gracefully ignores the copyActiveItem parameter', () => {
|
|
pane1.destroyItem(item1);
|
|
expect(pane1.getActiveItem()).toBe(undefined);
|
|
|
|
const pane2 = pane1.split('horizontal', 'before', {
|
|
copyActiveItem: true
|
|
});
|
|
expect(container.root.children).toEqual([pane2, pane1]);
|
|
|
|
expect(pane2.getActiveItem()).toBe(undefined);
|
|
});
|
|
});
|
|
});
|
|
|
|
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.andCallFake((options, callback) => callback(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.andCallFake((options, callback) => callback(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.andCallFake((options, callback) => callback(0));
|
|
showSaveDialog.andCallFake((options, callback) => callback(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((options, callback) => {
|
|
confirmations++;
|
|
if (confirmations === 1) {
|
|
callback(0); // click save
|
|
} else {
|
|
callback(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((options, callback) => {
|
|
confirmations++;
|
|
callback(0);
|
|
}); // save and then save as
|
|
|
|
showSaveDialog.andCallFake((options, callback) => callback('new/path'));
|
|
|
|
await pane.close();
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
|
|
expect(confirmations).toBe(2);
|
|
expect(
|
|
atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]
|
|
).toEqual({});
|
|
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((options, callback) => {
|
|
confirmations++;
|
|
if (confirmations < 3) {
|
|
callback(0); // save, save as, save as
|
|
} else {
|
|
callback(2); // don't save
|
|
}
|
|
});
|
|
|
|
showSaveDialog.andCallFake((options, callback) => callback('new/path'));
|
|
|
|
await pane.close();
|
|
expect(atom.applicationDelegate.confirm).toHaveBeenCalled();
|
|
expect(confirmations).toBe(3);
|
|
expect(
|
|
atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]
|
|
).toEqual({});
|
|
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]);
|
|
});
|
|
});
|
|
});
|