/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
import DeleteCommand from '../src/deletecommand';
import Delete from '../src/delete';
import ChangeBuffer from '../src/utils/changebuffer';
import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
describe( 'DeleteCommand', () => {
let editor, model, doc;
testUtils.createSinonSandbox();
beforeEach( () => {
return ModelTestEditor.create()
.then( newEditor => {
editor = newEditor;
model = editor.model;
doc = model.document;
const command = new DeleteCommand( editor, 'backward' );
editor.commands.add( 'delete', command );
model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
model.schema.register( 'heading1', { inheritAllFrom: '$block' } );
} );
} );
afterEach( () => {
return editor.destroy();
} );
it( 'has direction', () => {
const command = new DeleteCommand( editor, 'forward' );
expect( command ).to.have.property( 'direction', 'forward' );
} );
describe( 'buffer', () => {
it( 'has buffer getter', () => {
expect( editor.commands.get( 'delete' ).buffer ).to.be.an.instanceof( ChangeBuffer );
} );
it( 'has a buffer limit configured to default value of 20', () => {
expect( editor.commands.get( 'delete' ).buffer ).to.have.property( 'limit', 20 );
} );
it( 'has a buffer configured to config.typing.undoStep', () => {
return VirtualTestEditor
.create( {
plugins: [ Delete ],
typing: {
undoStep: 5
}
} )
.then( editor => {
expect( editor.commands.get( 'delete' ).buffer ).to.have.property( 'limit', 5 );
} );
} );
} );
describe( 'execute()', () => {
it( 'uses enqueueChange', () => {
setData( model, 'foo[]bar' );
model.enqueueChange( () => {
editor.execute( 'delete' );
// We expect that command is executed in enqueue changes block. Since we are already in
// an enqueued block, the command execution will be postponed. Hence, no changes.
expect( getData( model ) ).to.equal( 'foo[]bar' );
} );
// After all enqueued changes are done, the command execution is reflected.
expect( getData( model ) ).to.equal( 'fo[]bar' );
} );
it( 'locks buffer when executing', () => {
setData( model, 'foo[]bar' );
const buffer = editor.commands.get( 'delete' )._buffer;
const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
editor.execute( 'delete' );
expect( lockSpy.calledOnce ).to.be.true;
expect( unlockSpy.calledOnce ).to.be.true;
} );
it( 'deletes previous character when selection is collapsed', () => {
setData( model, 'foo[]bar' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( 'fo[]bar' );
} );
it( 'deletes selection contents', () => {
setData( model, 'fo[ob]ar' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( 'fo[]ar' );
} );
it( 'deletes contents of selection passed in options', () => {
setData( model, 'fo[ob]ar' );
const selection = model.createSelection( model.createRangeIn( model.document.getRoot() ) );
editor.execute( 'delete', { selection } );
expect( getData( model ) ).to.equal( '[]' );
} );
it( 'merges elements', () => {
setData( model, 'foo[]bar' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( 'foo[]bar' );
} );
it( 'does not try to delete when selection is at the boundary', () => {
const spy = sinon.spy();
editor.model.on( 'deleteContent', spy );
setData( model, '[]foo' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '[]foo' );
expect( spy.callCount ).to.equal( 0 );
} );
it( 'passes options to modifySelection', () => {
const spy = sinon.spy();
editor.model.on( 'modifySelection', spy );
setData( model, 'foo[]bar' );
editor.commands.get( 'delete' ).direction = 'forward';
editor.execute( 'delete', { unit: 'word' } );
expect( spy.callCount ).to.equal( 1 );
const modifyOpts = spy.args[ 0 ][ 1 ][ 1 ];
expect( modifyOpts ).to.have.property( 'direction', 'forward' );
expect( modifyOpts ).to.have.property( 'unit', 'word' );
} );
it( 'passes options to deleteContent #1', () => {
const spy = sinon.spy();
editor.model.on( 'deleteContent', spy );
setData( model, 'foo[]bar' );
editor.execute( 'delete' );
expect( spy.callCount ).to.equal( 1 );
const deleteOpts = spy.args[ 0 ][ 1 ][ 1 ];
expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', true );
} );
it( 'passes options to deleteContent #2', () => {
const spy = sinon.spy();
editor.model.on( 'deleteContent', spy );
setData( model, '[foobar]' );
editor.execute( 'delete' );
expect( spy.callCount ).to.equal( 1 );
const deleteOpts = spy.args[ 0 ][ 1 ][ 1 ];
expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', false );
} );
it( 'should pass the "direction" option to Model#deleteContent method', () => {
const spy = sinon.spy();
const forwardCommand = new DeleteCommand( editor, 'forward' );
editor.commands.add( 'forwardDelete', forwardCommand );
editor.model.on( 'deleteContent', spy );
setData( model, 'foo[]bar' );
editor.execute( 'delete' );
expect( spy.callCount ).to.equal( 1 );
let deleteOpts = spy.args[ 0 ][ 1 ][ 1 ];
expect( deleteOpts ).to.have.property( 'direction', 'backward' );
editor.execute( 'forwardDelete' );
expect( spy.callCount ).to.equal( 2 );
deleteOpts = spy.args[ 1 ][ 1 ][ 1 ];
expect( deleteOpts ).to.have.property( 'direction', 'forward' );
} );
it( 'leaves an empty paragraph after removing the whole content from editor', () => {
setData( model, '[Header 1Some text.]' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '[]' );
} );
it( 'leaves an empty paragraph after removing the whole content inside limit element', () => {
model.schema.register( 'section', {
inheritAllFrom: '$root',
allowIn: '$root',
isLimit: true
} );
setData( model,
'Foo' +
'' +
'[Header 1' +
'Some text.]' +
'' +
'Bar.'
);
editor.execute( 'delete' );
expect( getData( model ) ).to.equal(
'Foo' +
'' +
'Bar.'
);
} );
it( 'leaves an empty paragraph after removing another paragraph from block element', () => {
model.schema.register( 'section', {
inheritAllFrom: '$block',
isLimit: true
} );
model.schema.register( 'blockQuote', { inheritAllFrom: '$block' } );
model.schema.extend( 'section', { allowIn: '$root' } );
model.schema.extend( 'paragraph', { allowIn: 'section' } );
model.schema.extend( 'blockQuote', { allowIn: 'section' } );
model.schema.extend( 'paragraph', { allowIn: 'blockQuote' } );
setData( model, '' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '' );
} );
it( 'leaves an empty paragraph after removing the whole content when root element was not added as Schema limit', () => {
model.schema.extend( '$root', {
isLimit: false
} );
expect( model.schema.isLimit( '$root' ) ).to.be.false;
setData( model, '[]' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '[]' );
} );
it( 'replaces an empty element with paragraph', () => {
setData( model, '[]' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '[]' );
} );
it( 'does not replace an element when Backspace or Delete key is held', () => {
setData( model, 'Bar[]' );
for ( let sequence = 1; sequence < 10; ++sequence ) {
editor.execute( 'delete', { sequence } );
}
expect( getData( model ) ).to.equal( '[]' );
} );
it( 'does not replace with paragraph in another paragraph already occurs in limit element', () => {
setData( model, '[]' );
const element = doc.getRoot().getNodeByPath( [ 0 ] );
editor.execute( 'delete' );
expect( element ).is.equal( doc.getRoot().getNodeByPath( [ 0 ] ) );
} );
it( 'does not replace an element if a paragraph is not allowed in current position', () => {
model.schema.addChildCheck( ( ctx, childDef ) => {
if ( ctx.endsWith( '$root' ) && childDef.name == 'paragraph' ) {
return false;
}
} );
setData( model, '[]' );
editor.execute( 'delete' );
expect( getData( model ) ).to.equal( '[]' );
} );
} );
} );