Explorar el Código

Merge pull request #381 from ckeditor/t/360

Tree View test utilties.
Piotrek Koszuliński hace 9 años
padre
commit
c53737db44

+ 18 - 0
packages/ckeditor5-engine/src/treeview/documentfragment.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import utils from '../../utils/utils.js';
+import EmitterMixin from '../../utils/emittermixin.js';
 
 /**
  * DocumentFragment class.
@@ -98,6 +99,7 @@ export default class DocumentFragment {
 	 * @returns {Number} Number of inserted nodes.
 	 */
 	insertChildren( index, nodes ) {
+		this._fireChange( 'CHILDREN', this );
 		let count = 0;
 
 		if ( !utils.isIterable( nodes ) ) {
@@ -123,10 +125,26 @@ export default class DocumentFragment {
 	 * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
 	 */
 	removeChildren( index, howMany = 1 ) {
+		this._fireChange( 'CHILDREN', this );
+
 		for ( let i = index; i < index + howMany; i++ ) {
 			this._children[ i ].parent = null;
 		}
 
 		return this._children.splice( index, howMany );
 	}
+
+	/**
+	 * Fires `change` event with given type of the change.
+	 *
+	 * @private
+	 * @param {engine.treeView.ChangeType} type Type of the change.
+	 * @param {engine.treeView.Node} node Changed node.
+	 * @fires engine.treeView.Node#change
+	 */
+	_fireChange( type, node ) {
+		this.fire( 'change', type, node );
+	}
 }
+
+utils.mix( DocumentFragment, EmitterMixin );

+ 437 - 0
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -0,0 +1,437 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import { stringify, parse } 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';
+import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+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';
+
+describe( 'view test utils', () => {
+	describe( 'stringify', () => {
+		it( 'should write text', () => {
+			const text = new Text( 'foobar' );
+			expect( stringify( text ) ).to.equal( 'foobar' );
+		} );
+
+		it( 'should write elements and texts', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+
+			expect( stringify( p ) ).to.equal( '<p><b>foobar</b></p>' );
+		} );
+
+		it( 'should write elements with attributes', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', {
+				foo: 'bar'
+			}, text );
+			const p = new Element( 'p', {
+				baz: 'qux',
+				bar: 'taz',
+				class: 'short wide'
+			}, b );
+
+			expect( stringify( p ) ).to.equal( '<p class="short wide" baz="qux" bar="taz"><b foo="bar">foobar</b></p>' );
+		} );
+
+		it( 'should write selection ranges inside elements', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const p = new Element( 'p', null, [ b1, b2 ] );
+			const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
+		} );
+
+		it( 'should write collapsed selection ranges inside elements', () => {
+			const text = new Text( 'foobar' );
+			const p = new Element( 'p', null, text );
+			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection ) ).to.equal( '<p>[]foobar</p>' );
+		} );
+
+		it( 'should write selection ranges inside text', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const p = new Element( 'p', null, [ b1, b2 ] );
+			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
+		} );
+
+		it( 'should write collapsed selection ranges inside texts', () => {
+			const text = new Text( 'foobar' );
+			const p = new Element( 'p', null, text );
+			const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection ) ).to.equal( '<p>{}foobar</p>' );
+		} );
+
+		it( 'should write ranges that start inside text end ends between elements', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const p = new Element( 'p', null, [ b1, b2 ] );
+			const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
+		} );
+
+		it( 'should write elements types as namespaces when needed', () => {
+			const text = new Text( 'foobar' );
+			const b = new AttributeElement( 'b', null, text );
+			const p = new ContainerElement( 'p', null, b );
+
+			expect( stringify( p, null, { showType: true } ) )
+				.to.equal( '<container:p><attribute:b>foobar</attribute:b></container:p>' );
+		} );
+
+		it( 'should not write element type when type is not specified', () => {
+			const p = new Element( 'p' );
+			expect( stringify( p, null, { showType: true } ) ).to.equal( '<p></p>' );
+		} );
+
+		it( 'should write elements priorities when needed', () => {
+			const text = new Text( 'foobar' );
+			const b = new AttributeElement( 'b', null, text );
+			const p = new ContainerElement( 'p', null, b );
+
+			expect( stringify( p, null, { showPriority: true } ) )
+				.to.equal( '<p><b:10>foobar</b:10></p>' );
+		} );
+
+		it( 'should parse DocumentFragment as root', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const fragment = new DocumentFragment( [ b1, b2 ] );
+			expect( stringify( fragment, null ) ).to.equal( '<b>foobar</b><b>bazqux</b>' );
+		} );
+
+		it( 'should not write ranges outside elements - end position outside element', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+			const range = Range.createFromParentsAndOffsets( p, 0, p, 5 );
+
+			expect( stringify( p, range ) ).to.equal( '<p>[<b>foobar</b></p>' );
+		} );
+
+		it( 'should not write ranges outside elements - start position outside element', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+			const range = Range.createFromParentsAndOffsets( p, -1, p, 1 );
+
+			expect( stringify( p, range ) ).to.equal( '<p><b>foobar</b>]</p>' );
+		} );
+
+		it( 'should not write ranges outside elements - end position outside text', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+			const range = Range.createFromParentsAndOffsets( text, 0, text, 7 );
+
+			expect( stringify( p, range ) ).to.equal( '<p><b>{foobar</b></p>' );
+		} );
+
+		it( 'should not write ranges outside elements - start position outside text', () => {
+			const text = new Text( 'foobar' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+			const range = Range.createFromParentsAndOffsets( text, -1, text, 2 );
+
+			expect( stringify( p, range ) ).to.equal( '<p><b>fo}obar</b></p>' );
+		} );
+
+		it( 'should write multiple ranges from selection #1', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const p = new Element( 'p', null, [ b1, b2 ] );
+			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
+			const range2 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
+			const selection = new Selection();
+			selection.setRanges( [ range2, range1 ] );
+
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
+		} );
+
+		it( 'should write multiple ranges from selection #2', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b = new Element( 'b', null, text1 );
+			const p = new Element( 'p', null, [ b, text2 ] );
+			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
+			const range2 = Range.createFromParentsAndOffsets( text2, 0, text2, 3 );
+			const range3 = Range.createFromParentsAndOffsets( text2, 3, text2, 4 );
+			const range4 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
+			const selection = new Selection();
+			selection.setRanges( [ range1, range2, range3, range4 ] );
+
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
+		} );
+
+		it( 'should use Position instance instead of Selection', () => {
+			const text = new Text( 'foobar' );
+			const position = new Position( text, 3 );
+			const string = stringify( text, position );
+			expect( string ).to.equal( 'foo{}bar' );
+		} );
+
+		it( 'should use Range instance instead of Selection', () => {
+			const text = new Text( 'foobar' );
+			const range = Range.createFromParentsAndOffsets( text, 3, text, 4 );
+			const string = stringify( text, range );
+			expect( string ).to.equal( 'foo{b}ar' );
+		} );
+	} );
+
+	describe( 'parse', () => {
+		it( 'should parse text', () => {
+			const text = parse( 'foobar' );
+			expect( text ).to.be.instanceOf( Text );
+			expect( text.data ).to.equal( 'foobar' );
+		} );
+
+		it( 'should parse elements and texts', () => {
+			const view = parse( '<b>foobar</b>' );
+			const element = new Element( 'b' );
+
+			expect( view ).to.be.instanceof( Element );
+			expect( view.isSimilar( element ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 1 );
+			const text = view.getChild( 0 );
+			expect( text ).to.be.instanceof( Text );
+			expect( text.data ).to.equal( 'foobar' );
+		} );
+
+		it( 'should parse element attributes', () => {
+			const view = parse( '<b name="foo" title="bar" class="foo bar" style="color:red;"></b>' );
+			const element = new Element( 'b', { name: 'foo', title: 'bar', class: 'foo bar', style: 'color:red;' } );
+
+			expect( view ).to.be.instanceof( Element );
+			expect( view.isSimilar( element ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should parse element type', () => {
+			const view1 = parse( '<attribute:b></attribute:b>' );
+			const attribute = new AttributeElement( 'b' );
+			const view2 = parse( '<container:p></container:p>' );
+			const container = new ContainerElement( 'p' );
+
+			expect( view1 ).to.be.instanceof( AttributeElement );
+			expect( view1.isSimilar( attribute ) ).to.be.true;
+			expect( view2 ).to.be.instanceof( ContainerElement );
+			expect( view2.isSimilar( container ) ).to.be.true;
+		} );
+
+		it( 'should parse element priority', () => {
+			const parsed1 = parse( '<b:12></b:12>' );
+			const attribute1 = new AttributeElement( 'b' );
+			attribute1.priority = 12;
+			const parsed2 = parse( '<attribute:b:44></attribute:b:44>' );
+			const attribute2 = new AttributeElement( 'b' );
+			attribute2.priority = 44;
+
+			parsed1.isSimilar( attribute1 );
+			expect( parsed1.isSimilar( attribute1 ) ).to.be.true;
+			expect( parsed2.isSimilar( attribute2 ) ).to.be.true;
+		} );
+
+		it( 'should create DocumentFragment when multiple elements on root', () => {
+			const view = parse( '<b></b><i></i>' );
+			expect( view ).to.be.instanceOf( DocumentFragment );
+			expect( view.getChildCount() ).to.equal( 2 );
+			expect( view.getChild( 0 ).isSimilar( new Element( 'b' ) ) ).to.be.true;
+			expect( view.getChild( 1 ).isSimilar( new Element( 'i' ) ) ).to.be.true;
+		} );
+
+		it( 'should paste nested elements and texts', () => {
+			const parsed = parse( '<container:p>foo<b:12>bar<i:25>qux</i:25></b:12></container:p>' );
+			expect( parsed.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
+			expect( parsed.getChildCount() ).to.equal( 2 );
+			expect( parsed.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'foo' );
+			const b = parsed.getChild( 1 );
+			expect( b ).to.be.instanceof( AttributeElement );
+			expect( b.priority ).to.equal( 12 );
+			expect( b.getChildCount() ).to.equal( 2 );
+			expect( b.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'bar' );
+			const i = b.getChild( 1 );
+			expect( i ).to.be.instanceof( AttributeElement );
+			expect( i.priority ).to.equal( 25 );
+			expect( i.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'qux' );
+		} );
+
+		it( 'should parse selection range inside text', () => {
+			const { view, selection } = parse( 'f{oo}b{}ar' );
+			expect( view ).to.be.instanceof( Text );
+			expect( view.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = [ ...selection.getRanges() ];
+
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 3 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 4, view, 4 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse selection range between elements', () => {
+			const { view, selection } = parse( '<p>[<b>foobar]</b>[]</p>' );
+			expect( view ).to.be.instanceof( Element );
+			expect( view.getChildCount() ).to.equal( 1 );
+			const b = view.getChild( 0 );
+			expect( b ).to.be.instanceof( Element );
+			expect( b.name ).to.equal( 'b' );
+			expect( b.getChildCount() ).to.equal( 1 );
+			const text = b.getChild( 0 );
+			expect( text ).to.be.instanceof( Text );
+			expect( text.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = [ ...selection.getRanges() ];
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, b, 1 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse ranges #1', () => {
+			const { view, selection } = parse( '<container:p>foo{bar]</container:p>' );
+			expect( view.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 1 );
+			const text = view.getChild( 0 );
+			expect( text ).to.be.instanceof( Text );
+			expect( text.data ).to.equal( 'foobar' );
+			expect( selection.rangeCount ).to.equal( 1 );
+			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( text, 3, view, 1 ) ) ).to.be.true;
+		} );
+
+		it( 'should parse ranges #2', () => {
+			const { view, selection } = parse( '<attribute:b>[foob}ar<i>{baz</i>]</attribute:b>' );
+			expect( view.isSimilar( new AttributeElement( 'b' ) ) ).to.be.true;
+			expect( view.getChildCount() ).to.equal( 2 );
+			const text1 = view.getChild( 0 );
+			expect( text1 ).to.be.instanceof( Text );
+			expect( text1.data ).to.equal( 'foobar' );
+			const i = view.getChild( 1 );
+			expect( i.isSimilar( new Element( 'i' ) ) ).to.be.true;
+			expect( i.getChildCount() ).to.equal( 1 );
+			const text2 = i.getChild( 0 );
+			expect( text2 ).to.be.instanceof( Text );
+			expect( text2.data ).to.equal( 'baz' );
+			expect( selection.rangeCount ).to.equal( 2 );
+			const ranges = [ ...selection.getRanges() ];
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, text1, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( text2, 0, view, 2 ) ) ).to.be.true;
+		} );
+
+		it( 'should use ranges order when provided', () => {
+			const { view, selection } = parse( '{f}oo{b}arb{a}z', { order: [ 3, 1, 2 ] } );
+			expect( selection.rangeCount ).to.equal( 3 );
+			const ranges = [ ...selection.getRanges() ];
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
+			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( selection.anchor.isEqual( ranges[ 2 ].start ) ).to.be.true;
+			expect( selection.focus.isEqual( ranges[ 2 ].end ) ).to.be.true;
+		} );
+
+		it( 'should set last range backward if needed', () => {
+			const { view, selection } = parse( '{f}oo{b}arb{a}z', { order: [ 3, 1, 2 ], lastRangeBackward: true } );
+			expect( selection.rangeCount ).to.equal( 3 );
+			const ranges = [ ...selection.getRanges() ];
+			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
+			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( selection.anchor.isEqual( ranges[ 2 ].end ) ).to.be.true;
+			expect( selection.focus.isEqual( ranges[ 2 ].start ) ).to.be.true;
+		} );
+
+		it( 'should return DocumentFragment if range is around single element', () => {
+			const { view, selection } = parse( '[<b>foobar</b>]' );
+			expect( view ).to.be.instanceOf( DocumentFragment );
+			expect( selection.rangeCount ).to.equal( 1 );
+			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+		} );
+
+		it( 'should throw when ranges order does not include all ranges', () => {
+			expect( () => {
+				parse( '{}foobar{}', { order: [ 1 ] } );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when ranges order is invalid', () => {
+			expect( () => {
+				parse( '{}foobar{}', { order: [ 1, 4 ] } );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when element range delimiter is inside text node', () => {
+			expect( () => {
+				parse( 'foo[bar' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when text range delimiter is inside empty text node', () => {
+			expect( () => {
+				parse( '<b>foo</b>}' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when end of range is found before start', () => {
+			expect( () => {
+				parse( 'fo}obar' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when intersecting ranges found', () => {
+			expect( () => {
+				parse( '[fo{o}bar]' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when opened ranges are left', () => {
+			expect( () => {
+				parse( 'fo{obar' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when wrong tag name is provided #1', () => {
+			expect( () => {
+				parse( '<b:bar></b:bar>' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when wrong tag name is provided #2', () => {
+			expect( () => {
+				parse( '<container:b:bar></container:b:bar>' );
+			} ).to.throw( Error );
+		} );
+
+		it( 'should throw when wrong tag name is provided #3', () => {
+			expect( () => {
+				parse( '<container:b:10:test></container:b:10:test>' );
+			} ).to.throw( Error );
+		} );
+	} );
+} );

+ 846 - 0
packages/ckeditor5-engine/tests/_utils/view.js

@@ -0,0 +1,846 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import Selection from '/ckeditor5/engine/treeview/selection.js';
+import Range from '/ckeditor5/engine/treeview/range.js';
+import Position from '/ckeditor5/engine/treeview/position.js';
+import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+import ViewText from '/ckeditor5/engine/treeview/text.js';
+
+const DomDocumentFragment = window.DocumentFragment;
+const DomElement = window.Element;
+
+const ELEMENT_RANGE_START_TOKEN = '[';
+const ELEMENT_RANGE_END_TOKEN = ']';
+const TEXT_RANGE_START_TOKEN = '{';
+const TEXT_RANGE_END_TOKEN = '}';
+
+/**
+ * Converts view elements to its HTML-like string representation.
+ * Root element can be provided as {@link engine.treeView.Text Text}:
+ *
+ *		const text = new Text( 'foobar' );
+ *		stringify( text ); // 'foobar'
+ *
+ * or as {@link engine.treeView.Element Element}:
+ *
+ *		const element = new Element( 'p', null, new Text( 'foobar' ) );
+ *		stringify( element ); // '<p>foobar</p>'
+ *
+ * or as {@link engine.treeView.DocumentFragment DocumentFragment}:
+ *
+ *		const text = new Text( 'foobar' );
+ *		const b = new Element( 'b', { name: 'test' }, text );
+ *		const p = new Element( 'p', { style: 'color:red;' } );
+ *		const fragment = new DocumentFragment( [ p, b ] );
+ *
+ *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
+ *
+ * Additionally {@link engine.treeView.Selection Selection} instance can be provided, then ranges from that selection
+ * will be included in output data.
+ * If range position is placed inside element node, it will be represented with `[` and `]`:
+ *
+ *		const text = new Text( 'foobar' );
+ *		const b = new Element( 'b', null, text );
+ *		const p = new Element( 'p', null, b );
+ *		const selection = new Selection();
+ *		selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
+ *
+ *		stringify( p, selection ); // '<p>[<b>foobar</b>]</p>'
+ *
+ * If range is placed inside text node, it will be represented with `{` and `}`:
+ *
+ *		const text = new Text( 'foobar' );
+ *		const b = new Element( 'b', null, text );
+ *		const p = new Element( 'p', null, b );
+ *		const selection = new Selection();
+ *		selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
+ *
+ *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
+ *
+ * Multiple ranges are supported:
+ *
+ *		const text = new Text( 'foobar' );
+ *		const selection = new Selection();
+ *		selection.addRange( Range.createFromParentsAndOffsets( text, 0, text, 1 ) );
+ *		selection.addRange( Range.createFromParentsAndOffsets( text, 3, text, 5 ) );
+ *
+ *		stringify( text, selection ); // '{f}oo{ba}r'
+ *
+ * Instead of {@link engine.treeView.Selection Selection} instance {@link engine.treeView.Range Range} or
+ * {@link engine.treeView.Position Position} instance can be provided. 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.
+ *
+ *		const text = new Text( 'foobar' );
+ *		const range = Range.createFromParentsAndOffsets( text, 0, text, 1 );
+ *		const position = new Position( text, 3 );
+ *
+ *		stringify( text, range ); // '{f}oobar'
+ *		stringify( text, position ); // 'foo{}bar'
+ *
+ * Additional options object can be provided.
+ * If `options.showType` is set to `true`, element's types will be
+ * presented for {@link engine.treeView.AttributeElement AttributeElements} and {@link engine.treeView.ContainerElement
+ * ContainerElements}:
+ *
+ *		const attribute = new AttributeElement( 'b' );
+ *		const container = new ContainerElement( 'p' );
+ *		getData( attribute, null, { showType: true } ); // '<attribute:b></attribute:b>'
+ *		getData( container, null, { showType: true } ); // '<container:p></container:p>'
+ *
+ * If `options.showPriority` is set to `true`, priority will be displayed for all
+ * {@link engine.treeView.AttributeElement AttributeElements}.
+ *
+ *		const attribute = new AttributeElement( 'b' );
+ *		attribute.priority = 20;
+ *		getData( attribute, null, { showPriority: true } ); // <b:20></b:20>
+ *
+ * @param {engine.treeView.Text|engine.treeView.Element|engine.treeView.DocumentFragment} node Node to stringify.
+ * @param {engine.treeView.Selection|engine.treeView.Position|engine.treeView.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.
+ * @param {Object} [options] Object with additional options.
+ * @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} HTML-like string representing the view.
+ */
+export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
+	let selection;
+
+	if ( selectionOrPositionOrRange instanceof Position ) {
+		selection = new Selection();
+		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+	} else if ( selectionOrPositionOrRange instanceof Range ) {
+		selection = new Selection( );
+		selection.addRange( selectionOrPositionOrRange );
+	} else {
+		selection = selectionOrPositionOrRange;
+	}
+
+	const viewStringify = new ViewStringify( node, selection, options );
+
+	return viewStringify.stringify();
+}
+
+/**
+ * Parses HTML-like string and returns view tree nodes.
+ * Simple string will be converted to {@link engine.treeView.Text Text} node:
+ *
+ *		parse( 'foobar' ); // Returns instance of Text.
+ *
+ * {@link engine.treeView.Element Elements} will be parsed with attributes an children:
+ *
+ *		parse( '<b name="baz">foobar</b>' ); // Returns instance of Element with `baz` attribute and text child node.
+ *
+ * Multiple nodes provided on root level will be converted to {@link engine.treeView.DocumentFragment DocumentFragment}:
+ *
+ *		parse( '<b>foo</b><i>bar</i>' ); // Returns DocumentFragment with two child elements.
+ *
+ * Method can parse multiple {@link engine.treeView.Range ranges} provided in string data and return
+ * {@link engine.treeView.Selection Selection} instance containing these ranges. Ranges placed inside
+ * {@link engine.treeView.Text Text} nodes should be marked using `{` and `}` brackets:
+ *
+ *		const { text, selection } = parse( 'f{ooba}r' );
+ *
+ * Ranges placed outside text nodes should be marked using `[` and `]` brackets:
+ *
+ *		const { root, selection } = parse( '<p>[<b>foobar</b>]</p>' );
+ *
+ * Sometimes there is a need for defining order of ranges inside created selection. This can be achieved by providing
+ * ranges order array as additional parameter:
+ *
+ *		const { root, selection } = parse( '{fo}ob{ar}{ba}z', { order: [ 2, 3, 1 ] } );
+ *
+ * In above example first range (`{fo}`) will be added to selection as second one, second range (`{ar}`) will be added
+ * as third and third range (`{ba}`) will be added as first one.
+ *
+ * If selection's last range should be added as backward one (so the {@link engine.treeView.Selection#anchor selection
+ * anchor} is represented by `end` position and {@link engine.treeView.Selection#focus selection focus} is
+ * represented by `start` position) use `lastRangeBackward` flag:
+ *
+ *		const { root, selection } = parse( `{foo}bar{baz}`, { lastRangeBackward: true } );
+ *
+ * @param {String} data HTML-like string to be parsed.
+ * @param {Object} options
+ * @param {Array.<Number>} [options.order] Array with order of parsed ranges added to returned
+ * {@link engine.treeView.Selection Selection} instance. Each element should represent desired position of each range in
+ * 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.
+ * @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.
+ */
+export function parse( data, options = { } ) {
+	options.order = options.order || [];
+	const viewParser = new ViewParser();
+	const rangeParser = new RangeParser();
+
+	const view = viewParser.parse( data );
+	const ranges = rangeParser.parse( view, options.order );
+
+	// When ranges are present - return object containing view, and selection.
+	if ( ranges.length ) {
+		const selection = new Selection();
+		selection.setRanges( ranges, !!options.lastRangeBackward );
+
+		return {
+			view: view,
+			selection: selection
+		};
+	}
+
+	return view;
+}
+
+/**
+ * Private helper class used for converting ranges represented as text inside view {@link engine.treeView.Text Text nodes}.
+ *
+ * @private
+ */
+class RangeParser {
+	/**
+	 * Parses the view, and returns ranges represented inside {@link engine.treeView.Text Text nodes}.
+	 * Method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If text node is empty after
+	 * the process - it will be removed too.
+	 *
+	 * @param {engine.treeView.Node} node Starting node.
+	 * @param {Array.<Number>} order Ranges order. Each element should represent desired position of the range after
+	 * sorting. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+	 * @returns {Array.<engine.treeView.Range>} Array with ranges found.
+	 */
+	parse( node, order ) {
+		this._positions = [];
+
+		// Remove all range brackets from view nodes and save their positions.
+		this._getPositions( node );
+
+		// Create ranges using gathered positions.
+		let ranges = this._createRanges();
+
+		// Sort ranges if needed.
+		if ( order.length ) {
+			if ( order.length != ranges.length ) {
+				throw new Error(
+					`Parse error - there are ${ ranges.length} ranges found, but ranges order array contains ${ order.length } elements.`
+				);
+			}
+
+			ranges = this._sortRanges( ranges,  order );
+		}
+
+		return ranges;
+	}
+
+	/**
+	 * Gathers positions of brackets inside view tree starting from provided node. Method will remove all occurrences of
+	 * `{`, `}`, `[` and `]` from found text nodes. If text node is empty after the process - it will be removed
+	 * too.
+	 *
+	 * @private
+	 * @param {engine.treeView.Node} node Staring node.
+	 */
+	_getPositions( node ) {
+		if ( node instanceof ViewDocumentFragment || node instanceof ViewElement ) {
+			// Copy elements into the array, when nodes will be removed from parent node this array will still have all the
+			// items needed for iteration.
+			const children = [ ...node.getChildren() ];
+
+			for ( let child of children ) {
+				this._getPositions( child );
+			}
+		}
+
+		if ( node instanceof ViewText ) {
+			const regexp = new RegExp(
+				`[ ${ TEXT_RANGE_START_TOKEN }${ TEXT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_START_TOKEN } ]`,
+				'g'
+			);
+			let text = node.data;
+			let match;
+			let offset = 0;
+			const brackets = [];
+
+			// Remove brackets from text and store info about offset inside text node.
+			while ( ( match = regexp.exec( text ) ) ) {
+				const index = match.index;
+				const bracket = match[ 0 ];
+
+				brackets.push( {
+					bracket: bracket,
+					textOffset: index - offset
+				} );
+
+				offset++;
+			}
+			text = text.replace( regexp, '' );
+			node.data = text;
+			const index = node.getIndex();
+			const parent = node.parent;
+
+			// Remove empty text nodes.
+			if ( !text ) {
+				node.remove();
+			}
+
+			for ( let item of brackets ) {
+				// Non-empty text node.
+				if ( text ) {
+					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+						// Store information about text range delimiter.
+						this._positions.push( {
+							bracket: item.bracket,
+							position: new Position( node, item.textOffset )
+						} );
+					} else {
+						// Check if element range delimiter is not placed inside text node.
+						if ( item.textOffset !== 0 && item.textOffset !== text.length ) {
+							throw new Error( `Parse error - range delimiter '${ item.bracket }' is placed inside text node.` );
+						}
+
+						// If bracket is placed at the end of the text node - it should be positioned after it.
+						const offset = ( item.textOffset === 0 ? index : index + 1 );
+
+						// Store information about element range delimiter.
+						this._positions.push( {
+							bracket: item.bracket,
+							position: new Position( parent, offset )
+						} );
+					}
+				} else {
+					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+						throw new Error( `Parse error - text range delimiter '${ item.bracket }' is placed inside empty text node. ` );
+					}
+
+					// Store information about element range delimiter.
+					this._positions.push( {
+						bracket: item.bracket,
+						position: new Position( parent, index )
+					} );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Sort ranges in given order. Ranges order should be an array, each element should represent desired position
+	 * of the range after sorting.
+	 * For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+	 *
+	 * @private
+	 * @param {Array.<engine.treeView.Range>} ranges Ranges to sort.
+	 * @param {Array.<Number>} rangesOrder Array with new ranges order.
+	 * @returns {Array} Sorted ranges array.
+	 */
+	_sortRanges( ranges, rangesOrder ) {
+		const sortedRanges = [];
+		let index = 0;
+
+		for ( let newPosition of rangesOrder ) {
+			if ( ranges[ newPosition - 1 ] === undefined ) {
+				throw new Error( 'Parse error - provided ranges order is invalid.' );
+			}
+
+			sortedRanges[ newPosition - 1 ] = ranges[ index ];
+			index++;
+		}
+
+		return sortedRanges;
+	}
+
+	/**
+	 * Uses all found bracket positions to create ranges from them.
+	 *
+	 * @private
+	 * @returns {Array.<engine.treeView.Range}
+	 */
+	_createRanges() {
+		const ranges = [];
+		let range = null;
+
+		for ( let item of this._positions ) {
+			// When end of range is found without opening.
+			if ( !range && ( item.bracket == ELEMENT_RANGE_END_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) ) {
+				throw new Error( `Parse error - end of range was found '${ item.bracket }' but range was not started before.` );
+			}
+
+			// When second start of range is found when one is already opened - selection does not allow intersecting
+			// ranges.
+			if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
+				throw new Error( `Parse error - start of range was found '${ item.bracket }' but one range is already started.` );
+			}
+
+			if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
+				range = new Range( item.position, item.position );
+			} else {
+				range.end = item.position;
+				ranges.push( range );
+				range = null;
+			}
+		}
+
+		// Check if all ranges have proper ending.
+		if ( range !== null ) {
+			throw new Error( 'Parse error - range was started but no end delimiter was found.' );
+		}
+
+		return ranges;
+	}
+}
+
+/**
+ * Private helper class used to convert given HTML-like string to view tree.
+ *
+ * @private
+ */
+class ViewParser {
+	/**
+	 * Parses HTML-like string to view tree elements.
+	 *
+	 * @param {String} data
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
+	 */
+	parse( data ) {
+		const htmlProcessor = new HtmlDataProcessor();
+
+		// Convert HTML string to DOM.
+		const domRoot = htmlProcessor.toDom( data );
+
+		// Convert DOM to View.
+		return this._walkDom( domRoot );
+	}
+
+	/**
+	 * Walks through DOM elements and converts them to tree view elements.
+	 *
+	 * @private
+	 * @param {Node} domNode
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
+	 */
+	_walkDom( domNode ) {
+		const isDomElement = domNode instanceof DomElement;
+
+		if ( isDomElement || domNode instanceof DomDocumentFragment ) {
+			const children = domNode.childNodes;
+			const length = children.length;
+
+			// If there is only one element inside DOM DocumentFragment - use it as root.
+			if ( !isDomElement && length == 1 ) {
+				return this._walkDom( domNode.childNodes[ 0 ] );
+			}
+
+			let viewElement;
+
+			if ( isDomElement ) {
+				viewElement = this._convertElement( domNode );
+			} else {
+				viewElement = new ViewDocumentFragment();
+			}
+
+			for ( let i = 0; i < children.length; i++ ) {
+				const child = children[ i ];
+				viewElement.appendChildren( this._walkDom( child ) );
+			}
+
+			return viewElement;
+		}
+
+		return new ViewText( domNode.textContent );
+	}
+
+	/**
+	 * Converts DOM Element to {engine.treeView.Element view Element}.
+	 *
+	 * @private
+	 * @param {Element} domElement DOM element to convert.
+	 * @returns {engine.treeView.Element|engine.treeView.AttributeElement|engine.treeView.ContainerElement} Tree view
+	 * element converted from DOM element.
+	 */
+	_convertElement( domElement ) {
+		const info = this._convertElementName( domElement );
+		let viewElement;
+
+		if ( info.type == 'attribute' ) {
+			viewElement = new AttributeElement( info.name );
+
+			if ( info.priority !== null ) {
+				viewElement.priority = info.priority;
+			}
+		} else if ( info.type == 'container' ) {
+			viewElement = new ContainerElement( info.name );
+		} else {
+			viewElement = new ViewElement( info.name );
+		}
+
+		const attributes = domElement.attributes;
+		const attributesCount = attributes.length;
+
+		for ( let i = 0; i < attributesCount; i++ ) {
+			const attribute = attributes[ i ];
+			viewElement.setAttribute( attribute.name, attribute.value );
+		}
+
+		return viewElement;
+	}
+
+	/**
+	 * Converts DOM element tag name to information needed for creating {@link engine.treeView.Element view Element} instance.
+	 * Name can be provided in couple formats: as a simple element's name (`div`), as a type and name (`container:div`,
+	 * `attribute:span`), as a name and priority (`span:12`) and as a type, priority, name trio (`attribute:span:12`);
+	 *
+	 * @private
+	 * @param {Element} element DOM Element which tag name should be converted.
+	 * @returns {Object} info Object with parsed information.
+	 * @returns {String} info.name Parsed name of the element.
+	 * @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
+	 * @returns {Number|null} info.priority Parsed priority of the element.
+	 */
+	_convertElementName( element ) {
+		const parts = element.tagName.toLowerCase().split( ':' );
+
+		if ( parts.length == 1 ) {
+			return {
+				name: parts[ 0 ],
+				type: null,
+				priority: null
+			};
+		}
+
+		if ( parts.length == 2 ) {
+			// Check if type and name: container:div.
+			const type = this._convertType( parts[ 0 ] );
+
+			if ( type ) {
+				return {
+					name: parts[ 1 ],
+					type: type,
+					priority: null
+				};
+			}
+
+			// Check if name and priority: span:10.
+			const priority = this._convertPriority( parts[ 1 ] );
+
+			if ( priority !== null ) {
+				return {
+					name: parts[ 0 ],
+					type: 'attribute',
+					priority: priority
+				};
+			}
+
+			throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
+		}
+
+		// Check if name is in format type:name:priority.
+		if ( parts.length === 3 ) {
+			const type = this._convertType( parts[ 0 ] );
+			const priority = this._convertPriority( parts[ 2 ] );
+
+			if ( type && priority !== null ) {
+				return {
+					name: parts[ 1 ],
+					type: type,
+					priority: priority
+				};
+			}
+		}
+
+		throw new Error( `Parse error - cannot parse element's tag name: ${ element.tagName.toLowerCase() }.` );
+	}
+
+	/**
+	 * Checks if element's type is allowed. Returns `attribute`, `container` or `null`.
+	 *
+	 * @private
+	 * @param {String} type
+	 * @returns {String|null}
+	 */
+	_convertType( type ) {
+		if ( type == 'container' || type == 'attribute' ) {
+			return type;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Checks if given priority is allowed. Returns null if priority cannot be converted.
+	 *
+	 * @private
+	 * @param {String} priorityString
+	 * @returns {Number|Null}
+	 */
+	_convertPriority( priorityString ) {
+		const priority = parseInt( priorityString, 10 );
+
+		if ( !isNaN( priority ) ) {
+			return priority;
+		}
+
+		return null;
+	}
+
+}
+
+/**
+ * Private helper class used for converting view tree to string.
+ *
+ * @private
+ */
+class ViewStringify {
+	/**
+	 * Creates ViewStringify instance.
+	 *
+	 * @param root
+	 * @param {engine.treeView.Selection} [selection=null] Selection which ranges should be also converted to string.
+	 * @param {Object} [options] Options object.
+	 * @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.
+	 */
+	constructor( root, selection = null, options = {} ) {
+		this.root = root;
+		this.selection = selection;
+		this.ranges = [];
+
+		if ( this.selection ) {
+			this.ranges = [ ...selection.getRanges() ];
+		}
+
+		this.showType = !!options.showType;
+		this.showPriority = !!options.showPriority;
+	}
+
+	/**
+	 * Converts view to string.
+	 *
+	 * @returns {String} String representation of the view elements.
+	 */
+	stringify() {
+		let result = '';
+		this._walkView( this.root, ( chunk ) => {
+			result += chunk;
+		} );
+
+		return result;
+	}
+
+	/**
+	 * Executes simple walker that iterates over all elements in the view tree starting from root element.
+	 * Calls `callback` with parsed chunks of string data.
+	 *
+	 * @private
+	 * @param {engine.treeView.DocumentFragment|engine.treeView.Element|engine.treeView.Text} root
+	 * @param {Function} callback
+	 */
+	_walkView( root, callback ) {
+		const isElement = root instanceof ViewElement;
+
+		if ( isElement || root instanceof ViewDocumentFragment ) {
+			if ( isElement ) {
+				callback( this._stringifyElementOpen( root ) );
+			}
+
+			let offset = 0;
+			callback( this._stringifyElementRanges( root, offset ) );
+
+			for ( let child of root.getChildren() ) {
+				this._walkView( child, callback );
+				offset++;
+				callback( this._stringifyElementRanges( root, offset ) );
+			}
+
+			if ( isElement ) {
+				callback( this._stringifyElementClose( root ) );
+			}
+		}
+
+		if ( root instanceof ViewText ) {
+			callback( this._stringifyTextRanges( root ) );
+		}
+	}
+
+	/**
+	 * Checks if given {@link engine.treeView.Element Element} has {@link engine.treeView.Range#start range start} or
+	 * {@link engine.treeView.Range#start range end} placed at given offset and returns its string representation.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @param {Number} offset
+	 */
+	_stringifyElementRanges( element, offset ) {
+		let start = '';
+		let end = '';
+		let collapsed = '';
+
+		for ( let range of this.ranges ) {
+			if ( range.start.parent == element && range.start.offset === offset ) {
+				if ( range.isCollapsed ) {
+					collapsed += ELEMENT_RANGE_START_TOKEN + ELEMENT_RANGE_END_TOKEN;
+				} else {
+					start += ELEMENT_RANGE_START_TOKEN;
+				}
+			}
+
+			if ( range.end.parent === element && range.end.offset === offset && !range.isCollapsed ) {
+				end += ELEMENT_RANGE_END_TOKEN;
+			}
+		}
+
+		return end + collapsed + start;
+	}
+
+	/**
+	 * Checks if given {@link engine.treeView.Element Text node} has {@link engine.treeView.Range#start range start} or
+	 * {@link engine.treeView.Range#start range end} placed somewhere inside. Returns string representation of text
+	 * with range delimiters placed inside.
+	 *
+	 * @private
+	 * @param {engine.treeView.Text} node
+	 */
+	_stringifyTextRanges( node ) {
+		const length = node.data.length;
+		let result = node.data.split( '' );
+
+		// Add one more element for ranges ending after last character in text.
+		result[ length ] = '';
+
+		// Represent each letter as object with information about opening/closing ranges at each offset.
+		result = result.map( ( letter ) => {
+			return {
+				letter: letter,
+				start: '',
+				end: '',
+				collapsed: ''
+			};
+		}  );
+
+		for ( let range of this.ranges ) {
+			const start = range.start;
+			const end = range.end;
+
+			if ( start.parent == node && start.offset >= 0 && start.offset <= length ) {
+				if ( range.isCollapsed ) {
+					result[ end.offset ].collapsed += TEXT_RANGE_START_TOKEN + TEXT_RANGE_END_TOKEN;
+				} else {
+					result[ start.offset ].start += TEXT_RANGE_START_TOKEN;
+				}
+			}
+
+			if ( end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed  ) {
+				result[ end.offset ].end += TEXT_RANGE_END_TOKEN;
+			}
+		}
+
+		return result.map( item => item.end + item.collapsed + item.start + item.letter ).join( '' );
+	}
+
+	/**
+	 * Converts passed {@link engine.treeView.Element Element} to opening tag.
+	 * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>` or
+	 * `<attribute:a>`), contain priority information ( `<attribute:a priority=20>` ). Element's attributes also
+	 * will be included (`<a href="http://ckeditor.com" name="foobar">`).
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @returns {String}
+	 */
+	_stringifyElementOpen( element ) {
+		const priority = this._stringifyElementPriority( element );
+		const type = this._stringifyElementType( element );
+		const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
+		const attributes = this._stringifyElementAttributes( element );
+		const parts = [ name, attributes ];
+
+		return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
+	}
+
+	/**
+	 * Converts passed {@link engine.treeView.Element Element} to closing tag.
+	 * Depending on current configuration opening tag can be simple (`</a>`) or contain type prefix (`</container:p>` or
+	 * `</attribute:a>`).
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @returns {String}
+	 */
+	_stringifyElementClose( element ) {
+		const priority = this._stringifyElementPriority( element );
+		const type = this._stringifyElementType( element );
+		const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
+
+		return `</${ name }>`;
+	}
+
+	/**
+	 * Converts passed {@link engine.treeView.Element Element's} type to its string representation
+	 * Returns 'attribute' for {@link engine.treeView.AttributeElement AttributeElements} and
+	 * 'container' for {@link engine.treeView.ContainerElement ContainerElements}. Returns empty string when current
+	 * configuration is preventing showing elements' types.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @returns {String}
+	 */
+	_stringifyElementType( element ) {
+		if ( this.showType ) {
+			if ( element instanceof AttributeElement ) {
+				return 'attribute';
+			}
+
+			if ( element instanceof ContainerElement ) {
+				return 'container';
+			}
+		}
+
+		return '';
+	}
+
+	/**
+	 * Converts passed {@link engine.treeView.Element Element} to its priority representation.
+	 * Priority string representation will be returned when passed element is an instance of
+	 * {@link engine.treeView.AttributeElement AttributeElement} and current configuration allow to show priority.
+	 * Otherwise returns empty string.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @returns {String}
+	 */
+	_stringifyElementPriority( element ) {
+		if ( this.showPriority && element instanceof AttributeElement ) {
+			return element.priority;
+		}
+
+		return '';
+	}
+
+	/**
+	 * Converts passed {@link engine.treeView.Element Element} attributes to their string representation.
+	 * If element has no attributes - empty string is returned.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} element
+	 * @returns {String}
+	 */
+	_stringifyElementAttributes( element ) {
+		const attributes = [];
+
+		for ( let attribute of element.getAttributeKeys() ) {
+			attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` );
+		}
+
+		return attributes.join( ' ' );
+	}
+}

+ 29 - 0
packages/ckeditor5-engine/tests/treeview/documentfragment.js

@@ -88,6 +88,24 @@ describe( 'DocumentFragment', () => {
 				expect( count2 ).to.equal( 1 );
 				expect( count3 ).to.equal( 1 );
 			} );
+
+			it( 'should fire change event when inserting', ( done ) => {
+				fragment.once( 'change', ( event, type ) => {
+					expect( type ).to.equal( 'CHILDREN' );
+					done();
+				} );
+
+				fragment.insertChildren( 0, el1 );
+			} );
+
+			it( 'should fire change event when appending', ( done ) => {
+				fragment.once( 'change', ( event, type ) => {
+					expect( type ).to.equal( 'CHILDREN' );
+					done();
+				} );
+
+				fragment.appendChildren( el1 );
+			} );
 		} );
 
 		describe( 'getChildIndex', () => {
@@ -154,6 +172,17 @@ describe( 'DocumentFragment', () => {
 				expect( removed.length ).to.equal( 1 );
 				expect( removed[ 0 ] ).to.have.property( 'name' ).that.equals( 'el2' );
 			} );
+
+			it( 'should fire change event', ( done ) => {
+				fragment.appendChildren( el1 );
+
+				fragment.once( 'change', ( event, type ) => {
+					expect( type ).to.equal( 'CHILDREN' );
+					done();
+				} );
+
+				fragment.removeChildren( 0 );
+			} );
 		} );
 	} );
 

+ 0 - 161
packages/ckeditor5-engine/tests/treeview/writer/_utils/utils.js

@@ -1,161 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-import Range from '/ckeditor5/engine/treeview/range.js';
-import Position from '/ckeditor5/engine/treeview/position.js';
-
-const utils = {
-	/**
-	 * Helper function that is used to create treeView elements from description object.
-	 *
-	 * @param {engine.treeView.Writer} writer Writer instance. Used to set priorities.
-	 * @param {Object} description Description object.
-	 * @param {engine.treeView.Range} [range] Optional parameter, used in recurrent calls.
-	 * @param {engine.treeView.Position} [position] Optional parameter, used in recurrent calls.
-	 * @returns {Object} Returns object with `node`, `range`, `position` fields, containing created node and, optionally
-	 * range and position if description object contain information about them.
-	 */
-	create( writer, description, range, position ) {
-		const node = new description.instanceOf();
-
-		if ( !range ) {
-			range = Range.createFromParentsAndOffsets( node, 0, node, 0 );
-		}
-
-		if ( !position ) {
-			position = new Position( node, 0 );
-		}
-
-		if ( description.name ) {
-			node.name = description.name;
-		}
-
-		if ( description.data ) {
-			node.data = description.data;
-		}
-
-		if ( description.attributes ) {
-			Object.keys( description.attributes ).forEach( ( key ) => {
-				node.setAttribute( key, description.attributes[ key ] );
-			} );
-		}
-
-		if ( description.priority !== undefined ) {
-			node.priority = description.priority;
-		}
-
-		if ( description.rangeStart !== undefined ) {
-			range.start.parent = node;
-			range.start.offset = description.rangeStart;
-		}
-
-		if ( description.rangeEnd !== undefined ) {
-			range.end.parent = node;
-			range.end.offset = description.rangeEnd;
-		}
-
-		if ( description.position !== undefined ) {
-			position.parent = node;
-			position.offset = description.position;
-		}
-
-		if ( description.children ) {
-			description.children.forEach( ( desc, index ) => {
-				const created = utils.create( writer, desc, range, position );
-				node.insertChildren( index, created.node );
-			} );
-		}
-
-		return { node, range, position };
-	},
-
-	/**
-	 * Helper function that is used to test output of writer methods by providing declarative description of the
-	 * expected output.
-	 * Examples:
-	 * 		test element: `<p>fo[o<b>ba]r</b></p>`
-	 * 		description: {
-	 * 			name: 'p',
-	 * 	    	instanceOf: Element,
-	 * 	    	children:[
-	 * 	    		{
-	 * 	    			instanceOf: Text,
-	 * 	    			data: 'foo',
-	 * 	    			rangeStart: 2
- 	 * 	    		},
-	 *				{
-	 *					name: 'b'
-	 *					instanceOf: Element
-	 *					priority: 1,
-	 *					children: [
-	 *						{ instanceOf: Text, data: 'bar', rangeEnd: 2 }
-	 *					]
-	 *				}
-	 * 	    	]
-	 * 		}
-	 *
-	 *
-	 * @param {engine.treeView.Writer} writer Writer instance. Used to test priority.
-	 * @param {engine.treeView.Range|engine.treeView.Position } location Range instance or Position instance.
-	 * Treated as Range when when `rangeStart`, `rangeEnd` is used, treated as Position when `position` is used.
-	 * @param {engine.treeView.Node} node Element to check.
-	 * @param {Object} description Object describing expected element and its children.
-	 */
-	test( writer, location, node, description ) {
-		// If no root node provided - iterate over node array.
-		if ( description instanceof Array && node instanceof Array ) {
-			node.forEach( ( n, i ) => {
-				utils.test( writer, location, n, description[ i ] );
-			} );
-		}
-
-		if ( description.instanceOf ) {
-			expect( node ).to.be.instanceof( description.instanceOf );
-		}
-
-		if ( description.name ) {
-			expect( description.name ).to.equal( node.name  );
-		}
-
-		if ( description.data ) {
-			expect( description.data ).to.equal( node.data );
-		}
-
-		if ( description.priority !== undefined ) {
-			expect( description.priority ).to.equal( node.priority );
-		}
-
-		if ( description.rangeStart !== undefined ) {
-			expect( node ).to.equal( location.start.parent );
-			expect( description.rangeStart ).to.equal( location.start.offset );
-		}
-
-		if ( description.rangeEnd !== undefined ) {
-			expect( node ).to.equal( location.end.parent );
-			expect( description.rangeEnd ).to.equal( location.end.offset );
-		}
-
-		if ( description.attributes ) {
-			Object.keys( description.attributes ).forEach( ( key ) => {
-				expect( description.attributes[ key ] ).to.equal( node.getAttribute( key ) );
-			} );
-		}
-
-		if ( description.position !== undefined ) {
-			expect( node ).to.equal( location.parent );
-			expect( description.position ).to.equal( location.offset );
-		}
-
-		if ( description.children ) {
-			expect( description.children.length ).to.equal( node.getChildCount() );
-			description.children.forEach( ( desc, index ) => {
-				utils.test( writer, location, node.getChild( index ), desc );
-			} );
-		}
-	}
-};
-
-export default utils;

+ 60 - 264
packages/ckeditor5-engine/tests/treeview/writer/breakattributes.js

@@ -9,294 +9,90 @@
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import Position from '/ckeditor5/engine/treeview/position.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test break position.
+	 *
+	 * @param {String} input
+	 * @param {String} expected
+	 */
+	function test( input, expected ) {
+		const { view, selection } = parse( input );
+		const newPosition = writer.breakAttributes( selection.getFirstPosition() );
+		expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
 
 	describe( 'breakAttributes', () => {
-		// <p>{|foobar}</p> -> <p>|{foobar}</p>
-		it( '<p>{|foobar}</p>', () => {
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 0 }
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
+		it( 'should move position from begin of text node to the element', () => {
+			test( '<container:p>{}foobar</container:p>', '<container:p>[]foobar</container:p>' );
 		} );
 
-		it( '<p>foo|bar</p>', () => {
-			// <p>{foo|bar}</p> -> <p>{foo}|{bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 3 }
-				]
-			} );
+		it( 'should split text node', () => {
+			const text = new Text( 'foobar' );
+			const container = new ContainerElement( 'p', null, text );
+			const position = new Position( text, 3 );
 
-			const newPosition = writer.breakAttributes( created.position );
+			const newPosition = writer.breakAttributes( position );
 
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			expect( container.getChildCount() ).to.equal( 2 );
+			expect( container.getChild( 0 ) ).to.be.instanceOf( Text ).and.have.property( 'data' ).that.equal( 'foo' );
+			expect( container.getChild( 1 ) ).to.be.instanceOf( Text ).and.have.property( 'data' ).that.equal( 'bar' );
+			expect( newPosition.isEqual( new Position( container, 1 ) ) ).to.be.true;
 		} );
 
-		it( '<p>{foobar|}</p>', () => {
-			// <p>{foobar|}</p> -> <p>{foobar}|</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 6 }
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
+		it( 'should move position from end of text node to the element', () => {
+			test( '<container:p>foobar{}</container:p>', '<container:p>foobar[]</container:p>' );
 		} );
 
-		it( '<p><b>{foo|bar}</b></p>', () => {
-			// <p><b>{foo|bar}</b></p> -> <p><b>{foo}</b>|<b>{bar}</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar', position: 3 }
-						]
-					}
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					}
-				]
-			} );
+		it( 'should split attribute element', () => {
+			test(
+				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
+				'<container:p><attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1></container:p>'
+			);
 		} );
 
-		it( '<p><b><u>{|foobar}</u></b></p>', () => {
-			// <p><b><u>{|foobar}</u></b></p> -> <p>|<b><u>{foobar}</u></b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar', position: 0 }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar' }
-								]
-							}
-						]
-					}
-				]
-			} );
+		it( 'should move from beginning of the nested text node to the container', () => {
+			test(
+				'<container:p><attribute:b:1><attribute:u:1>{}foobar</attribute:u:1></attribute:b:1></container:p>',
+				'<container:p>[]<attribute:b:1><attribute:u:1>foobar</attribute:u:1></attribute:b:1></container:p>'
+			);
 		} );
 
-		// <p><b><u>{foo|ba}r</u></b></p> -> <p><b><u>{foo}</u></b>|<b></u>{bar}</u></b></p>
-		it( '<p><b><u>{foo|bar}</u></b></p>', () => {
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar', position: 3 }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foo' }
-								]
-							}
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'bar' }
-								]
-							}
-						]
-					}
-				]
-			} );
+		it( 'should split nested attributes', () => {
+			test(
+				'<container:p><attribute:b:1><attribute:u:1>foo{}bar</attribute:u:1></attribute:b:1></container:p>',
+				'<container:p>' +
+					'<attribute:b:1>' +
+						'<attribute:u:1>' +
+							'foo' +
+						'</attribute:u:1>' +
+					'</attribute:b:1>' +
+					'[]' +
+					'<attribute:b:1>' +
+						'<attribute:u:1>' +
+							'bar' +
+						'</attribute:u:1>' +
+					'</attribute:b:1>' +
+				'</container:p>'
+			);
 		} );
 
-		it( '<p><b><u>{foobar|}</u></b></p>', () => {
-			// <p><b><u>{foobar|}</u></b></p> -> <p><b><u>{foobar}</u></b>|</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar', position: 6 }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const newPosition = writer.breakAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar' }
-								]
-							}
-						]
-					}
-				]
-			} );
+		it( 'should move from end of the nested text node to the container', () => {
+			test(
+				'<container:p><attribute:b:1><attribute:u:1>foobar{}</attribute:u:1></attribute:b:1></container:p>',
+				'<container:p><attribute:b:1><attribute:u:1>foobar</attribute:u:1></attribute:b:1>[]</container:p>'
+			);
 		} );
 	} );
 } );

+ 41 - 179
packages/ckeditor5-engine/tests/treeview/writer/breakrange.js

@@ -9,16 +9,24 @@
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions.
+	 *
+	 * @param {String} input
+	 * @param {String} expected
+	 */
+	function test( input, expected ) {
+		const { view, selection } = parse( input );
+		const newRange = writer.breakRange( selection.getFirstRange() );
+		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
@@ -34,198 +42,52 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should break at collapsed range and return collapsed one', () => {
-			// <p>{foo[]bar}</p> -> <p>{foo}[]{bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 3, rangeEnd: 3 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			test(
+				'<container:p>foo{}bar</container:p>',
+				'<container:p>foo[]bar</container:p>'
+			);
 		} );
 
 		it( 'should break inside text node #1', () => {
-			// <p>{foo[bar]baz}</p> -> <p>{foo}[{bar}]{baz}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeStart: 3, rangeEnd: 6 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'bar' },
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
+			test(
+				'<container:p>foo{bar}baz</container:p>',
+				'<container:p>foo[bar]baz</container:p>'
+			);
 		} );
 
 		it( 'should break inside text node #2', () => {
-			// <p>{foo[barbaz]}</p> -> <p>{foo}[{barbaz}]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeStart: 3, rangeEnd: 9 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'barbaz' }
-				]
-			} );
+			test(
+				'<container:p>foo{barbaz}</container:p>',
+				'<container:p>foo[barbaz]</container:p>'
+			);
 		} );
 
 		it( 'should break inside text node #3', () => {
-			// <p>{foo[barbaz}]</p> -> <p>{foo}[{barbaz}]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeStart: 3 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'barbaz' }
-				]
-			} );
+			test(
+				'<container:p>foo{barbaz]</container:p>',
+				'<container:p>foo[barbaz]</container:p>'
+			);
 		} );
 
 		it( 'should break inside text node #4', () => {
-			// <p>{[foo]barbaz}</p> -> <p>[{foo}]{barbaz]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeStart: 0, rangeEnd: 3 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'barbaz' }
-				]
-			} );
+			test(
+				'<container:p>{foo}barbaz</container:p>',
+				'<container:p>[foo]barbaz</container:p>'
+			);
 		} );
 
 		it( 'should break inside text node #5', () => {
-			// <p>[{foo]barbaz}</p> -> <p>[{foo}]{barbaz]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeEnd: 3 }
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'barbaz' }
-				]
-			} );
+			test(
+				'<container:p>[foo}barbaz</container:p>',
+				'<container:p>[foo]barbaz</container:p>'
+			);
 		} );
 
 		it( 'should break placed inside different nodes', () => {
-			// <p>{foo[bar}<b>{baz]qux}</b></p>
-			// <p>{foo}[{bar}<b>{baz}</b>]<b>qux</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 3 },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bazqux', rangeEnd: 3 }
-						]
-					}
-				]
-			} );
-
-			const newRange = writer.breakRange( created.range );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 3,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'bar' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'qux' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>foo{bar<attribute:b:1>baz}qux</attribute:b:1></container:p>',
+				'<container:p>foo[bar<attribute:b:1>baz</attribute:b:1>]<attribute:b:1>qux</attribute:b:1></container:p>'
+			);
 		} );
 	} );
 } );

+ 78 - 324
packages/ckeditor5-engine/tests/treeview/writer/insert.js

@@ -8,366 +8,120 @@
 'use strict';
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
-import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
-import Position from '/ckeditor5/engine/treeview/position.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions.
+	 *
+	 * @param {String} input
+	 * @param {Array.<String>} nodesToInsert
+	 * @param {String} expected
+	 */
+	function test( input, nodesToInsert, expected ) {
+		nodesToInsert = nodesToInsert.map( node => parse( node ) );
+		const { view, selection } = parse( input );
+		const newRange = writer.insert( selection.getFirstPosition(), nodesToInsert );
+		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
 
 	describe( 'insert', () => {
 		it( 'should return collapsed range in insertion position when using empty array', () => {
-			// <p>{foo|bar}</p> -> <p>{foo[]bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 3 }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, [] );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 3, rangeEnd: 3 }
-				]
-			} );
+			test(
+				'<container:p>foo{}bar</container:p>',
+				[],
+				'<container:p>foo{}bar</container:p>'
+			);
 		} );
 
 		it( 'should insert text into another text node #1', () => {
-			// <p>{foo|bar}</p> insert {baz}
-			// <p>{foo[baz]bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 3 }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, new Text( 'baz' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobazbar', rangeStart: 3, rangeEnd: 6 }
-				]
-			} );
+			test(
+				'<container:p>foo{}bar</container:p>',
+				[ 'baz' ],
+				'<container:p>foo{baz}bar</container:p>'
+			);
 		} );
 
 		it( 'should insert text into another text node #2', () => {
-			// <p>{foobar|}</p> insert {baz}
-			// <p>{foobar[baz}]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 6 }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, new Text( 'baz' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobarbaz', rangeStart: 6 }
-				]
-			} );
+			test(
+				'<container:p>foobar{}</container:p>',
+				[ 'baz' ],
+				'<container:p>foobar{baz]</container:p>'
+			);
 		} );
 
 		it( 'should insert text into another text node #3', () => {
-			// <p>{|foobar}</p> insert {baz}
-			// <p>[{baz]foobar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 0 }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, new Text( 'baz' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{ instanceOf: Text, data: 'bazfoobar', rangeEnd: 3 }
-				]
-			} );
+			test(
+				'<container:p>{}foobar</container:p>',
+				[ 'baz' ],
+				'<container:p>[baz}foobar</container:p>'
+			);
 		} );
 
 		it( 'should break attributes when inserting into text node', () => {
-			// <p>{foo|bar}</p> insert <b>{baz}</b>
-			// <p>{foo}[<b>baz</b>]{bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 3 }
-				]
-			} );
-			const toInsert = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'b',
-				priority: 1,
-				children: [
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, toInsert.node );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: ContainerElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			test(
+				'<container:p>foo{}bar</container:p>',
+				[ '<attribute:b:1>baz</attribute:b:1>' ],
+				'<container:p>foo[<attribute:b:1>baz</attribute:b:1>]bar</container:p>'
+			);
 		} );
 
-		it( 'should merge text ndoes', () => {
-			// <p>|{foobar}</p> insert {baz}
-			// <p>[{baz]foobar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, new Text( 'baz' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{ instanceOf: Text, data: 'bazfoobar', rangeEnd: 3 }
-				]
-			} );
+		it( 'should merge text nodes', () => {
+			test(
+				'<container:p>[]foobar</container:p>',
+				[ 'baz' ],
+				'<container:p>[baz}foobar</container:p>'
+			);
 		} );
 
 		it( 'should merge same attribute nodes', () => {
-			// <p><b>{foo|bar}</b></p> insert <b>{baz}</b>
-			// <p><b>{foo[baz]bar}</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar', position: 3 }
-						]
-					}
-				]
-			} );
-			const toInsert = create( writer, {
-				instanceOf: AttributeElement,
-				name: 'b',
-				priority: 1,
-				children: [
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, toInsert.node );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobazbar', rangeStart: 3, rangeEnd: 6 }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
+				[ '<attribute:b:1>baz</attribute:b:1>' ],
+				'<container:p><attribute:b:1>foo{baz}bar</attribute:b:1></container:p>'
+			);
 		} );
 
 		it( 'should not merge different attributes', () => {
-			// <p><b>{foo|bar}</b></p> insert <b>{baz}</b> ( different priority )
-			// <p><b>{foo}</b>[<b>{baz}</b>]<b>{bar}</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar', position: 3 }
-						]
-					}
-				]
-			} );
-			const toInsert = create( writer, {
-				instanceOf: AttributeElement,
-				name: 'b',
-				priority: 2,
-				children: [
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-
-			const newRange = writer.insert( created.position, toInsert.node );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
+				[ '<attribute:b:2>baz</attribute:b:2>' ],
+				'<container:p>' +
+					'<attribute:b:1>' +
+						'foo' +
+					'</attribute:b:1>' +
+					'[' +
+					'<attribute:b:2>' +
+						'baz' +
+					'</attribute:b:2>' +
+					']' +
+					'<attribute:b:1>' +
+						'bar' +
+					'</attribute:b:1>' +
+				'</container:p>'
+			);
 		} );
 
 		it( 'should allow to insert multiple nodes', () => {
-			// <p>|</p> insert <b>{foo}</b>{bar}
-			// <p>[<b>{foo}</b>{bar}]</p>
-			const root = new ContainerElement( 'p' );
-			const toInsert = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'fake',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} ).node.getChildren();
-			const position = new Position( root, 0 );
-
-			const newRange = writer.insert( position, toInsert );
-			test( writer, newRange, root, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			test(
+				'<container:p>[]</container:p>',
+				[ '<attribute:b:1>foo</attribute:b:1>', 'bar' ],
+				'<container:p>[<attribute:b:1>foo</attribute:b:1>bar]</container:p>'
+			);
 		} );
 
 		it( 'should merge after inserting multiple nodes', () => {
-			// <p><b>{qux}</b>|{baz}</p> insert <b>{foo}</b>{bar}
-			// <p><b>{qux[foo}</b>{bar]baz}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'qux' }
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-			const toInsert = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'fake',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} ).node.getChildren();
-
-			const newRange = writer.insert( created.position, toInsert );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'quxfoo', rangeStart: 3 }
-						]
-					},
-					{ instanceOf: Text, data: 'barbaz', rangeEnd: 3 }
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1>qux</attribute:b:1>[]baz</container:p>',
+				[ '<attribute:b:1>foo</attribute:b:1>', 'bar' ],
+				'<container:p><attribute:b:1>qux{foo</attribute:b:1>bar}baz</container:p>'
+			);
 		} );
 	} );
 } );

+ 56 - 226
packages/ckeditor5-engine/tests/treeview/writer/mergeattributes.js

@@ -9,270 +9,100 @@
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import Position from '/ckeditor5/engine/treeview/position.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test merge position.
+	 *
+	 * @param {String} input
+	 * @param {String} expected
+	 */
+	function test( input, expected ) {
+		const { view, selection } = parse( input );
+		const newPosition = writer.mergeAttributes( selection.getFirstPosition() );
+		expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
 
 	describe( 'mergeAttributes', () => {
 		it( 'should not merge if inside text node', () => {
-			// <p>{fo|obar}</p>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 2 }
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test( '<container:p>fo{}bar</container:p>', '<container:p>fo{}bar</container:p>' );
 		} );
 
 		it( 'should not merge if between containers', () => {
-			// <div><p>{foo}</p>|<p>{bar}</p></div>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'div',
-				position: 1,
-				children: [
-					{
-						instanceOf: ContainerElement,
-						name: 'p',
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: ContainerElement,
-						name: 'p',
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					}
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:div><container:p>foo</container:p>[]<container:p>bar</container:p></container:div>',
+				'<container:div><container:p>foo</container:p>[]<container:p>bar</container:p></container:div>'
+			);
 		} );
 
 		it( 'should return same position when inside empty container', () => {
-			// <p>|</p>
-			const description = { instanceOf: ContainerElement, name: 'p', position: 0 };
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:p>[]</container:p>',
+				'<container:p>[]</container:p>'
+			);
 		} );
 
 		it( 'should not merge when position is placed at the beginning of the container', () => {
-			// <p>|<b></b></p>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1
-					}
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:p>[]<attribute:b:1></attribute:b:1></container:p>',
+				'<container:p>[]<attribute:b:1></attribute:b:1></container:p>'
+			);
 		} );
 
 		it( 'should not merge when position is placed at the end of the container', () => {
-			// <p><b></b>|</p>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1
-					}
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:p><attribute:b:1></attribute:b:1>[]</container:p>',
+				'<container:p><attribute:b:1></attribute:b:1>[]</container:p>'
+			);
 		} );
 
 		it( 'should merge when placed between two text nodes', () => {
-			// <p>{foo}|{bar}</p> -> <p>{foo|bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
-
-			const newPosition = writer.mergeAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', position: 3 }
-				]
-			} );
+			// <p>foobar</p> -> <p>foo|bar</p>
+			const t1 = new Text( 'foo' );
+			const t2 = new Text( 'bar' );
+			const p = new ContainerElement( 'p', null, [ t1, t2 ] );
+			const position = new Position( p, 1 );
+
+			const newPosition = writer.mergeAttributes( position );
+			expect( stringify( p, newPosition ) ).to.equal( '<p>foo{}bar</p>' );
 		} );
 
 		it( 'should merge when placed between similar attribute nodes', () => {
-			// <p><b foo="bar"></b>|<b foo="bar"></b></p> -> <p><b foo="bar">|</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' }
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' }
-					}
-				]
-			} );
-
-			const newPosition = writer.mergeAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						position: 0,
-						attributes: { foo: 'bar' }
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:1 foo="bar"></attribute:b:1></container:p>',
+				'<container:p><attribute:b:1 foo="bar">[]</attribute:b:1></container:p>'
+			);
 		} );
 
 		it( 'should not merge when placed between non-similar attribute nodes', () => {
-			// <p><b foo="bar"></b>|<b foo="baz"></b></p> ->
-			// <p><b foo="bar"></b>|<b foo="baz"></b></p>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' }
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'baz' }
-					}
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:1 foo="baz"></attribute:b:1></container:p>',
+				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:1 foo="baz"></attribute:b:1></container:p>'
+			);
 		} );
 
 		it( 'should not merge when placed between similar attribute nodes with different priority', () => {
-			// <p><b foo="bar"></b>|<b foo="bar"></b></p> -> <p><b foo="bar"></b>|<b foo="bar"></b></p>
-			const description =  {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' }
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						attributes: { foo: 'bar' }
-					}
-				]
-			};
-			const created = create( writer, description );
-			const newPosition = writer.mergeAttributes( created.position );
-			test( writer, newPosition, created.node, description );
+			test(
+				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:2 foo="bar"></attribute:b:2></container:p>',
+				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:2 foo="bar"></attribute:b:2></container:p>'
+			);
 		} );
 
 		it( 'should merge attribute nodes and their contents if possible', () => {
-			// <p><b foo="bar">{foo}</b>|<b foo="bar">{bar}</b></p>
-			// <p><b foo="bar">{foo}|{bar}</b></p>
-			// <p><b foo="bar">{foo|bar}</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' },
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' },
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					}
-				]
-			} );
-
-			const newPosition = writer.mergeAttributes( created.position );
-
-			test( writer, newPosition, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						attributes: { foo: 'bar' },
-						children: [
-							{ instanceOf: Text, data: 'foobar', position: 3 }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1 foo="bar">foo</attribute:b:1>[]<attribute:b:1 foo="bar">bar</attribute:b:1></container:p>',
+				'<container:p><attribute:b:1 foo="bar">foo{}bar</attribute:b:1></container:p>'
+			);
 		} );
 	} );
 } );

+ 35 - 226
packages/ckeditor5-engine/tests/treeview/writer/remove.js

@@ -9,17 +9,29 @@
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
 import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test ranges.
+	 *
+	 * @param {String} input
+	 * @param {String} expectedResult
+	 * @param {String} expectedRemoved
+	 */
+	function test( input, expectedResult, expectedRemoved ) {
+		const { view, selection } = parse( input );
+		const range = selection.getFirstRange();
+		const removed = writer.remove( range );
+		expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
+		expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
@@ -45,242 +57,39 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should remove single text node', () => {
-			// <p>[{foobar}]</p> -> <p>|</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: []
-			} );
-
-			// Test removed nodes.
-			test( writer, null, Array.from( removed.getChildren() ), [
-				{ instanceOf: Text, data: 'foobar' }
-			] );
+			test( '<container:p>[foobar]</container:p>', '<container:p>[]</container:p>', 'foobar' );
 		} );
 
 		it( 'should not leave empty text nodes', () => {
-			// <p>{[foobar]}</p> -> <p>|</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 0, rangeEnd: 6 }
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 0,
-				children: []
-			} );
-
-			// Test removed nodes.
-			test( writer, null, removed, [
-				{ instanceOf: Text, data: 'foobar' }
-			] );
+			test( '<container:p>{foobar}</container:p>', '<container:p>[]</container:p>', 'foobar' );
 		} );
 
 		it( 'should remove part of the text node', () => {
-			// <p>{f[oob]ar}</p> -> <p>{f|ar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 1, rangeEnd: 4 }
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'far', position: 1 }
-				]
-			} );
-
-			// Test removed nodes.
-			test( writer, null, removed, [
-				{ instanceOf: Text, data: 'oob' }
-			] );
+			test( '<container:p>f{oob}ar</container:p>', '<container:p>f{}ar</container:p>', 'oob' );
 		} );
 
 		it( 'should remove parts of nodes', () => {
-			// <p>{f[oo}<b>{ba]r}</b></p> -> <p>{f}|<b>r</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foo', rangeStart: 1 },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar', rangeEnd: 2 }
-						]
-					}
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				position: 1,
-				children: [
-					{ instanceOf: Text, data: 'f' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'r' }
-						]
-					}
-				]
-			} );
-
-			// Test removed nodes.
-			test( writer, null, removed, [
-				{ instanceOf: Text, data: 'oo' },
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					priority: 1,
-					children: [
-						{ instanceOf: Text, data: 'ba' }
-					]
-				}
-			] );
+			test(
+				'<container:p>f{oo<attribute:b:10>ba}r</attribute:b:10></container:p>',
+				'<container:p>f[]<attribute:b:10>r</attribute:b:10></container:p>',
+				'oo<attribute:b:10>ba</attribute:b:10>'
+			);
 		} );
 
 		it( 'should merge after removing #1', () => {
-			// <p><b>foo</b>[{bar}]<b>bazqux</b></p> -> <p><b>foo|bazqux</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bazqux' }
-						]
-					}
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobazqux', position: 3 }
-						]
-					}
-				]
-			} );
-
-			// Test removed nodes.
-			test( writer, null, removed, [
-				{ instanceOf: Text, data: 'bar' }
-			] );
+			test(
+				'<container:p><attribute:b:1>foo</attribute:b:1>[bar]<attribute:b:1>bazqux</attribute:b:1></container:p>',
+				'<container:p><attribute:b:1>foo{}bazqux</attribute:b:1></container:p>',
+				'bar'
+			);
 		} );
 
 		it( 'should merge after removing #2', () => {
-			// <p><b>fo[o</b>{bar}<b>ba]zqux</b></p> -> <p><b>fo|zqux</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo', rangeStart: 2 }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bazqux', rangeEnd: 2 }
-						]
-					}
-				]
-			} );
-
-			const removed = writer.remove( created.range );
-			test( writer, created.range.start, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'fozqux', position: 2 }
-						]
-					}
-				]
-			} );
-
-			// Test removed nodes.
-			test( writer, null, removed, [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					priority: 1,
-					children: [
-						{ instanceOf: Text, data: 'o' }
-					]
-				},
-				{ instanceOf: Text, data: 'bar' },
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					priority: 1,
-					children: [
-						{ instanceOf: Text, data: 'ba' }
-					]
-				}
-			] );
+			test(
+				'<container:p><attribute:b:1>fo{o</attribute:b:1>bar<attribute:b:1>ba}zqux</attribute:b:1></container:p>',
+				'<container:p><attribute:b:1>fo{}zqux</attribute:b:1></container:p>',
+				'<attribute:b:1>o</attribute:b:1>bar<attribute:b:1>ba</attribute:b:1>'
+			);
 		} );
 	} );
 } );

+ 164 - 814
packages/ckeditor5-engine/tests/treeview/writer/unwrap.js

@@ -14,50 +14,44 @@ import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions.
+	 *
+	 * @param {String} input
+	 * @param {String} unwrapAttribute
+	 * @param {String} expected
+	 */
+	function test( input, unwrapAttribute, expected ) {
+		const { view, selection } = parse( input );
+		const newRange = writer.unwrap( selection.getFirstRange(), parse( unwrapAttribute ) );
+		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
 
 	describe( 'unwrap', () => {
 		it( 'should do nothing on collapsed ranges', () => {
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foo', rangeStart: 1, rangeEnd: 1 }
-				]
-			};
-			const created = create( writer, description );
-			const newRange = writer.unwrap( created.range, new AttributeElement( 'b' ), 1 );
-			test( writer, newRange, created.node, description );
+			test(
+				'<container:p>f{}oo</container:p>',
+				'<attribute:b:10></attribute:b:10>',
+				'<container:p>f{}oo</container:p>'
+			);
 		} );
 
 		it( 'should do nothing on single text node', () => {
-			// <p>[{foobar}]</p>
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			};
-
-			const created = create( writer, description );
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, description );
+			test(
+				'<container:p>[foobar]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[foobar]</container:p>'
+			);
 		} );
 
 		it( 'should throw error when element is not instance of AttributeElement', () => {
@@ -88,841 +82,197 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should unwrap single node', () => {
-			// <p>[<b>{foobar}</b>]<p> -> <p>[{foobar}]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[foobar]</container:p>'
+			);
 		} );
 
 		it( 'should not unwrap attributes with different priorities #1', () => {
-			// <p>[<b>{foobar}</b>]<p> -> <p>[<b>{foobar}</b>]</p>
-			// Unwrapped with <b> but using different priority.
-			const description =  {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					}
-				]
-			};
-			const created = create( writer, description );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 2;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, description );
+			test(
+				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
+				'<attribute:b:2></attribute:b:2>',
+				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not unwrap attributes with different priorities #2', () => {
-			// <p>[<b>{foo}</b><b>{bar}</b><b>{baz}</b>]<p> -> <p>[{foo}<b>bar</b>{baz}]</p>
-			// <b> around `bar` has different priority than others.
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 2;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
+			test(
+				'<container:p>' +
+				'[' +
+					'<attribute:b:2>foo</attribute:b:2>' +
+					'<attribute:b:1>bar</attribute:b:1>' +
+					'<attribute:b:2>baz</attribute:b:2>' +
+				']' +
+				'</container:p>',
+				'<attribute:b:2></attribute:b:2>',
+				'<container:p>[foo<attribute:b:1>bar</attribute:b:1>baz]</container:p>'
+			);
 		} );
 
 		it( 'should unwrap part of the node', () => {
-			// <p>[{baz}<b>{foo]bar}</b><p> -> <p>[{bazfoo}]<b>{bar}</b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{ instanceOf: Text, data: 'baz' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar', rangeEnd: 3 }
-						]
-
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'bazfoo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-
-					}
-				]
-			} );
+			test(
+				'<container:p>[baz<attribute:b:1>foo}bar</attribute:b:1>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[bazfoo]<attribute:b:1>bar</attribute:b:1></container:p>'
+			);
 		} );
 
 		it( 'should unwrap nested attributes', () => {
-			// <p>[<u><b>{foobar}</b></u>]</p> -> <p>[<u>{foobar}</u>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'b',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foobar' }
-								]
-							}
-						]
-					}
-				]
-			} );
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-
-			const newRange = writer.unwrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:u:1><attribute:b:1>foobar</attribute:b:1></attribute:u:1>]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:u:1>foobar</attribute:u:1>]</container:p>'
+			);
 		} );
 
 		it( 'should merge unwrapped nodes #1', () => {
-			// <p>{foo}[<b>{bar}</b>]{bom}</p> -> <p>{foo[bar]bom}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{ instanceOf: Text, data: 'bom' }
-				]
-			} );
-
-			const b =  new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: Text,
-						data: 'foobarbom',
-						rangeStart: 3,
-						rangeEnd: 6
-					}
-				]
-			} );
+			test(
+				'<container:p>foo[<attribute:b:1>bar</attribute:b:1>]baz</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>foo{bar}baz</container:p>'
+			);
 		} );
 
 		it( 'should merge unwrapped nodes #2', () => {
-			// <p>{foo}<u>{bar}</u>[<b><u>{bazqux}</u></b>]</p> -> <p>{foo}<u>{bar[bazqux}</u>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 2,
-				rangeEnd: 3,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'bazqux' }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: Text,
-						data: 'foo'
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'barbazqux', rangeStart: 3 }
-						]
-					}
-				]
-			} );
+			const input = '<container:p>' +
+			'foo' +
+				'<attribute:u:1>bar</attribute:u:1>' +
+				'[' +
+				'<attribute:b:1>' +
+					'<attribute:u:1>bazqux</attribute:u:1>' +
+				'</attribute:b:1>' +
+				']' +
+			'</container:p>';
+			const attribute = '<attribute:b:1></attribute:b:1>';
+			const result = '<container:p>foo<attribute:u:1>bar{bazqux</attribute:u:1>]</container:p>';
+
+			test( input, attribute, result );
 		} );
 
 		it( 'should merge unwrapped nodes #3', () => {
-			// <p>{foo}<u>{bar}</u>[<b><u>{baz]qux}</u></b></p> -> <p>{foo}<u>{bar[baz}</u>]<b><u>{qux}</u></b></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'bazqux', rangeEnd: 3 }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: Text,
-						data: 'foo'
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'barbaz', rangeStart: 3 }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'qux' }
-								]
-							}
-						]
-					}
-				]
-			} );
+			const input = '<container:p>' +
+				'foo' +
+				'<attribute:u:1>bar</attribute:u:1>' +
+				'[' +
+				'<attribute:b:1>' +
+					'<attribute:u:1>baz}qux</attribute:u:1>' +
+				'</attribute:b:1>' +
+			'</container:p>';
+			const attribute = '<attribute:b:1></attribute:b:1>';
+			const result = '<container:p>' +
+				'foo' +
+				'<attribute:u:1>bar{baz</attribute:u:1>]' +
+				'<attribute:b:1>' +
+					'<attribute:u:1>qux</attribute:u:1>' +
+				'</attribute:b:1>' +
+			'</container:p>';
+
+			test( input, attribute, result );
 		} );
 
 		it( 'should merge unwrapped nodes #4', () => {
-			// <p>{foo}<u>{bar}</u>[<b><u>{baz}</u></b>]<u>qux</u></p> -> <p>{foo}<u>{bar[baz]qux}</u></p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 2,
-				rangeEnd: 3,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'baz' }
-								]
-							}
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'qux' }
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{
-						instanceOf: Text,
-						data: 'foo'
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'barbazqux', rangeStart: 3, rangeEnd: 6 }
-						]
-					}
-				]
-			} );
+			const input = '<container:p>' +
+				'foo' +
+				'<attribute:u:1>bar</attribute:u:1>' +
+				'[' +
+				'<attribute:b:1>' +
+					'<attribute:u:1>baz</attribute:u:1>' +
+				'</attribute:b:1>' +
+				']' +
+				'<attribute:u:1>qux</attribute:u:1>' +
+			'</container:p>';
+			const attribute = '<attribute:b:1></attribute:b:1>';
+			const result = '<container:p>' +
+				'foo' +
+				'<attribute:u:1>bar{baz}qux</attribute:u:1>' +
+			'</container:p>';
+
+			test( input, attribute, result );
 		} );
 
 		it( 'should merge unwrapped nodes #5', () => {
-			// <p>[<b><u>{foo}</u></b><b><u>{bar}</u></b><b><u>{baz}</u></b>]</p> -> <p>[<u>{foobarbaz}</u>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foo' }
-								]
-							}
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'bar' }
-								]
-							}
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'u',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'baz' }
-								]
-							}
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobarbaz' }
-						]
-					}
-				]
-			} );
+			const input = '<container:p>' +
+				'[' +
+				'<attribute:b:1><attribute:u:1>foo</attribute:u:1></attribute:b:1>' +
+				'<attribute:b:1><attribute:u:1>bar</attribute:u:1></attribute:b:1>' +
+				'<attribute:b:1><attribute:u:1>baz</attribute:u:1></attribute:b:1>' +
+				']' +
+			'</container:p>';
+			const attribute = '<attribute:b:1></attribute:b:1>';
+			const result = '<container:p>[<attribute:u:1>foobarbaz</attribute:u:1>]</container:p>';
+
+			test( input, attribute, result );
 		} );
 
 		it( 'should unwrap mixed ranges #1', () => {
-			// <p>[<u><b>{foo}]</b></u></p> -> <p>[<u>{foo}</u>]</p
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'b',
-								priority: 1,
-								rangeEnd: 1,
-								children: [
-									{ instanceOf: Text, data: 'foo' }
-								]
-							}
-						]
-					}
-				]
-			} );
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					}
-				]
-			} );
+			const input = '<container:p>' +
+				'[' +
+				'<attribute:u:1>' +
+					'<attribute:b:1>foo]</attribute:b:1>' +
+				'</attribute:u:1>' +
+			'</container:p>';
+			const attribute = '<attribute:b:1></attribute:b:1>';
+			const result = '<container:p>[<attribute:u:1>foo</attribute:u:1>]</container:p>';
+
+			test( input, attribute, result );
 		} );
 
 		it( 'should unwrap mixed ranges #2', () => {
-			// <p>[<u><b>{foo]}</b></u></p> -> <p>[<u>{foo}</u>]</p
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'b',
-								priority: 1,
-								children: [
-									{ instanceOf: Text, data: 'foo', rangeEnd: 3 }
-								]
-							}
-						]
-					}
-				]
-			} );
-			const b = new AttributeElement( 'b' );
-			b.priority = 1;
-			const newRange = writer.unwrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:u:1><attribute:b:1>foo}</attribute:b:1></attribute:u></container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:u:1>foo</attribute:u:1>]</container:p>'
+			);
 		} );
 
 		it( 'should unwrap single element by removing matching attributes', () => {
-			// <p>[<b foo="bar" baz="qux" >test</b>]</p>
-			// unwrap using <b baz="qux"></b>
-			// <p>[<b foo="bar">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				foo: 'bar',
-				baz: 'qux'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				baz: 'qux'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.getAttribute( 'foo' ) ).to.equal( 'bar' );
-			expect( b.hasAttribute( 'baz' ) ).to.be.false;
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 foo="bar" baz="qux">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 baz="qux"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 foo="bar">test</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not unwrap single element when attributes are different', () => {
-			// <p>[<b foo="bar" baz="qux">test</b>]</p>
-			// unwrap using <b baz="qux" test="true"></b>
-			// <p>[<b foo="bar" baz="qux">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				foo: 'bar',
-				baz: 'qux'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				baz: 'qux',
-				test: 'true'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.getAttribute( 'foo' ) ).to.equal( 'bar' );
-			expect( b.getAttribute( 'baz' ) ).to.equal( 'qux' );
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 foo="bar" baz="qux">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 baz="qux" test="true"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 foo="bar" baz="qux">test</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should unwrap single element by removing matching classes', () => {
-			// <p>[<b class="foo bar baz">test</b>]</p>
-			// unwrap using <b class="baz foo"></b>
-			// <p>[<b class="bar">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				class: 'foo bar baz'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				class: 'baz foo'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.hasClass( 'bar' ) ).to.be.true;
-			expect( b.hasClass( 'foo' ) ).to.be.false;
-			expect( b.hasClass( 'baz' ) ).to.be.false;
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 class="baz foo"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 class="bar">test</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not unwrap single element when classes are different', () => {
-			// <p>[<b class="foo bar baz">test</b>]</p>
-			// unwrap using <b class="baz foo qux"></b>
-			// <p>[<b class="foo bar baz">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				class: 'foo bar baz'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				class: 'baz foo qux'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.hasClass( 'bar' ) ).to.be.true;
-			expect( b.hasClass( 'foo' ) ).to.be.true;
-			expect( b.hasClass( 'baz' ) ).to.be.true;
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 class="baz foo qux"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should unwrap single element by removing matching styles', () => {
-			// <p>[<b style="color:red; position:absolute; top:10px;">test</b>]</p>
-			// unwrap using <b style="position: absolute;"></b>
-			// <p>[<b style="color:red; top:10px;">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				style: 'color:red; position:absolute; top:10px;'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				style: 'position: absolute;'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.getStyle( 'color' ) ).to.equal( 'red' );
-			expect( b.getStyle( 'top' ) ).to.equal( '10px' );
-			expect( b.hasStyle( 'position' ) ).to.be.false;
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 style="position: absolute;"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 style="color:red;top:10px;">test</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not unwrap single element when styles are different', () => {
-			// <p>[<b style="color:red; position:absolute; top:10px;">test</b>]</p>
-			// unwrap using <b style="position: relative;"></b>
-			// <p>[<b style="color:red; position:absolute; top:10px;">test</b>]</p>
-			const text = new Text( 'test' );
-			const b = new AttributeElement( 'b', {
-				style: 'color:red; position:absolute; top:10px;'
-			}, text );
-			const p = new ContainerElement( 'p', null, b );
-			const wrapper = new AttributeElement( 'b', {
-				style: 'position: relative;'
-			} );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-
-			const newRange = writer.unwrap( range, wrapper );
-			expect( b.getStyle( 'color' ) ).to.equal( 'red' );
-			expect( b.getStyle( 'top' ) ).to.equal( '10px' );
-			expect( b.getStyle( 'position' ) ).to.equal( 'absolute' );
-			expect( b.parent ).to.equal( p );
-			test( writer, newRange, p, {
-				instanceof: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceof: Text, data: 'test' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>',
+				'<attribute:b:1 style="position: relative;"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>'
+			);
 		} );
 	} );
 } );

+ 135 - 663
packages/ckeditor5-engine/tests/treeview/writer/wrap.js

@@ -11,69 +11,47 @@ import Writer from '/ckeditor5/engine/treeview/writer.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
-import { DEFAULT_PRIORITY } from '/ckeditor5/engine/treeview/attributeelement.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions.
+	 *
+	 * @param {String} input
+	 * @param {String} unwrapAttribute
+	 * @param {String} expected
+	 */
+	function test( input, unwrapAttribute, expected ) {
+		const { view, selection } = parse( input );
+		const newRange = writer.wrap( selection.getFirstRange(), parse( unwrapAttribute ) );
+		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
 
 	describe( 'wrap', () => {
 		it( 'should do nothing on collapsed ranges', () => {
-			const description = {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foo', rangeStart: 1, rangeEnd: 1 }
-				]
-			};
-			const created = create( writer, description );
-			const newRange = writer.wrap( created.range, new AttributeElement( 'b' ) );
-			test( writer, newRange, created.node, description );
+			test(
+				'<container:p>f{}oo</container:p>',
+				'<attribute:b></attribute:b>',
+				'<container:p>f{}oo</container:p>'
+			);
 		} );
 
 		it( 'wraps single text node', () => {
-			// <p>[{foobar}]</p>
-			// wrap <b>
-			// <p>[<b>{foobar}<b>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[foobar]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should throw error when element is not instance of AttributeElement', () => {
@@ -104,671 +82,165 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'wraps part of a single text node #1', () => {
-			// <p>[{foo]bar}</p>
-			// wrap with <b>
-			// <p>[<b>{foo}</b>]{bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeEnd: 3 }
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			test(
+				'<container:p>[foo}bar</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b:1>foo</attribute:b:1>]bar</container:p>'
+			);
 		} );
 
 		it( 'wraps part of a single text node #2', () => {
-			// <p>{[foo]bar}</p>
-			// wrap with <b>
-			// <p>[<b>{foo}</b>]{bar}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 0, rangeEnd: 3 }
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' }
-				]
-			} );
+			test(
+				'<container:p>{foo}bar</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b:1>foo</attribute:b:1>]bar</container:p>'
+			);
 		} );
 
 		it( 'wraps part of a single text node #3', () => {
-			// <p>{foo[bar]}</p>
-			// wrap with <b>
-			// <p>{foo}[<b>{bar}</b>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				children: [
-					{ instanceOf: Text, data: 'foobar', rangeStart: 3, rangeEnd: 6 }
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>foo{bar}</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>foo[<attribute:b:1>bar</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not wrap inside nested containers', () => {
-			// <div>[{foobar}<p>{baz}</p>]</div>
-			// wrap with <b>
-			// <div>[<b>{foobar}</b><p>{baz}</p>]</div>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'div',
-				rangeStart: 0,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foobar' },
-					{
-						instanceOf: ContainerElement,
-						name: 'p',
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					}
-				]
-			} );
-
-			const newRange = writer.wrap( created.range, new AttributeElement( 'b' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'div',
-				rangeStart: 0,
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					},
-					{
-						instanceOf: ContainerElement,
-						name: 'p',
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:div>[foobar<container:p>baz</container:p>]</container:div>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:div>[<attribute:b:1>foobar</attribute:b:1><container:p>baz</container:p>]</container:div>'
+			);
 		} );
 
 		it( 'wraps according to priorities', () => {
-			// <p>[<u>{foobar}</u>]</p>
-			// wrap with <b> that has higher priority than <u>
-			// <p>[<u><b>{foobar}</b></u>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 2;
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'u',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'b',
-								priority: 2,
-								children: [
-									{ instanceOf: Text, data: 'foobar' }
-								]
-							}
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:u:1>foobar</attribute:u:1>]</container:p>',
+				'<attribute:b:2></attribute:b:2>',
+				'<container:p>[<attribute:u:1><attribute:b:2>foobar</attribute:b:2></attribute:u:1>]</container:p>'
+			);
 		} );
 
 		it( 'merges wrapped nodes #1', () => {
-			// <p>[<b>{foo}</b>{bar}<b>{baz}</b>]</p>
-			// wrap with <b>
-			// <p>[<b>{foobarbaz}</b>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'bar' },
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					}
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			const newRange = writer.wrap( created.range, b );
-
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobarbaz' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1>foo</attribute:b:1>bar<attribute:b:1>baz</attribute:b:1>]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b:1>foobarbaz</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'merges wrapped nodes #2', () => {
-			// <p><b>{foo}</b>[{bar]baz}</p>
-			// wrap with <b>
-			// <p><b>{foo[bar}</b>]{baz}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{ instanceOf: Text, data: 'barbaz', rangeEnd: 3 }
-				]
-			} );
-
-			const newRange = writer.wrap( created.range, new AttributeElement( 'b' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobar', rangeStart: 3 }
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1>foo</attribute:b:1>[bar}baz</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p><attribute:b:1>foo{bar</attribute:b:1>]baz</container:p>'
+			);
 		} );
 
 		it( 'merges wrapped nodes #3', () => {
-			// <p><b>{foobar}</b>[{baz}]</p>
-			// wrap with <b>
-			// <p><b>{foobar[baz}</b>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 1,
-				rangeEnd: 2,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobar' }
-						]
-					},
-					{ instanceOf: Text, data: 'baz', rangeEnd: 3 }
-				]
-			} );
-
-			const newRange = writer.wrap( created.range, new AttributeElement( 'b' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foobarbaz', rangeStart: 6 }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1>foobar</attribute:b:1>[baz]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p><attribute:b:1>foobar{baz</attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'merges wrapped nodes #4', () => {
-			// <p>[{foo}<i>{bar}</i>]{baz}</p>
-			// wrap with <b>
-			// <p>[<b>{foo}<i>{bar}</i></b>]{baz}</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 2,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'i',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-
-			const newRange = writer.wrap( created.range, new AttributeElement( 'b' ) );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: DEFAULT_PRIORITY,
-						children: [
-							{ instanceOf: Text, data: 'foo' },
-							{
-								instanceOf: AttributeElement,
-								name: 'i',
-								priority: DEFAULT_PRIORITY,
-								children: [
-									{ instanceOf: Text, data: 'bar' }
-								]
-							}
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
+			test(
+				'<container:p>[foo<attribute:i:1>bar</attribute:i:1>]baz</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b:1>foo<attribute:i:1>bar</attribute:i:1></attribute:b:1>]baz</container:p>'
+			);
 		} );
 
 		it( 'merges wrapped nodes #5', () => {
-			// <p>[{foo}<i>{bar}</i>{baz}]</p>
-			// wrap with <b>, that has higher priority than <i>
-			// <p>[<b>{foo}</b><i><b>{bar}</b></i><b>{baz}</b>]</p>
-			const created = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{ instanceOf: Text, data: 'foo' },
-					{
-						instanceOf: AttributeElement,
-						name: 'i',
-						priority: 1,
-						children: [
-							{ instanceOf: Text, data: 'bar' }
-						]
-					},
-					{ instanceOf: Text, data: 'baz' }
-				]
-			} );
-
-			const b = new AttributeElement( 'b' );
-			b.priority = 2;
-			const newRange = writer.wrap( created.range, b );
-			test( writer, newRange, created.node, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 3,
-				children: [
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						children: [
-							{ instanceOf: Text, data: 'foo' }
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'i',
-						priority: 1,
-						children: [
-							{
-								instanceOf: AttributeElement,
-								name: 'b',
-								priority: 2,
-								children: [
-									{ instanceOf: Text, data: 'bar' }
-								]
-							}
-						]
-					},
-					{
-						instanceOf: AttributeElement,
-						name: 'b',
-						priority: 2,
-						children: [
-							{ instanceOf: Text, data: 'baz' }
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p>[foo<attribute:i:1>bar</attribute:i:1>baz]</container:p>',
+				'<attribute:b:2></attribute:b:2>',
+				'<container:p>' +
+				'[' +
+					'<attribute:b:2>foo</attribute:b:2>' +
+					'<attribute:i:1>' +
+						'<attribute:b:2>bar</attribute:b:2>' +
+					'</attribute:i:1>' +
+					'<attribute:b:2>baz</attribute:b:2>' +
+				']' +
+				'</container:p>'
+			);
 		} );
 
 		it( 'should wrap single element by merging attributes', () => {
-			// <p>[<b foo="bar" one="two"></b>]</p>
-			// wrap with <b baz="qux" one="two"></b>
-			// <p>[<b foo="bar" one="two" baz="qux"></b>]</p>
-			const b = new AttributeElement( 'b', {
-				foo: 'bar',
-				one: 'two'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				baz: 'qux',
-				one: 'two'
-			} );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getAttribute( 'foo' ) ).to.equal( 'bar' );
-			expect( b.getAttribute( 'baz' ) ).to.equal( 'qux' );
-			expect( b.getAttribute( 'one' ) ).to.equal( 'two' );
-
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 foo="bar" one="two"></attribute:b:1>]</container:p>',
+				'<attribute:b:1 baz="qux" one="two"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 foo="bar" one="two" baz="qux"></attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not merge attributes when they differ', () => {
-			// <p>[<b foo="bar" ></b>]</p>
-			// wrap with <b foo="baz"></b>
-			// <p>[<b foo="baz"><b foo="bar"></b></b>]</p>
-			const b = new AttributeElement( 'b', {
-				foo: 'bar'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				foo: 'baz'
-			} );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getAttribute( 'foo' ) ).to.equal( 'bar' );
-			expect( b.parent.isSimilar( wrapper ) ).to.be.true;
-			expect( b.parent.getAttribute( 'foo' ) ).to.equal( 'baz' );
-
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [
-						{ instanceOf: AttributeElement, name: 'b', children: [] }
-					] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 foo="bar"></attribute:b:1>]</container:p>',
+				'<attribute:b:1 foo="baz"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 foo="baz"><attribute:b:1 foo="bar"></attribute:b:1></attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should wrap single element by merging classes', () => {
-			// <p>[<b class="foo bar baz" ></b>]</p>
-			// wrap with <b class="foo bar qux jax"></b>
-			// <p>[<b class="foo bar baz qux jax"></b>]</p>
-			const b = new AttributeElement( 'b', {
-				class: 'foo bar baz'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				class: 'foo bar qux jax'
-			} );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.hasClass( 'foo', 'bar', 'baz', 'qux', 'jax' ) ).to.be.true;
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 class="foo bar baz"></attribute:b:1>]</container:p>',
+				'<attribute:b:1 class="foo bar qux jax"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 class="foo bar baz qux jax"></attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should wrap single element by merging styles', () => {
-			// <p>[<b style="color:red; position: absolute;"></b>]</p>
-			// wrap with <b style="color:red; top: 20px;"></b>
-			// <p>[<b class="color:red; position: absolute; top:20px;"></b>]</p>
-			const b = new AttributeElement( 'b', {
-				style: 'color: red; position: absolute;'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				style: 'color:red; top: 20px;'
-			} );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getStyle( 'color' ) ).to.equal( 'red' );
-			expect( b.getStyle( 'position' ) ).to.equal( 'absolute' );
-			expect( b.getStyle( 'top' ) ).to.equal( '20px' );
-
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 style="color:red; position: absolute;"></attribute:b:1>]</container:p>',
+				'<attribute:b:1 style="color:red; top: 20px;"></attribute:b:1>',
+				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:20px;"></attribute:b:1>]</container:p>'
+			);
 		} );
 
 		it( 'should not merge styles when they differ', () => {
-			// <p>[<b style="color:red;"></b>]</p>
-			// wrap with <b style="color:black;"></b>
-			// <p>[<b style="color:black;"><b style="color:red;"></b></b>]</p>
-			const b = new AttributeElement( 'b', {
-				style: 'color:red'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				style: 'color:black'
-			} );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getStyle( 'color' ) ).to.equal( 'red' );
-			expect( b.parent.isSimilar( wrapper ) ).to.be.true;
-			expect( b.parent.getStyle( 'color' ) ).to.equal( 'black' );
-
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [
-						{ instanceOf: AttributeElement, name: 'b', children: [] }
-					] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:1 style="color:red;"></attribute:b:1>]</container:p>',
+				'<attribute:b:1 style="color:black;"></attribute:b:1>',
+				'<container:p>' +
+				'[' +
+					'<attribute:b:1 style="color:black;">' +
+						'<attribute:b:1 style="color:red;"></attribute:b:1>' +
+					'</attribute:b:1>' +
+				']' +
+				'</container:p>'
+			);
 		} );
 
 		it( 'should not merge single elements when they have different priority', () => {
-			// <p>[<b style="color:red;"></b>]</p>
-			// wrap with <b style="color:red;"></b> with different priority
-			// <p>[<b style="color:red;"><b style="color:red;"></b></b>]</p>
-			const b = new AttributeElement( 'b', {
-				style: 'color:red'
-			} );
-			const p = new ContainerElement( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const wrapper = new AttributeElement( 'b', {
-				style: 'color:red'
-			} );
-			wrapper.priority = b.priority - 1;
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getStyle( 'color' ) ).to.equal( 'red' );
-			expect( b.parent.isSimilar( wrapper ) ).to.be.true;
-			expect( b.parent.getStyle( 'color' ) ).to.equal( 'red' );
-
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: AttributeElement, name: 'b', children: [
-						{ instanceOf: AttributeElement, name: 'b', children: [] }
-					] }
-				]
-			} );
+			test(
+				'<container:p>[<attribute:b:2 style="color:red;"></attribute:b:2>]</container:p>',
+				'<attribute:b:1 style="color:red;"></attribute:b:1>',
+				'<container:p>' +
+				'[' +
+					'<attribute:b:1 style="color:red;">' +
+						'<attribute:b:2 style="color:red;"></attribute:b:2>' +
+					'</attribute:b:1>' +
+				']</container:p>'
+			);
 		} );
 
 		it( 'should be merged with outside element when wrapping all children', () => {
-			// <p><b foo="bar">[{foobar}<i>{baz}</i>]</b></p>
-			// wrap with <b baz="qux"></b>
-			// <p>[<b foo="bar" baz="qux">{foobar}</b>]</p>
-			const text1 = new Text( 'foobar' );
-			const text2 = new Text( 'baz' );
-			const i = new AttributeElement( 'i', null, text2 );
-			const b = new AttributeElement( 'b', { foo: 'bar' }, [ text1, i ] );
-			const p = new ContainerElement( 'p', null, [ b ] );
-			const wrapper = new AttributeElement( 'b', { baz: 'qux' } );
-			const range = Range.createFromParentsAndOffsets( b, 0, b, 2 );
-
-			const newRange = writer.wrap( range, wrapper );
-			expect( b.getAttribute( 'foo' ) ).to.equal( 'bar' );
-			expect( b.getAttribute( 'baz' ) ).to.equal( 'qux' );
-			test( writer, newRange, p, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{
-						instanceof: AttributeElement,
-						name: 'b',
-						children: [
-							{ instanceOf: Text, data: 'foobar' },
-							{
-								instanceOf: AttributeElement,
-								name: 'i',
-								children: [
-									{ instanceOf: Text, data: 'baz' }
-								]
-							}
-						]
-					}
-				]
-			} );
+			test(
+				'<container:p><attribute:b:1 foo="bar">[foobar<attribute:i:1>baz</attribute:i:1>]</attribute:b:1></container:p>',
+				'<attribute:b:1 baz="qux"></attribute:b:1>',
+				'<container:p>' +
+				'[' +
+					'<attribute:b:1 foo="bar" baz="qux">' +
+						'foobar' +
+						'<attribute:i:1>baz</attribute:i:1>' +
+					'</attribute:b:1>' +
+				']' +
+				'</container:p>'
+			);
 		} );
 	} );
 } );

+ 59 - 258
packages/ckeditor5-engine/tests/treeview/writer/wrapposition.js

@@ -11,17 +11,26 @@ import Writer from '/ckeditor5/engine/treeview/writer.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
-import { DEFAULT_PRIORITY } from '/ckeditor5/engine/treeview/attributeelement.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'wrapPosition', () => {
-	const create = utils.create;
-	const test = utils.test;
 	let writer;
 
+	/**
+	 * Executes test using `parse` and `stringify` utils functions.
+	 *
+	 * @param {String} input
+	 * @param {String} unwrapAttribute
+	 * @param {String} expected
+	 */
+	function test( input, unwrapAttribute, expected ) {
+		const { view, selection } = parse( input );
+		const newPosition = writer.wrapPosition( selection.getFirstPosition(), parse( unwrapAttribute ) );
+		expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
+	}
+
 	beforeEach( () => {
 		writer = new Writer();
 	} );
@@ -37,278 +46,70 @@ describe( 'wrapPosition', () => {
 	} );
 
 	it( 'should wrap position at the beginning of text node', () => {
-		// <p>{|foobar}</p>
-		// wrap with <b>
-		// <p><b>|<b>{foobar}</p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: Text, data: 'foobar', position: 0 }
-			]
-		};
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: AttributeElement, name: 'b', position: 0 },
-				{ instanceOf: Text, data: 'foobar' }
-			]
-		} );
+		test(
+			'<container:p>{}foobar</container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p><attribute:b:1>[]</attribute:b:1>foobar</container:p>'
+		);
 	} );
 
 	it( 'should wrap position inside text node', () => {
-		// <p>{foo|bar}</p>
-		// wrap with <b>
-		// <p>{foo}<b>|</b>{bar}</p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: Text, data: 'foobar', position: 3 }
-			]
-		};
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: Text, data: 'foo' },
-				{ instanceOf: AttributeElement, name: 'b', position: 0 },
-				{ instanceOf: Text, data: 'bar' }
-			]
-		} );
+		test(
+			'<container:p>foo{}bar</container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p>foo<attribute:b:1>[]</attribute:b:1>bar</container:p>'
+		);
 	} );
 
 	it( 'should wrap position at the end of text node', () => {
-		// <p>{foobar|}</p>
-		// wrap with <b>
-		// <p>{foobar}<b>|</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: Text, data: 'foobar', position: 6 }
-			]
-		};
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{ instanceOf: Text, data: 'foobar' },
-				{ instanceOf: AttributeElement, name: 'b', position: 0 }
-			]
-		} );
+		test(
+			'<container:p>foobar{}</container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p>foobar<attribute:b:1>[]</attribute:b:1></container:p>'
+		);
 	} );
 
 	it( 'should merge with existing attributes #1', () => {
-		// <p><b>{foo}</b>|</p>
-		// wrap with <b>
-		// <p><b>{foo|}</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			position: 1,
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foobar' }
-					]
-				}
-			]
-		};
-
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					priority: DEFAULT_PRIORITY,
-					children: [
-						{ instanceOf: Text, data: 'foobar', position: 6 }
-					]
-				}
-			]
-		} );
+		test(
+			'<container:p><attribute:b:1>foo</attribute:b:1>[]</container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p><attribute:b:1>foo{}</attribute:b:1></container:p>'
+		);
 	} );
 
 	it( 'should merge with existing attributes #2', () => {
-		// <p>|<b>{foo}</b></p>
-		// wrap with <b>
-		// <p><b>{|foo}</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			position: 0,
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foobar' }
-					]
-				}
-			]
-		};
-
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					priority: DEFAULT_PRIORITY,
-					children: [
-						{ instanceOf: Text, data: 'foobar', position: 0 }
-					]
-				}
-			]
-		} );
+		test(
+			'<container:p>[]<attribute:b:1>foo</attribute:b:1></container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p><attribute:b:1>{}foo</attribute:b:1></container:p>'
+		);
 	} );
 
 	it( 'should wrap when inside nested attributes', () => {
-		// <p><b>{foo|bar}</b></p>
-		// wrap with <u>
-		// <p><b>{foo}</b><u><b>|</b></u><b>{bar}</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foobar', position: 3 }
-					]
-				}
-			]
-		};
-
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'u' ) );
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foo' }
-					]
-				},
-				{
-					instanceOf: AttributeElement,
-					name: 'u',
-					children: [
-						{ instanceOf: AttributeElement, name: 'b', children: [] }
-					]
-				},
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'bar' }
-					]
-				}
-			]
-		}  );
+		test(
+			'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
+			'<attribute:u:1></attribute:u:1>',
+			'<container:p>' +
+				'<attribute:b:1>foo</attribute:b:1>' +
+				'<attribute:u:1><attribute:b:1>[]</attribute:b:1></attribute:u:1>' +
+				'<attribute:b:1>bar</attribute:b:1>' +
+			'</container:p>'
+		);
 	} );
 
 	it( 'should merge when wrapping between same attribute', () => {
-		// <p><b>{foo}</b>|<b>{bar}</b></p>
-		// wrap with <b>
-		// <p><b>{foo|bar}</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			position: 1,
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foo' }
-					]
-				},
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'bar' }
-					]
-				}
-			]
-		};
-
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foobar', position: 3 }
-					]
-				}
-			]
-		}  );
+		test(
+			'<container:p><attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1></container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>'
+		);
 	} );
 
-	it( 'should return same position when inside same attribute', () => {
-		// <p><b>{foobar}|</b></p>
-		// wrap with <b>
-		// <p><b>{foobar|}</b></p>
-		const description = {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					position: 1,
-					children: [
-						{ instanceOf: Text, data: 'foobar' }
-					]
-				}
-			]
-		};
-
-		const created = create( writer, description );
-		const newPosition = writer.wrapPosition( created.position, new AttributeElement( 'b' ) );
-
-		test( writer, newPosition, created.node, {
-			instanceOf: ContainerElement,
-			name: 'p',
-			children: [
-				{
-					instanceOf: AttributeElement,
-					name: 'b',
-					children: [
-						{ instanceOf: Text, data: 'foobar', position: 6 }
-					]
-				}
-			]
-		} );
+	it( 'should move position to text node if in same attribute', () => {
+		test(
+			'<container:p><attribute:b:1>foobar[]</attribute:b:1></container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p><attribute:b:1>foobar{}</attribute:b:1></container:p>'
+		);
 	} );
 } );

+ 8 - 22
packages/ckeditor5-engine/tests/treeview/writer/writer.js

@@ -12,10 +12,9 @@ import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
-import utils from '/tests/engine/treeview/writer/_utils/utils.js';
+import { parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
-	const create = utils.create;
 	let writer;
 
 	beforeEach( () => {
@@ -47,33 +46,20 @@ describe( 'Writer', () => {
 
 	describe( 'move', () => {
 		it( 'should move nodes using remove and insert methods', () => {
-			// <p>[{foobar}]</p>
-			// Move to <div>|</div>
-			// <div>[{foobar}]</div>
-			const source = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'p',
-				rangeStart: 0,
-				rangeEnd: 1,
-				children: [
-					{ instanceOf: Text, data: 'foobar' }
-				]
-			} );
-			const target = create( writer, {
-				instanceOf: ContainerElement,
-				name: 'div',
-				position: 0
-			} );
+			const { selection: sourceSelection } = parse( '<container:p>[foobar]</container:p>' );
+			const { selection: targetSelection } = parse( '<container:div>[]</container:div>' );
 
 			const removeSpy = sinon.spy( writer, 'remove' );
 			const insertSpy = sinon.spy( writer, 'insert' );
+			const sourceRange = sourceSelection.getFirstRange();
+			const targetPosition = targetSelection.getFirstPosition();
 
-			const newRange = writer.move( source.range, target.position );
+			const newRange = writer.move( sourceRange, targetPosition );
 
 			sinon.assert.calledOnce( removeSpy );
-			sinon.assert.calledWithExactly( removeSpy, source.range );
+			sinon.assert.calledWithExactly( removeSpy, sourceRange );
 			sinon.assert.calledOnce( insertSpy );
-			sinon.assert.calledWithExactly( insertSpy, target.position, removeSpy.firstCall.returnValue );
+			sinon.assert.calledWithExactly( insertSpy, targetPosition, removeSpy.firstCall.returnValue );
 			expect( newRange ).to.equal( insertSpy.firstCall.returnValue );
 		} );
 	} );