/** * @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 */ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver'; import View from '../../../src/view/view'; import DomEventData from '../../../src/view/observer/domeventdata'; import createViewRoot from '../_utils/createroot'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import { setData, stringify } from '../../../src/dev-utils/view'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { StylesProcessor } from '../../../src/view/stylesmap'; describe( 'FakeSelectionObserver', () => { let observer, view, viewDocument, root, domRoot; testUtils.createSinonSandbox(); before( () => { domRoot = createElement( document, 'div', { contenteditable: 'true' } ); document.body.appendChild( domRoot ); } ); after( () => { domRoot.parentElement.removeChild( domRoot ); } ); beforeEach( () => { view = new View( new StylesProcessor() ); viewDocument = view.document; root = createViewRoot( viewDocument ); view.attachDomRoot( domRoot ); observer = view.getObserver( FakeSelectionObserver ); viewDocument.selection._setTo( null, { fake: true } ); } ); afterEach( () => { view.destroy(); } ); it( 'should do nothing if selection is not fake', () => { viewDocument.selection._setTo( null, { fake: false } ); return checkEventPrevention( keyCodes.arrowleft, false ); } ); it( 'should do nothing if is disabled', () => { observer.disable(); return checkEventPrevention( keyCodes.arrowleft, false ); } ); it( 'should prevent default for left arrow key', ( ) => { return checkEventPrevention( keyCodes.arrowleft ); } ); it( 'should prevent default for right arrow key', ( ) => { return checkEventPrevention( keyCodes.arrowright ); } ); it( 'should prevent default for up arrow key', ( ) => { return checkEventPrevention( keyCodes.arrowup ); } ); it( 'should prevent default for down arrow key', ( ) => { return checkEventPrevention( keyCodes.arrowdown ); } ); it( 'should fire selectionChange event with new selection when left arrow key is pressed', () => { return checkSelectionChange( 'foo[bar]baz', keyCodes.arrowleft, 'foo[]barbaz' ); } ); it( 'should fire selectionChange event with new selection when right arrow key is pressed', () => { return checkSelectionChange( 'foo[bar]baz', keyCodes.arrowright, 'foobar[]baz' ); } ); it( 'should fire selectionChange event with new selection when up arrow key is pressed', () => { return checkSelectionChange( 'foo[bar]baz', keyCodes.arrowup, 'foo[]barbaz' ); } ); it( 'should fire selectionChange event with new selection when down arrow key is pressed', () => { return checkSelectionChange( 'foo[bar]baz', keyCodes.arrowdown, 'foobar[]baz' ); } ); it( 'should fire `selectionChangeDone` event after selection stop changing', () => { const clock = testUtils.sinon.useFakeTimers(); const spy = sinon.spy(); viewDocument.on( 'selectionChangeDone', spy ); // Change selection. changeFakeSelectionPressing( keyCodes.arrowdown ); // Wait 100ms. clock.tick( 100 ); // Check if spy was called. sinon.assert.notCalled( spy ); // Change selection one more time. changeFakeSelectionPressing( keyCodes.arrowdown ); // Wait 210ms (debounced function should be called). clock.tick( 210 ); sinon.assert.calledOnce( spy ); } ); it( 'should not fire `selectionChangeDone` event when observer will be destroyed', () => { const clock = testUtils.sinon.useFakeTimers(); const spy = sinon.spy(); viewDocument.on( 'selectionChangeDone', spy ); // Change selection. changeFakeSelectionPressing( keyCodes.arrowdown ); // Wait 100ms. clock.tick( 100 ); // And destroy observer. observer.destroy(); // Wait another 110ms. clock.tick( 110 ); // Check that event won't be called. sinon.assert.notCalled( spy ); } ); // Checks if preventDefault method was called by FakeSelectionObserver for specified key code. // // @param {Number} keyCode // @param {Boolean} shouldPrevent If set to true method checks if event was prevented. // @returns {Promise} function checkEventPrevention( keyCode, shouldPrevent = true ) { return new Promise( resolve => { const data = { keyCode, preventDefault: sinon.spy() }; viewDocument.once( 'keydown', () => { if ( shouldPrevent ) { sinon.assert.calledOnce( data.preventDefault ); } else { sinon.assert.notCalled( data.preventDefault ); } resolve(); }, { priority: 'lowest' } ); viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) ); } ); } // Checks if proper selectionChange event is fired by FakeSelectionObserver for specified key. // // @param {String} initialData // @param {Number} keyCode // @param {String} output // @returns {Promise} function checkSelectionChange( initialData, keyCode, output ) { return new Promise( resolve => { viewDocument.once( 'selectionChange', ( eventInfo, data ) => { expect( stringify( root.getChild( 0 ), data.newSelection, { showType: true } ) ).to.equal( output ); resolve(); } ); setData( view, initialData ); changeFakeSelectionPressing( keyCode ); } ); } // Sets fake selection to the document and fire `keydown` event what cause `selectionChange` event. // // @param {Number} keyCode function changeFakeSelectionPressing( keyCode ) { viewDocument.selection._setTo( viewDocument.selection.getRanges(), { backward: viewDocument.selection.isBackward, fake: true } ); const data = { keyCode, preventDefault: sinon.spy() }; viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) ); } } );