8
0
Quellcode durchsuchen

Merge pull request #411 from ckeditor/t/385

Unification of model and view test utils.
Piotr Jasiun vor 9 Jahren
Ursprung
Commit
268d7a29d1

+ 151 - 74
packages/ckeditor5-engine/tests/_utils-tests/model.js

@@ -5,24 +5,94 @@
 
 'use strict';
 
-import { getData, setData } from '/tests/engine/_utils/model.js';
+import { stringify, parse, getData, setData } from '/tests/engine/_utils/model.js';
 import Document from '/ckeditor5/engine/treemodel/document.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
+import Position from '/ckeditor5/engine/treemodel/position.js';
 
 describe( 'model test utils', () => {
-	let document, root, selection;
+	let document, root, selection, sandbox;
 
 	beforeEach( () => {
 		document = new Document();
 		root = document.createRoot( 'main', '$root' );
 		selection = document.selection;
-
+		sandbox = sinon.sandbox.create();
 		selection.removeAllRanges();
 	} );
 
+	afterEach( () => {
+		sandbox.restore();
+	} );
+
 	describe( 'getData', () => {
+		it( 'should use stringify method', () => {
+			const stringifySpy = sandbox.spy( getData, '_stringify' );
+			root.appendChildren( new Element( 'b', null, [ 'btext' ] ) );
+
+			expect( getData( document, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
+			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, [ 'btext' ] ) );
+			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+
+			expect( getData( document ) ).to.equal( '<selection><b>btext</b></selection>' );
+			sinon.assert.calledOnce( stringifySpy );
+			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
+		} );
+	} );
+
+	describe( 'setData', () => {
+		it( 'should use parse method', () => {
+			const parseSpy = sandbox.spy( setData, '_parse' );
+			const options = {};
+			const data = '<b>btext</b>text';
+
+			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 = '<selection><b>btext</b></selection>';
+
+			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 );
+		} );
+	} );
+
+	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, 'btext' ), 'atext' ] );
+			expect( stringify( element ) ).to.equal( '<a><b>btext</b>atext</a>' );
+		} );
+
+		it( 'should stringify document fragment', () => {
+			const fragment = new DocumentFragment( [ new Element( 'b', null, 'btext' ), 'atext' ] );
+			expect( stringify( fragment ) ).to.equal( '<b>btext</b>atext' );
+		} );
+
 		it( 'writes elements and texts', () => {
 			root.appendChildren( [
 				new Element( 'a', null, 'atext' ),
@@ -34,7 +104,7 @@ describe( 'model test utils', () => {
 				new Element( 'd' )
 			] );
 
-			expect( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<a>atext</a><b><c1></c1>ctext<c2></c2></b><d></d>'
 			);
 		} );
@@ -48,7 +118,7 @@ describe( 'model test utils', () => {
 
 			// 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( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<a bar=1 car=false foo=true><b barFoo={"x":1,"y":2} fooBar="x y"></b></a>'
 			);
 		} );
@@ -63,7 +133,7 @@ describe( 'model test utils', () => {
 				] )
 			] );
 
-			expect( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<$text bold=true>foo</$text>' +
 				'bar' +
 				'<$text bold=true italic=true>bom</$text>' +
@@ -71,9 +141,8 @@ describe( 'model test utils', () => {
 			);
 		} );
 
-		describe( 'options.selection', () => {
+		describe( 'selection', () => {
 			let elA, elB;
-			const options = { selection: true };
 
 			beforeEach( () => {
 				elA = new Element( 'a' );
@@ -89,10 +158,9 @@ describe( 'model test utils', () => {
 
 			it( 'writes selection in an empty root', () => {
 				const root = document.createRoot( 'empty', '$root' );
-
 				selection.collapse( root );
 
-				expect( getData( document, 'empty', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection />'
 				);
 			} );
@@ -100,7 +168,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed in an element', () => {
 				selection.collapse( root );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -108,7 +176,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed in a text', () => {
 				selection.collapse( root, 3 );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>fo<selection />o<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -116,7 +184,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed at the text left boundary', () => {
 				selection.collapse( elA, 'AFTER' );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a><selection />foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -124,7 +192,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed at the text right boundary', () => {
 				selection.collapse( elB, 'BEFORE' );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>bar</$text><selection bold=true /><b></b>'
 				);
 			} );
@@ -135,7 +203,7 @@ describe( 'model test utils', () => {
 				// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
 				selection.clearAttributes();
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>bar</$text><b></b><selection />'
 				);
 			} );
@@ -144,7 +212,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root );
 				selection.setAttributesTo( { italic: true, bold: true } );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection )  ).to.equal(
 					'<selection bold=true italic=true /><a></a>foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -152,7 +220,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed selection in a text with attributes', () => {
 				selection.collapse( root, 5 );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>b<selection bold=true />ar</$text><b></b>'
 				);
 			} );
@@ -162,7 +230,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( root, 0, root, 4 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection><a></a>foo</selection><$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -172,7 +240,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( root, 2, root, 3 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>f<selection>o</selection>o<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -182,7 +250,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( elA, 0, elB, 0 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
 				);
 			} );
@@ -193,14 +261,30 @@ describe( 'model test utils', () => {
 					true
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a><selection backward></a>foo<$text bold=true>bar</$text><b></selection></b>'
 				);
 			} );
