| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /* global document */
- /* bender-tags: ui */
- import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
- import Collection from 'ckeditor5/utils/collection.js';
- import testUtils from 'tests/core/_utils/utils.js';
- import View from 'ckeditor5/ui/view.js';
- import ViewCollection from 'ckeditor5/ui/viewcollection.js';
- import Template from 'ckeditor5/ui/template.js';
- import normalizeHtml from 'tests/utils/_utils/normalizehtml.js';
- let collection;
- testUtils.createSinonSandbox();
- describe( 'ViewCollection', () => {
- beforeEach( createTestCollection );
- describe( 'constructor()', () => {
- it( 'sets basic properties and attributes', () => {
- expect( collection.locale ).to.be.undefined;
- expect( collection.ready ).to.be.false;
- expect( collection._parentElement ).to.be.null;
- expect( collection._boundItemsToViewsMap ).to.be.instanceOf( Map );
- } );
- it( 'accepts locale and defines the locale property', () => {
- const locale = { t() {} };
- expect( new ViewCollection( locale ).locale ).to.equal( locale );
- } );
- 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.ready = true;
- 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>' );
- } );
- } );
- describe( 'init()', () => {
- it( 'should return a promise', () => {
- expect( collection.init() ).to.be.instanceof( Promise );
- } );
- it( 'should throw if already initialized', () => {
- return collection.init()
- .then( () => {
- collection.init();
- throw new Error( 'This should not be executed.' );
- } )
- .catch( err => {
- expect( err ).to.be.instanceof( CKEditorError );
- expect( err.message ).to.match( /ui-viewcollection-init-reinit/ );
- } );
- } );
- it( 'calls #init on all views in the collection', () => {
- const viewA = new View();
- const viewB = new View();
- viewA.element = document.createElement( 'a' );
- viewB.element = document.createElement( 'b' );
- const spyA = testUtils.sinon.spy( viewA, 'init' );
- const spyB = testUtils.sinon.spy( viewB, 'init' );
- collection.setParent( document.body );
- collection.add( viewA );
- collection.add( viewB );
- return collection.init().then( () => {
- sinon.assert.calledOnce( spyA );
- sinon.assert.calledOnce( spyB );
- sinon.assert.callOrder( spyA, spyB );
- expect( viewA.element.parentNode ).to.equal( collection._parentElement );
- expect( viewA.element.nextSibling ).to.equal( viewB.element );
- expect( collection.ready ).to.be.true;
- } );
- } );
- } );
- describe( 'destroy()', () => {
- it( 'should return a promise', () => {
- expect( collection.destroy() ).to.be.instanceof( Promise );
- } );
- 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 );
- return collection.destroy().then( () => {
- sinon.assert.calledOnce( spyA );
- sinon.assert.calledOnce( spyB );
- sinon.assert.callOrder( spyA, spyB );
- } );
- } );
- } );
- describe( 'add', () => {
- it( 'returns a promise', () => {
- expect( collection.add( {} ) ).to.be.instanceof( Promise );
- } );
- it( 'initializes the new view in the collection', () => {
- let view = new View();
- let spy = testUtils.sinon.spy( view, 'init' );
- expect( collection.ready ).to.be.false;
- expect( view.ready ).to.be.false;
- return collection.add( view ).then( () => {
- expect( collection.ready ).to.be.false;
- expect( view.ready ).to.be.false;
- sinon.assert.notCalled( spy );
- view = new View();
- spy = testUtils.sinon.spy( view, 'init' );
- collection.ready = true;
- return collection.add( view ).then( () => {
- expect( view.ready ).to.be.true;
- sinon.assert.calledOnce( spy );
- } );
- } );
- } );
- } );
- describe( 'setParent', () => {
- it( 'sets #_parentElement', () => {
- const el = {};
- expect( collection._parentElement ).to.be.null;
- collection.setParent( el );
- expect( collection._parentElement ).to.equal( el );
- } );
- } );
- describe( 'bindTo', () => {
- class ViewClass extends View {
- constructor( locale, data ) {
- super( locale );
- this.template = new Template( {
- tag: 'b'
- } );
- this.data = data;
- }
- }
- it( 'provides "as" interface', () => {
- const returned = collection.bindTo( {} );
- expect( returned ).to.have.keys( 'as' );
- expect( returned.as ).to.be.a( 'function' );
- } );
- describe( 'as', () => {
- it( 'does not chain', () => {
- const returned = collection.bindTo( new Collection() ).as( ViewClass );
- expect( returned ).to.be.undefined;
- } );
- it( 'binds collection as a view factory – initial content', () => {
- const locale = {};
- const items = new Collection();
- items.add( { id: '1' } );
- items.add( { id: '2' } );
- collection = new ViewCollection( locale );
- collection.bindTo( items ).as( ViewClass );
- expect( collection ).to.have.length( 2 );
- expect( collection.get( 0 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 1 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 0 ).locale ).to.equal( locale );
- expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
- } );
- it( 'binds collection as a view factory – new content', () => {
- const locale = {};
- const items = new Collection();
- collection = new ViewCollection( locale );
- collection.bindTo( items ).as( ViewClass );
- expect( collection ).to.have.length( 0 );
- items.add( { id: '1' } );
- items.add( { id: '2' } );
- expect( collection ).to.have.length( 2 );
- expect( collection.get( 0 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 1 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 0 ).locale ).to.equal( locale );
- expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
- } );
- it( 'binds collection as a view factory – item removal', () => {
- const locale = {};
- const items = new Collection();
- collection = new ViewCollection( locale );
- collection.bindTo( items ).as( ViewClass );
- expect( collection ).to.have.length( 0 );
- items.add( { id: '1' } );
- items.add( { id: '2' } );
- expect( collection ).to.have.length( 2 );
- expect( collection.get( 0 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 1 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 0 ).locale ).to.equal( locale );
- expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
- items.remove( 1 );
- expect( collection.get( 0 ).data ).to.equal( items.get( 0 ) );
- items.remove( 0 );
- expect( collection ).to.have.length( 0 );
- } );
- it( 'binds collection as a view factory – custom factory', () => {
- const locale = {};
- const items = new Collection();
- collection = new ViewCollection( locale );
- collection.bindTo( items ).as( ( item ) => {
- return new ViewClass( locale, item );
- } );
- expect( collection ).to.have.length( 0 );
- items.add( { id: '1' } );
- items.add( { id: '2' } );
- expect( collection ).to.have.length( 2 );
- expect( collection.get( 0 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 1 ) ).to.be.instanceOf( ViewClass );
- expect( collection.get( 0 ).locale ).to.equal( locale );
- expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) );
- } );
- } );
- } );
- describe( 'delegate', () => {
- it( 'should throw when event names are not strings', () => {
- expect( () => {
- collection.delegate();
- } ).to.throw( CKEditorError, /ui-viewcollection-delegate-wrong-events/ );
- expect( () => {
- collection.delegate( new Date() );
- } ).to.throw( CKEditorError, /ui-viewcollection-delegate-wrong-events/ );
- expect( () => {
- collection.delegate( 'color', new Date() );
- } ).to.throw( CKEditorError, /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 );
- }
|