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 }).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() 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).toEqual([
        ['will-destroy-item', { item: item2, index: 1 }],
        ['will-destroy-pane-item', { item: item2, index: 1, 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]);
    });
  });
});
