Browse Source

Made _showPanel and _hidePanel protected and refactored tests to not show errors on the browser console.

Oskar Wróbel 8 years ago
parent
commit
f1d14b95a7

+ 10 - 9
packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js

@@ -131,32 +131,33 @@ export default class ContextualToolbar extends Plugin {
 	/**
 	 * Adds panel view to the {@link: #_balloon} and attaches panel to the selection.
 	 *
-	 * @private
+	 * @protected
+	 * @return {Promise} A promise resolved when the {@link #toolbarView} {@link module:ui/view~View#init} is done.
 	 */
 	_showPanel() {
 		const editingView = this.editor.editing.view;
 
 		// Do not add toolbar to the balloon stack twice.
 		if ( this._balloon.hasView( this.toolbarView ) ) {
-			return;
+			return Promise.resolve();
 		}
 
 		// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
 		if ( !editingView.isFocused || editingView.selection.isCollapsed ) {
-			return;
+			return Promise.resolve();
 		}
 
+		// Update panel position when selection changes while balloon will be opened (by a collaboration).
+		this.listenTo( this.editor.editing.view, 'render', () => {
+			this._balloon.updatePosition( this._getBalloonPositionData() );
+		} );
+
 		// Add panel to the common editor contextual balloon.
-		this._balloon.add( {
+		return this._balloon.add( {
 			view: this.toolbarView,
 			position: this._getBalloonPositionData(),
 			balloonClassName: 'ck-toolbar__container'
 		} );
-
-		// Update panel position when selection changes while balloon will be opened (by a collaboration).
-		this.listenTo( this.editor.editing.view, 'render', () => {
-			this._balloon.updatePosition( this._getBalloonPositionData() );
-		} );
 	}
 
 	/**

+ 233 - 108
packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js

@@ -36,11 +36,12 @@ describe( 'ContextualToolbar', () => {
 			contextualToolbar = editor.plugins.get( ContextualToolbar );
 			balloon = editor.plugins.get( ContextualBalloon );
 
+			// There is no point to execute BalloonPanelView attachTo and pin methods so lets override it.
+			sandbox.stub( balloon.view, 'attachTo', () => {} );
+			sandbox.stub( balloon.view, 'pin', () => {} );
+
 			// Focus the engine.
 			editor.editing.view.isFocused = true;
-
-			// Init child view.
-			return contextualToolbar.toolbarView.init();
 		} );
 	} );
 
@@ -101,176 +102,300 @@ describe( 'ContextualToolbar', () => {
 		}, 100 );
 	} );
 
-	it( 'should open when selection stops changing', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+	describe( 'pluginName', () => {
+		it( 'should return plugin by its name', () => {
+			expect( editor.plugins.get( 'contextualtoolbar' ) ).to.equal( contextualToolbar );
+		} );
+	} );
 
-		expect( balloon.visibleView ).to.null;
+	describe( '_showPanel()', () => {
+		let balloonAddSpy, forwardSelectionRect, backwardSelectionRect;
+
+		beforeEach( () => {
+			forwardSelectionRect = {
+				top: 100,
+				height: 10,
+				bottom: 110,
+				left: 200,
+				width: 50,
+				right: 250
+			};
+
+			backwardSelectionRect = {
+				top: 100,
+				height: 10,
+				bottom: 110,
+				left: 200,
+				width: 50,
+				right: 250
+			};
+
+			stubSelectionRect( forwardSelectionRect, backwardSelectionRect );
+
+			balloonAddSpy = sandbox.spy( balloon, 'add' );
+			editor.editing.view.isFocused = true;
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should return promise', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
-	} );
+			const returned = contextualToolbar._showPanel();
 
-	it( 'should add additional class to the ContextualBalloon#view', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			expect( returned ).to.instanceof( Promise );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+			return returned;
+		} );
 
-		expect( balloon.view.className ).to.equal( 'ck-toolbar__container' );
-	} );
+		it( 'should add #toolbarView to the #_balloon and attach the #_balloon to the selection for the forward selection', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+
+			const defaultPositions = BalloonPanelView.defaultPositions;
+
+			return contextualToolbar._showPanel().then( () => {
+				sinon.assert.calledWithExactly( balloonAddSpy, {
+					view: contextualToolbar.toolbarView,
+					balloonClassName: 'ck-toolbar__container',
+					position: {
+						target: forwardSelectionRect,
+						positions: [ defaultPositions.southEastArrowNorth, defaultPositions.northEastArrowSouth ]
+					}
+				} );
+			} );
+		} );
 
-	it( 'should close when selection starts changing by a directChange', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+		it( 'should add #toolbarView to the #_balloon and attach the #_balloon to the selection for the backward selection', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>', { lastRangeBackward: true } );
+
+			const defaultPositions = BalloonPanelView.defaultPositions;
+
+			return contextualToolbar._showPanel()
+				.then( () => {
+					sinon.assert.calledWithExactly( balloonAddSpy, {
+						view: contextualToolbar.toolbarView,
+						balloonClassName: 'ck-toolbar__container',
+						position: {
+							target: backwardSelectionRect,
+							positions: [ defaultPositions.northWestArrowSouth, defaultPositions.southWestArrowNorth ]
+						}
+					} );
+				} );
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should update balloon position on ViewDocument#render event while balloon is added to the #_balloon', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
+			const spy = sinon.spy( balloon, 'updatePosition' );
 
-		editor.document.selection.fire( 'change:range', { directChange: true } );
+			editor.editing.view.fire( 'render' );
 
-		expect( balloon.visibleView ).to.null;
-	} );
+			return contextualToolbar._showPanel()
+				.then( () => {
+					sinon.assert.notCalled( spy );
 
-	it( 'should not close when selection starts changing by not a directChange', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+					editor.editing.view.fire( 'render' );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+					sinon.assert.calledOnce( spy );
+				} );
+		} );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
+		it( 'should not add #toolbarView to the #_balloon more than once', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
 
-		editor.document.selection.fire( 'change:range', { directChange: false } );
+			return contextualToolbar._showPanel()
+				.then( () => contextualToolbar._showPanel() )
+				.then( () => {
+					sinon.assert.calledOnce( balloonAddSpy );
+				} );
+		} );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
-	} );
+		it( 'should not add #toolbarView to the #_balloon when editor is not focused', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+			editor.editing.view.isFocused = false;
 
-	it( 'should close when selection starts changing by not a directChange but will become collapsed', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			return contextualToolbar._showPanel()
+				.then( () => {
+					sinon.assert.notCalled( balloonAddSpy );
+				} );
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should not add #toolbarView to the #_balloon when selection is collapsed', () => {
+			setData( editor.document, '<paragraph>b[]ar</paragraph>' );
 
-		// Collapse range silently (without firing `change:range` { directChange: true } event).
-		const range = editor.document.selection._ranges[ 0 ];
-		range.end = range.start;
+			return contextualToolbar._showPanel()
+				.then( () => {
+					sinon.assert.notCalled( balloonAddSpy );
+				} );
+		} );
+	} );
 
-		editor.document.selection.fire( 'change:range', { directChange: false } );
+	describe( '_hidePanel()', () => {
+		let removeBalloonSpy;
 
-		expect( balloon.visibleView ).to.null;
-	} );
+		beforeEach( () => {
+			removeBalloonSpy = sandbox.spy( balloon, 'remove' );
+			editor.editing.view.isFocused = true;
+		} );
 
-	it( 'should open with specified positions configuration for the forward selection', () => {
-		const spy = sandbox.stub( balloon, 'add', () => {} );
-		const defaultPositions = BalloonPanelView.defaultPositions;
+		it( 'should remove #toolbarView from the #_balloon', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
 
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			return contextualToolbar._showPanel()
+				.then( () => {
+					contextualToolbar._hidePanel();
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+					sinon.assert.calledWithExactly( removeBalloonSpy, contextualToolbar.toolbarView );
+				} );
+		} );
 
-		expect( spy.firstCall.args[ 0 ].position.positions ).to.deep.equal(
-			[ defaultPositions.southEastArrowNorth, defaultPositions.northEastArrowSouth ]
-		);
-	} );
+		it( 'should stop update balloon position on ViewDocument#render event', () => {
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+
+			const spy = sinon.spy( balloon, 'updatePosition' );
 
-	it( 'should open with specified positions configuration for the backward selection', () => {
-		const spy = sandbox.stub( balloon, 'add', () => {} );
-		const defaultPositions = BalloonPanelView.defaultPositions;
+			return contextualToolbar._showPanel()
+				.then( () => {
+					contextualToolbar._hidePanel();
 
-		setData( editor.document, '<paragraph>[bar]</paragraph>', { lastRangeBackward: true } );
+					editor.editing.view.fire( 'render' );
+
+					sinon.assert.notCalled( spy );
+				} );
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should not remove #ttolbarView when is not added to the #_balloon', () => {
+			contextualToolbar._hidePanel();
 
-		expect( spy.firstCall.args[ 0 ].position.positions ).to.deep.equal(
-			[ defaultPositions.northWestArrowSouth, defaultPositions.southWestArrowNorth ]
-		);
+			sinon.assert.notCalled( removeBalloonSpy );
+		} );
 	} );
 
-	it( 'should not open if the collapsed selection is moving', () => {
-		setData( editor.document, '<paragraph>ba[]r</paragraph>' );
+	describe( 'destroy()', () => {
+		it( 'should not fire `_selectionChangeDebounced` after plugin destroy', ( done ) => {
+			const spy = sandbox.spy();
 
-		editor.document.selection.fire( 'change:range', {} );
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+			contextualToolbar.on( '_selectionChangeDebounced', spy );
 
-		setData( editor.document, '<paragraph>b[]ar</paragraph>' );
+			editor.document.selection.fire( 'change:range', { directChange: true } );
 
-		editor.document.selection.fire( 'change:range', {} );
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+			contextualToolbar.destroy();
 
-		expect( balloon.visibleView ).to.null;
+			setTimeout( () => {
+				sinon.assert.notCalled( spy );
+				done();
+			}, 200 );
+		} );
 	} );
 
-	it( 'should hide if the editor loses focus', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
-		editor.ui.focusTracker.isFocused = true;
+	describe( 'showing and hiding', () => {
+		let showPanelSpy, hidePanelSpy;
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		beforeEach( () => {
+			setData( editor.document, '<paragraph>[bar]</paragraph>' );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
+			// Methods are stubbed because return internal promise which can't be returned in test.
+			showPanelSpy = sandbox.stub( contextualToolbar, '_showPanel', () => {} );
+			hidePanelSpy = sandbox.stub( contextualToolbar, '_hidePanel', () => {} );
+		} );
 
-		editor.ui.focusTracker.isFocused = false;
+		it( 'should open when selection stops changing', () => {
+			sinon.assert.notCalled( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-		expect( balloon.visibleView ).to.null;
-	} );
+			contextualToolbar.fire( '_selectionChangeDebounced' );
 
-	it( 'should do nothing when panel is being added to balloon stack twice', () => {
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should close when selection starts changing by a directChange', () => {
+			contextualToolbar.fire( '_selectionChangeDebounced' );
+
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-		expect( balloon.visibleView ).to.equal( contextualToolbar.toolbarView );
+			editor.document.selection.fire( 'change:range', { directChange: true } );
 
-		expect( () => {
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.calledOnce( hidePanelSpy );
+		} );
+
+		it( 'should not close when selection starts changing by not a directChange', () => {
 			contextualToolbar.fire( '_selectionChangeDebounced' );
-		} ).to.not.throw();
-	} );
 
-	it( 'should update balloon position when toolbar is opened and editor content has changed', () => {
-		const spy = sandbox.stub( balloon, 'updatePosition' );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			editor.document.selection.fire( 'change:range', { directChange: false } );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+		} );
 
-		sinon.assert.notCalled( spy );
+		it( 'should close when selection starts changing by not a directChange but will become collapsed', () => {
+			contextualToolbar.fire( '_selectionChangeDebounced' );
 
-		editor.editing.view.fire( 'render' );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-		sinon.assert.calledOnce( spy );
-	} );
+			// Collapse range silently (without firing `change:range` { directChange: true } event).
+			const range = editor.document.selection._ranges[ 0 ];
+			range.end = range.start;
 
-	it( 'should update balloon position when toolbar is closed', () => {
-		const spy = sandbox.spy( balloon, 'updatePosition' );
+			editor.document.selection.fire( 'change:range', { directChange: false } );
 
-		setData( editor.document, '<paragraph>[bar]</paragraph>' );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.calledOnce( hidePanelSpy );
+		} );
 
-		contextualToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should hide if the editor loses focus', () => {
+			editor.ui.focusTracker.isFocused = true;
 
-		// Hide toolbar.
-		editor.document.selection.fire( 'change:range', { directChange: true } );
+			contextualToolbar.fire( '_selectionChangeDebounced' );
 
-		editor.editing.view.fire( 'render' );
+			sinon.stub( balloon, 'visibleView', { get: () => contextualToolbar.toolbarView } );
 
-		sinon.assert.notCalled( spy );
-	} );
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-	describe( 'pluginName', () => {
-		it( 'should return plugin by name', () => {
-			expect( editor.plugins.get( 'contextualballoon' ) ).to.equal( balloon );
+			editor.ui.focusTracker.isFocused = false;
+
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.calledOnce( hidePanelSpy );
 		} );
-	} );
 
-	describe( 'destroy()', () => {
-		it( 'should not fire `_selectionChangeDebounced` after plugin destroy', ( done ) => {
-			const spy = sandbox.spy();
+		it( 'should not hide if the editor loses focus and #toolbarView is not visible', () => {
+			editor.ui.focusTracker.isFocused = true;
 
-			contextualToolbar.on( '_selectionChangeDebounced', spy );
+			contextualToolbar.fire( '_selectionChangeDebounced' );
 
-			editor.document.selection.fire( 'change:range', { directChange: true } );
+			sinon.stub( balloon, 'visibleView', { get: () => null } );
 
-			contextualToolbar.destroy();
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 
-			setTimeout( () => {
-				sinon.assert.notCalled( spy );
-				done();
-			}, 200 );
+			editor.ui.focusTracker.isFocused = false;
+
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
 		} );
 	} );
+
+	function stubSelectionRect( forwardSelectionRect, backwardSelectionRect ) {
+		const editingView = editor.editing.view;
+		const originalViewRangeToDom = editingView.domConverter.viewRangeToDom;
+
+		// Mock selection rect.
+		sandbox.stub( editingView.domConverter, 'viewRangeToDom', ( ...args ) => {
+			const domRange = originalViewRangeToDom.apply( editingView.domConverter, args );
+
+			sandbox.stub( domRange, 'getClientRects', () => {
+				return {
+					length: 2,
+					item: id => id === 0 ? forwardSelectionRect : backwardSelectionRect
+				};
+			} );
+
+			return domRange;
+		} );
+	}
 } );