+
+			it( 'uses range and coverts it to selection', () => {
+				const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
+
+				expect( stringify( root, range ) ).to.equal(
+					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
+				);
+			} );
+
+			it( 'uses position and converts it to collapsed selection', () => {
+				const position = new Position( root, [ 0 ] );
+
+				expect( stringify( root, position ) ).to.equal(
+					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
+				);
+			} );
 		} );
 	} );
 
-	describe( 'setData', () => {
+	describe( 'parse', () => {
 		test( 'creates elements', {
 			data: '<a></a><b><c></c></b>'
 		} );
@@ -212,21 +296,21 @@ describe( 'model test utils', () => {
 		test( 'sets elements attributes', {
 			data: '<a foo=1 bar=true car="x y"><b x="y"></b></a>',
 			output: '<a bar=true car="x y" foo=1><b x="y"></b></a>',
-			check() {
+			check( root ) {
 				expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
 			}
 		} );
 
 		test( 'sets complex attributes', {
 			data: '<a foo={"a":1,"b":"c"}></a>',
-			check() {
+			check( root ) {
 				expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.have.property( 'a', 1 );
 			}
 		} );
 
 		test( 'sets text attributes', {
 			data: '<$text bold=true italic=true>foo</$text><$text bold=true>bar</$text>bom',
-			check() {
+			check( root ) {
 				expect( root.getChildCount() ).to.equal( 9 );
 				expect( root.getChild( 0 ) ).to.have.property( 'character', 'f' );
 				expect( root.getChild( 0 ).getAttribute( 'italic' ) ).to.equal( true );
@@ -235,139 +319,132 @@ describe( 'model test utils', () => {
 
 		it( 'throws when unexpected closing tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a><b></a></b>' );
-			} ).to.throw();
+				parse( '<a><b></a></b>' );
+			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
 		} );
 
 		it( 'throws when unexpected attribute', () => {
 			expect( () => {
-				setData( document, 'main', '<a ?></a>' );
-			} ).to.throw();
+				parse( '<a ?></a>' );
+			} ).to.throw( Error, 'Parse error - unexpected token: ?.' );
 		} );
 
 		it( 'throws when incorrect tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a' );
-			} ).to.throw();
+				parse( '<a' );
+			} ).to.throw( Error, 'Parse error - unexpected token: <a.' );
 		} );
 
 		it( 'throws when missing closing tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a><b></b>' );
-			} ).to.throw();
+				parse( '<a><b></b>' );
+			} ).to.throw( Error, 'Parse error - missing closing tags: a.' );
 		} );
 
 		it( 'throws when missing opening tag for text', () => {
 			expect( () => {
-				setData( document, 'main', '</$text>' );
-			} ).to.throw();
+				parse( '</$text>' );
+			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
 		} );
 
 		it( 'throws when missing closing tag for text', () => {
 			expect( () => {
-				setData( document, 'main', '<$text>' );
-			} ).to.throw();
+				parse( '<$text>' );
+			} ).to.throw( Error, 'Parse error - missing closing tags: $text.' );
 		} );
 
 		describe( 'selection', () => {
-			const getDataOptions = { selection: true };
-
 			test( 'sets collapsed selection in an element', {
 				data: '<a><selection /></a>',
-				getDataOptions,
-				check() {
-					expect( document.selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
+				check( root, selection ) {
+					expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
 				}
 			} );
 
 			test( 'sets collapsed selection between elements', {
-				data: '<a></a><selection /><b></b>',
-				getDataOptions
+				data: '<a></a><selection /><b></b>'
 			} );
 
 			test( 'sets collapsed selection before a text', {
-				data: '<a></a><selection />foo',
-				getDataOptions
+				data: '<a></a><selection />foo'
 			} );
 
 			test( 'sets collapsed selection after a text', {
-				data: 'foo<selection />',
-				getDataOptions
+				data: 'foo<selection />'
 			} );
 
 			test( 'sets collapsed selection within a text', {
 				data: 'foo<selection />bar',
-				getDataOptions,
-				check() {
+				check( root ) {
 					expect( root.getChildCount() ).to.equal( 6 );
 				}
 			} );
 
 			test( 'sets selection attributes', {
 				data: 'foo<selection bold=true italic=true />bar',
-				getDataOptions,
-				check() {
+				check( root, selection ) {
 					expect( selection.getAttribute( 'italic' ) ).to.be.true;
 				}
 			} );
 
 			test( 'sets collapsed selection between text and text with attributes', {
 				data: 'foo<selection /><$text bold=true>bar</$text>',
-				getDataOptions,
-				check() {
+				check( root, selection ) {
 					expect( root.getChildCount() ).to.equal( 6 );
-					expect( document.selection.getAttribute( 'bold' ) ).to.be.undefined;
+					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
 				}
 			} );
 
 			test( 'sets selection containing an element', {
-				data: 'x<selection><a></a></selection>',
-				getDataOptions
+				data: 'x<selection><a></a></selection>'
 			} );
 
 			test( 'sets selection with attribute containing an element', {
-				data: 'x<selection bold=true><a></a></selection>',
-				getDataOptions
+				data: 'x<selection bold=true><a></a></selection>'
 			} );
 
 			test( 'sets a backward selection containing an element', {
-				data: 'x<selection backward bold=true><a></a></selection>',
-				getDataOptions
+				data: 'x<selection backward bold=true><a></a></selection>'
 			} );
 
 			test( 'sets selection within a text', {
-				data: 'x<selection bold=true>y</selection>z',
-				getDataOptions
+				data: 'x<selection bold=true>y</selection>z'
 			} );
 
 			test( 'sets selection within a text with different attributes', {
-				data: '<$text bold=true>fo<selection bold=true>o</$text>ba</selection>r',
-				getDataOptions
+				data: '<$text bold=true>fo<selection bold=true>o</$text>ba</selection>r'
 			} );
 
 			it( 'throws when missing selection start', () => {
 				expect( () => {
-					setData( document, 'main', 'foo</selection>' );
-				} ).to.throw();
+					parse( 'foo</selection>' );
+				} ).to.throw( Error, 'Parse error - missing selection start.' );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					setData( document, 'main', '<selection>foo' );
-				} ).to.throw();
+					parse( '<selection>foo' );
+				} ).to.throw( Error, 'Parse error - missing selection end.' );
 			} );
 		} );
 
 		function test( title, options ) {
 			it( title, () => {
-				let output = options.output || options.data;
-
-				setData( document, 'main', options.data, options.setDataOptions );
+				const output = options.output || options.data;
+				const data = parse( options.data );
+				let model, selection;
+
+				if ( data.selection && data.model ) {
+					model = data.model;
+					selection = data.selection;
+				} else {
+					model = data;
+				}
 
-				expect( getData( document, 'main', options.getDataOptions ) ).to.equal( output );
+				expect( stringify( model, selection ) ).to.equal( output );
 
 				if ( options.check ) {
-					options.check();
+					options.check( model, selection );
 				}
 			} );
 		}

