/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* global document */
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import Title from '../src/title';
import Heading from '../src/heading';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import { setData, getData, stringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
describe( 'Title', () => {
let element, editor, model;
beforeEach( () => {
element = document.createElement( 'div' );
document.body.appendChild( element );
return ClassicTestEditor.create( element, {
plugins: [ Title, Heading, BlockQuote, Clipboard, Image, ImageUpload, Enter, Undo ]
} ).then( _editor => {
editor = _editor;
model = editor.model;
} );
} );
afterEach( () => {
return editor.destroy().then( () => element.remove() );
} );
it( 'should requires Paragraph plugin', () => {
expect( Title.requires ).to.have.members( [ Paragraph ] );
} );
it( 'should have plugin name property', () => {
expect( Title.pluginName ).to.equal( 'Title' );
} );
it( 'should set proper schema rules', () => {
expect( model.schema.isRegistered( 'title' ) ).to.equal( true );
expect( model.schema.isBlock( 'title' ) ).to.equal( true );
expect( model.schema.isRegistered( 'title-content' ) ).to.equal( true );
expect( model.schema.isBlock( 'title-content' ) ).to.equal( true );
expect( model.schema.checkChild( 'title', '$text' ) ).to.equal( false );
expect( model.schema.checkChild( 'title', '$block' ) ).to.equal( false );
expect( model.schema.checkChild( 'title', 'title-content' ) ).to.equal( true );
expect( model.schema.checkChild( '$root', 'title' ) ).to.equal( true );
expect( model.schema.checkChild( '$root', 'title-content' ) ).to.equal( false );
expect( model.schema.checkChild( '$block', 'title-content' ) ).to.equal( false );
expect( model.schema.checkChild( 'title-content', '$text' ) ).to.equal( true );
expect( model.schema.checkChild( 'title-content', '$block' ) ).to.equal( false );
expect( model.schema.checkAttribute( [ 'title-content' ], 'alignment' ) ).to.equal( true );
model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
expect( model.schema.checkAttribute( [ 'title-content', '$text' ], 'bold' ) ).to.equal( false );
} );
it( 'should convert title to h1', () => {
setData( model,
'
Foo' +
'Bar'
);
expect( editor.getData() ).to.equal( 'Foo
Bar
' );
} );
it( 'should convert h1 to the title if it is the first root child', () => {
editor.setData( 'Foo
Bar
' );
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should avoid calling post-fixers to parse view to correct model (h1)', () => {
const modelFrag = editor.data.parse( 'Foo
Bar
' );
expect( stringify( modelFrag ) ).to.equal(
'Foo' +
'Bar'
);
} );
it( 'should avoid calling post-fixers to parse view to correct model (h2)', () => {
const modelFrag = editor.data.parse( 'Foo
Bar
' );
expect( stringify( modelFrag ) ).to.equal(
'Foo' +
'Bar'
);
} );
it( 'should avoid calling post-fixers to parse view to correct model (h3)', () => {
const modelFrag = editor.data.parse( 'Foo
Bar
' );
expect( stringify( modelFrag ) ).to.equal(
'Foo' +
'Bar'
);
} );
it( 'should allow to override custom v->m title converter', () => {
const spy = sinon.spy();
editor.data.upcastDispatcher.on( 'element:h1', ( evt, data, api ) => {
api.consumable.consume( data.viewItem, { name: true } );
spy();
}, { priority: 'highest' } );
editor.setData( 'Foo
Bar
' );
sinon.assert.called( spy );
} );
describe( 'model post-fixing', () => {
it( 'should set title and content elements', () => {
setData( model,
'Foo' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should create a content element when only title has been set', () => {
setData( model, 'Foo' );
expect( getData( model ) ).to.equal(
'[]Foo' +
''
);
} );
it( 'should create a title and content elements when are missing', () => {
setData( model, '' );
expect( getData( model ) ).to.equal(
'[]' +
''
);
} );
it( 'should change heading element to title when is set as a first root child', () => {
setData( model,
'Foo' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should change paragraph element to title when is set as a first root child', () => {
setData( model,
'Foo' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should change paragraph element to title and then change additional title elements to paragraphs', () => {
setData( model,
'Foo' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should change title element to a paragraph when is not a first root child #1', () => {
setData( model,
'Foo' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar'
);
} );
it( 'should change title element to a paragraph when is not a first root child #2', () => {
setData( model,
'Foo' +
'Bar' +
'Biz'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'Bar' +
'Biz'
);
} );
it( 'should move title at the beginning of the root when first root child is not allowed to be a title #1', () => {
setData( model,
'Foo
' +
'Bar'
);
expect( getData( model ) ).to.equal(
'[]Bar' +
'Foo
'
);
} );
it( 'should move title at the beginning of the root when first root child is not allowed to be a title #2', () => {
setData( model,
'Foo
' +
'Bar
' +
'Biz'
);
expect( getData( model ) ).to.equal(
'[]Biz' +
'Foo
' +
'Bar
'
);
} );
it( 'should move title at the beginning of the root when first root child is not allowed to be a title #3', () => {
setData( model,
'Foo
' +
'Bar' +
'Biz'
);
expect( getData( model ) ).to.equal(
'[]Biz' +
'Foo
' +
'Bar'
);
} );
it( 'should create a missing title element before an element that cannot to be a title element', () => {
setData( model, 'Foo
' );
expect( getData( model ) ).to.equal(
'[]' +
'Foo
'
);
} );
it( 'should clear element from attributes when changing to title element', () => {
model.schema.extend( '$text', { allowAttributes: 'foo' } );
model.schema.extend( 'paragraph', { allowAttributes: [ 'foo', 'alignment' ] } );
setData( model,
'F<$text foo="true">o$text>o' +
'B<$text foo="true">a$text>r'
);
expect( getData( model ) ).to.equal(
'[]Foo' +
'B<$text foo="true">a$text>r'
);
} );
} );
describe( 'removes extra paragraph', () => {
it( 'should remove the extra paragraph when pasting to the editor with body placeholder', () => {
setData( model, '[]' );
const dataTransferMock = {
getData: type => {
if ( type === 'text/html' ) {
return 'Title
Body
';
}
},
types: [],
files: []
};
editor.editing.view.document.fire( 'paste', {
dataTransfer: dataTransferMock,
stopPropagation() {},
preventDefault() {}
} );
expect( getData( model ) ).to.equal(
'Title' +
'Body[]'
);
} );
it( 'should not remove the extra paragraph when pasting to the editor with directly created body element', () => {
setData( model,
'[]' +
''
);
const dataTransferMock = {
getData: type => {
if ( type === 'text/html' ) {
return 'Title
Body
';
}
},
types: [],
files: []
};
editor.editing.view.document.fire( 'paste', {
dataTransfer: dataTransferMock,
stopPropagation() {},
preventDefault() {}
} );
expect( getData( model ) ).to.equal(
'Title' +
'Body[]' +
''
);
} );
it( 'should remove the extra paragraph when pressing enter in the title', () => {
setData( model, 'fo[]o' );
editor.execute( 'enter' );
expect( getData( model ) ).to.equal(
'fo' +
'[]o'
);
} );
it( 'should not remove the extra paragraph when pressing enter in the title when body is created directly', () => {
setData( model,
'fo[]o' +
''
);
editor.execute( 'enter' );
expect( getData( model ) ).to.equal(
'fo' +
'[]o' +
''
);
} );
} );
describe( 'getTitle()', () => {
it( 'should return content of a title element', () => {
setData( model,
'Foo' +
'Bar'
);
expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( 'Foo' );
} );
it( 'should return content of an empty title element', () => {
setData( model,
'' +
'Bar'
);
expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( '' );
} );
it( 'should return marker - starts and ends inside a title', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model, 'Foo Bar' );
const title = model.document.getRoot().getChild( 0 ).getChild( 0 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( title, 1 ), model.createPositionAt( title, 5 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal(
'Foo Bar'
);
} );
it( 'should return marker - starts inside a title ends inside a body', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model,
'Foo Bar' +
'Biz'
);
const title = model.document.getRoot().getChild( 0 ).getChild( 0 );
const body = model.document.getRoot().getChild( 1 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( title, 1 ), model.createPositionAt( body, 3 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal(
'Foo Bar'
);
} );
} );
describe( 'getBody()', () => {
it( 'should return all data except the title element', () => {
setData( model,
'Foo' +
'Bar' +
'Biz'
);
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( 'Bar
Biz
' );
} );
it( 'should return empty paragraph when body is empty', () => {
setData( model, 'Foo' );
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( '
' );
} );
it( 'should return marker - starts and ends inside a body', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model,
'' +
'Foo Bar'
);
const body = model.document.getRoot().getChild( 1 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( body, 1 ), model.createPositionAt( body, 5 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal(
'Foo Bar
'
);
} );
it( 'should return marker - starts inside a title ends inside a body', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model,
'Foo' +
'Bar'
);
const title = model.document.getRoot().getChild( 0 ).getChild( 0 );
const body = model.document.getRoot().getChild( 1 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( title, 1 ), model.createPositionAt( body, 2 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal(
'Bar
'
);
} );
it( 'should return marker - starts at the beginning of the body ends inside the body', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model,
'Foo' +
'Bar'
);
const body = model.document.getRoot().getChild( 1 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( body, 0 ), model.createPositionAt( body, 2 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal(
'Bar
'
);
} );
it( 'should do nothing when marker is fully out of the body range', () => {
editor.conversion.for( 'downcast' ).markerToElement( { model: 'comment', view: 'comment' } );
setData( model,
'Foo' +
'Bar'
);
const title = model.document.getRoot().getChild( 0 ).getChild( 0 );
model.change( writer => {
writer.addMarker( 'comment', {
range: model.createRange( model.createPositionAt( title, 1 ), model.createPositionAt( title, 2 ) ),
usingOperation: true
} );
} );
expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( 'Bar
' );
} );
} );
describe( 'placeholders', () => {
let viewRoot;
beforeEach( () => {
viewRoot = editor.editing.view.document.getRoot();
} );
it( 'should attach placeholder placeholder to title and body', () => {
setData( model,
'Foo' +
'Bar'
);
const title = viewRoot.getChild( 0 );
const body = viewRoot.getChild( 1 );
expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'Type your title' );
expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Type or paste your content here.' );
expect( title.hasClass( 'ck-placeholder' ) ).to.equal( false );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
} );
it( 'should show placeholder in empty title and body', () => {
setData( model,
'' +
''
);
const title = viewRoot.getChild( 0 );
const body = viewRoot.getChild( 1 );
expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'Type your title' );
expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Type or paste your content here.' );
expect( title.hasClass( 'ck-placeholder' ) ).to.equal( true );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( true );
} );
it( 'should hide placeholder from body with more than one child elements', () => {
setData( editor.model,
'Foo' +
'' +
''
);
const body = viewRoot.getChild( 1 );
expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Type or paste your content here.' );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
} );
it( 'should hide placeholder from body with element other than paragraph', () => {
setData( editor.model,
'Foo' +
''
);
const body = viewRoot.getChild( 1 );
expect( body.hasAttribute( 'data-placeholder' ) ).to.equal( true );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
} );
it( 'should hide placeholder when title element become not empty', () => {
setData( model,
'' +
'[]'
);
expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
model.change( writer => {
writer.appendText( 'Bar', null, model.document.getRoot().getChild( 0 ).getChild( 0 ) );
} );
expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( false );
} );
it( 'should hide placeholder when body element become not empty', () => {
setData( model,
'Foo' +
''
);
expect( viewRoot.getChild( 1 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
model.change( writer => {
writer.appendText( 'Bar', null, model.document.getRoot().getChild( 1 ) );
} );
expect( viewRoot.getChild( 1 ).hasClass( 'ck-placeholder' ) ).to.equal( false );
} );
it( 'should properly map the body placeholder in DOM when undoing', () => {
const viewRoot = editor.editing.view.document.getRoot();
const domConverter = editor.editing.view.domConverter;
let bodyDomElement;
setData( editor.model,
'[Foo' +
'Bar' +
'Baz]'
);
editor.model.deleteContent( editor.model.document.selection );
bodyDomElement = domConverter.mapViewToDom( viewRoot.getChild( 1 ) );
expect( bodyDomElement.dataset.placeholder ).to.equal( 'Type or paste your content here.' );
expect( bodyDomElement.classList.contains( 'ck-placeholder' ) ).to.equal( true );
editor.execute( 'undo' );
bodyDomElement = domConverter.mapViewToDom( viewRoot.getChild( 1 ) );
expect( bodyDomElement.dataset.placeholder ).to.equal( 'Type or paste your content here.' );
expect( bodyDomElement.classList.contains( 'ck-placeholder' ) ).to.equal( false );
} );
describe( 'custom placeholder defined using configuration', () => {
let element, editor, model;
beforeEach( () => {
element = document.createElement( 'div' );
document.body.appendChild( element );
return ClassicTestEditor.create( element, {
plugins: [ Title, Heading, BlockQuote, Clipboard, Image, ImageUpload, Enter, Undo ],
title: {
placeholder: 'foo'
},
placeholder: 'bar'
} ).then( _editor => {
editor = _editor;
model = editor.model;
} );
} );
afterEach( () => {
return editor.destroy().then( () => element.remove() );
} );
it( 'should show custom placeholder in title and body', () => {
const viewRoot = editor.editing.view.document.getRoot();
setData( model,
'' +
''
);
const title = viewRoot.getChild( 0 );
const body = viewRoot.getChild( 1 );
expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'foo' );
expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'bar' );
expect( title.hasClass( 'ck-placeholder' ) ).to.equal( true );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( true );
} );
} );
describe( 'custom placeholder defined using textarea attribte', () => {
let element, editor, model;
beforeEach( () => {
element = document.createElement( 'textarea' );
element.setAttribute( 'placeholder', 'bom' );
document.body.appendChild( element );
return ClassicTestEditor.create( element, {
plugins: [ Title, Heading, BlockQuote, Clipboard, Image, ImageUpload, Enter, Undo ],
title: {
placeholder: 'foo'
}
} ).then( _editor => {
editor = _editor;
model = editor.model;
} );
} );
afterEach( () => {
return editor.destroy().then( () => element.remove() );
} );
it( 'should show custom placeholder in title and body', () => {
const viewRoot = editor.editing.view.document.getRoot();
setData( model,
'' +
''
);
const title = viewRoot.getChild( 0 );
const body = viewRoot.getChild( 1 );
expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'foo' );
expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'bom' );
expect( title.hasClass( 'ck-placeholder' ) ).to.equal( true );
expect( body.hasClass( 'ck-placeholder' ) ).to.equal( true );
} );
} );
} );
describe( 'Tab press handling', () => {
it( 'should handle tab key when the selection is at the beginning of the title', () => {
setData( model,
'[]foo' +
'bar'
);
const eventData = getEventData( keyCodes.tab );
editor.keystrokes.press( eventData );
sinon.assert.calledOnce( eventData.preventDefault );
sinon.assert.calledOnce( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'foo' +
'[]bar'
);
} );
it( 'should handle tab key when the selection is at the end of the title', () => {
setData( model,
'foo[]' +
'bar'
);
const eventData = getEventData( keyCodes.tab );
editor.keystrokes.press( eventData );
sinon.assert.calledOnce( eventData.preventDefault );
sinon.assert.calledOnce( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'foo' +
'[]bar'
);
} );
it( 'should not handle tab key when the selection is in the title and body', () => {
setData( model,
'fo[o' +
'b]ar'
);
const eventData = getEventData( keyCodes.tab );
editor.keystrokes.press( eventData );
sinon.assert.notCalled( eventData.preventDefault );
sinon.assert.notCalled( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'fo[o' +
'b]ar'
);
} );
it( 'should not handle tab key when the selection is in the body', () => {
setData( model,
'foo' +
'[]bar'
);
const eventData = getEventData( keyCodes.tab );
editor.keystrokes.press( eventData );
sinon.assert.notCalled( eventData.preventDefault );
sinon.assert.notCalled( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'foo' +
'[]bar'
);
} );
} );
describe( 'Shift + Tab press handling', () => {
it( 'should handle shift + tab keys when the selection is at the beginning of the body', () => {
setData( model,
'foo' +
'[]bar'
);
const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
editor.keystrokes.press( eventData );
sinon.assert.calledOnce( eventData.preventDefault );
sinon.assert.calledOnce( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'[]foo' +
'bar'
);
} );
it( 'should not handle shift + tab keys when the selection is not at the beginning of the body', () => {
setData( model,
'foo' +
'b[]ar'
);
const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
editor.keystrokes.press( eventData );
sinon.assert.notCalled( eventData.preventDefault );
sinon.assert.notCalled( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'foo' +
'b[]ar'
);
} );
it( 'should not handle shift + tab keys when the selection is not collapsed', () => {
setData( model,
'foo' +
'[b]ar'
);
const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
editor.keystrokes.press( eventData );
sinon.assert.notCalled( eventData.preventDefault );
sinon.assert.notCalled( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'foo' +
'[b]ar'
);
} );
it( 'should not handle shift + tab keys when the selection is in the title', () => {
setData( model,
'[]foo' +
'bar'
);
const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
editor.keystrokes.press( eventData );
sinon.assert.notCalled( eventData.preventDefault );
sinon.assert.notCalled( eventData.stopPropagation );
expect( getData( model ) ).to.equal(
'[]foo' +
'bar'
);
} );
} );
} );
function getEventData( keyCode, { shiftKey = false } = {} ) {
return {
keyCode,
shiftKey,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
};
}