/**
* @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( '
' );
const viewB = new View();
viewB.element = document.createElement( 'i' );
collection.add( viewB, 0 );
expect( normalizeHtml( parentElement.outerHTML ) ).to.equal( '
' );
collection.remove( viewA );
expect( normalizeHtml( parentElement.outerHTML ) ).to.equal( '
' );
collection.remove( viewB );
expect( normalizeHtml( parentElement.outerHTML ) ).to.equal( '' );
} );
// #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
} );
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( '
' );
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 );
}