|
|
@@ -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;
|
|
|
+ } );
|
|
|
+ }
|
|
|
} );
|