+ 102 - 2
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-import { stringify, parse } from '/tests/engine/_utils/view.js';
+import { parse, stringify, getData, setData }from '/tests/engine/_utils/view.js';
 import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
@@ -14,8 +14,94 @@ import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
 import Selection from '/ckeditor5/engine/treeview/selection.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
+import TreeView from '/ckeditor5/engine/treeview/treeview.js';
 
 describe( 'view test utils', () => {
+	describe( 'getData, setData', () => {
+		let sandbox;
+
+		beforeEach( () => {
+			sandbox = sinon.sandbox.create();
+		} );
+
+		afterEach( () => {
+			sandbox.restore();
+		} );
+
+		describe( 'getData', () => {
+			it( 'should use stringify method', () => {
+				const element = document.createElement( 'div' );
+				const stringifySpy = sandbox.spy( getData, '_stringify' );
+				const treeView = new TreeView();
+				const options = { showType: false, showPriority: false, withoutSelection: true };
+				const root = treeView.createRoot( element );
+				root.appendChildren( new Element( 'p' ) );
+
+				expect( getData( treeView, options ) ).to.equal( '<p></p>' );
+				sinon.assert.calledOnce( stringifySpy );
+				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
+				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null );
+				const stringifyOptions = stringifySpy.firstCall.args[ 2 ];
+				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+			} );
+
+			it( 'should use stringify method with selection', () => {
+				const element = document.createElement( 'div' );
+				const stringifySpy = sandbox.spy( getData, '_stringify' );
+				const treeView = new TreeView();
+				const options = { showType: false, showPriority: false };
+				const root = treeView.createRoot( element );
+				root.appendChildren( new Element( 'p' ) );
+
+				treeView.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+
+				expect( getData( treeView, options ) ).to.equal( '[<p></p>]' );
+				sinon.assert.calledOnce( stringifySpy );
+				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
+				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( treeView.selection );
+				const stringifyOptions = stringifySpy.firstCall.args[ 2 ];
+				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+			} );
+		} );
+
+		describe( 'setData', () => {
+			it( 'should use parse method', () => {
+				const treeView = new TreeView();
+				const data = 'foobar<b>baz</b>';
+				const parseSpy = sandbox.spy( setData, '_parse' );
+
+				treeView.createRoot( document.createElement( 'div' ) );
+				setData( treeView, data );
+
+				expect( getData( treeView ) ).to.equal( 'foobar<b>baz</b>' );
+				sinon.assert.calledOnce( parseSpy );
+				const args = parseSpy.firstCall.args;
+				expect( args[ 0 ] ).to.equal( data );
+				expect( args[ 1 ] ).to.be.an( 'object' );
+				expect( args[ 1 ].rootElement ).to.equal( treeView.getRoot() );
+			} );
+
+			it( 'should use parse method with selection', () => {
+				const treeView = new TreeView();
+				const data = '[<b>baz</b>]';
+				const parseSpy = sandbox.spy( setData, '_parse' );
+
+				treeView.createRoot( document.createElement( 'div' ) );
+				setData( treeView, data );
+
+				expect( getData( treeView ) ).to.equal( '[<b>baz</b>]' );
+				const args = parseSpy.firstCall.args;
+				expect( args[ 0 ] ).to.equal( data );
+				expect( args[ 1 ] ).to.be.an( 'object' );
+				expect( args[ 1 ].rootElement ).to.equal( treeView.getRoot() );
+			} );
+		} );
+	} );
+
 	describe( 'stringify', () => {
 		it( 'should write text', () => {
 			const text = new Text( 'foobar' );
@@ -433,5 +519,19 @@ describe( 'view test utils', () => {
 				parse( '<container:b:10:test></container:b:10:test>' );
 			} ).to.throw( Error );
 		} );
+
+		it( 'should use provided root element #1', () => {
+			const root = new Element( 'p' );
+			const data = parse( '<span>text</span>', { rootElement: root } );
+
+			expect( stringify( data ) ).to.equal( '<p><span>text</span></p>' );
+		} );
+
+		it( 'should use provided root element #2', () => {
+			const root = new Element( 'p' );
+			const data = parse( '<span>text</span><b>test</b>', { rootElement: root } );
+
+			expect( stringify( data ) ).to.equal( '<p><span>text</span><b>test</b></p>' );
+		} );
 	} );
-} );
+} );

