Bladeren bron

Tests: added tests with unicode characters and also did some minor fixes in tests.

Szymon Cofalik 9 jaren geleden
bovenliggende
commit
89fffdcc38

+ 11 - 0
packages/ckeditor5-engine/tests/_utils-tests/model.js

@@ -46,6 +46,13 @@ describe( 'model test utils', () => {
 			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
 		} );
 
+		it( 'should support unicode', () => {
+			root.appendChildren( 'நிலைக்கு' );
+			document.selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+
+			expect( getData( document ) ).to.equal( 'நி<selection>லைக்</selection>கு' );
+		} );
+
 		it( 'should throw an error when passing invalid document', () => {
 			expect( () => {
 				getData( { invalid: 'document' } );
@@ -127,6 +134,10 @@ describe( 'model test utils', () => {
 			test( '<b><selection backward>foo bar</b></selection>' );
 		} );
 
+		it( 'should support unicode', () => {
+			test( 'நி<selection>லைக்</selection>கு' );
+		} );
+
 		it( 'should throw an error when passing invalid document', () => {
 			expect( () => {
 				setData( { invalid: 'document' } );

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

@@ -154,6 +154,17 @@ describe( 'view test utils', () => {
 			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
 		} );
 
+		it( 'should support unicode', () => {
+			const text = new Text( 'நிலைக்கு' );
+			const b = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b );
+			const range = Range.createFromParentsAndOffsets( p, 0, text, 4 );
+			const selection = new Selection();
+			selection.addRange( range );
+
+			expect( stringify( p, selection ) ).to.equal( '<p>[<b>நிலை}க்கு</b></p>' );
+		} );
+
 		it( 'should write collapsed selection ranges inside elements', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
@@ -444,6 +455,29 @@ describe( 'view test utils', () => {
 			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true;
 		} );
 
+		it( 'should support unicode', () => {
+			const { view, selection } = parse( '<p>[<b>நிலை}க்கு</b></p>' );
+
+			expect( view ).to.be.instanceof( Element );
+			expect( view.name ).to.equal( 'p' );
+			expect( view.childCount ).to.equal( 1 );
+
+			const b = view.getChild( 0 );
+			expect( b.name ).to.equal( 'b' );
+			expect( b.childCount ).to.equal( 1 );
+
+			const text = b.getChild( 0 );
+			expect( text.data ).to.equal( 'நிலைக்கு' );
+
+			expect( selection.rangeCount ).to.equal( 1 );
+			const range = selection.getFirstRange();
+
+			expect( range.start.parent ).to.equal( view );
+			expect( range.start.offset ).to.equal( 0 );
+			expect( range.end.parent ).to.equal( text );
+			expect( range.end.offset ).to.equal( 4 );
+		} );
+
 		it( 'should parse ranges #1', () => {
 			const { view, selection } = parse( '<container:p>foo{bar]</container:p>' );
 			expect( view.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;

+ 8 - 0
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -76,6 +76,14 @@ describe( 'default converters', () => {
 			);
 		} );
 
+		it( 'in same container with unicode characters', () => {
+			test(
+				[ 2, 6 ],
+				'நிலைக்கு',
+				'நி{லைக்}கு'
+			);
+		} );
+
 		it( 'in same container, over attribute', () => {
 			test(
 				[ 1, 5 ],

+ 78 - 0
packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js

@@ -85,6 +85,15 @@ describe( 'insertText', () => {
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
 	} );
+
+	it( 'should support unicode', () => {
+		modelRoot.appendChildren( new ModelText( 'நிலைக்கு' ) );
+		dispatcher.on( 'insert:$text', insertText() );
+
+		dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
+	} );
 } );
 
 describe( 'insertElement', () => {
@@ -271,6 +280,28 @@ describe( 'wrap/unwrap', () => {
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
 	} );
+
+	it( 'should support unicode', () => {
+		const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
+		const viewP = new ViewContainerElement( 'p' );
+		const viewB = new ViewAttributeElement( 'b' );
+
+		modelRoot.appendChildren( modelElement );
+		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
+		dispatcher.on( 'insert:$text', insertText() );
+		dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
+		dispatcher.on( 'removeAttribute:bold', unwrap( viewB ) );
+
+		dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
+
+		modelWriter.removeAttribute( ModelRange.createFromElement( modelElement ), 'bold' );
+
+		dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelElement ), 'bold', true, null );
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
+	} );
 } );
 
 describe( 'move', () => {
@@ -301,6 +332,30 @@ describe( 'move', () => {
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div><div>bar</div><div>foo<img></img>xxyy</div></div>' );
 	} );
+
+	it( 'should support unicode', () => {
+		const modelDivA = new ModelElement( 'div', null, 'நிலைக்கு' );
+		const modelDivB = new ModelElement( 'div' );
+
+		modelRoot.appendChildren( [ modelDivA, modelDivB ] );
+		dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
+		dispatcher.on( 'insert:$text', insertText() );
+		dispatcher.on( 'move', move() );
+
+		dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+		modelWriter.move(
+			ModelRange.createFromParentsAndOffsets( modelDivA, 2, modelDivA, 6 ),
+			ModelPosition.createAt( modelDivB, 'end' )
+		);
+
+		dispatcher.convertMove(
+			ModelPosition.createFromParentAndOffset( modelDivA, 2 ),
+			ModelRange.createFromParentsAndOffsets( modelDivB, 0, modelDivB, 4 )
+		);
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div><div>நிகு</div><div>லைக்</div></div>' );
+	} );
 } );
 
 describe( 'remove', () => {
@@ -329,4 +384,27 @@ describe( 'remove', () => {
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div><div>bar</div></div>' );
 	} );
+
+	it( 'should support unicode', () => {
+		const modelDiv = new ModelElement( 'div', null, 'நிலைக்கு' );
+
+		modelRoot.appendChildren( modelDiv );
+		dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
+		dispatcher.on( 'insert:$text', insertText() );
+		dispatcher.on( 'remove', remove() );
+
+		dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+		modelWriter.move(
+			ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelDiv, 6 ),
+			ModelPosition.createAt( modelDoc.graveyard, 'end' )
+		);
+
+		dispatcher.convertRemove(
+			ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
+			ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 6 )
+		);
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div><div>கு</div></div>' );
+	} );
 } );

+ 30 - 13
packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js

@@ -47,7 +47,24 @@ describe( 'convertSelectionChange', () => {
 
 		convertSelection( null, { newSelection: viewSelection } );
 
-		expect( modelGetData( model ) ).to.equals( '<paragraph>f<selection />oo</paragraph><paragraph>bar</paragraph>' );
+		expect( modelGetData( model ) ).to.equal( '<paragraph>f<selection />oo</paragraph><paragraph>bar</paragraph>' );
+	} );
+
+	it( 'should support unicode', () => {
+		modelSetData( model, '<paragraph>நிலைக்கு</paragraph>' );
+		viewSetData( view, '<p>நிலைக்கு</p>' );
+
+		// Re-bind elements that were just re-set.
+		mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) );
+
+		const viewSelection = new ViewSelection();
+		viewSelection.addRange(
+			ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 )
+		);
+
+		convertSelection( null, { newSelection: viewSelection } );
+
+		expect( modelGetData( model ) ).to.equal( '<paragraph>நி<selection>லைக்</selection>கு</paragraph>' );
 	} );
 
 	it( 'should convert multi ranges selection', () => {
@@ -60,24 +77,24 @@ describe( 'convertSelectionChange', () => {
 		convertSelection( null, { newSelection: viewSelection } );
 
 		// Too bad getData shows only the first range.
-		expect( modelGetData( model ) ).to.equals(
+		expect( modelGetData( model ) ).to.equal(
 			'<paragraph>f<selection>o</selection>o</paragraph><paragraph>bar</paragraph>' );
 
 		const ranges = Array.from( model.selection.getRanges() );
-		expect( ranges.length ).to.equals( 2 );
+		expect( ranges.length ).to.equal( 2 );
 
-		expect( ranges[ 0 ].start.parent ).to.equals( modelRoot.getChild( 0 ) );
-		expect( ranges[ 0 ].start.offset ).to.equals( 1 );
-		expect( ranges[ 0 ].end.parent ).to.equals( modelRoot.getChild( 0 ) );
-		expect( ranges[ 0 ].end.offset ).to.equals( 2 );
+		expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) );
+		expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+		expect( ranges[ 0 ].end.parent ).to.equal( modelRoot.getChild( 0 ) );
+		expect( ranges[ 0 ].end.offset ).to.equal( 2 );
 
-		expect( ranges[ 1 ].start.parent ).to.equals( modelRoot.getChild( 1 ) );
-		expect( ranges[ 1 ].start.offset ).to.equals( 1 );
-		expect( ranges[ 1 ].end.parent ).to.equals( modelRoot.getChild( 1 ) );
-		expect( ranges[ 1 ].end.offset ).to.equals( 2 );
+		expect( ranges[ 1 ].start.parent ).to.equal( modelRoot.getChild( 1 ) );
+		expect( ranges[ 1 ].start.offset ).to.equal( 1 );
+		expect( ranges[ 1 ].end.parent ).to.equal( modelRoot.getChild( 1 ) );
+		expect( ranges[ 1 ].end.offset ).to.equal( 2 );
 	} );
 
-	it( 'should convert revers selection', () => {
+	it( 'should convert reverse selection', () => {
 		const viewSelection = new ViewSelection();
 		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), true );
@@ -85,7 +102,7 @@ describe( 'convertSelectionChange', () => {
 		convertSelection( null, { newSelection: viewSelection } );
 
 		// Too bad getData shows only the first range.
-		expect( modelGetData( model ) ).to.equals(
+		expect( modelGetData( model ) ).to.equal(
 			'<paragraph>f<selection backward>o</selection>o</paragraph><paragraph>bar</paragraph>' );
 	} );
 } );

+ 11 - 0
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -71,6 +71,17 @@ describe( 'convertText', () => {
 		expect( result ).to.be.instanceof( ModelText );
 		expect( result.data ).to.equal( 'foobar' );
 	} );
+
+	it( 'should support unicode', () => {
+		const viewText = new ViewText( 'நிலைக்கு' );
+
+		dispatcher.on( 'text', convertText() );
+
+		const result = dispatcher.convert( viewText, objWithContext );
+
+		expect( result ).to.be.instanceof( ModelText );
+		expect( result.data ).to.equal( 'நிலைக்கு' );
+	} );
 } );
 
 describe( 'convertToModelFragment', () => {

+ 27 - 0
packages/ckeditor5-engine/tests/model/text.js

@@ -67,4 +67,31 @@ describe( 'Text', () => {
 			expect( Array.from( deserialized.getAttributes() ) ).to.deep.equal( [ [ 'bold', true ] ] );
 		} );
 	} );
+
+	// All characters, code points, combined symbols, etc. can be looked up in browsers console to better understand what is going on.
+	describe( 'unicode support', () => {
+		it( 'should normalize strings kept in data', () => {
+			// This is a letter "n" with so-called combining mark, similar to ~, which code point is \u0303.
+			// Those two characters together combines to "ñ", but that character already has it's code point: \u00F1.
+			let dataCombined = '\u006E\u0303';
+			let textN = new Text( dataCombined );
+
+			expect( textN.data ).to.equal( '\u00F1' ); // "ñ" got normalized to \u00F1.
+			expect( textN.data.length ).to.equal( 1 ); // It is now just one character.
+			expect( textN.offsetSize ).to.equal( 1 ); // And has correct offset size.
+		} );
+
+		it( 'should be properly serialized and de-serialized', () => {
+			let textQ = new Text( 'நி' );
+			let json = jsonParseStringify( textQ );
+
+			expect( json ).to.deep.equal( {
+				data: 'நி'
+			} );
+
+			let deserialized = Text.fromJSON( json );
+
+			expect( deserialized.data ).to.equal( 'நி' );
+		} );
+	} );
 } );

+ 21 - 0
packages/ckeditor5-engine/tests/model/textproxy.js

@@ -9,6 +9,7 @@ import Element from '/ckeditor5/engine/model/element.js';
 import Text from '/ckeditor5/engine/model/text.js';
 import TextProxy from '/ckeditor5/engine/model/textproxy.js';
 import Document from '/ckeditor5/engine/model/document.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 describe( 'TextProxy', () => {
 	let doc, element, textProxy, root, textProxyNoParent, text, textNoParent;
@@ -81,6 +82,26 @@ describe( 'TextProxy', () => {
 		expect( fullTextProxy.isPartial ).to.be.false;
 	} );
 
+	it( 'should throw if wrong offsetInText is passed', () => {
+		expect( () => {
+			new TextProxy( text, -1, 2 );
+		} ).to.throw( CKEditorError, /model-textproxy-wrong-offsetintext/ );
+
+		expect( () => {
+			new TextProxy( text, 9, 1 );
+		} ).to.throw( CKEditorError, /model-textproxy-wrong-offsetintext/ );
+	} );
+
+	it( 'should throw if wrong length is passed', () => {
+		expect( () => {
+			new TextProxy( text, 2, -1 );
+		} ).to.throw( CKEditorError, /model-textproxy-wrong-length/ );
+
+		expect( () => {
+			new TextProxy( text, 2, 9 );
+		} ).to.throw( CKEditorError, /model-textproxy-wrong-length/ );
+	} );
+
 	describe( 'getPath', () => {
 		it( 'should return path to the text proxy', () => {
 			expect( textProxy.getPath() ).to.deep.equal( [ 0, 5 ] );

+ 28 - 0
packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

@@ -69,6 +69,21 @@ describe( 'DomConverter', () => {
 			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
 		} );
 
+		it( 'should support unicode', () => {
+			const domText = document.createTextNode( 'நிலைக்கு' );
+			const domP = createElement( document, 'p', { 'class': 'foo' }, [ domText ] );
+
+			const viewP = converter.domToView( domP, { bind: true } );
+
+			expect( viewP.childCount ).to.equal( 1 );
+
+			const viewText = viewP.getChild( 0 );
+			expect( viewText.data ).to.equal( 'நிலைக்கு' );
+
+			expect( converter.getCorrespondingDom( viewP ) ).to.equal( domP );
+			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
+		} );
+
 		it( 'should create tree of view elements from DOM element without children', () => {
 			const domImg = createElement( document, 'img' );
 			const domText = document.createTextNode( 'foo' );
@@ -199,6 +214,19 @@ describe( 'DomConverter', () => {
 			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>fo{}o<b>bar</b></p>' );
 		} );
 
+		it( 'should support unicode', () => {
+			const domText = document.createTextNode( 'நிலைக்கு' );
+			const domP = createElement( document, 'p', null, [ domText ] );
+
+			const viewP = parse( '<p>நிலைக்கு</p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = converter.domPositionToView( domText, 4 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>நிலை{}க்கு</p>' );
+		} );
+
 		it( 'should converter position in element', () => {
 			const domText = document.createTextNode( 'foo' );
 			const domB = createElement( document, 'b', null, 'bar' );

+ 27 - 0
packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js

@@ -79,6 +79,19 @@ describe( 'DomConverter', () => {
 			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
 		} );
 
+		it( 'should support unicode', () => {
+			const viewText = new ViewText( 'நிலைக்கு' );
+			const viewP = new ViewElement( 'p', null, viewText );
+
+			const domP = converter.viewToDom( viewP, document, { bind: true } );
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'நிலைக்கு' );
+
+			expect( converter.getCorrespondingView( domP ) ).to.equal( viewP );
+			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
+		} );
+
 		it( 'should create tree of DOM elements from view element without children', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
@@ -215,6 +228,20 @@ describe( 'DomConverter', () => {
 			expect( domPosition.parent ).to.equal( domFoo );
 		} );
 
+		it( 'should support unicode', () => {
+			const domText = document.createTextNode( 'நிலைக்கு' );
+			const domP = createElement( document, 'p', null, domText );
+			const { view: viewP, selection } = parse( '<container:p>நிலை{}க்கு</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 4 );
+			expect( domPosition.parent ).to.equal( domText );
+		} );
+
 		it( 'should convert the position in the empty element', () => {
 			const domP = createElement( document, 'p' );
 			const { view: viewP, selection } = parse( '<container:p>[]</container:p>' );

+ 21 - 10
packages/ckeditor5-engine/tests/view/text.js

@@ -5,15 +5,15 @@
 
 /* bender-tags: view */
 
-import ViewNode from '/ckeditor5/engine/view/node.js';
-import ViewText from '/ckeditor5/engine/view/text.js';
+import Node from '/ckeditor5/engine/view/node.js';
+import Text from '/ckeditor5/engine/view/text.js';
 
 describe( 'Element', () => {
 	describe( 'constructor', () => {
 		it( 'should create element without attributes', () => {
-			const text = new ViewText( 'foo' );
+			const text = new Text( 'foo' );
 
-			expect( text ).to.be.an.instanceof( ViewNode );
+			expect( text ).to.be.an.instanceof( Node );
 			expect( text.data ).to.equal( 'foo' );
 			expect( text ).to.have.property( 'parent' ).that.is.null;
 		} );
@@ -21,7 +21,7 @@ describe( 'Element', () => {
 
 	describe( 'clone', () => {
 		it( 'should return new text with same data', () => {
-			const text = new ViewText( 'foo bar' );
+			const text = new Text( 'foo bar' );
 			const clone = text.clone();
 
 			expect( clone ).to.not.equal( text );
@@ -30,7 +30,7 @@ describe( 'Element', () => {
 	} );
 
 	describe( 'isSimilar', () => {
-		const text = new ViewText( 'foo' );
+		const text = new Text( 'foo' );
 
 		it( 'should return false when comparing to non-text', () => {
 			expect( text.isSimilar( null ) ).to.be.false;
@@ -41,13 +41,13 @@ describe( 'Element', () => {
 			expect( text.isSimilar( text ) ).to.be.true;
 		} );
 
-		it( 'sould return true when data is the same', () => {
-			const other = new ViewText( 'foo' );
+		it( 'should return true when data is the same', () => {
+			const other = new Text( 'foo' );
 
 			expect( text.isSimilar( other ) ).to.be.true;
 		} );
 
-		it( 'sould return false when data is not the same', () => {
+		it( 'should return false when data is not the same', () => {
 			const other = text.clone();
 			other.data = 'not-foo';
 
@@ -57,10 +57,21 @@ describe( 'Element', () => {
 
 	describe( 'setText', () => {
 		it( 'should change the text', () => {
-			const text = new ViewText( 'foo' );
+			const text = new Text( 'foo' );
 			text.data = 'bar';
 
 			expect( text.data ).to.equal( 'bar' );
 		} );
 	} );
+
+	// This is same set of tests as in engine.model.Text tests. Look there for comments on tests.
+	describe( 'unicode support', () => {
+		it( 'should normalize strings kept in data', () => {
+			let dataCombined = '\u006E\u0303';
+			let textN = new Text( dataCombined );
+
+			expect( textN.data ).to.equal( '\u00F1' );
+			expect( textN.data.length ).to.equal( 1 );
+		} );
+	} );
 } );

+ 21 - 0
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -10,6 +10,7 @@ import Text from '/ckeditor5/engine/view/text.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
 
@@ -40,6 +41,26 @@ describe( 'TextProxy', () => {
 			expect( startTextProxy.isPartial ).to.be.true;
 			expect( fullTextProxy.isPartial ).to.be.false;
 		} );
+
+		it( 'should throw if wrong offsetInText is passed', () => {
+			expect( () => {
+				new TextProxy( text, -1, 2 );
+			} ).to.throw( CKEditorError, /view-textproxy-wrong-offsetintext/ );
+
+			expect( () => {
+				new TextProxy( text, 9, 1 );
+			} ).to.throw( CKEditorError, /view-textproxy-wrong-offsetintext/ );
+		} );
+
+		it( 'should throw if wrong length is passed', () => {
+			expect( () => {
+				new TextProxy( text, 2, -1 );
+			} ).to.throw( CKEditorError, /view-textproxy-wrong-length/ );
+
+			expect( () => {
+				new TextProxy( text, 2, 9 );
+			} ).to.throw( CKEditorError, /view-textproxy-wrong-length/ );
+		} );
 	} );
 
 	describe( 'getDocument', () => {

+ 8 - 0
packages/ckeditor5-engine/tests/view/writer/insert.js

@@ -138,6 +138,14 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'should insert unicode text into unicode text', () => {
+			test(
+				'நி{}க்கு',
+				[ 'லை' ],
+				'நி{லை}க்கு'
+			);
+		} );
+
 		it( 'should throw when inserting Element', () => {
 			const element = new Element( 'b' );
 			const container = new ContainerElement( 'p' );

+ 9 - 0
packages/ckeditor5-engine/tests/view/writer/move.js

@@ -55,6 +55,15 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'should support unicode', () => {
+			test(
+				'<container:p>நி{லை}க்கு</container:p>',
+				'<container:p>நி{}கு</container:p>',
+				'<container:p>நிக்கு</container:p>',
+				'<container:p>நி{லை}கு</container:p>'
+			);
+		} );
+
 		it( 'should move parts of nodes', () => {
 			test(
 				'<container:p>f{oo<attribute:b:10>ba}r</attribute:b:10></container:p>',

+ 8 - 0
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -79,6 +79,14 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'should support unicode', () => {
+			test(
+				'<container:p>நி{லை<attribute:b:10>க்}கு</attribute:b:10></container:p>',
+				'<container:p>நி[]<attribute:b:10>கு</attribute:b:10></container:p>',
+				'லை<attribute:b:10>க்</attribute:b:10>'
+			);
+		} );
+
 		it( 'should merge after removing #1', () => {
 			test(
 				'<container:p><attribute:b:1>foo</attribute:b:1>[bar]<attribute:b:1>bazqux</attribute:b:1></container:p>',

+ 8 - 0
packages/ckeditor5-engine/tests/view/writer/unwrap.js

@@ -121,6 +121,14 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'should support unicode', () => {
+			test(
+				'<container:p>[நிலை<attribute:b:1>க்}கு</attribute:b:1>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[நிலைக்]<attribute:b:1>கு</attribute:b:1></container:p>'
+			);
+		} );
+
 		it( 'should unwrap nested attributes', () => {
 			test(
 				'<container:p>[<attribute:u:1><attribute:b:1>foobar</attribute:b:1></attribute:u:1>]</container:p>',

+ 8 - 0
packages/ckeditor5-engine/tests/view/writer/wrap.js

@@ -107,6 +107,14 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'should support unicode', () => {
+			test(
+				'<container:p>நி{லை}க்கு</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>நி[<attribute:b:1>லை</attribute:b:1>]க்கு</container:p>'
+			);
+		} );
+
 		it( 'wraps part of a single text node #3', () => {
 			test(
 				'<container:p>foo{bar}</container:p>',

+ 8 - 0
packages/ckeditor5-engine/tests/view/writer/wrapposition.js

@@ -54,6 +54,14 @@ describe( 'wrapPosition', () => {
 		);
 	} );
 
+	it( 'should support unicode', () => {
+		test(
+			'<container:p>நிலை{}க்கு</container:p>',
+			'<attribute:b:1></attribute:b:1>',
+			'<container:p>நிலை<attribute:b:1>[]</attribute:b:1>க்கு</container:p>'
+		);
+	} );
+
 	it( 'should wrap position inside document fragment', () => {
 		test(
 			'<attribute:b:1>foo</attribute:b:1>[]<attribute:b:3>bar</attribute:b:3>',