text: /**
   * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
   * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
   */
  
  /* global document */
  
  import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  import View from '../src/view';
  import ViewCollection from '../src/viewcollection';
  import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
  import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  
  let collection;
  
  describe('ViewCollection', () => {
    testUtils.createSinonSandbox();
  
    beforeEach(createTestCollection);
  
    describe('constructor()', () => {
      it('sets basic properties and attributes', () => {
        expect(collection._parentElement).to.be.null;
        expect(collection._idProperty).to.equal('viewUid');
      });
  
      it('allows setting initial collection items', () => {
        const view1 = new View();
        const view2 = new View();
        const collection = new ViewCollection([view1, view2]);
  
        expect(collection).to.have.length(2);
        expect(collection.get(0)).to.equal(view1);
        expect(collection.get(1)).to.equal(view2);
      });
  
      describe('child view management in DOM', () => {
        it('manages view#element of the children in DOM', () => {
          const parentElement = document.createElement('p');
          collection.setParent(parentElement);
  
          const viewA = new View();
  
          expect(() => {
            collection.add(viewA);
            collection.remove(viewA);
          }).to.not.throw();
  
          expect(() => {
            collection.add(viewA);
            collection.remove(viewA);
          }).to.not.throw();
  
          viewA.element = document.createElement('b');
          collection.add(viewA);
  
          expect(normalizeHtml(parentElement.outerHTML)).to.equal('<p><b></b></p>');
  
          const viewB = new View();
          viewB.element = document.createElement('i');
  
          collection.add(viewB, 0);
          expect(normalizeHtml(parentElement.outerHTML)).to.equal('<p><i></i><b></b></p>');
  
          collection.remove(viewA);
          expect(normalizeHtml(parentElement.outerHTML)).to.equal('<p><i></i></p>');
  
          collection.remove(viewB);
          expect(normalizeHtml(parentElement.outerHTML)).to.equal('<p></p>');
        });
  
        // #145
        it('always keeps the collection synchronized with DOM', () => {
          const view = new View();
          const viewA = getView('A');
          const viewB = getView('B');
  
          function getView(text) {
            const view = new View();
  
            view.setTemplate({
              tag: 'li',
              children: [
                {
                  text,
                },
              ],
            });
  
            return view;
          }
  
          // Fill the collection with children.
          collection.add(viewA);
          collection.add(viewB);
  
          // Put the collection in the template.
          view.setTemplate({
            tag: 'ul',
            children: collection,
          });
  
          // Render view#template along with collection of children.
          view.element;
  
          const viewC = getView('C');
  
          // Modify the collection, while the view#element has already
          // been rendered but the view has **not** been inited yet.
          collection.add(viewC);
          collection.remove(1);
  
          view.render();
  
          expect(view.element.childNodes).to.have.length(2);
          expect(view.element.childNodes[0]).to.equal(viewA.element);
          expect(view.element.childNodes[1]).to.equal(viewC.element);
        });
      });
    });
  
    describe('destroy()', () => {
      it('calls #destroy on all views in the collection', () => {
        const viewA = new View();
        const viewB = new View();
  
        const spyA = testUtils.sinon.spy(viewA, 'destroy');
        const spyB = testUtils.sinon.spy(viewB, 'destroy');
  
        collection.add(viewA);
        collection.add(viewB);
  
        collection.destroy();
        sinon.assert.calledOnce(spyA);
        sinon.assert.calledOnce(spyB);
        sinon.assert.callOrder(spyA, spyB);
      });
    });
  
    describe('add()', () => {
      it('renders the new view in the collection', () => {
        const view = new View();
        const spy = testUtils.sinon.spy(view, 'render');
  
        expect(view.isRendered).to.be.false;
  
        collection.add(view);
        expect(view.isRendered).to.be.true;
        sinon.assert.calledOnce(spy);
      });
  
      it('works for a view with a Number view#id attribute', () => {
        const view = new View();
  
        view.set('id', 1);
  
        collection.add(view);
        expect(view.id).to.equal(1);
        expect(view.viewUid).to.be.a('string');
      });
    });
  
    describe('setParent()', () => {
      it('sets #_parentElement', () => {
        const el = {};
        expect(collection._parentElement).to.be.null;
  
        collection.setParent(el);
        expect(collection._parentElement).to.equal(el);
      });
  
      it('udpates initial collection items in DOM', () => {
        const view1 = new View();
        view1.element = document.createElement('i');
        sinon.spy(view1, 'render');
  
        const view2 = new View();
        view2.element = document.createElement('b');
        sinon.spy(view2, 'render');
  
        const collection = new ViewCollection([view1, view2]);
        const parentElement = document.createElement('div');
  
        expect(collection).to.have.length(2);
        expect(collection.get(0)).to.equal(view1);
        expect(collection.get(1)).to.equal(view2);
  
        collection.setParent(parentElement);
        expect(normalizeHtml(parentElement.outerHTML)).to.equal('<div><i></i><b></b></div>');
        sinon.assert.calledOnce(view1.render);
        sinon.assert.calledOnce(view2.render);
      });
    });
  
    describe('delegate()', () => {
      it('should throw when event names are not strings', () => {
        expectToThrowCKEditorError(() => {
          collection.delegate();
        }, /ui-viewcollection-delegate-wrong-events/);
  
        expectToThrowCKEditorError(() => {
          collection.delegate(new Date());
        }, /ui-viewcollection-delegate-wrong-events/);
  
        expectToThrowCKEditorError(() => {
          collection.delegate('color', new Date());
        }, /ui-viewcollection-delegate-wrong-events/);
      });
  
      it('returns object', () => {
        expect(collection.delegate('foo')).to.be.an('object');
      });
  
      it('provides "to()" interface', () => {
        const delegate = collection.delegate('foo');
  
        expect(delegate).to.have.keys('to');
        expect(delegate.to).to.be.a('function');
      });
  
      describe('to()', () => {
        it('does not chain', () => {
          const returned = collection.delegate('foo').to({});
  
          expect(returned).to.be.undefined;
        });
  
        it('forwards an event to another observable – existing view', done => {
          const target = new View();
          const view = new View();
  
          collection.add(view);
          collection.delegate('foo').to(target);
  
          target.on('foo', (...args) => {
            assertDelegated(args, {
              expectedName: 'foo',
              expectedSource: view,
              expectedPath: [view, target],
              expectedData: [],
            });
  
            done();
          });
  
          view.fire('foo');
        });
  
        it('forwards an event to another observable – new view', done => {
          const target = new View();
          const view = new View();
  
          collection.delegate('foo').to(target);
          collection.add(view);
  
          target.on('foo', (...args) => {
            assertDelegated(args, {
              expectedName: 'foo',
              expectedSource: view,
              expectedPath: [view, target],
              expectedData: [],
            });
  
            done();
          });
  
          view.fire('foo');
        });
  
        it('forwards multiple events to another observable', () => {
          const target = new View();
          const viewA = new View();
          const viewB = new View();
          const viewC = new View();
          const spyFoo = sinon.spy();
          const spyBar = sinon.spy();
          const spyBaz = sinon.spy();
  
          collection.delegate('foo', 'bar', 'baz').to(target);
          collection.add(viewA);
          collection.add(viewB);
          collection.add(viewC);
  
          target.on('foo', spyFoo);
          target.on('bar', spyBar);
          target.on('baz', spyBaz);
  
          viewA.fire('foo');
  
          sinon.assert.calledOnce(spyFoo);
          sinon.assert.notCalled(spyBar);
          sinon.assert.notCalled(spyBaz);
  
          assertDelegated(spyFoo.args[0], {
            expectedName: 'foo',
            expectedSource: viewA,
            expectedPath: [viewA, target],
            expectedData: [],
          });
  
          viewB.fire('bar');
  
          sinon.assert.calledOnce(spyFoo);
          sinon.assert.calledOnce(spyBar);
          sinon.assert.notCalled(spyBaz);
  
          assertDelegated(spyBar.args[0], {
            expectedName: 'bar',
            expectedSource: viewB,
            expectedPath: [viewB, target],
            expectedData: [],
          });
  
          viewC.fire('baz');
  
          sinon.assert.calledOnce(spyFoo);
          sinon.assert.calledOnce(spyBar);
          sinon.assert.calledOnce(spyBaz);
  
          assertDelegated(spyBaz.args[0], {
            expectedName: 'baz',
            expectedSource: viewC,
            expectedPath: [viewC, target],
            expectedData: [],
          });
  
          viewC.fire('not-delegated');
  
          sinon.assert.calledOnce(spyFoo);
          sinon.assert.calledOnce(spyBar);
          sinon.assert.calledOnce(spyBaz);
        });
  
        it('does not forward events which are not supposed to be delegated', () => {
          const target = new View();
          const view = new View();
          const spyFoo = sinon.spy();
          const spyBar = sinon.spy();
          const spyBaz = sinon.spy();
  
          collection.delegate('foo', 'bar', 'baz').to(target);
          collection.add(view);
  
          target.on('foo', spyFoo);
          target.on('bar', spyBar);
          target.on('baz', spyBaz);
  
          view.fire('foo');
          view.fire('bar');
          view.fire('baz');
          view.fire('not-delegated');
  
          sinon.assert.callOrder(spyFoo, spyBar, spyBaz);
          sinon.assert.callCount(spyFoo, 1);
          sinon.assert.callCount(spyBar, 1);
          sinon.assert.callCount(spyBaz, 1);
        });
  
        it('stops forwarding when view removed from the collection', () => {
          const target = new View();
          const view = new View();
          const spy = sinon.spy();
  
          collection.delegate('foo').to(target);
          target.on('foo', spy);
  
          collection.add(view);
          view.fire('foo');
  
          sinon.assert.callCount(spy, 1);
  
          collection.remove(0);
          view.fire('foo');
  
          sinon.assert.callCount(spy, 1);
        });
  
        it('supports deep event delegation', done => {
          const target = new View();
          const viewA = new View();
          const viewAA = new View();
          const data = {};
  
          const deepCollection = viewA.createCollection();
  
          collection.add(viewA);
          collection.delegate('foo').to(target);
  
          deepCollection.add(viewAA);
          deepCollection.delegate('foo').to(viewA);
  
          target.on('foo', (...args) => {
            assertDelegated(args, {
              expectedName: 'foo',
              expectedSource: viewAA,
              expectedPath: [viewAA, viewA, target],
              expectedData: [data],
            });
  
            done();
          });
  
          viewAA.fire('foo', data);
        });
      });
    });
  });
  
  function createTestCollection() {
    collection = new ViewCollection();
  }
  
  function assertDelegated(evtArgs, { expectedName, expectedSource, expectedPath, expectedData }) {
    const evtInfo = evtArgs[0];
  
    expect(evtInfo.name).to.equal(expectedName);
    expect(evtInfo.source).to.equal(expectedSource);
    expect(evtInfo.path).to.deep.equal(expectedPath);
    expect(evtArgs.slice(1)).to.deep.equal(expectedData);
  }
  