+ 122 - 27
packages/ckeditor5-engine/tests/_utils/model.js

@@ -9,39 +9,106 @@ import TreeWalker from '/ckeditor5/engine/treemodel/treewalker.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
+import RootElement from '/ckeditor5/engine/treemodel/rootelement.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
+import Selection from '/ckeditor5/engine/treemodel/selection.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
 
 /**
- * Writes the contents of the document to an HTML-like string.
+ * Writes the contents of the {@link engine.treeModel.Document Document} to an HTML-like string.
  *
  * @param {engine.treeModel.Document} document
- * @param {String} rootName
  * @param {Object} [options]
- * @param {Boolean} [options.selection] Whether to write the selection.
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
+ * be not included in returned string.
+ * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * default `main` name will be used.
  * @returns {String} The stringified data.
  */
-export function getData( document, rootName, options ) {
+export function getData( document, options = {} ) {
+	const withoutSelection = !!options.withoutSelection;
+	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
+
+	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, document.selection );
+}
+
+// Set stringify as getData private method - needed for testing/spying.
+getData._stringify = stringify;
+
+/**
+ * Sets the contents of the {@link engine.treeModel.Document Document} provided as HTML-like string.
+ *
+ * @param {engine.treeModel.Document} document
+ * @param {String} data HTML-like string to write into Document.
+ * @param {Object} options
+ * @param {String} [options.rootName] Root name where parsed data will be stored. If not provided, default `main` name will be
+ * used.
+ */
+export function setData( document, data, options = {} ) {
+	setData._parse( data, {
+		document: document,
+		rootName: options.rootName
+	} );
+}
+
+// Set parse as setData private method - needed for testing/spying.
+setData._parse = parse;
+
+/**
+ * Converts model nodes to HTML-like string representation.
+ *
+ * @param {engine.treeModel.RootElement|engine.treeModel.Element|engine.treeModel.Text|
+ * engine.treeModel.DocumentFragment} node Node to stringify.
+ * @param {engine.treeModel.Selection|engine.treeModel.Position|engine.treeModel.Range} [selectionOrPositionOrRange = null ]
+ * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
+ * converted to selection containing this range. If Position instance is provided - it will be converted to selection
+ * containing one range collapsed at this position.
+ * @returns {String} HTML-like string representing the model.
+ */
+export function stringify( node, selectionOrPositionOrRange = null ) {
+	let selection;
+	let document;
+
+	if ( node instanceof RootElement ) {
+		document = node.document;
+	} else if ( node instanceof Element || node instanceof Text ) {
+		// If root is Element or Text - wrap it with DocumentFragment.
+		node = new DocumentFragment( node );
+	}
+
+	document = document || new Document();
+
 	const walker = new TreeWalker( {
-		boundaries: Range.createFromElement( root )
+		boundaries: Range.createFromElement( node )
 	} );
-	let ret = '';
-	let lastPosition = Position.createFromParentAndOffset( root, 0 );
-	const selection = document.selection;
 
-	options = options || {};
+	if ( selectionOrPositionOrRange instanceof Selection ) {
+		selection = selectionOrPositionOrRange;
+	} else if ( selectionOrPositionOrRange instanceof Range ) {
+		selection = document.selection;
+		selection.addRange( selectionOrPositionOrRange );
+	} else if ( selectionOrPositionOrRange instanceof Position ) {
+		selection = document.selection;
+		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+	}
+
+	let ret = '';
+	let lastPosition = Position.createFromParentAndOffset( node, 0 );
+	const withSelection = !!selection;
 
 	for ( let value of walker ) {
-		if ( options.selection ) {
+		if ( withSelection ) {
 			ret += writeSelection( value.previousPosition, selection );
 		}
 
-		ret += writeItem( value, selection, options );
+		ret += writeItem( value, selection, { selection: withSelection } );
 
 		lastPosition = value.nextPosition;
 	}
 
-	if ( options.selection ) {
+	if ( withSelection ) {
 		ret += writeSelection( lastPosition, selection );
 	}
 
@@ -49,20 +116,37 @@ export function getData( document, rootName, options ) {
 }
 
 /**
- * Sets the contents of the model and the selection in it.
+ * Parses HTML-like string and returns model {@link engine.treeModel.RootElement rootElement}.
  *
- * @param {engine.treeModel.Document} document
- * @param {String} rootName
- * @param {String} data
+ * @param {String} data HTML-like string to be parsed.
+ * @param {Object} options
+ * @param {engine.treeModel.Document} [options.document] Document from which root element and selection will be used. If
+ * not provided new {engine.treeModel.Document document} instance will be created.
+ * @param {String} [options.rootName='main'] When `document` option is provided this root name will be used to create
+ * {engine.treeModel.RootElement RootElement} instance.
+ * @returns {engine.treeModel.RootElement|Object} Returns parsed RootElement or object with two fields `model`
+ * and `selection` when selection ranges were included in data to parse.
  */
-export function setData( document, rootName, data ) {
-	let appendTo = document.getRoot( rootName );
+export function parse( data, options = {} ) {
+	let document, root;
+	let withSelection = false;
+	const rootName = options.rootName || 'main';
+
+	if ( options.document ) {
+		document = options.document;
+		root = document.getRoot( rootName );
+		root.removeChildren( 0, root.getChildCount() );
+	} else {
+		document = new Document();
+		root = document.createRoot( rootName );
+	}
+
 	const path = [];
 	let selectionStart, selectionEnd, selectionAttributes, textAttributes;
 
 	const handlers = {
 		text( token ) {
-			appendTo.appendChildren( new Text( token.text, textAttributes ) );
+			root.appendChildren( new Text( token.text, textAttributes ) );
 		},
 
 		textStart( token ) {
@@ -80,9 +164,9 @@ export function setData( document, rootName, data ) {
 
 		openingTag( token ) {
 			let el = new Element( token.name, token.attributes );
-			appendTo.appendChildren( el );
+			root.appendChildren( el );
 
-			appendTo = el;
+			root = el;
 
 			path.push( token.name );
 		},
@@ -92,25 +176,27 @@ export function setData( document, rootName, data ) {
 				throw new Error( 'Parse error - unexpected closing tag.' );
 			}
 
-			appendTo = appendTo.parent;
+			root = root.parent;
 		},
 
 		collapsedSelection( token ) {
-			document.selection.collapse( appendTo, 'END' );
+			withSelection = true;
+			document.selection.collapse( root, 'END' );
 			document.selection.setAttributesTo( token.attributes );
 		},
 
 		selectionStart( token ) {
-			selectionStart = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
+			selectionStart = Position.createFromParentAndOffset( root, root.getChildCount() );
 			selectionAttributes = token.attributes;
 		},
 
 		selectionEnd() {
 			if ( !selectionStart ) {
-				throw new Error( 'Parse error - missing selection start' );
+				throw new Error( 'Parse error - missing selection start.' );
 			}
 
-			selectionEnd = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
+			withSelection = true;
+			selectionEnd = Position.createFromParentAndOffset( root, root.getChildCount() );
 
 			document.selection.setRanges(
 				[ new Range( selectionStart, selectionEnd ) ],
@@ -134,6 +220,15 @@ export function setData( document, rootName, data ) {
 	if ( selectionStart && !selectionEnd ) {
 		throw new Error( 'Parse error - missing selection end.' );
 	}
+
+	if ( withSelection ) {
+		return {
+			model: root,
+			selection: document.selection
+		};
+	}
+
+	return root;
 }
 
 // -- getData helpers ---------------------------------------------------------
@@ -324,7 +419,7 @@ function consumeNextToken( data ) {
 		}
 	}
 
-	throw new Error( 'Parse error - unpexpected token: ' + data + '.' );
+	throw new Error( 'Parse error - unexpected token: ' + data + '.' );
 }
 
 function parseAttributes( attrsString ) {

+ 90 - 35
packages/ckeditor5-engine/tests/_utils/view.js

@@ -24,7 +24,62 @@ const TEXT_RANGE_START_TOKEN = '{';
 const TEXT_RANGE_END_TOKEN = '}';
 
 /**
- * Converts view elements to its HTML-like string representation.
+ * Writes the contents of the {@link engine.treeView.TreeView TreeView} to an HTML-like string.
+ *
+ * @param {engine.treeView.TreeView} treeView
+ * @param {Object} [options]
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
+ * be not included in returned string.
+ * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * default `main` name will be used.
+ * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+ * instead of `<p>` and `<attribute:b>` instead of `<b>`).
+ * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
+ * (`<span:12>`, `<b:10>`).
+ * @returns {String} The stringified data.
+ */
+export function getData( treeView, options = {} ) {
+	const withoutSelection = !!options.withoutSelection;
+	const rootName = options.rootName || 'main';
+	const root = treeView.getRoot( rootName );
+	const stringifyOptions = {
+		showType: options.showType,
+		showPriority: options.showPriority,
+		ignoreRoot: true
+	};
+
+	return withoutSelection ?
+		getData._stringify( root, null, stringifyOptions ) :
+		getData._stringify( root, treeView.selection, stringifyOptions );
+}
+
+// Set stringify as getData private method - needed for testing/spying.
+getData._stringify = stringify;
+
+/**
+ * Sets the contents of the {@link engine.treeView.TreeView TreeView} provided as HTML-like string.
+ *
+ * @param {engine.treeView.TreeView} treeView
+ * @param {String} data HTML-like string to write into TreeView.
+ * @param {Object} options
+ * @param {String} [rootName] Root name where parsed data will be stored. If not provided, default `main` name will be
+ * used.
+ */
+export function setData( treeView, data, options = {} ) {
+	const rootName = options.rootName || 'main';
+	const root = treeView.getRoot( rootName );
+	const result = setData._parse( data, { rootElement: root } );
+
+	if ( result.view && result.selection ) {
+		treeView.selection.setTo( result.selection );
+	}
+}
+
+// Set parse as setData private method - needed for testing/spying.
+setData._parse = parse;
+
+/**
+ * Converts view elements to HTML-like string representation.
  * Root element can be provided as {@link engine.treeView.Text Text}:
  *
  *		const text = new Text( 'foobar' );
@@ -114,6 +169,8 @@ const TEXT_RANGE_END_TOKEN = '}';
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
  * (`<span:12>`, `<b:10>`).
+ * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing will not be printed.
+ * Mainly used by `getData` function to ignore {@link engine.treeView.TreeView TreeView's} root element.
  * @returns {String} HTML-like string representing the view.
  */
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
@@ -179,6 +236,10 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * selection instance. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward to the returned
  * {@link engine.treeView.Selection Selection} instance.
+ * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [options.rootElement=null] Default root to use when parsing elements.
+ * When set to `null` root element will be created automatically. If set to
+ * {@link engine.treeView.Element Element} or {@link engine.treeView.DocumentFragment DocumentFragment} - this node
+ * will be used as root for all parsed nodes.
  * @returns {engine.treeView.Text|engine.treeView.Element|engine.treeView.DocumentFragment|Object} Returns parsed view node
  * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
  */
@@ -187,7 +248,7 @@ export function parse( data, options = {} ) {
 	const viewParser = new ViewParser();
 	const rangeParser = new RangeParser();
 
-	const view = viewParser.parse( data );
+	const view = viewParser.parse( data, options.rootElement );
 	const ranges = rangeParser.parse( view, options.order );
 
 	// When ranges are present - return object containing view, and selection.
@@ -409,16 +470,25 @@ class ViewParser {
 	 * Parses HTML-like string to view tree elements.
 	 *
 	 * @param {String} data
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [rootElement=null] Default root to use when parsing elements.
+	 * When set to `null` root element will be created automatically. If set to
+	 * {@link engine.treeView.Element Element} or {@link engine.treeView.DocumentFragment DocumentFragment} - this node
+	 * will be used as root for all parsed nodes.
 	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
 	 */
-	parse( data ) {
+	parse( data, rootElement = null ) {
 		const htmlProcessor = new HtmlDataProcessor();
 
 		// Convert HTML string to DOM.
 		const domRoot = htmlProcessor.toDom( data );
 
+		// If root element is provided - remove all children from it.
+		if ( rootElement ) {
+			rootElement.removeChildren( 0, rootElement.getChildCount() );
+		}
+
 		// Convert DOM to View.
-		return this._walkDom( domRoot );
+		return this._walkDom( domRoot, rootElement );
 	}
 
 	/**
@@ -426,9 +496,11 @@ class ViewParser {
 	 *
 	 * @private
 	 * @param {Node} domNode
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [rootElement=null] Default root element to use
+	 * when parsing elements.
 	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
 	 */
-	_walkDom( domNode ) {
+	_walkDom( domNode, rootElement = null ) {
 		const isDomElement = domNode instanceof DomElement;
 
 		if ( isDomElement || domNode instanceof DomDocumentFragment ) {
@@ -437,15 +509,19 @@ class ViewParser {
 
 			// If there is only one element inside DOM DocumentFragment - use it as root.
 			if ( !isDomElement && length == 1 ) {
-				return this._walkDom( domNode.childNodes[ 0 ] );
+				return this._walkDom( domNode.childNodes[ 0 ], rootElement );
 			}
 
 			let viewElement;
 
 			if ( isDomElement ) {
 				viewElement = this._convertElement( domNode );
+
+				if ( rootElement ) {
+					rootElement.appendChildren( viewElement );
+				}
 			} else {
-				viewElement = new ViewDocumentFragment();
+				viewElement = rootElement ? rootElement : new ViewDocumentFragment();
 			}
 
 			for ( let i = 0; i < children.length; i++ ) {
@@ -453,7 +529,7 @@ class ViewParser {
 				viewElement.appendChildren( this._walkDom( child ) );
 			}
 
-			return viewElement;
+			return rootElement ? rootElement : viewElement;
 		}
 
 		return new ViewText( domNode.textContent );
@@ -609,6 +685,8 @@ class ViewStringify {
 	 * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
 	 * instead of `<p>` and `<attribute:b>` instead of `<b>`.
 	 * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
+	 * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing tag will not
+	 * be outputted.
 	 */
 	constructor( root, selection = null, options = {} ) {
 		this.root = root;
@@ -621,6 +699,7 @@ class ViewStringify {
 
 		this.showType = !!options.showType;
 		this.showPriority = !!options.showPriority;
+		this.ignoreRoot = !!options.ignoreRoot;
 	}
 
 	/**
@@ -647,9 +726,10 @@ class ViewStringify {
 	 */
 	_walkView( root, callback ) {
 		const isElement = root instanceof ViewElement;
+		const ignore = this.ignoreRoot && this.root === root;
 
 		if ( isElement || root instanceof ViewDocumentFragment ) {
-			if ( isElement ) {
+			if ( isElement && !ignore ) {
 				callback( this._stringifyElementOpen( root ) );
 			}
 
@@ -662,7 +742,7 @@ class ViewStringify {
 				callback( this._stringifyElementRanges( root, offset ) );
 			}
 
-			if ( isElement ) {
+			if ( isElement && !ignore ) {
 				callback( this._stringifyElementClose( root ) );
 			}
 		}
@@ -844,28 +924,3 @@ class ViewStringify {
 		return attributes.join( ' ' );
 	}
 }
-
-export function setData( treeView, data ) {
-	let view, selection;
-
-	const result = parse( data );
-
-	if ( result.view && result.selection ) {
-		selection = result.selection;
-		view = result.view;
-	} else {
-		view = result;
-	}
-
-	const root = treeView.getRoot();
-	root.removeChildren( 0, root.getChildCount() );
-	root.appendChildren( view );
-
-	if ( selection ) {
-		treeView.selection.setTo( selection );
-	}
-}
-
-export function getData( treeView ) {
-	return stringify( treeView.getRoot(), treeView.selection );
-}

+ 6 - 6
packages/ckeditor5-engine/tests/treemodel/composer/composer.js

@@ -23,18 +23,18 @@ describe( 'Composer', () => {
 
 	describe( 'constructor', () => {
 		it( 'attaches deleteContents default listener', () => {
-			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
 
 			const batch = document.batch();
 
 			composer.fire( 'deleteContents', { batch, selection: document.selection } );
 
-			expect( getData( document, 'main' ) ).to.equal( '<p>f</p><p>r</p>' );
+			expect( getData( document ) ).to.equal( '<p>f<selection /></p><p>r</p>' );
 			expect( batch.deltas ).to.not.be.empty;
 		} );
 
 		it( 'attaches deleteContents default listener which passes options', () => {
-			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
 
 			const batch = document.batch();
 
@@ -44,11 +44,11 @@ describe( 'Composer', () => {
 				options: { merge: true }
 			} );
 
-			expect( getData( document, 'main' ) ).to.equal( '<p>fr</p>' );
+			expect( getData( document ) ).to.equal( '<p>f<selection />r</p>' );
 		} );
 
 		it( 'attaches modifySelection default listener', () => {
-			setData( document, 'main', '<p>foo<selection />bar</p>' );
+			setData( document, '<p>foo<selection />bar</p>' );
 
 			composer.fire( 'modifySelection', {
 				selection: document.selection,
@@ -57,7 +57,7 @@ describe( 'Composer', () => {
 				}
 			} );
 
-			expect( getData( document, 'main', { selection: true } ) )
+			expect( getData( document ) )
 				.to.equal( '<p>fo<selection backward>o</selection>bar</p>' );
 		} );
 	} );

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/composer/deletecontents.js

@@ -215,11 +215,11 @@ describe( 'Delete utils', () => {
 
 		function test( title, input, output, options ) {
 			it( title, () => {
-				setData( document, 'main', input );
+				setData( document, input );
 
 				deleteContents( document.batch(), document.selection, options );
 
-				expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
+				expect( getData( document ) ).to.equal( output );
 			} );
 		}
 	} );

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/composer/modifyselection.js

@@ -229,11 +229,11 @@ describe( 'Delete utils', () => {
 
 	function test( title, input, output, options ) {
 		it( title, () => {
-			setData( document, 'main', input );
+			setData( document, input );
 
 			modifySelection( document.selection, options );
 
-			expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
+			expect( getData( document ) ).to.equal( output );
 		} );
 	}
 } );