text:
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]);
    });
  });
});


text:
#ifndef SHD_EVENT_H_
#define SHD_EVENT_H_

#include "shadow.h"

typedef struct _Event Event;
typedef struct _EventFunctionTable EventFunctionTable;

/* required functions */
typedef void (*EventRunFunc)(Event* event, gpointer node); /* XXX: type is "Node*" */
typedef void (*EventFreeFunc)(Event* event);

void shadowevent_init(Event* event, EventFunctionTable* vtable);
gboolean shadowevent_run(Event* event);
void shadowevent_setSequence(Event* event, SimulationTime sequence);
SimulationTime shadowevent_getTime(Event* event);
void shadowevent_setTime(Event* event, SimulationTime time);
gpointer shadowevent_getNode(Event* event); /* XXX: return type is "Node*" */
void shadowevent_setNode(Event* event, gpointer node); /* XXX: type is "Node*" */
gint shadowevent_compare(const Event* a, const Event* b, gpointer user_data);
void shadowevent_free(Event* event);

#endif /* SHD_EVENT_H_ */

redpajama_set_name: RedPajamaGithub


text:
package net.tridentsdk.world.gen;

/**
 * This class generates props in the world such as trees,
 * flowers, tall grass, etc...
 *
 * @author TridentSDK
 * @since 0.5-alpha
 */
public interface PropGenerator {
    /**
     * A prop generator is implemented by overriding this
     * method and writing the generated blocks to the
     * context.
     *
     * @param chunkX the chunk x
     * @param chunkZ the chunk z
     * @param height the highest block at the X/Z
     * @param context the context
     */
    void generate(int chunkX, int chunkZ, int height, GeneratorContext context);
}


text:
Filters
=======

