/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
import { stringify, parse, getData, setData } from '/ckeditor5/engine/dev-utils/model.js';
import Document from '/ckeditor5/engine/model/document.js';
import DocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
import Element from '/ckeditor5/engine/model/element.js';
import Text from '/ckeditor5/engine/model/text.js';
import Range from '/ckeditor5/engine/model/range.js';
import Position from '/ckeditor5/engine/model/position.js';
import count from '/ckeditor5/utils/count.js';
describe( 'model test utils', () => {
let document, root, selection, sandbox;
beforeEach( () => {
document = new Document();
root = document.createRoot();
selection = document.selection;
sandbox = sinon.sandbox.create();
selection.removeAllRanges();
document.schema.registerItem( 'a', '$inline' );
document.schema.allow( { name: 'a', inside: '$root' } );
document.schema.allow( { name: 'a', inside: '$root', attributes: [ 'bar', 'car', 'foo' ] } );
document.schema.registerItem( 'b', '$inline' );
document.schema.allow( { name: 'b', inside: '$root' } );
document.schema.allow( { name: 'b', inside: '$root', attributes: [ 'barFoo', 'fooBar', 'x' ] } );
document.schema.registerItem( 'c', '$inline' );
document.schema.allow( { name: 'c', inside: '$root' } );
document.schema.registerItem( 'paragraph', '$block' );
document.schema.allow( { name: '$text', inside: '$root' } );
} );
afterEach( () => {
sandbox.restore();
} );
describe( 'getData', () => {
it( 'should use stringify method', () => {
const stringifySpy = sandbox.spy( getData, '_stringify' );
root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
expect( getData( document, { withoutSelection: true } ) ).to.equal( 'btext' );
sinon.assert.calledOnce( stringifySpy );
sinon.assert.calledWithExactly( stringifySpy, root );
} );
it( 'should use stringify method with selection', () => {
const stringifySpy = sandbox.spy( getData, '_stringify' );
root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
expect( getData( document ) ).to.equal( '[btext]' );
sinon.assert.calledOnce( stringifySpy );
sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
} );
it( 'should throw an error when passing invalid document', () => {
expect( () => {
getData( { invalid: 'document' } );
} ).to.throw( TypeError, 'Document needs to be an instance of engine.model.Document.' );
} );
} );
describe( 'setData', () => {
it( 'should use parse method', () => {
const parseSpy = sandbox.spy( setData, '_parse' );
const options = {};
const data = 'btexttext';
setData( document, data, options );
expect( getData( document, { withoutSelection: true } ) ).to.equal( data );
sinon.assert.calledOnce( parseSpy );
const args = parseSpy.firstCall.args;
expect( args[ 0 ] ).to.equal( data );
} );
it( 'should use parse method with selection', () => {
const parseSpy = sandbox.spy( setData, '_parse' );
const options = {};
const data = '[btext]';
setData( document, data, options );
expect( getData( document ) ).to.equal( data );
sinon.assert.calledOnce( parseSpy );
const args = parseSpy.firstCall.args;
expect( args[ 0 ] ).to.equal( data );
} );
it( 'should insert text', () => {
test( 'this is test text', '[]this is test text' );
} );
it( 'should insert text with selection around', () => {
test( '[this is test text]' );
} );
it( 'should insert text with selection inside #1', () => {
test( 'this [is test] text' );
} );
it( 'should insert text with selection inside #2', () => {
test( '[this is test] text' );
} );
it( 'should insert text with selection inside #2', () => {
test( 'this is [test text]' );
} );
it( 'should insert unicode text with selection', () => {
test( 'நி[லைக்]கு' );
} );
it( 'should insert element', () => {
test( 'foo bar', '[]foo bar' );
} );
it( 'should insert element with selection inside #1', () => {
test( '[foo ]bar' );
} );
it( 'should insert element with selection inside #2', () => {
test( '[foo ]bar' );
} );
it( 'should insert element with selection inside #3', () => {
test( '[foo bar]' );
} );
it( 'should insert backward selection', () => {
setData( document, '[foo bar]', { lastRangeBackward: true } );
expect( getData( document ) ).to.equal( '[foo bar]' );
expect( document.selection.isBackward ).to.true;
} );
it( 'should throw an error when passing invalid document', () => {
expect( () => {
setData( { invalid: 'document' } );
} ).to.throw( TypeError, 'Document needs to be an instance of engine.model.Document.' );
} );
function test( data, expected ) {
expected = expected || data;
setData( document, data );
expect( getData( document ) ).to.equal( expected );
}
} );
describe( 'stringify', () => {
it( 'should stringify text', () => {
const text = new Text( 'text', { underline: true, bold: true } );
expect( stringify( text ) ).to.equal( '<$text bold="true" underline="true">text$text>' );
} );
it( 'should stringify element', () => {
const element = new Element( 'a', null, [
new Element( 'b', null, new Text( 'btext' ) ),
new Text( 'atext' )
] );
expect( stringify( element ) ).to.equal( 'btextatext' );
} );
it( 'should stringify document fragment', () => {
const fragment = new DocumentFragment( [
new Element( 'b', null, new Text( 'btext' ) ),
new Text( 'atext' )
] );
expect( stringify( fragment ) ).to.equal( 'btextatext' );
} );
it( 'writes elements and texts', () => {
root.appendChildren( [
new Element( 'a', null, new Text( 'atext' ) ),
new Element( 'b', null, [
new Element( 'c1' ),
new Text( 'ctext' ),
new Element( 'c2' )
] ),
new Element( 'd' )
] );
expect( stringify( root ) ).to.equal(
'atextctext'
);
} );
it( 'writes element attributes', () => {
root.appendChildren(
new Element( 'a', { foo: true, bar: 1, car: false }, [
new Element( 'b', { fooBar: 'x y', barFoo: { x: 1, y: 2 } } )
] )
);
// Note: attributes are written in a very simplistic way, because they are not to be parsed. They are just
// to be compared in the tests with some patterns.
expect( stringify( root ) ).to.equal(
''
);
} );
it( 'writes text attributes', () => {
root.appendChildren( [
new Text( 'foo', { bold: true } ),
new Text( 'bar' ),
new Text( 'bom', { bold: true, italic: true } ),
new Element( 'a', null, [
new Text( 'pom', { underline: true, bold: true } )
] )
] );
expect( stringify( root ) ).to.equal(
// Because of https://github.com/ckeditor/ckeditor5-engine/issues/562 attributes are not merged
'<$text bold="true">foo$text>bar<$text italic="true"><$text bold="true">bom$text>$text>' +
'<$text bold="true" underline="true">pom$text>'
);
} );
it( 'writes unicode text', () => {
root.appendChildren( new Text( 'நிலைக்கு' ) );
expect( stringify( root ) ).to.equal( 'நிலைக்கு' );
} );
describe( 'selection', () => {
let elA, elB;
beforeEach( () => {
elA = new Element( 'a' );
elB = new Element( 'b' );
root.appendChildren( [
elA,
new Text( 'foo' ),
new Text( 'bar', { bold: true } ),
elB
] );
} );
it( 'writes selection in an empty root', () => {
const root = document.createRoot( '$root', 'empty' );
selection.collapse( root );
expect( stringify( root, selection ) ).to.equal(
'[]'
);
} );
it( 'writes selection collapsed in an element', () => {
selection.collapse( root );
expect( stringify( root, selection ) ).to.equal(
'[]foo<$text bold="true">bar$text>'
);
} );
it( 'writes selection collapsed in a text', () => {
selection.collapse( root, 3 );
expect( stringify( root, selection ) ).to.equal(
'fo[]o<$text bold="true">bar$text>'
);
} );
it( 'writes selection collapsed at the text left boundary', () => {
selection.collapse( elA, 'after' );
expect( stringify( root, selection ) ).to.equal(
'[]foo<$text bold="true">bar$text>'
);
} );
it( 'writes selection collapsed at the text right boundary', () => {
selection.collapse( elB, 'before' );
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">bar[]$text>'
);
} );
it( 'writes selection collapsed at the end of the root', () => {
selection.collapse( root, 'end' );
// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
selection.clearAttributes();
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">bar$text>[]'
);
} );
it( 'writes selection collapsed selection in a text with attributes', () => {
selection.collapse( root, 5 );
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">b[]ar$text>'
);
} );
it( 'writes flat selection containing couple of nodes', () => {
selection.addRange(
Range.createFromParentsAndOffsets( root, 0, root, 4 )
);
expect( stringify( root, selection ) ).to.equal(
'[foo]<$text bold="true">bar$text>'
);
} );
it( 'writes flat selection within text', () => {
selection.addRange(
Range.createFromParentsAndOffsets( root, 2, root, 3 )
);
expect( stringify( root, selection ) ).to.equal(
'f[o]o<$text bold="true">bar$text>'
);
} );
it( 'writes multi-level selection', () => {
selection.addRange(
Range.createFromParentsAndOffsets( elA, 0, elB, 0 )
);
expect( stringify( root, selection ) ).to.equal(
'[foo<$text bold="true">bar$text>]'
);
} );
it( 'writes selection when is backward', () => {
selection.addRange(
Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
true
);
expect( stringify( root, selection ) ).to.equal(
'[foo<$text bold="true">bar$text>]'
);
} );
it( 'writes selection in unicode text', () => {
const root = document.createRoot( '$root', 'empty' );
root.appendChildren( new Text( 'நிலைக்கு' ) );
selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
expect( stringify( root, selection ) ).to.equal( 'நி[லைக்]கு' );
} );
it( 'uses range and coverts it to selection', () => {
const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
expect( stringify( root, range ) ).to.equal(
'[foo<$text bold="true">bar$text>]'
);
} );
it( 'uses position and converts it to collapsed selection', () => {
const position = new Position( root, [ 0 ] );
expect( stringify( root, position ) ).to.equal(
'[]foo<$text bold="true">bar$text>'
);
} );
} );
} );
describe( 'parse', () => {
test( 'creates empty DocumentFragment from empty string', {
data: '',
check( fragment ) {
expect( fragment ).to.be.instanceOf( DocumentFragment );
}
} );
test( 'creates empty DocumentFragment with selection', {
data: '[]',
check( fragment, selection ) {
expect( fragment ).to.be.instanceOf( DocumentFragment );
expect( fragment.childCount ).to.equal( 0 );
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( fragment, 0, fragment, 0 ) ) ).to.be.true;
}
} );
test( 'returns Element if range is around single element', {
data: '[]',
check( el, selection ) {
const fragment = el.parent;
expect( el ).to.be.instanceOf( Element );
expect( fragment ).to.be.instanceOf( DocumentFragment );
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( fragment, 0, fragment, 1 ) ) ).to.be.true;
}
} );
test( 'returns DocumentFragment when multiple elements on root', {
data: '',
check( fragment ) {
expect( fragment ).to.be.instanceOf( DocumentFragment );
expect( fragment.childCount ).to.equal( 2 );
}
} );
test( 'creates elements', {
data: ''
} );
test( 'creates text nodes', {
data: 'foobarbom'
} );
test( 'creates text nodes with unicode text', {
data: 'நிலைக்கு'
} );
test( 'sets elements attributes', {
data: '',
output: '',
check( root ) {
expect( root.getChild( 0 ).getAttribute( 'bar' ) ).to.equal( true );
expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.equal( 1 );
expect( root.getChild( 1 ).getAttribute( 'x' ) ).to.equal( 'y' );
}
} );
test( 'sets text attributes', {
data: '<$text bar="true" car="x y" foo="1">foo$text><$text x="y">bar$text>bom',
check( root ) {
expect( root.childCount ).to.equal( 3 );
expect( root.maxOffset ).to.equal( 9 );
expect( root.getChild( 0 ).getAttribute( 'bar' ) ).to.equal( true );
expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.equal( 1 );
expect( root.getChild( 1 ).getAttribute( 'x' ) ).to.equal( 'y' );
expect( count( root.getChild( 2 ).getAttributes() ) ).to.equal( 0 );
}
} );
test( 'creates element with complex attributes', {
data: ``,
output: '',
check( a ) {
expect( count( a.getAttributes() ) ).to.equal( 1 );
expect( a.getAttribute( 'foo' ) ).to.have.property( 'x', 1 );
expect( a.getAttribute( 'foo' ) ).to.have.property( 'y', 2 );
}
} );
test( 'returns single parsed element', {
data: '',
check( p ) {
expect( p instanceof Element ).to.be.true;
}
} );
test( 'returns DocumentFragment for multiple parsed elements', {
data: '',
check( fragment ) {
expect( fragment instanceof DocumentFragment ).to.be.true;
}
} );
it( 'throws when invalid XML', () => {
expect( () => {
parse( '', document.schema );
} ).to.throw( Error, /Parse error/ );
} );
it( 'throws when try to set element not registered in schema', () => {
expect( () => {
parse( '', document.schema );
} ).to.throw( Error, `Element 'xyz' not allowed in context.` );
} );
it( 'throws when try to set text directly to $root without registering it', () => {
const doc = new Document();
expect( () => {
parse( 'text', doc.schema );
} ).to.throw( Error, `Element '$text' not allowed in context.` );
} );
describe( 'selection', () => {
test( 'sets collapsed selection in an element', {
data: '[]',
check( root, selection ) {
expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
}
} );
test( 'sets collapsed selection between elements', {
data: '[]'
} );
test( 'sets collapsed selection before a text', {
data: '[]foo'
} );
test( 'sets collapsed selection after a text', {
data: 'foo[]'
} );
test( 'sets collapsed selection within a text', {
data: 'foo[]bar',
check( text, selection ) {
expect( text.offsetSize ).to.equal( 6 );
expect( text.getPath() ).to.deep.equal( [ 0 ] );
expect( selection.getFirstRange().start.path ).to.deep.equal( [ 3 ] );
expect( selection.getFirstRange().end.path ).to.deep.equal( [ 3 ] );
}
} );
it( 'sets selection attributes', () => {
const result = parse( 'foo[]bar', document.schema, { selectionAttributes: {
bold: true,
italic: true
} } );
expect( stringify( result.model, result.selection ) ).to.equal( 'foo<$text bold="true" italic="true">[]$text>bar' );
} );
test( 'sets collapsed selection between text and text with attributes', {
data: 'foo[]<$text bold="true">bar$text>',
check( root, selection ) {
expect( root.maxOffset ).to.equal( 6 );
expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
}
} );
test( 'sets selection containing an element', {
data: 'x[]'
} );
it( 'sets selection with attribute containing an element', () => {
const result = parse( 'x[]', document.schema, { selectionAttributes: {
bold: true
} } );
expect( stringify( result.model, result.selection ) ).to.equal( 'x[]' );
expect( result.selection.getAttribute( 'bold' ) ).to.be.true;
} );
it( 'sets a backward selection containing an element', () => {
const result = parse( 'x[]', document.schema, {
lastRangeBackward: true
} );
expect( stringify( result.model, result.selection ) ).to.equal( 'x[]' );
expect( result.selection.isBackward ).to.true;
} );
test( 'sets selection within a text', {
data: 'x[y]z'
} );
it( 'sets selection within a text with different attributes', () => {
const result = parse( '<$text bold="true">fo[o$text>ba]r', document.schema, {
selectionAttributes: { bold: true }
} );
expect( stringify( result.model, result.selection ) ).to.equal( '<$text bold="true">fo[o$text>ba]r' );
expect( result.selection.getAttribute( 'bold' ) ).to.true;
} );
it( 'throws when missing selection start', () => {
expect( () => {
parse( 'foo]' );
} ).to.throw( Error );
} );
it( 'throws when missing selection end', () => {
expect( () => {
parse( '[foo' );
} ).to.throw( Error );
} );
} );
function test( title, options ) {
it( title, () => {
const output = options.output || options.data;
const data = parse( options.data, document.schema );
let model, selection;
if ( data.selection && data.model ) {
model = data.model;
selection = data.selection;
} else {
model = data;
}
expect( stringify( model, selection ) ).to.equal( output );
if ( options.check ) {
options.check( model, selection );
}
} );
}
} );
} );