|
|
@@ -0,0 +1,447 @@
|
|
|
+/**
|
|
|
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
|
|
|
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
|
+ */
|
|
|
+
|
|
|
+/* globals document, setTimeout, Event, console */
|
|
|
+
|
|
|
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
|
|
|
+
|
|
|
+import Rect from '../../src/dom/rect';
|
|
|
+import global from '../../src/dom/global';
|
|
|
+import DomEmitterMixin from '../../src/dom/emittermixin';
|
|
|
+import ResizeObserver from '../../src/dom/resizeobserver';
|
|
|
+
|
|
|
+describe( 'ResizeObserver()', () => {
|
|
|
+ let elementA, elementB;
|
|
|
+
|
|
|
+ testUtils.createSinonSandbox();
|
|
|
+
|
|
|
+ beforeEach( () => {
|
|
|
+ elementA = document.createElement( 'div' );
|
|
|
+ elementA.id = 'A';
|
|
|
+ elementB = document.createElement( 'div' );
|
|
|
+ elementB.id = 'B';
|
|
|
+ } );
|
|
|
+
|
|
|
+ afterEach( () => {
|
|
|
+ // Make it look like the module was loaded from scratch.
|
|
|
+ ResizeObserver._observerInstance = null;
|
|
|
+ ResizeObserver._elementCallbacks = null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'constructor()', () => {
|
|
|
+ it( 'should use the native implementation if available', () => {
|
|
|
+ const spy = sinon.spy();
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( () => {
|
|
|
+ return {
|
|
|
+ observe: spy,
|
|
|
+ unobserve: sinon.spy()
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observer = new ResizeObserver( elementA, () => {} );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should use the polyfill when no native implementation available', () => {
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( null );
|
|
|
+
|
|
|
+ const observer = new ResizeObserver( elementA, () => {} );
|
|
|
+
|
|
|
+ expect( ResizeObserver._observerInstance.constructor.name ).to.equal( 'ResizeObserverPolyfill' );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should re-use the same native observer instance over and over again', () => {
|
|
|
+ const elementA = document.createElement( 'div' );
|
|
|
+ const elementB = document.createElement( 'div' );
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( () => {
|
|
|
+ return {
|
|
|
+ observe() {},
|
|
|
+ unobserve() {}
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, () => {} );
|
|
|
+ const observerB = new ResizeObserver( elementB, () => {} );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( global.window.ResizeObserver );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+ observerB.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should react to resizing of an element', () => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ let resizeCallback;
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
|
|
|
+ resizeCallback = callback;
|
|
|
+
|
|
|
+ return {
|
|
|
+ observe() {},
|
|
|
+ unobserve() {}
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+ sinon.assert.calledWithExactly( callbackA.firstCall, { target: elementA } );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should be able to observe the same element along with other observers', () => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ const callbackB = sinon.spy();
|
|
|
+ let resizeCallback;
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
|
|
|
+ resizeCallback = callback;
|
|
|
+
|
|
|
+ return {
|
|
|
+ observe() {},
|
|
|
+ unobserve() {}
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+ const observerB = new ResizeObserver( elementA, callbackB );
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+ sinon.assert.calledWithExactly( callbackA, { target: elementA } );
|
|
|
+ sinon.assert.calledOnce( callbackB );
|
|
|
+ sinon.assert.calledWithExactly( callbackB, { target: elementA } );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+ observerB.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should not be affected by other observers being destroyed', () => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ const callbackB = sinon.spy();
|
|
|
+ let resizeCallback;
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
|
|
|
+ resizeCallback = callback;
|
|
|
+
|
|
|
+ return {
|
|
|
+ observe() {},
|
|
|
+ unobserve() {}
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+ const observerB = new ResizeObserver( elementA, callbackB );
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+ sinon.assert.calledWithExactly( callbackA, { target: elementA } );
|
|
|
+ sinon.assert.calledOnce( callbackB );
|
|
|
+ sinon.assert.calledWithExactly( callbackB, { target: elementA } );
|
|
|
+
|
|
|
+ observerB.destroy();
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledTwice( callbackA );
|
|
|
+ sinon.assert.calledWithExactly( callbackA.secondCall, { target: elementA } );
|
|
|
+ sinon.assert.calledOnce( callbackB );
|
|
|
+ sinon.assert.calledWithExactly( callbackB, { target: elementA } );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'destroy()', () => {
|
|
|
+ it( 'should make the observer stop responding to resize of an element', () => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ let resizeCallback;
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
|
|
|
+ resizeCallback = callback;
|
|
|
+
|
|
|
+ return {
|
|
|
+ observe() {},
|
|
|
+ unobserve() {}
|
|
|
+ };
|
|
|
+ } );
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+
|
|
|
+ resizeCallback( [
|
|
|
+ { target: elementA }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should not throw if called multiple times', () => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ observerA.destroy();
|
|
|
+ observerA.destroy();
|
|
|
+ } ).should.not.throw;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'ResizeObserverPolyfill', () => {
|
|
|
+ let callback, elementRectA, elementRectB;
|
|
|
+
|
|
|
+ beforeEach( () => {
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( null );
|
|
|
+
|
|
|
+ callback = sinon.spy();
|
|
|
+
|
|
|
+ elementRectA = {
|
|
|
+ top: 10,
|
|
|
+ right: 20,
|
|
|
+ bottom: 20,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 20
|
|
|
+ };
|
|
|
+
|
|
|
+ elementRectB = {
|
|
|
+ top: 0,
|
|
|
+ right: 10,
|
|
|
+ bottom: 10,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 10
|
|
|
+ };
|
|
|
+
|
|
|
+ elementA.getBoundingClientRect = () => elementRectA;
|
|
|
+ elementB.getBoundingClientRect = () => elementRectB;
|
|
|
+
|
|
|
+ document.body.appendChild( elementA );
|
|
|
+ document.body.appendChild( elementB );
|
|
|
+ } );
|
|
|
+
|
|
|
+ afterEach( () => {
|
|
|
+ elementA.remove();
|
|
|
+ elementB.remove();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'mixes DomEmitterMixin', () => {
|
|
|
+ const observer = new ResizeObserver( elementA, () => {} );
|
|
|
+
|
|
|
+ expect( testUtils.isMixed( ResizeObserver._observerInstance.constructor, DomEmitterMixin ) ).to.be.true;
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'observe()', () => {
|
|
|
+ it( 'calls the callback immediatelly', () => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ const { target, contentRect } = callback.firstCall.args[ 0 ];
|
|
|
+
|
|
|
+ expect( target ).to.equal( elementA );
|
|
|
+ expect( contentRect ).to.be.instanceOf( Rect );
|
|
|
+ expect( contentRect ).to.deep.equal( elementRectA );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not execute the callback if element has no parent in DOM', () => {
|
|
|
+ const warnSpy = testUtils.sinon.spy( console, 'warn' );
|
|
|
+
|
|
|
+ elementA.remove();
|
|
|
+
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+
|
|
|
+ sinon.assert.notCalled( callback );
|
|
|
+ sinon.assert.notCalled( warnSpy );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'starts periodic check and asynchronously does not execute the callback if the element rect is the same', done => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ done();
|
|
|
+ }, 200 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'starts periodic check and asynchronously executes the callback if the element rect changed', done => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ const newRect = {
|
|
|
+ top: 30,
|
|
|
+ right: 10,
|
|
|
+ bottom: 40,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 10
|
|
|
+ };
|
|
|
+
|
|
|
+ elementA.getBoundingClientRect = () => newRect;
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledTwice( callback );
|
|
|
+
|
|
|
+ const { target, contentRect } = callback.secondCall.args[ 0 ];
|
|
|
+
|
|
|
+ expect( target ).to.equal( elementA );
|
|
|
+ expect( contentRect ).to.deep.equal( newRect );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ done();
|
|
|
+ }, 200 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'starts periodic check and asynchronously executes the callback if multiple element rects changed', done => {
|
|
|
+ const callbackA = sinon.spy();
|
|
|
+ const callbackB = sinon.spy();
|
|
|
+
|
|
|
+ const observerA = new ResizeObserver( elementA, callbackA );
|
|
|
+ const observerB = new ResizeObserver( elementB, callbackB );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callbackA );
|
|
|
+ sinon.assert.calledOnce( callbackB );
|
|
|
+
|
|
|
+ const newRectA = {
|
|
|
+ top: 30,
|
|
|
+ right: 10,
|
|
|
+ bottom: 40,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 10
|
|
|
+ };
|
|
|
+
|
|
|
+ const newRectB = {
|
|
|
+ top: 30,
|
|
|
+ right: 100,
|
|
|
+ bottom: 40,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 100
|
|
|
+ };
|
|
|
+
|
|
|
+ elementA.getBoundingClientRect = () => newRectA;
|
|
|
+ elementB.getBoundingClientRect = () => newRectB;
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledTwice( callbackA );
|
|
|
+ sinon.assert.calledTwice( callbackB );
|
|
|
+
|
|
|
+ const { target: targetA, contentRect: contentRectA } = callbackA.secondCall.args[ 0 ];
|
|
|
+ const { target: targetB, contentRect: contentRectB } = callbackB.secondCall.args[ 0 ];
|
|
|
+
|
|
|
+ expect( targetA ).to.equal( elementA );
|
|
|
+ expect( contentRectA ).to.deep.equal( newRectA );
|
|
|
+
|
|
|
+ expect( targetB ).to.equal( elementB );
|
|
|
+ expect( contentRectB ).to.deep.equal( newRectB );
|
|
|
+
|
|
|
+ observerA.destroy();
|
|
|
+ observerB.destroy();
|
|
|
+ done();
|
|
|
+ }, 200 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'starts periodic check and synchronously responds to window resize', () => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ const newRectA = {
|
|
|
+ top: 30,
|
|
|
+ right: 10,
|
|
|
+ bottom: 40,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 10
|
|
|
+ };
|
|
|
+
|
|
|
+ elementA.getBoundingClientRect = () => newRectA;
|
|
|
+
|
|
|
+ global.window.dispatchEvent( new Event( 'resize' ) );
|
|
|
+
|
|
|
+ sinon.assert.calledTwice( callback );
|
|
|
+
|
|
|
+ const { target: targetA, contentRect: contentRectA } = callback.secondCall.args[ 0 ];
|
|
|
+
|
|
|
+ expect( targetA ).to.equal( elementA );
|
|
|
+ expect( contentRectA ).to.deep.equal( newRectA );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'unobserve()', () => {
|
|
|
+ it( 'removes the element from the observer so no future changes to the element execute the callback', done => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ const newRect = {
|
|
|
+ top: 30,
|
|
|
+ right: 10,
|
|
|
+ bottom: 40,
|
|
|
+ left: 0,
|
|
|
+ height: 10,
|
|
|
+ width: 10
|
|
|
+ };
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+
|
|
|
+ elementA.getBoundingClientRect = () => newRect;
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ done();
|
|
|
+ }, 200 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'disables the Emitter when no elements left in the observer', () => {
|
|
|
+ const observer = new ResizeObserver( elementA, callback );
|
|
|
+ const stopCheckSpy = testUtils.sinon.spy( ResizeObserver._observerInstance, '_stopPeriodicCheck' );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( callback );
|
|
|
+
|
|
|
+ observer.destroy();
|
|
|
+ sinon.assert.calledOnce( stopCheckSpy );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+} );
|