Filters are objects that run before and/or after [controller actions](structure-controllers.md#actions). For example,
an access control filter may run before actions to ensure that they are allowed to be accessed by particular end users;
a content compression filter may run after actions to compress the response content before sending them out to end users.

A filter may consist of a pre-filter (filtering logic applied *before* actions) and/or a post-filter (logic applied
*after* actions).

## Using Filters

Filters are essentially a special kind of [behaviors](concept-behaviors.md). Therefore, using filters is the same
as [using behaviors](concept-behaviors.md#attaching-behaviors). You can declare filters in a controller class
by overriding its [[yii\base\Controller::behaviors()|behaviors()]] method like the following:

```php
public function behaviors()
{
    return [
        [
            'class' => 'yii\filters\HttpCache',
            'only' => ['index', 'view'],
            'lastModified' => function ($action, $params) {
                $q = new \yii\db\Query();
                return $q->from('user')->max('updated_at');
            },
        ],
    ];
}
```

By default, filters declared in a controller class will be applied to *all* actions in that controller. You can,
however, explicitly specify which actions the filter should be applied to by configuring the
[[yii\base\ActionFilter::only|only]] property. In the above example, the `HttpCache` filter only applies to the
`index` and `view` actions. You can also configure the [[yii\base\ActionFilter::except|except]] property to prevent
some actions from being filtered.

Besides controllers, you can also declare filters in a [module](structure-modules.md) or [application](structure-applications.md).
When you do so, the filters will be applied to *all* controller actions belonging to that module or application,
unless you configure the filters' [[yii\base\ActionFilter::only|only]] and [[yii\base\ActionFilter::except|except]]
properties like described above.

> Note: When declaring filters in modules or applications, you should use [routes](structure-controllers.md#routes)
  instead of action IDs in the [[yii\base\ActionFilter::only|only]] and [[yii\base\ActionFilter::except|except]] properties.
  This is because action IDs alone cannot fully specify actions within the scope of a module or application.

When multiple filters are configured for a single action, they are applied according to the rules described below:

* Pre-filtering
    - Apply filters declared in the application in the order they are listed in `behaviors()`.
    - Apply filters declared in the module in the order they are listed in `behaviors()`.
    - Apply filters declared in the controller in the order they are listed in `behaviors()`.
    - If any of the filters cancel the action execution, the filters (both pre-filters and post-filters) after it will
      not be applied.
* Running the action if it passes the pre-filtering.
* Post-filtering
    - Apply filters declared in the controller in the reverse order they are listed in `behaviors()`.
    - Apply filters declared in the module in the reverse order they are listed in `behaviors()`.
    - Apply filters declared in the application in the reverse order they are listed in `behaviors()`.

## Creating Filters

To create a new action filter, extend from [[yii\base\ActionFilter]] and override the
[[yii\base\ActionFilter::beforeAction()|beforeAction()]] and/or [[yii\base\ActionFilter::afterAction()|afterAction()]]
methods. The former will be executed before an action runs while the latter after an action runs.
The return value of [[yii\base\ActionFilter::beforeAction()|beforeAction()]] determines whether an action should
be executed or not. If it is `false`, the filters after this one will be skipped and the action will not be executed.

The following example shows a filter that logs the action execution time:

```php
namespace app\components;

use Yii;
use yii\base\ActionFilter;

class ActionTimeFilter extends ActionFilter
{
    private $_startTime;

    public function beforeAction($action)
    {
        $this->_startTime = microtime(true);
        return parent::beforeAction($action);
    }

    public function afterAction($action, $result)
    {
        $time = microtime(true) - $this->_startTime;
        Yii::debug("Action '{$action->uniqueId}' spent $time second.");
        return parent::afterAction($action, $result);
    }
}
```

## Core Filters

Yii provides a set of commonly used filters, found primarily under the `yii\filters` namespace. In the following,
we will briefly introduce these filters.

### [[yii\filters\AccessControl|AccessControl]]

AccessControl provides simple access control based on a set of [[yii\filters\AccessControl::rules|rules]].
In particular, before an action is executed, AccessControl will examine the listed rules and find the first one
that matches the current context variables (such as user IP address, user login status, etc.) The matching
rule will dictate whether to allow or deny the execution of the requested action. If no rule matches, the access
will be denied.

The following example shows how to allow authenticated users to access the `create` and `update` actions
while denying all other users from accessing these two actions.

```php
use yii\filters\AccessControl;

public function behaviors()
{
    return [
        'access' => [
            'class' => AccessControl::class,
            'only' => ['create', 'update'],
            'rules' => [
                // allow authenticated users
                [
                    'allow' => true,
                    'roles' => ['@'],
                ],
                // everything else is denied by default
            ],
        ],
    ];
}
```

For more details about access control in general, please refer to the [Authorization](security-authorization.md) section.

### Authentication Method Filters

Authentication method filters are used to authenticate a user using various methods, such as
[HTTP Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), [OAuth 2](https://oauth.net/2/).
These filter classes are all under the `yii\filters\auth` namespace.

The following example shows how you can use [[yii\filters\auth\HttpBasicAuth]] to authenticate a user using
an access token based on HTTP Basic Auth method. Note that in order for this to work, your
[[yii\web\User::identityClass|user identity class]] must implement the [[yii\web\IdentityInterface::findIdentityByAccessToken()|findIdentityByAccessToken()]]
method.

```php
use yii\filters\auth\HttpBasicAuth;

public function behaviors()
{
    return [
        'basicAuth' => [
            'class' => HttpBasicAuth::class,
        ],
    ];
}
```

Authentication method filters are commonly used in implementing RESTful APIs. For more details, please refer to the
RESTful [Authentication](rest-authentication.md) section.

### [[yii\filters\ContentNegotiator|ContentNegotiator]]

ContentNegotiator supports response format negotiation and application language negotiation. It will try to
determine the response format and/or language by examining `GET` parameters and `Accept` HTTP header.

In the following example, ContentNegotiator is configured to support JSON and XML response formats, and
English (United States) and German languages.

```php
use yii\filters\ContentNegotiator;
use yii\web\Response;

public function behaviors()
{
    return [
        [
            'class' => ContentNegotiator::class,
            'formats' => [
                'application/json' => Response::FORMAT_JSON,
                'application/xml' => Response::FORMAT_XML,
            ],
            'languages' => [
                'en-US',
                'de',
            ],
        ],
    ];
}
```

Response formats and languages often need to be determined much earlier during
the [application lifecycle](structure-applications.md#application-lifecycle). For this reason, ContentNegotiator
is designed in a way such that it can also be used as a [bootstrapping component](structure-applications.md#bootstrap)
besides being used as a filter. For example, you may configure it in the [application configuration](structure-applications.md#application-configurations)
like the following:

```php
use yii\filters\ContentNegotiator;
use yii\web\Response;

[
    'bootstrap' => [
        [
            'class' => ContentNegotiator::class,
            'formats' => [
                'application/json' => Response::FORMAT_JSON,
                'application/xml' => Response::FORMAT_XML,
            ],
            'languages' => [
                'en-US',
                'de',
            ],
        ],
    ],
];
```

> Info: In case the preferred content type and language cannot be determined from a request, the first format and
  language listed in [[formats]] and [[languages]] will be used.

### [[yii\filters\HttpCache|HttpCache]]

HttpCache implements client-side caching by utilizing the `Last-Modified` and `Etag` HTTP headers.
For example,

```php
use yii\filters\HttpCache;

public function behaviors()
{
    return [
        [
            'class' => HttpCache::class,
            'only' => ['index'],
            'lastModified' => function ($action, $params) {
                $q = new \yii\db\Query();
                return $q->from('user')->max('updated_at');
            },
        ],
    ];
}
```

Please refer to the [HTTP Caching](caching-http.md) section for more details about using HttpCache.

### [[yii\filters\PageCache|PageCache]]

PageCache implements server-side caching of whole pages. In the following example, PageCache is applied
to the `index` action to cache the whole page for maximum 60 seconds or until the count of entries in the `post`
table changes. It also stores different versions of the page depending on the chosen application language.

```php
use yii\filters\PageCache;
use yii\caching\DbDependency;

public function behaviors()
{
    return [
        'pageCache' => [
            'class' => PageCache::class,
            'only' => ['index'],
            'duration' => 60,
            'dependency' => [
                'class' => DbDependency::class,
                'sql' => 'SELECT COUNT(*) FROM post',
            ],
            'variations' => [
                \Yii::$app->language,
            ]
        ],
    ];
}
```

Please refer to the [Page Caching](caching-page.md) section for more details about using PageCache.

### [[yii\filters\RateLimiter|RateLimiter]]

RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket).
It is primarily used in implementing RESTful APIs. Please refer to the [Rate Limiting](rest-rate-limiting.md) section
for details about using this filter.

### [[yii\filters\VerbFilter|VerbFilter]]

VerbFilter checks if the HTTP request methods are allowed by the requested actions. If not allowed, it will
throw an HTTP 405 exception. In the following example, VerbFilter is declared to specify a typical set of allowed
request methods for CRUD actions.

```php
use yii\filters\VerbFilter;

public function behaviors()
{
    return [
        'verbs' => [
            'class' => VerbFilter::class,
            'actions' => [
                'index'  => ['get'],
                'view'   => ['get'],
                'create' => ['get', 'post'],
                'update' => ['get', 'put', 'post'],
                'delete' => ['post', 'delete'],
            ],
        ],
    ];
}
```

### [[yii\filters\Cors|Cors]]

Cross-origin resource sharing [CORS](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.)
on a Web page to be requested from another domain outside the domain the resource originated from.
In particular, JavaScript's AJAX calls can use the XMLHttpRequest mechanism. Such "cross-domain" requests would
otherwise be forbidden by Web browsers, per the same origin security policy.
CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request.

The [[yii\filters\Cors|Cors filter]] should be defined before Authentication / Authorization filters to make sure the CORS headers
will always be sent.

```php
use yii\filters\Cors;
use yii\helpers\ArrayHelper;

public function behaviors()
{
    return ArrayHelper::merge([
        [
            'class' => Cors::class,
        ],
    ], parent::behaviors());
}
```

Also check the section on [REST Controllers](rest-controllers.md#cors) if you want to add the CORS filter to an
[[yii\rest\ActiveController]] class in your API.

The Cors filtering could be tuned using the [[yii\filters\Cors::$cors|$cors]] property.

* `cors['Origin']`: array used to define allowed origins. Can be `['*']` (everyone) or `['https://www.myserver.net', 'https://www.myotherserver.com']`. Default to `['*']`.
* `cors['Access-Control-Request-Method']`: array of allowed verbs like `['GET', 'OPTIONS', 'HEAD']`.  Default to `['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']`.
* `cors['Access-Control-Request-Headers']`: array of allowed headers. Can be `['*']` all headers or specific ones `['X-Request-With']`. Default to `['*']`.
* `cors['Access-Control-Allow-Credentials']`: define if current request can be made using credentials. Can be `true`, `false` or `null` (not set). Default to `null`.
* `cors['Access-Control-Max-Age']`: define lifetime of pre-flight request. Default to `86400`.

For example, allowing CORS for origin : `https://www.myserver.net` with method `GET`, `HEAD` and `OPTIONS` :

```php
use yii\filters\Cors;
use yii\helpers\ArrayHelper;

public function behaviors()
{
    return ArrayHelper::merge([
        [
            'class' => Cors::class,
            'cors' => [
                'Origin' => ['https://www.myserver.net'],
                'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
            ],
        ],
    ], parent::behaviors());
}
```

You may tune the CORS headers by overriding default parameters on a per action basis.
For example adding the `Access-Control-Allow-Credentials` for the `login` action could be done like this :

```php
use yii\filters\Cors;
use yii\helpers\ArrayHelper;

public function behaviors()
{
    return ArrayHelper::merge([
        [
            'class' => Cors::class,
            'cors' => [
                'Origin' => ['https://www.myserver.net'],
                'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
            ],
            'actions' => [
                'login' => [
                    'Access-Control-Allow-Credentials' => true,
                ]
            ]
        ],
    ], parent::behaviors());
}
```


text:
using System;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using Test.Set;

namespace PotionTest.Schema {

    [TestFixture()]
    public class GeneratedPatternBufferSetTest {

        [Test()]
        public void TestIntSetObjectSerialization() {
            SetTestPatternBuffer patternBuffer = new SetTestPatternBuffer();
            IntSetObject o1 = new IntSetObject(new HashSet<int>() { 1, 5, 900, 7038 });
            byte[] bytes = patternBuffer.Energize(o1);
            // 1 byte     IntSetObject type ID
            // 1 byte     null bits
            // 1 byte     set count
            // 4*4 bytes  4 invariant ints
            Assert.AreEqual(19, bytes.Length);
            object o2 = patternBuffer.Energize(bytes);
            IntSetObject p1 = (IntSetObject)o2;
            Assert.That(p1 is IntSetObject, Is.True);
            Assert.That(p1.IntSetValue.Count, Is.EqualTo(4));
            Assert.That(p1.IntSetValue.Count, Is.EqualTo(o1.IntSetValue.Count));
            Assert.That(o1.IntSetValue.Contains(1), Is.True);
            Assert.That(p1.IntSetValue.Contains(1), Is.True);
            Assert.That(o1.IntSetValue.Contains(5), Is.True);
            Assert.That(p1.IntSetValue.Contains(5), Is.True);
            Assert.That(o1.IntSetValue.Contains(900), Is.True);
            Assert.That(p1.IntSetValue.Contains(900), Is.True);
            Assert.That(o1.IntSetValue.Contains(7038), Is.True);
            Assert.That(p1.IntSetValue.Contains(7038), Is.True);
            Assert.That(o1 == p1, Is.False);
        }

        //[Test()]
        //public void TestVIntSetObjectSerialization() {
        //    SetTestPatternBuffer patternBuffer = new SetTestPatternBuffer();
        //    VIntSetObject o1 = new VIntSetObject(new HashSet<int>() { 1, 5, 900, 7038 });
        //    byte[] bytes = patternBuffer.Energize(o1);
        //    // 1 byte       VIntSetObject type ID
        //    // 1 byte       set count
        //    // 2 bytes      1
        //    // 2 bytes      5
        //    // 3 bytes      900
        //    // 3 bytes      7038
        //    Assert.That(bytes.Length, Is.EqualTo(12));
        //    object o2 = patternBuffer.Energize(bytes);
        //    VIntSetObject p1 = (VIntSetObject)o2;
        //    Assert.That(p1 is VIntSetObject, Is.True);
        //    Assert.That(p1.VintSetValue.Count, Is.EqualTo(4));
        //    Assert.That(p1.VintSetValue.Count, Is.EqualTo(o1.VintSetValue.Count));
        //    Assert.That(o1.VintSetValue.Contains(1), Is.True);
        //    Assert.That(p1.VintSetValue.Contains(1), Is.True);
        //    Assert.That(o1.VintSetValue.Contains(5), Is.True);
        //    Assert.That(p1.VintSetValue.Contains(5), Is.True);
        //    Assert.That(o1.VintSetValue.Contains(900), Is.True);
        //    Assert.That(p1.VintSetValue.Contains(900), Is.True);
        //    Assert.That(o1.VintSetValue.Contains(7038), Is.True);
        //    Assert.That(p1.VintSetValue.Contains(7038), Is.True);
        //    Assert.That(o1 == p1, Is.False);
        //}

        [Test()]
        public void TestStringSetObjectSerialization() {
            SetTestPatternBuffer patternBuffer = new SetTestPatternBuffer();
            StringSetObject o1 = new StringSetObject(new HashSet<string>() { "foo", "bar" });
            byte[] bytes = patternBuffer.Energize(o1);
            Assert.That(bytes.Length, Is.EqualTo(11));
            object o2 = patternBuffer.Energize(bytes);
            StringSetObject p1 = (StringSetObject)o2;
            Assert.That(p1 is StringSetObject, Is.True);
            Assert.That(p1.StringSetValue.Count, Is.EqualTo(2));
            Assert.That(o1 == p1, Is.False);
        }

        [Test()]
        public void TestThingSetObjectSerialization() {
            SetTestPatternBuffer patternBuffer = new SetTestPatternBuffer();
            Thing1 t1 = new Thing1(1, 2);
            Thing1 t2 = new Thing1(3, 4);
            Thing1SetObject o1 = new Thing1SetObject(new HashSet<Thing1>() { t1, t2 });
            byte[] bytes = patternBuffer.Energize(o1);
            // 1 byte   - Thing1SetObject type ID
            // 1 byte   - set count
            // 8 bytes  - 2 invariant ints
            // 8 bytes  - 2 invariant ints
            Assert.That(bytes.Length, Is.EqualTo(19));
            object o2 = patternBuffer.Energize(bytes);
            Thing1SetObject p1 = (Thing1SetObject)o2;
            Assert.That(p1 is Thing1SetObject, Is.True);
            Assert.That(p1.ThingSetValue.Count, Is.EqualTo(2));
            Assert.That(o1 == p1, Is.False);
        }

        [Test()]
        public void TestAbstractThingSetObjectSerialization() {
            SetTestPatternBuffer patternBuffer = new SetTestPatternBuffer();
            AbstractThing t1 = new Thing1(1, 2);
            AbstractThing t2 = new Thing2(3, 4);
            AbstractThingSetObject o1 = new AbstractThingSetObject(new HashSet<AbstractThing>() { t1, t2 });
            byte[] bytes = patternBuffer.Energize(o1);
            // 1 byte       AbstractThingSetObject type ID
            // 1 byte       null bits
            // 1 byte       set count
            // 1 byte       Thing1 type ID
            // 8 bytes      Thing1 2 invariant ints
            // 1 byte       Thing2 type ID
            // 8 bytes      Thing2 2 invariant ints
            Assert.That(bytes.Length, Is.EqualTo(21));
            object o2 = patternBuffer.Energize(bytes);
            AbstractThingSetObject p1 = (AbstractThingSetObject)o2;
            Assert.That(p1 is AbstractThingSetObject, Is.True);
            Assert.That(p1.AbstractThingSetValue.Count, Is.EqualTo(2));
            HashSet<int> expected = new HashSet<int>() { 1, 3 };
            foreach (AbstractThing t in p1.AbstractThingSetValue) {
                Console.WriteLine(t.IntValue1);
                expected.Remove(t.IntValue1);
            }
            Assert.That(expected.Count, Is.EqualTo(0));
            Assert.That(o1 == p1, Is.False);
        }

    }

}


text:
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-icons/iron-icons.js';
import '@polymer/paper-input/paper-input.js';
import '@polymer/polymer/lib/elements/dom-repeat.js';
import '@cwmr/paper-chip/paper-chip.js';

/**
* `paper-tags-input`
* Material Design input control for a collection of tags
*
* @customElement
* @polymer
* @demo demo/index.html
*/
class PaperTagsInput extends PolymerElement {
    static get template() {
        return html`
            <style>
                :host {
                    display: block;
                }
                :host[hidden] {
                    display: none !important;
                }
                input {
                    text-transform: lowercase;
                    height: 36px;
                    width: auto !important;
                    padding-left: 0.5em;
                }
                paper-chip {
                    margin: 2px;
                    padding-right: 6px;
                    cursor: pointer;
                }
                iron-icon {
                    --iron-icon-height: 20px;
                    --iron-icon-width: 20px;
                    color: var(--disabled-text-color);
                }
            </style>

            <paper-input label="[[label]]" placeholder="+tag" on-keydown="_onInputKeydown">
                <div slot="prefix">
                    <template is="dom-repeat" items="[[tags]]">
                        <paper-chip selectable="">[[item]] <iron-icon icon="icons:cancel" on-click="_onTagRemoveClicked"></iron-icon></paper-chip>
                    </template>
                </div>
            </paper-input>
`;
    }

    static get is() { return 'paper-tags-input'; }
    static get properties() {
        return {
            label: {
                type: String,
                value: 'Tags'
            },
            tags: {
                type: Array,
                notify: true,
                value: function() { return []; }
            }
        };
    }

    addTag(tag) {
        if (this.tags === null) {
            this.tags = [];
        }

        var trimmedTag = tag.replace(/^\s+/, '').replace(/\s+$/, '');
        if (trimmedTag !== '') {
            var tagIndex = this.tags.indexOf(trimmedTag);
            if (tagIndex === -1) {
                this.push('tags', trimmedTag);
            }
        }
    }
    removeTag(tag) {
        if (this.tags === null) {
            return;
        }
        var tagIndex = this.tags.indexOf(tag);
        if (tagIndex > -1) {
            this.splice('tags', tagIndex, 1);
        }
    }

    _onTagRemoveClicked(e) {
        this.removeTag(e.model.item);
    }
    _onInputKeydown(e) {
        if (e.keyCode === 13) {
            this.addTag(e.target.value.toLowerCase());
            e.target.value = '';
        }
    }
}

window.customElements.define(PaperTagsInput.is, PaperTagsInput);


text:
InfoBarContainer::Delegate::~Delegate() {
}

InfoBarContainer::InfoBarContainer(
    Delegate* delegate,
    chrome::search::SearchModel* search_model)
    : delegate_(delegate),
      infobar_service_(NULL),
      infobars_shown_(true),
      search_model_(search_model),
      top_arrow_target_height_(InfoBar::kDefaultArrowTargetHeight) {
  if (search_model_)
    search_model_->AddObserver(this);
}

InfoBarContainer::~InfoBarContainer() {
  // RemoveAllInfoBarsForDestruction() should have already cleared our infobars.
  DCHECK(infobars_.empty());
  if (search_model_)
    search_model_->RemoveObserver(this);
}

void InfoBarContainer::ChangeInfoBarService(InfoBarService* infobar_service) {
  registrar_.RemoveAll();

  // Note that HideAllInfoBars() sets |infobars_shown_| to false, because that's
  // what the other, instant-related callers want; but here we actually
  // explicitly want to reset this variable to true.  So do that after calling
  // the function.
  HideAllInfoBars();
  infobars_shown_ = true;
  infobars_shown_time_ = base::TimeTicks();

  infobar_service_ = infobar_service;
  if (infobar_service_) {
    content::Source<InfoBarService> source(infobar_service_);
    registrar_.Add(this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_ADDED,
                   source);
    registrar_.Add(this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
                   source);
    registrar_.Add(this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REPLACED,
                   source);

    for (size_t i = 0; i < infobar_service_->GetInfoBarCount(); ++i) {
      // As when we removed the infobars above, we prevent callbacks to
      // OnInfoBarAnimated() for each infobar.
      AddInfoBar(
          infobar_service_->GetInfoBarDelegateAt(i)->CreateInfoBar(
              infobar_service_),
          i, false, NO_CALLBACK);
    }
  }

  // Now that everything is up to date, signal the delegate to re-layout.
  OnInfoBarStateChanged(false);
}

int InfoBarContainer::GetVerticalOverlap(int* total_height) {
  // Our |total_height| is the sum of the preferred heights of the InfoBars
  // contained within us plus the |vertical_overlap|.
  int vertical_overlap = 0;
  int next_infobar_y = 0;

  for (InfoBars::iterator i(infobars_.begin()); i != infobars_.end(); ++i) {
    InfoBar* infobar = *i;
    next_infobar_y -= infobar->arrow_height();
    vertical_overlap = std::max(vertical_overlap, -next_infobar_y);
    next_infobar_y += infobar->total_height();
  }

  if (total_height)
    *total_height = next_infobar_y + vertical_overlap;
  return vertical_overlap;
}

void InfoBarContainer::SetMaxTopArrowHeight(int height) {
  // Decrease the height by the arrow stroke thickness, which is the separator
  // line height, because the infobar arrow target heights are without-stroke.
  top_arrow_target_height_ = std::min(
      std::max(height - InfoBar::kSeparatorLineHeight, 0),
      InfoBar::kMaximumArrowTargetHeight);
  UpdateInfoBarArrowTargetHeights();
}

void InfoBarContainer::OnInfoBarStateChanged(bool is_animating) {
  if (delegate_)
    delegate_->InfoBarContainerStateChanged(is_animating);
  UpdateInfoBarArrowTargetHeights();
  PlatformSpecificInfoBarStateChanged(is_animating);
}

void InfoBarContainer::RemoveInfoBar(InfoBar* infobar) {
  infobar->set_container(NULL);
  InfoBars::iterator i(std::find(infobars_.begin(), infobars_.end(), infobar));
  DCHECK(i != infobars_.end());
  PlatformSpecificRemoveInfoBar(infobar);
  infobars_.erase(i);
}

void InfoBarContainer::RemoveAllInfoBarsForDestruction() {
  // Before we remove any children, we reset |delegate_|, so that no removals
  // will result in us trying to call
  // delegate_->InfoBarContainerStateChanged().  This is important because at
  // this point |delegate_| may be shutting down, and it's at best unimportant
  // and at worst disastrous to call that.
  delegate_ = NULL;

  for (size_t i = infobars_.size(); i > 0; --i)
    infobars_[i - 1]->CloseSoon();

  ChangeInfoBarService(NULL);
}

void InfoBarContainer::Observe(int type,
                               const content::NotificationSource& source,
                               const content::NotificationDetails& details) {
  // When infobars are supposed to be hidden, we shouldn't try to hide or show
  // anything in response to any notifications.  Once infobars get un-hidden
  // via ChangeInfoBarService(), we'll get the updated set of visible infobars
  // from the InfoBarService.
  if (!infobars_shown_)
    return;

  switch (type) {
    case chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_ADDED:
      AddInfoBar(
          content::Details<InfoBarAddedDetails>(details)->CreateInfoBar(
              infobar_service_),
          infobars_.size(), true, WANT_CALLBACK);
      break;

    case chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED: {
      InfoBarRemovedDetails* removed_details =
          content::Details<InfoBarRemovedDetails>(details).ptr();
      HideInfoBar(removed_details->first, removed_details->second);
      break;
    }

    case chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REPLACED: {
      InfoBarReplacedDetails* replaced_details =
          content::Details<InfoBarReplacedDetails>(details).ptr();
      AddInfoBar(replaced_details->second->CreateInfoBar(infobar_service_),
          HideInfoBar(replaced_details->first, false), false, WANT_CALLBACK);
      break;
    }

    default:
      NOTREACHED();
      break;
  }
}

void InfoBarContainer::ModeChanged(const chrome::search::Mode& old_mode,
                                   const chrome::search::Mode& new_mode) {
  // Hide infobars when showing Instant Extended suggestions.
  if (new_mode.is_search_suggestions()) {
    // If suggestions are being shown on a |DEFAULT| page, delay the hiding
    // until notification that instant preview is ready is received via
    // PreviewStateChanged(); this prevents jankiness caused by infobars hiding
    // followed by suggestions appearing.
    if (new_mode.is_origin_default())
      return;
    HideAllInfoBars();
    OnInfoBarStateChanged(false);
  } else {
    ChangeInfoBarService(infobar_service_);
    infobars_shown_time_ = base::TimeTicks::Now();
  }
}

void InfoBarContainer::PreviewStateChanged(const InstantModel& model) {
  // If suggestions are being shown on a |DEFAULT| page, hide the infobars now.
  // See comments for ModeChanged() for explanation.
  if (model.mode().is_search_suggestions() &&
      model.mode().is_origin_default()) {
    HideAllInfoBars();
    OnInfoBarStateChanged(false);
  }
}

size_t InfoBarContainer::HideInfoBar(InfoBarDelegate* delegate,
                                     bool use_animation) {
  bool should_animate = use_animation &&
      ((base::TimeTicks::Now() - infobars_shown_time_) >
          base::TimeDelta::FromMilliseconds(50));

  // Search for the infobar associated with |delegate|.  We cannot search for
  // |delegate| in |tab_helper_|, because an InfoBar remains alive until its
  // close animation completes, while the delegate is removed from the tab
  // immediately.
  for (InfoBars::iterator i(infobars_.begin()); i != infobars_.end(); ++i) {
    InfoBar* infobar = *i;
    if (infobar->delegate() == delegate) {
      size_t position = i - infobars_.begin();
      // We merely need hide the infobar; it will call back to RemoveInfoBar()
      // itself once it's hidden.
      infobar->Hide(should_animate);
      infobar->CloseSoon();
      UpdateInfoBarArrowTargetHeights();
      return position;
    }
  }
  NOTREACHED();
  return infobars_.size();
}

void InfoBarContainer::HideAllInfoBars() {
  infobars_shown_ = false;
  while (!infobars_.empty()) {
    InfoBar* infobar = infobars_.front();
    // Inform the infobar that it's hidden.  If it was already closing, this
    // closes its delegate.
    infobar->Hide(false);
  }
}

void InfoBarContainer::AddInfoBar(InfoBar* infobar,
                                  size_t position,
                                  bool animate,
                                  CallbackStatus callback_status) {
  DCHECK(std::find(infobars_.begin(), infobars_.end(), infobar) ==
      infobars_.end());
  DCHECK_LE(position, infobars_.size());
  infobars_.insert(infobars_.begin() + position, infobar);
  UpdateInfoBarArrowTargetHeights();
  PlatformSpecificAddInfoBar(infobar, position);
  if (callback_status == WANT_CALLBACK)
    infobar->set_container(this);
  infobar->Show(animate);
  if (callback_status == NO_CALLBACK)
    infobar->set_container(this);
}

void InfoBarContainer::UpdateInfoBarArrowTargetHeights() {
  for (size_t i = 0; i < infobars_.size(); ++i)
    infobars_[i]->SetArrowTargetHeight(ArrowTargetHeightForInfoBar(i));
}

int InfoBarContainer::ArrowTargetHeightForInfoBar(size_t infobar_index) const {
  if (!delegate_ || !delegate_->DrawInfoBarArrows(NULL))
    return 0;
  if (infobar_index == 0)
    return top_arrow_target_height_;
  const ui::SlideAnimation& first_infobar_animation =
      const_cast<const InfoBar*>(infobars_.front())->animation();
  if ((infobar_index > 1) || first_infobar_animation.IsShowing())
    return InfoBar::kDefaultArrowTargetHeight;
  // When the first infobar is animating closed, we animate the second infobar's
  // arrow target height from the default to the top target height.  Note that
  // the animation values here are going from 1.0 -> 0.0 as the top bar closes.
  return top_arrow_target_height_ + static_cast<int>(
      (InfoBar::kDefaultArrowTargetHeight - top_arrow_target_height_) *
          first_infobar_animation.GetCurrentValue());
}

#endif  // TOOLKIT_VIEWS || defined(TOOLKIT_GTK)


text:
require 'spec_helper'

describe SocialMediaParser do
  let(:parser){ described_class.parse profile_attributes }

  context "correct class" do
    let(:profile_attributes) { {url: "https://instagram.com/jimmykimmellive"} }

    it "returns a Instagram object" do
      expect(parser).to be_a SocialMediaParser::Provider::Instagram
    end
  end

  context "with instagram url and provider" do
    let(:profile_attributes){ {url: "https://instagram.com/jimmykimmellive", provider: "instagram"} }

    it "returns the parsed attributes" do
      expect(parser.url).to eq "https://instagram.com/jimmykimmellive"
      expect(parser.provider).to eq "instagram"
      expect(parser.username).to eq "jimmykimmellive"
    end
  end

  context "with instagram as provider and username as url_or_username" do
    let(:profile_attributes){ {url_or_username: "jimmykimmellive", provider: "instagram"} }

    it "returns the parsed attributes" do
      expect(parser.url).to eq "https://www.instagram.com/jimmykimmellive"
      expect(parser.provider).to eq "instagram"
      expect(parser.username).to eq "jimmykimmellive"
    end
  end

  context "with instagram as provider and username as url_or_username" do
    let(:profile_attributes){ {url_or_username: "https://instagram.com/jimmykimmellive", provider: "instagram"} }

    it "returns the parsed attributes" do
      expect(parser.url).to eq "https://instagram.com/jimmykimmellive"
      expect(parser.provider).to eq "instagram"
      expect(parser.username).to eq "jimmykimmellive"
    end
  end

  context "url variations" do
    it "parses username from url with trailing slash" do
      parser = described_class.parse "http://instagram.com/jimmyfallon/"
      expect(parser.username).to eq "jimmyfallon"
    end

    it "parses username from url with www" do
      parser = described_class.parse "http://www.instagram.com/jimmyfallon"
      expect(parser.username).to eq "jimmyfallon"
    end

    it "parses username from url without http and www" do
      parser = described_class.parse "instagram.com/jimmyfallon"
      expect(parser.username).to eq "jimmyfallon"
    end
  end
end


text:
package com.arinerron.forux.core;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;

public class SoundManager {
    private Game game = null;
    private HashMap<String, String> soundfiles = new HashMap<String, String>();

    protected SoundManager(Game game) {
        this.game = game;
    }

    public Game getGame() {
        return this.game;
    }

    public String register(String sound, String path) {
        return this.getSounds().put(sound, path);
    }

    public String unregister(String sound) {
        return this.getSounds().remove(sound);
    }

    public void unregisterAll() {
        this.getSounds().clear();
    }

    public HashMap<String, String> getSounds() {
        return this.soundfiles;
    }

    public SoundPlayer getSound(String name) {
        if(!this.getSounds().containsKey(name) || !this.getGame().getResourceManager().getAudioResource(this.getSounds().get(name)).exists())
            return null;
        return new SoundPlayer(this, this.getGame().getResourceManager().getAudioResource(this.getSounds().get(name)));
    }
}