Sfoglia il codice sorgente

Merge branch 'master' into t/1726

Piotr Jasiun 6 anni fa
parent
commit
e6479274bb
100 ha cambiato i file con 1274 aggiunte e 817 eliminazioni
  1. 1 3
      packages/ckeditor5-engine/.travis.yml
  2. 33 0
      packages/ckeditor5-engine/CHANGELOG.md
  3. 1 2
      packages/ckeditor5-engine/README.md
  4. 17 17
      packages/ckeditor5-engine/package.json
  5. 5 4
      packages/ckeditor5-engine/src/controller/datacontroller.js
  6. 7 4
      packages/ckeditor5-engine/src/conversion/conversion.js
  7. 3 4
      packages/ckeditor5-engine/src/conversion/downcasthelpers.js
  8. 12 3
      packages/ckeditor5-engine/src/conversion/mapper.js
  9. 1 1
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  10. 1 1
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  11. 26 0
      packages/ckeditor5-engine/src/model/differ.js
  12. 2 0
      packages/ckeditor5-engine/src/model/document.js
  13. 2 0
      packages/ckeditor5-engine/src/model/documentselection.js
  14. 2 1
      packages/ckeditor5-engine/src/model/liveposition.js
  15. 6 6
      packages/ckeditor5-engine/src/model/markercollection.js
  16. 3 2
      packages/ckeditor5-engine/src/model/model.js
  17. 2 2
      packages/ckeditor5-engine/src/model/node.js
  18. 12 6
      packages/ckeditor5-engine/src/model/nodelist.js
  19. 3 1
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  20. 1 1
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  21. 2 1
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  22. 3 3
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  23. 4 4
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  24. 4 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  25. 4 0
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  26. 4 4
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  27. 10 4
      packages/ckeditor5-engine/src/model/operation/utils.js
  28. 27 6
      packages/ckeditor5-engine/src/model/position.js
  29. 4 1
      packages/ckeditor5-engine/src/model/range.js
  30. 19 8
      packages/ckeditor5-engine/src/model/schema.js
  31. 12 4
      packages/ckeditor5-engine/src/model/selection.js
  32. 2 2
      packages/ckeditor5-engine/src/model/textproxy.js
  33. 5 1
      packages/ckeditor5-engine/src/model/treewalker.js
  34. 8 1
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  35. 69 26
      packages/ckeditor5-engine/src/model/writer.js
  36. 2 1
      packages/ckeditor5-engine/src/view/attributeelement.js
  37. 28 19
      packages/ckeditor5-engine/src/view/domconverter.js
  38. 53 48
      packages/ckeditor5-engine/src/view/downcastwriter.js
  39. 1 1
      packages/ckeditor5-engine/src/view/editableelement.js
  40. 4 1
      packages/ckeditor5-engine/src/view/emptyelement.js
  41. 1 1
      packages/ckeditor5-engine/src/view/node.js
  42. 44 0
      packages/ckeditor5-engine/src/view/observer/inputobserver.js
  43. 1 1
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  44. 1 3
      packages/ckeditor5-engine/src/view/placeholder.js
  45. 8 6
      packages/ckeditor5-engine/src/view/position.js
  46. 3 3
      packages/ckeditor5-engine/src/view/range.js
  47. 2 2
      packages/ckeditor5-engine/src/view/renderer.js
  48. 8 4
      packages/ckeditor5-engine/src/view/selection.js
  49. 2 2
      packages/ckeditor5-engine/src/view/textproxy.js
  50. 5 1
      packages/ckeditor5-engine/src/view/treewalker.js
  51. 1 1
      packages/ckeditor5-engine/src/view/uielement.js
  52. 8 1
      packages/ckeditor5-engine/src/view/view.js
  53. 31 31
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  54. 8 8
      packages/ckeditor5-engine/tests/conversion/conversion.js
  55. 11 6
      packages/ckeditor5-engine/tests/conversion/mapper.js
  56. 3 3
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  57. 3 0
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  58. 70 0
      packages/ckeditor5-engine/tests/model/differ.js
  59. 9 13
      packages/ckeditor5-engine/tests/model/document.js
  60. 5 5
      packages/ckeditor5-engine/tests/model/documentfragment.js
  61. 25 25
      packages/ckeditor5-engine/tests/model/documentselection.js
  62. 5 5
      packages/ckeditor5-engine/tests/model/element.js
  63. 6 4
      packages/ckeditor5-engine/tests/model/liveposition.js
  64. 13 13
      packages/ckeditor5-engine/tests/model/markercollection.js
  65. 9 1
      packages/ckeditor5-engine/tests/model/model.js
  66. 7 11
      packages/ckeditor5-engine/tests/model/node.js
  67. 12 11
      packages/ckeditor5-engine/tests/model/nodelist.js
  68. 8 7
      packages/ckeditor5-engine/tests/model/operation/attributeoperation.js
  69. 4 3
      packages/ckeditor5-engine/tests/model/operation/detachoperation.js
  70. 2 2
      packages/ckeditor5-engine/tests/model/operation/insertoperation.js
  71. 5 5
      packages/ckeditor5-engine/tests/model/operation/mergeoperation.js
  72. 6 5
      packages/ckeditor5-engine/tests/model/operation/moveoperation.js
  73. 6 5
      packages/ckeditor5-engine/tests/model/operation/renameoperation.js
  74. 11 11
      packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js
  75. 6 5
      packages/ckeditor5-engine/tests/model/operation/splitoperation.js
  76. 5 6
      packages/ckeditor5-engine/tests/model/operation/utils.js
  77. 21 20
      packages/ckeditor5-engine/tests/model/position.js
  78. 4 3
      packages/ckeditor5-engine/tests/model/range.js
  79. 39 40
      packages/ckeditor5-engine/tests/model/schema.js
  80. 17 17
      packages/ckeditor5-engine/tests/model/selection.js
  81. 10 9
      packages/ckeditor5-engine/tests/model/textproxy.js
  82. 10 9
      packages/ckeditor5-engine/tests/model/treewalker.js
  83. 37 0
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  84. 168 113
      packages/ckeditor5-engine/tests/model/writer.js
  85. 4 3
      packages/ckeditor5-engine/tests/view/attributeelement.js
  86. 19 19
      packages/ckeditor5-engine/tests/view/documentselection.js
  87. 14 19
      packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
  88. 35 34
      packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js
  89. 5 14
      packages/ckeditor5-engine/tests/view/domconverter/whitespace-handling-integration.js
  90. 18 16
      packages/ckeditor5-engine/tests/view/downcastwriter/breakattributes.js
  91. 6 5
      packages/ckeditor5-engine/tests/view/downcastwriter/breakcontainer.js
  92. 10 8
      packages/ckeditor5-engine/tests/view/downcastwriter/clear.js
  93. 17 14
      packages/ckeditor5-engine/tests/view/downcastwriter/insert.js
  94. 10 9
      packages/ckeditor5-engine/tests/view/downcastwriter/mergecontainers.js
  95. 6 5
      packages/ckeditor5-engine/tests/view/downcastwriter/move.js
  96. 16 14
      packages/ckeditor5-engine/tests/view/downcastwriter/remove.js
  97. 16 14
      packages/ckeditor5-engine/tests/view/downcastwriter/unwrap.js
  98. 18 16
      packages/ckeditor5-engine/tests/view/downcastwriter/wrap.js
  99. 4 3
      packages/ckeditor5-engine/tests/view/editableelement.js
  100. 11 8
      packages/ckeditor5-engine/tests/view/emptyelement.js

+ 1 - 3
packages/ckeditor5-engine/.travis.yml

@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
-  - secure: WGdwDEavTOIE/yw8pdpWmx3Cehm2/GN8gZMimuot8sWPbOZRuPh+2lnVWpASLe25qJUKKeLWYLRHccpMLKbyweiordSEBwzNrOGOFyUuaDPITNZZrp5ekh2vHBp1+mwhRpRekdyWdzzVsm01RkFrnJqQlaXxX9i/r9Tu4EsK54M=
-  - secure: BKoFnOWcfKn8zaTvilMn9UORF+gR1zBqetqai5FKfSKraJXqiBrz1AI3ecytCjJOKS1g6HoRtZHwVS4a+uG3JjK3bRERQF2NyHiyjbrsL+2HaKaRnVmox8PC/59m1PJlcRKAV9RTYJqL/oqa7N8OBegt4LpIVnEcil8WCErCF6Q=
+  - secure: GMesvAe0tAa8QfdhGApzmzjhf9mpfjL6jMx8aFqpRli5tuyX9JsjNjFELnU/MMlNDyySPwXAuQVrvcWkCFy9a3n705tGt/VdmStCrTdwV2oRNmNKqGgFovmiK3an+RAIts2PX2iguDjwiogqkAW0FstWAMdG0Yk4AVUxKC1niIo=

+ 33 - 0
packages/ckeditor5-engine/CHANGELOG.md

@@ -1,6 +1,39 @@
 Changelog
 =========
 
+## [13.2.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.2.0...v13.2.1) (2019-07-10)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [13.2.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.1...v13.2.0) (2019-07-04)
+
+### Features
+
+* Added `view.Document#event:beforeinput`. ([c74c3d6](https://github.com/ckeditor/ckeditor5-engine/commit/c74c3d6))
+* Introduced the `type` parameter to the `model.createBatch()` method. ([389b72e](https://github.com/ckeditor/ckeditor5-engine/commit/389b72e))
+
+### Bug fixes
+
+* `model.Writer#insert()` will no longer crash when the data to set contains markers that are already in the editor content. Closes [#1721](https://github.com/ckeditor/ckeditor5-engine/issues/1721). ([4ff0656](https://github.com/ckeditor/ckeditor5-engine/commit/4ff0656))
+* Selection will not change during forbidden copy-paste operation inside table cell. Closes [ckeditor/ckeditor5#1380](https://github.com/ckeditor/ckeditor5/issues/1380). ([ab15b17](https://github.com/ckeditor/ckeditor5-engine/commit/ab15b17))
+
+### Other changes
+
+* Changed how ` `s are generated on the view->DOM rendering. Closes [#1747](https://github.com/ckeditor/ckeditor5-engine/issues/1747). ([da5670a](https://github.com/ckeditor/ckeditor5-engine/commit/da5670a))
+
+
+## [13.1.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.0...v13.1.1) (2019-06-05)
+
+### Bug fixes
+
+* Prevented from losing selection attributes between operations (fixes a bug with text composition). Closes https://github.com/ckeditor/ckeditor5-typing/issues/188. ([42dcb25](https://github.com/ckeditor/ckeditor5-engine/commit/42dcb25))
+
+### Other changes
+
+* Added more cases of affected markers on merging in `model.Writer`. Closes [#1738](https://github.com/ckeditor/ckeditor5-engine/issues/1738). ([01ff6e6](https://github.com/ckeditor/ckeditor5-engine/commit/01ff6e6))
+
+
 ## [13.1.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.0.0...v13.1.0) (2019-04-10)
 
 ### Features

+ 1 - 2
packages/ckeditor5-engine/README.md

@@ -4,7 +4,6 @@ CKEditor 5 editing engine
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-engine.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-engine)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-engine/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-engine?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-engine/status.svg)](https://david-dm.org/ckeditor/ckeditor5-engine)
@@ -32,4 +31,4 @@ Additionally, refer to the [`@ckeditor/ckeditor5-engine` package](https://ckedit
 
 ## License
 
-Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 17 - 17
packages/ckeditor5-engine/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "13.1.0",
+  "version": "13.2.1",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
     "wysiwyg",
@@ -21,26 +21,26 @@
     "ckeditor 5"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^12.1.0",
+    "@ckeditor/ckeditor5-utils": "^13.0.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.0",
-    "@ckeditor/ckeditor5-block-quote": "^11.0.1",
-    "@ckeditor/ckeditor5-core": "^12.1.0",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.0",
-    "@ckeditor/ckeditor5-enter": "^11.0.1",
-    "@ckeditor/ckeditor5-essentials": "^11.0.1",
-    "@ckeditor/ckeditor5-heading": "^11.0.1",
-    "@ckeditor/ckeditor5-link": "^11.0.1",
-    "@ckeditor/ckeditor5-list": "^12.0.1",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.1",
-    "@ckeditor/ckeditor5-theme-lark": "^13.0.1",
-    "@ckeditor/ckeditor5-typing": "^12.0.1",
-    "@ckeditor/ckeditor5-undo": "^11.0.1",
-    "@ckeditor/ckeditor5-widget": "^11.0.1",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
+    "@ckeditor/ckeditor5-core": "^12.2.1",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
+    "@ckeditor/ckeditor5-enter": "^11.0.4",
+    "@ckeditor/ckeditor5-essentials": "^11.0.4",
+    "@ckeditor/ckeditor5-heading": "^11.0.4",
+    "@ckeditor/ckeditor5-link": "^11.1.1",
+    "@ckeditor/ckeditor5-list": "^12.0.4",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
+    "@ckeditor/ckeditor5-theme-lark": "^14.1.1",
+    "@ckeditor/ckeditor5-typing": "^12.1.1",
+    "@ckeditor/ckeditor5-undo": "^11.0.4",
+    "@ckeditor/ckeditor5-widget": "^11.0.4",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },

+ 5 - 4
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -142,7 +142,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-get-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.', this );
 		}
 
 		const root = this.model.document.getRoot( rootName );
@@ -240,7 +240,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-init-document-not-empty
 			 */
-			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
+			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.', this );
 		}
 
 		let initialData = {};
@@ -262,7 +262,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-init-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.', this );
 		}
 
 		this.model.enqueueChange( 'transparent', writer => {
@@ -297,6 +297,7 @@ export default class DataController {
 	 */
 	set( data ) {
 		let newData = {};
+
 		if ( typeof data === 'string' ) {
 			newData.main = data; // Default root is 'main'. To set data on a different root, object should be passed.
 		} else {
@@ -315,7 +316,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-set-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.', this );
 		}
 
 		this.model.enqueueChange( 'transparent', writer => {

+ 7 - 4
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -105,8 +105,11 @@ export default class Conversion {
 			 *
 			 * @error conversion-add-alias-dispatcher-not-registered
 			 */
-			throw new CKEditorError( 'conversion-add-alias-dispatcher-not-registered: ' +
-				'Trying to register and alias for a dispatcher that nas not been registered.' );
+			throw new CKEditorError(
+				'conversion-add-alias-dispatcher-not-registered: ' +
+				'Trying to register and alias for a dispatcher that nas not been registered.',
+				this
+			);
 		}
 
 		this._createConversionHelpers( { name: alias, dispatchers: [ dispatcher ], isDowncast } );
@@ -180,7 +183,7 @@ export default class Conversion {
 			 *
 			 * @error conversion-for-unknown-group
 			 */
-			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
+			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.', this );
 		}
 
 		return this._helpers.get( groupName );
@@ -585,7 +588,7 @@ export default class Conversion {
 			 *
 			 * @error conversion-group-exists
 			 */
-			throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.' );
+			throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.', this );
 		}
 
 		const helpers = isDowncast ? new DowncastHelpers( dispatchers ) : new UpcastHelpers( dispatchers );

+ 3 - 4
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -730,9 +730,8 @@ function removeUIElement() {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
 			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
 		}
 
@@ -1030,9 +1029,9 @@ function removeHighlight( highlightDescriptor ) {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
+
 			if ( element.is( 'attributeElement' ) ) {
 				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
 			} else {

+ 12 - 3
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -174,12 +174,21 @@ export default class Mapper {
 	}
 
 	/**
-	 * Unbinds all elements from given marker name.
+	 * Unbinds an element from given marker name.
 	 *
+	 * @param {module:engine/view/element~Element} element Element to unbind.
 	 * @param {String} name Marker name.
 	 */
-	unbindElementsFromMarkerName( name ) {
-		this._markerNameToElements.delete( name );
+	unbindElementFromMarkerName( element, name ) {
+		const elements = this._markerNameToElements.get( name );
+
+		if ( elements ) {
+			elements.delete( element );
+
+			if ( elements.size == 0 ) {
+				this._markerNameToElements.delete( name );
+			}
+		}
 	}
 
 	/**

+ 1 - 1
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -231,7 +231,7 @@ export default class UpcastDispatcher {
 			 *
 			 * @error view-conversion-dispatcher-incorrect-result
 			 */
-			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.' );
+			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', this );
 		}
 
 		return { modelRange: data.modelRange, modelCursor: data.modelCursor };

+ 1 - 1
packages/ckeditor5-engine/src/conversion/viewconsumable.js

@@ -503,7 +503,7 @@ class ViewElementConsumables {
 				 *
 				 * @error viewconsumable-invalid-attribute
 				 */
-				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
+				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.', this );
 			}
 
 			consumables.set( name, true );

+ 26 - 0
packages/ckeditor5-engine/src/model/differ.js

@@ -110,6 +110,32 @@ export default class Differ {
 		return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
 	}
 
+	/**
+	 * Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
+	 * set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
+	 *
+	 * @param {module:engine/model/item~Item} item Item to refresh.
+	 */
+	refreshItem( item ) {
+		if ( this._isInInsertedElement( item.parent ) ) {
+			return;
+		}
+
+		this._markRemove( item.parent, item.startOffset, item.offsetSize );
+		this._markInsert( item.parent, item.startOffset, item.offsetSize );
+
+		const range = Range._createOn( item );
+
+		for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
+			const markerRange = marker.getRange();
+
+			this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
+		}
+
+		// Clear cache after each buffered operation as it is no longer valid.
+		this._cachedChanges = null;
+	}
+
 	/**
 	 * Buffers the given operation. An operation has to be buffered before it is executed.
 	 *

+ 2 - 0
packages/ckeditor5-engine/src/model/document.js

@@ -127,6 +127,7 @@ export default class Document {
 				 */
 				throw new CKEditorError(
 					'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					this,
 					{ operation }
 				);
 			}
@@ -201,6 +202,7 @@ export default class Document {
 			 */
 			throw new CKEditorError(
 				'model-document-createRoot-name-exists: Root with specified name already exists.',
+				this,
 				{ name: rootName }
 			);
 		}

+ 2 - 0
packages/ckeditor5-engine/src/model/documentselection.js

@@ -639,6 +639,7 @@ class LiveSelection extends Selection {
 					 */
 					throw new CKEditorError(
 						'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
+						this,
 						{ range }
 					);
 				}
@@ -767,6 +768,7 @@ class LiveSelection extends Selection {
 			 */
 			throw new CKEditorError(
 				'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.',
+				this,
 				{ uid }
 			);
 		}

+ 2 - 1
packages/ckeditor5-engine/src/model/liveposition.js

@@ -46,7 +46,8 @@ export default class LivePosition extends Position {
 			 * @error liveposition-root-not-rootelement
 			 */
 			throw new CKEditorError(
-				'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.'
+				'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.',
+				root
 			);
 		}
 

+ 6 - 6
packages/ckeditor5-engine/src/model/markercollection.js

@@ -167,7 +167,7 @@ export default class MarkerCollection {
 		const marker = this._markers.get( markerName );
 
 		if ( !marker ) {
-			throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.' );
+			throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.', this );
 		}
 
 		const range = marker.getRange();
@@ -383,7 +383,7 @@ class Marker {
 	 */
 	get managedUsingOperations() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._managedUsingOperations;
@@ -396,7 +396,7 @@ class Marker {
 	 */
 	get affectsData() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._affectsData;
@@ -409,7 +409,7 @@ class Marker {
 	 */
 	getStart() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._liveRange.start.clone();
@@ -422,7 +422,7 @@ class Marker {
 	 */
 	getEnd() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._liveRange.end.clone();
@@ -442,7 +442,7 @@ class Marker {
 	 */
 	getRange() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._liveRange.toRange();

+ 3 - 2
packages/ckeditor5-engine/src/model/model.js

@@ -702,10 +702,11 @@ export default class Model {
 	 * * {@link #change `change()`},
 	 * * {@link #enqueueChange `enqueueChange()`}.
 	 *
+	 * @param {'transparent'|'default'} [type='default'] The type of the batch.
 	 * @returns {module:engine/model/batch~Batch}
 	 */
-	createBatch() {
-		return new Batch();
+	createBatch( type ) {
+		return new Batch( type );
 	}
 
 	/**

+ 2 - 2
packages/ckeditor5-engine/src/model/node.js

@@ -88,7 +88,7 @@ export default class Node {
 		}
 
 		if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
-			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;
@@ -112,7 +112,7 @@ export default class Node {
 		}
 
 		if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) {
-			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;

+ 12 - 6
packages/ckeditor5-engine/src/model/nodelist.js

@@ -124,7 +124,7 @@ export default class NodeList {
 			 *
 			 * @error nodelist-index-out-of-bounds
 			 */
-			throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.' );
+			throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.', this );
 		}
 
 		return this.getNodeStartOffset( node );
@@ -158,10 +158,13 @@ export default class NodeList {
 			 * @param {Number} offset
 			 * @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
 			 */
-			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.', {
-				offset,
-				nodeList: this
-			} );
+			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.',
+				this,
+				{
+					offset,
+					nodeList: this
+				}
+			);
 		}
 
 		return this.length;
@@ -183,7 +186,10 @@ export default class NodeList {
 				 *
 				 * @error nodelist-insertNodes-not-node
 				 */
-				throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
+				throw new CKEditorError(
+					'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.',
+					this
+				);
 			}
 		}
 

+ 3 - 1
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -128,7 +128,7 @@ export default class AttributeOperation extends Operation {
 			 *
 			 * @error attribute-operation-range-not-flat
 			 */
-			throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.' );
+			throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.', this );
 		}
 
 		for ( const item of this.range.getItems( { shallow: true } ) ) {
@@ -144,6 +144,7 @@ export default class AttributeOperation extends Operation {
 				throw new CKEditorError(
 					'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
 					'old attribute value.',
+					this,
 					{ item, key: this.key, value: this.oldValue }
 				);
 			}
@@ -158,6 +159,7 @@ export default class AttributeOperation extends Operation {
 				 */
 				throw new CKEditorError(
 					'attribute-operation-attribute-exists: The attribute with given key already exists.',
+					this,
 					{ node: item, key: this.key }
 				);
 			}

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/detachoperation.js

@@ -73,7 +73,7 @@ export default class DetachOperation extends Operation {
 			 *
 			 * @error detach-operation-on-document-node
 			 */
-			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' );
+			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.', this );
 		}
 	}
 

+ 2 - 1
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -115,7 +115,8 @@ export default class InsertOperation extends Operation {
 			 * @error insert-operation-position-invalid
 			 */
 			throw new CKEditorError(
-				'insert-operation-position-invalid: Insertion position is invalid.'
+				'insert-operation-position-invalid: Insertion position is invalid.',
+				this
 			);
 		}
 	}

+ 3 - 3
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -147,21 +147,21 @@ export default class MergeOperation extends Operation {
 			 *
 			 * @error merge-operation-source-position-invalid
 			 */
-			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
+			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.', this );
 		} else if ( !targetElement || !targetElement.is( 'element' ) || !targetElement.parent ) {
 			/**
 			 * Merge target position is invalid.
 			 *
 			 * @error merge-operation-target-position-invalid
 			 */
-			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
+			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.', this );
 		} else if ( this.howMany != sourceElement.maxOffset ) {
 			/**
 			 * Merge operation specifies wrong number of nodes to move.
 			 *
 			 * @error merge-operation-how-many-invalid
 			 */
-			throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
+			throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.', this );
 		}
 	}
 

+ 4 - 4
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -130,7 +130,7 @@ export default class MoveOperation extends Operation {
 			 * @error move-operation-position-invalid
 			 */
 			throw new CKEditorError(
-				'move-operation-position-invalid: Source position or target position is invalid.'
+				'move-operation-position-invalid: Source position or target position is invalid.', this
 			);
 		} else if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
 			/**
@@ -139,7 +139,7 @@ export default class MoveOperation extends Operation {
 			 * @error move-operation-nodes-do-not-exist
 			 */
 			throw new CKEditorError(
-				'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.'
+				'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.', this
 			);
 		} else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) {
 			/**
@@ -148,7 +148,7 @@ export default class MoveOperation extends Operation {
 			 * @error move-operation-range-into-itself
 			 */
 			throw new CKEditorError(
-				'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.'
+				'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.', this
 			);
 		} else if ( this.sourcePosition.root == this.targetPosition.root ) {
 			if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) {
@@ -161,7 +161,7 @@ export default class MoveOperation extends Operation {
 					 * @error move-operation-node-into-itself
 					 */
 					throw new CKEditorError(
-						'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.'
+						'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.', this
 					);
 				}
 			}

+ 4 - 2
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -94,7 +94,8 @@ export default class RenameOperation extends Operation {
 			 * @error rename-operation-wrong-position
 			 */
 			throw new CKEditorError(
-				'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.'
+				'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.',
+				this
 			);
 		} else if ( element.name !== this.oldName ) {
 			/**
@@ -103,7 +104,8 @@ export default class RenameOperation extends Operation {
 			 * @error rename-operation-wrong-name
 			 */
 			throw new CKEditorError(
-				'rename-operation-wrong-name: Element to change has different name than operation\'s old name.'
+				'rename-operation-wrong-name: Element to change has different name than operation\'s old name.',
+				this
 			);
 		}
 	}

+ 4 - 0
packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js

@@ -116,6 +116,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-not-a-root: The element to change is not a root element.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -132,6 +133,7 @@ export default class RootAttributeOperation extends Operation {
 			throw new CKEditorError(
 				'rootattribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
 				'old attribute value.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -146,6 +148,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-attribute-exists: The attribute with given key already exists.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -197,6 +200,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-fromjson-no-root: Cannot create RootAttributeOperation. Root with specified name does not exist.',
+				this,
 				{ rootName: json.root }
 			);
 		}

+ 4 - 4
packages/ckeditor5-engine/src/model/operation/splitoperation.js

@@ -149,28 +149,28 @@ export default class SplitOperation extends Operation {
 			 *
 			 * @error split-operation-position-invalid
 			 */
-			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.' );
+			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.', this );
 		} else if ( !element.parent ) {
 			/**
 			 * Cannot split root element.
 			 *
 			 * @error split-operation-split-in-root
 			 */
-			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.' );
+			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.', this );
 		} else if ( this.howMany != element.maxOffset - this.splitPosition.offset ) {
 			/**
 			 * Split operation specifies wrong number of nodes to move.
 			 *
 			 * @error split-operation-how-many-invalid
 			 */
-			throw new CKEditorError( 'split-operation-how-many-invalid: Split operation specifies wrong number of nodes to move.' );
+			throw new CKEditorError( 'split-operation-how-many-invalid: Split operation specifies wrong number of nodes to move.', this );
 		} else if ( this.graveyardPosition && !this.graveyardPosition.nodeAfter ) {
 			/**
 			 * Graveyard position invalid.
 			 *
 			 * @error split-operation-graveyard-position-invalid
 			 */
-			throw new CKEditorError( 'split-operation-graveyard-position-invalid: Graveyard position invalid.' );
+			throw new CKEditorError( 'split-operation-graveyard-position-invalid: Graveyard position invalid.', this );
 		}
 	}
 

+ 10 - 4
packages/ckeditor5-engine/src/model/operation/utils.js

@@ -69,8 +69,11 @@ export function _remove( range ) {
 		 *
 		 * @error operation-utils-remove-range-not-flat
 		 */
-		throw new CKEditorError( 'operation-utils-remove-range-not-flat: ' +
-			'Trying to remove a range which starts and ends in different element.' );
+		throw new CKEditorError(
+			'operation-utils-remove-range-not-flat: ' +
+			'Trying to remove a range which starts and ends in different element.',
+			this
+		);
 	}
 
 	const parent = range.start.parent;
@@ -105,8 +108,11 @@ export function _move( sourceRange, targetPosition ) {
 		 *
 		 * @error operation-utils-move-range-not-flat
 		 */
-		throw new CKEditorError( 'operation-utils-move-range-not-flat: ' +
-			'Trying to move a range which starts and ends in different element.' );
+		throw new CKEditorError(
+			'operation-utils-move-range-not-flat: ' +
+			'Trying to move a range which starts and ends in different element.',
+			this
+		);
 	}
 
 	const nodes = _remove( sourceRange );

+ 27 - 6
packages/ckeditor5-engine/src/model/position.js

@@ -61,7 +61,10 @@ export default class Position {
 			 *
 			 * @error model-position-root-invalid
 			 */
-			throw new CKEditorError( 'model-position-root-invalid: Position root invalid.' );
+			throw new CKEditorError(
+				'model-position-root-invalid: Position root invalid.',
+				root
+			);
 		}
 
 		if ( !( path instanceof Array ) || path.length === 0 ) {
@@ -71,7 +74,11 @@ export default class Position {
 			 * @error model-position-path-incorrect
 			 * @param path
 			 */
-			throw new CKEditorError( 'model-position-path-incorrect: Position path must be an array with at least one item.', { path } );
+			throw new CKEditorError(
+				'model-position-path-incorrect: Position path must be an array with at least one item.',
+				root,
+				{ path }
+			);
 		}
 
 		// Normalize the root and path (if element was passed).
@@ -858,7 +865,9 @@ export default class Position {
 				 */
 				throw new CKEditorError(
 					'model-createPositionAt-offset-required: ' +
-					'Model#createPositionAt() requires the offset when the first parameter is a model item.' );
+					'Model#createPositionAt() requires the offset when the first parameter is a model item.',
+					[ this, itemOrPosition ]
+				);
 			}
 
 			if ( !node.is( 'element' ) && !node.is( 'documentFragment' ) ) {
@@ -867,7 +876,10 @@ export default class Position {
 				 *
 				 * @error model-position-parent-incorrect
 				 */
-				throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' );
+				throw new CKEditorError(
+					'model-position-parent-incorrect: Position parent have to be a element or document fragment.',
+					[ this, itemOrPosition ]
+				);
 			}
 
 			const path = node.getPath();
@@ -894,7 +906,11 @@ export default class Position {
 			 * @error model-position-after-root
 			 * @param {module:engine/model/item~Item} root
 			 */
-			throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
+			throw new CKEditorError(
+				'model-position-after-root: You cannot make a position after root.',
+				[ this, item ],
+				{ root: item }
+			);
 		}
 
 		return this._createAt( item.parent, item.endOffset, stickiness );
@@ -916,7 +932,11 @@ export default class Position {
 			 * @error model-position-before-root
 			 * @param {module:engine/model/item~Item} root
 			 */
-			throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
+			throw new CKEditorError(
+				'model-position-before-root: You cannot make a position before root.',
+				item,
+				{ root: item }
+			);
 		}
 
 		return this._createAt( item.parent, item.startOffset, stickiness );
@@ -946,6 +966,7 @@ export default class Position {
 			 */
 			throw new CKEditorError(
 				'model-position-fromjson-no-root: Cannot create position for document. Root with specified name does not exist.',
+				doc,
 				{ rootName: json.root }
 			);
 		}

+ 4 - 1
packages/ckeditor5-engine/src/model/range.js

@@ -877,7 +877,10 @@ export default class Range {
 			 *
 			 * @error range-create-from-ranges-empty-array
 			 */
-			throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' );
+			throw new CKEditorError(
+				'range-create-from-ranges-empty-array: At least one range has to be passed.',
+				null
+			);
 		} else if ( ranges.length == 1 ) {
 			return ranges[ 0 ].clone();
 		}

+ 19 - 8
packages/ckeditor5-engine/src/model/schema.js

@@ -91,9 +91,13 @@ export default class Schema {
 			 * @param itemName The name of the model element that is being registered twice.
 			 * @error schema-cannot-register-item-twice
 			 */
-			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
-				itemName
-			} );
+			throw new CKEditorError(
+				'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.',
+				this,
+				{
+					itemName
+				}
+			);
 		}
 
 		this._sourceDefinitions[ itemName ] = [
@@ -136,9 +140,9 @@ export default class Schema {
 			 * {@link #register registered} yet.
 			 *
 			 * @param itemName The name of the model element which is being extended.
-			 * @error schema-cannot-register-item-twice
+			 * @error schema-cannot-extend-missing-item
 			 */
-			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
+			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', this, {
 				itemName
 			} );
 		}
@@ -352,7 +356,10 @@ export default class Schema {
 				 *
 				 * @error schema-check-merge-no-element-before
 				 */
-				throw new CKEditorError( 'schema-check-merge-no-element-before: The node before the merge position must be an element.' );
+				throw new CKEditorError(
+					'schema-check-merge-no-element-before: The node before the merge position must be an element.',
+					this
+				);
 			}
 
 			if ( !( nodeAfter instanceof Element ) ) {
@@ -361,7 +368,10 @@ export default class Schema {
 				 *
 				 * @error schema-check-merge-no-element-after
 				 */
-				throw new CKEditorError( 'schema-check-merge-no-element-after: The node after the merge position must be an element.' );
+				throw new CKEditorError(
+					'schema-check-merge-no-element-after: The node after the merge position must be an element.',
+					this
+				);
 			}
 
 			return this.checkMerge( nodeBefore, nodeAfter );
@@ -1018,7 +1028,7 @@ mix( Schema, ObservableMixin );
  * (paragraphs, lists items, headings, images) which, in turn, may contain text inside.
  *
  * By inheriting from the generic items you can define new items which will get extended by other editor features.
- * Read more about generic types in the {@linkTODO Defining schema} guide.
+ * Read more about generic types in the {@glink framework/guides/deep-dive/schema Defining schema} guide.
  *
  * # Example definitions
  *
@@ -1353,6 +1363,7 @@ export class SchemaContext {
  * @typedef {Object} module:engine/model/schema~AttributeProperties
  * @property {Boolean} [isFormatting] Indicates that the attribute should be considered as a visual formatting, like `bold`, `italic` or
  * `fontSize` rather than semantic attribute (such as `src`, `listType`, etc.). For example, it is used by the "Remove format" feature.
+ * @property {Boolean} [copyOnEnter] Indicates that given text attribute should be copied to the next block when enter is pressed.
  */
 
 function compileBaseItemRule( sourceItemRules, itemName ) {

+ 12 - 4
packages/ckeditor5-engine/src/model/selection.js

@@ -395,7 +395,9 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-setTo-required-second-parameter: ' +
-					'selection.setTo requires the second parameter when the first parameter is a node.' );
+					'selection.setTo requires the second parameter when the first parameter is a node.',
+					[ this, selectable ]
+				);
 			}
 
 			this._setRanges( [ range ], backward );
@@ -415,7 +417,10 @@ export default class Selection {
 			 *
 			 * @error model-selection-setTo-not-selectable
 			 */
-			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set the selection to the given place.' );
+			throw new CKEditorError(
+				'model-selection-setTo-not-selectable: Cannot set the selection to the given place.',
+				[ this, selectable ]
+			);
 		}
 	}
 
@@ -449,7 +454,8 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-set-ranges-not-range: ' +
-					'Selection range set to an object that is not an instance of model.Range.'
+					'Selection range set to an object that is not an instance of model.Range.',
+					[ this, newRanges ]
 				);
 			}
 
@@ -493,7 +499,8 @@ export default class Selection {
 			 * @error model-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.',
+				[ this, itemOrPosition ]
 			);
 		}
 
@@ -763,6 +770,7 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-range-intersects: Trying to add a range that intersects with another range in the selection.',
+					[ this, range ],
 					{ addedRange: range, intersectingRange: this._ranges[ i ] }
 				);
 			}

+ 2 - 2
packages/ckeditor5-engine/src/model/textproxy.js

@@ -64,7 +64,7 @@ export default class TextProxy {
 			 *
 			 * @error model-textproxy-wrong-offsetintext
 			 */
-			throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
+			throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.', this );
 		}
 
 		if ( length < 0 || offsetInText + length > textNode.offsetSize ) {
@@ -73,7 +73,7 @@ export default class TextProxy {
 			 *
 			 * @error model-textproxy-wrong-length
 			 */
-			throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.' );
+			throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.', this );
 		}
 
 		/**

+ 5 - 1
packages/ckeditor5-engine/src/model/treewalker.js

@@ -42,7 +42,10 @@ export default class TreeWalker {
 			 *
 			 * @error model-tree-walker-no-start-position
 			 */
-			throw new CKEditorError( 'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+			throw new CKEditorError(
+				'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.',
+				null
+			);
 		}
 
 		const direction = options.direction || 'forward';
@@ -50,6 +53,7 @@ export default class TreeWalker {
 		if ( direction != 'forward' && direction != 'backward' ) {
 			throw new CKEditorError(
 				'model-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
+				options,
 				{ direction }
 			);
 		}

+ 8 - 1
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -561,8 +561,15 @@ class Insertion {
 				this.position = this.writer.createPositionBefore( parent );
 
 				// Special case – parent is empty (<p>^</p>).
+				//
+				// 1. parent.isEmpty
 				// We can remove the element after moving insertion position out of it.
-				if ( parent.isEmpty ) {
+				//
+				// 2. parent.parent === allowedIn
+				// However parent should remain in place when allowed element is above limit element in document tree.
+				// For example there shouldn't be allowed to remove empty paragraph from tableCell, when is pasted
+				// content allowed in $root.
+				if ( parent.isEmpty && parent.parent === allowedIn ) {
 					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {

+ 69 - 26
packages/ckeditor5-engine/src/model/writer.js

@@ -177,8 +177,18 @@ export default class Writer {
 			// If it isn't the same root.
 			else {
 				if ( item.root.document ) {
-					// It is forbidden to move a node that was already in a document outside of it.
-					throw new Error( 'model-writer-insert-forbidden-move: Cannot move a node from a document to a different tree.' );
+					/**
+					 * Cannot move a node from a document to a different tree.
+					 * It is forbidden to move a node that was already in a document outside of it.
+					 *
+					 * @error model-writer-insert-forbidden-move
+					 */
+					throw new CKEditorError(
+						'model-writer-insert-forbidden-move: ' +
+						'Cannot move a node from a document to a different tree. ' +
+						'It is forbidden to move a node that was already in a document outside of it.',
+						this
+					);
 				} else {
 					// Move between two different document fragments or from document fragment to a document is possible.
 					// In that case, remove the item from it's original parent.
@@ -208,7 +218,13 @@ export default class Writer {
 					markerRange.end._getCombined( rangeRootPosition, position )
 				);
 
-				this.addMarker( markerName, { range, usingOperation: true, affectsData: true } );
+				const options = { range, usingOperation: true, affectsData: true };
+
+				if ( this.model.markers.has( markerName ) ) {
+					this.updateMarker( markerName, options );
+				} else {
+					this.addMarker( markerName, options );
+				}
 			}
 		}
 	}
@@ -456,7 +472,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-invalid-range
 			 */
-			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
+			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.', this );
 		}
 
 		if ( !range.isFlat ) {
@@ -465,7 +481,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-range-not-flat
 			 */
-			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
+			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.', this );
 		}
 
 		const position = Position._createAt( itemOrPosition, offset );
@@ -485,7 +501,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-different-document
 			 */
-			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
+			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.', this );
 		}
 
 		const version = range.root.document ? range.root.document.version : null;
@@ -537,7 +553,7 @@ export default class Writer {
 			 *
 			 * @error writer-merge-no-element-before
 			 */
-			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
+			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.', this );
 		}
 
 		if ( !( nodeAfter instanceof Element ) ) {
@@ -546,7 +562,7 @@ export default class Writer {
 			 *
 			 * @error writer-merge-no-element-after
 			 */
-			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
+			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.', this );
 		}
 
 		if ( !position.root.document ) {
@@ -696,7 +712,8 @@ export default class Writer {
 			 * @error writer-rename-not-element-instance
 			 */
 			throw new CKEditorError(
-				'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.'
+				'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.',
+				this
 			);
 		}
 
@@ -732,7 +749,7 @@ export default class Writer {
 			 *
 			 * @error writer-split-element-no-parent
 			 */
-			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
+			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.', this );
 		}
 
 		// When limit element is not defined lets set splitElement parent as limit.
@@ -741,7 +758,7 @@ export default class Writer {
 		}
 
 		if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
-			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
+			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.', this );
 		}
 
 		// We need to cache elements that will be created as a result of the first split because
@@ -791,7 +808,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-range-not-flat
 			 */
-			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
+			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.', this );
 		}
 
 		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
@@ -802,7 +819,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-element-not-empty
 			 */
-			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
+			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.', this );
 		}
 
 		if ( element.parent !== null ) {
@@ -811,7 +828,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-element-attached
 			 */
-			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
+			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.', this );
 		}
 
 		this.insert( element, range.start );
@@ -837,7 +854,7 @@ export default class Writer {
 			 *
 			 * @error writer-unwrap-element-no-parent
 			 */
-			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
+			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.', this );
 		}
 
 		this.move( Range._createIn( element ), this.createPositionAfter( element ) );
@@ -893,7 +910,8 @@ export default class Writer {
 			 * @error writer-addMarker-no-usingOperation
 			 */
 			throw new CKEditorError(
-				'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.'
+				'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.',
+				this
 			);
 		}
 
@@ -907,7 +925,7 @@ export default class Writer {
 			 *
 			 * @error writer-addMarker-marker-exists
 			 */
-			throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.' );
+			throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.', this );
 		}
 
 		if ( !range ) {
@@ -916,7 +934,10 @@ export default class Writer {
 			 *
 			 * @error writer-addMarker-no-range
 			 */
-			throw new CKEditorError( 'writer-addMarker-no-range: Range parameter is required when adding a new marker.' );
+			throw new CKEditorError(
+				'writer-addMarker-no-range: Range parameter is required when adding a new marker.',
+				this
+			);
 		}
 
 		if ( !usingOperation ) {
@@ -1009,7 +1030,7 @@ export default class Writer {
 			 *
 			 * @error writer-updateMarker-marker-not-exists
 			 */
-			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.' );
+			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.', this );
 		}
 
 		if ( !options ) {
@@ -1031,7 +1052,8 @@ export default class Writer {
 			 * @error writer-updateMarker-wrong-options
 			 */
 			throw new CKEditorError(
-				'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.'
+				'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.',
+				this
 			);
 		}
 
@@ -1082,7 +1104,7 @@ export default class Writer {
 			 *
 			 * @error writer-removeMarker-no-marker
 			 */
-			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
+			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.', this );
 		}
 
 		const marker = this.model.markers.get( name );
@@ -1315,7 +1337,7 @@ export default class Writer {
 		 * @error writer-incorrect-use
 		 */
 		if ( this.model._currentWriter !== this ) {
-			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
+			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.', this );
 		}
 	}
 
@@ -1349,10 +1371,31 @@ export default class Writer {
 				const elementBefore = positionOrRange.nodeBefore;
 				const elementAfter = positionOrRange.nodeAfter;
 
-				const affectedOnLeft = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
-				const affectedOnRight = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
-
-				isAffected = affectedOnLeft || affectedOnRight;
+				//               Start:  <p>Foo[</p><p>Bar]</p>
+				//         After merge:  <p>Foo[Bar]</p>
+				// After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
+
+				//               Start:  <p>[Foo</p><p>]Bar</p>
+				//         After merge:  <p>[Foo]Bar</p>
+				// After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
+
+				//               Start:  <p>[Foo</p>]<p>Bar</p>
+				//         After merge:  <p>[Foo]Bar</p>
+				// After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
+
+				//               Start:  <p>Foo</p>[<p>Bar]</p>
+				//         After merge:  <p>Foo[Bar]</p>
+				// After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
+
+				isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
 			}
 
 			if ( isAffected ) {

+ 2 - 1
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -116,7 +116,8 @@ export default class AttributeElement extends Element {
 			 */
 			throw new CKEditorError(
 				'attribute-element-get-elements-with-same-id-no-id: ' +
-				'Cannot get elements with the same id for an attribute element without id.'
+				'Cannot get elements with the same id for an attribute element without id.',
+				this
 			);
 		}
 

+ 28 - 19
packages/ckeditor5-engine/src/view/domconverter.js

@@ -889,7 +889,8 @@ export default class DomConverter {
 	 *
 	 * * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
 	 * element or if a previous text node ends with a space character,
-	 * * space at the end of the text node is changed to `&nbsp;` if this is the last text node in its container,
+	 * * space at the end of the text node is changed to `&nbsp;` if there are two spaces at the end of a node or if next node
+	 * starts with a space or if it is the last text node in its container,
 	 * * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x   x'` becomes `'x &nbsp; x'`).
 	 *
 	 * Content of {@link #preElements} is not processed.
@@ -918,15 +919,24 @@ export default class DomConverter {
 			}
 		}
 
-		// 2. Replace the last space with a nbsp if this is the last text node (container element boundary).
+		// 2. Replace the last space with nbsp if there are two spaces at the end or if the next node starts with space or there is no
+		// next node (container element boundary).
+		//
+		// Keep in mind that Firefox prefers $nbsp; before tag, not inside it:
+		//
+		// Foo <span>&nbsp;bar</span>  <-- bad.
+		// Foo&nbsp;<span> bar</span>  <-- good.
+		//
+		// More here: https://github.com/ckeditor/ckeditor5-engine/issues/1747.
 		if ( data.charAt( data.length - 1 ) == ' ' ) {
 			const nextNode = this._getTouchingViewTextNode( node, true );
 
-			if ( !nextNode ) {
+			if ( data.charAt( data.length - 2 ) == ' ' || !nextNode || nextNode.data.charAt( 0 ) == ' ' ) {
 				data = data.substr( 0, data.length - 1 ) + '\u00A0';
 			}
 		}
 
+		// 3. Create space+nbsp pairs.
 		return data.replace( / {2}/g, ' \u00A0' );
 	}
 
@@ -955,7 +965,9 @@ export default class DomConverter {
 	 * * multiple whitespaces are replaced to a single space,
 	 * * space at the beginning of a text node is removed if it is the first text node in its container
 	 * element or if the previous text node ends with a space character,
-	 * * space at the end of the text node is removed, if it is the last text node in its container.
+	 * * space at the end of the text node is removed if there are two spaces at the end of a node or if next node
+	 * starts with a space or if it is the last text node in its container
+	 * * nbsps are converted to spaces.
 	 *
 	 * @param {Node} node DOM text node to process.
 	 * @returns {String} Processed data.
@@ -998,28 +1010,25 @@ export default class DomConverter {
 		data = getDataWithoutFiller( new Text( data ) );
 
 		// At this point we should have removed all whitespaces from DOM text data.
-
-		// Now we have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
-		// First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view as
-		// ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them
-		// to `  ` which is what we expect to have in model/view.
+		//
+		// Now, We will reverse the process that happens in `_processDataFromViewText`.
+		//
+		// We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
+		// First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
+		// ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to `  `.
 		data = data.replace( / \u00A0/g, '  ' );
 
+		// Then, let's change the last nbsp to a space.
+		if ( /( |\u00A0)\u00A0$/.test( data ) || !nextNode || ( nextNode.data && nextNode.data.charAt( 0 ) == ' ' ) ) {
+			data = data.replace( /\u00A0$/, ' ' );
+		}
+
 		// Then, change &nbsp; character that is at the beginning of the text node to space character.
-		// As above, that &nbsp; was created for rendering reasons but it's real meaning is just a space character.
 		// We do that replacement only if this is the first node or the previous node ends on whitespace character.
 		if ( shouldLeftTrim ) {
 			data = data.replace( /^\u00A0/, ' ' );
 		}
 
-		// Since input text data could be: `x_ _`, we would not replace the first &nbsp; after `x` character.
-		// We have to fix it. Since we already change all ` &nbsp;`, we will have something like this at the end of text data:
-		// `x_ _ _` -> `x_    `. Find &nbsp; at the end of string (can be followed only by spaces).
-		// We do that replacement only if this is the last node or the next node starts with &nbsp; or is a <br>.
-		if ( isText( nextNode ) ? nextNode.data.charAt( 0 ) == '\u00A0' : true ) {
-			data = data.replace( /\u00A0( *)$/, ' $1' );
-		}
-
 		// At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
 		// changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
 		return data;
@@ -1048,7 +1057,7 @@ export default class DomConverter {
 	 * be trimmed from the right side.
 	 *
 	 * @param {Node} node
-	 * @param {Node} prevNode
+	 * @param {Node} nextNode
 	 */
 	_checkShouldRightTrimDomText( node, nextNode ) {
 		if ( nextNode ) {

+ 53 - 48
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -444,7 +444,8 @@ export default class DowncastWriter {
 			 * @error view-writer-break-non-container-element
 			 */
 			throw new CKEditorError(
-				'view-writer-break-non-container-element: Trying to break an element which is not a container element.'
+				'view-writer-break-non-container-element: Trying to break an element which is not a container element.',
+				this.document
 			);
 		}
 
@@ -454,7 +455,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-break-root
 			 */
-			throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
+			throw new CKEditorError( 'view-writer-break-root: Trying to break root element.', this.document );
 		}
 
 		if ( position.isAtStart ) {
@@ -577,7 +578,7 @@ export default class DowncastWriter {
 			 * @error view-writer-merge-containers-invalid-position
 			 */
 			throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
-				'Element before and after given position cannot be merged.' );
+				'Element before and after given position cannot be merged.', this.document );
 		}
 
 		const lastChild = prev.getChild( prev.childCount - 1 );
@@ -612,7 +613,7 @@ export default class DowncastWriter {
 		nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
 
 		// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
-		validateNodesToInsert( nodes );
+		validateNodesToInsert( nodes, this.document );
 
 		const container = getParentContainer( position );
 
@@ -622,7 +623,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-invalid-position-container
 			 */
-			throw new CKEditorError( 'view-writer-invalid-position-container' );
+			throw new CKEditorError( 'view-writer-invalid-position-container', this.document );
 		}
 
 		const insertionPosition = this._breakAttributes( position, true );
@@ -665,7 +666,7 @@ export default class DowncastWriter {
 	remove( rangeOrItem ) {
 		const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// If range is collapsed - nothing to remove.
 		if ( range.isCollapsed ) {
@@ -705,7 +706,7 @@ export default class DowncastWriter {
 	 * @param {module:engine/view/element~Element} element Element to remove.
 	 */
 	clear( range, element ) {
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// Create walker on given range.
 		// We walk backward because when we remove element during walk it modifies range end position.
@@ -809,10 +810,10 @@ export default class DowncastWriter {
     */
 	wrap( range, attribute ) {
 		if ( !( attribute instanceof AttributeElement ) ) {
-			throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
+			throw new CKEditorError( 'view-writer-wrap-invalid-attribute', this.document );
 		}
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		if ( !range.isCollapsed ) {
 			// Non-collapsed range. Wrap it with the attribute element.
@@ -854,10 +855,10 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-unwrap-invalid-attribute
 			 */
-			throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
+			throw new CKEditorError( 'view-writer-unwrap-invalid-attribute', this.document );
 		}
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// If range is collapsed - nothing to unwrap.
 		if ( range.isCollapsed ) {
@@ -1093,7 +1094,7 @@ export default class DowncastWriter {
 			// <p><span class="bar">abc</span></p>  -->  <p><span class="foo bar">abc</span></p>
 			//
 			if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
-				wrapPositions.push(	new Position( parent, i ) );
+				wrapPositions.push( new Position( parent, i ) );
 			}
 			//
 			// Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
@@ -1113,7 +1114,7 @@ export default class DowncastWriter {
 				parent._insertChild( i, newAttribute );
 				this._addToClonedElementsGroup( newAttribute );
 
-				wrapPositions.push(	new Position( parent, i ) );
+				wrapPositions.push( new Position( parent, i ) );
 			}
 			//
 			// If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
@@ -1489,7 +1490,7 @@ export default class DowncastWriter {
 		const rangeStart = range.start;
 		const rangeEnd = range.end;
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// Break at the collapsed position. Return new collapsed range.
 		if ( range.isCollapsed ) {
@@ -1534,7 +1535,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-cannot-break-empty-element
 			 */
-			throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
+			throw new CKEditorError( 'view-writer-cannot-break-empty-element', this.document );
 		}
 
 		// If position is placed inside UIElement - throw an exception as we cannot break inside.
@@ -1544,7 +1545,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-cannot-break-ui-element
 			 */
-			throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
+			throw new CKEditorError( 'view-writer-cannot-break-ui-element', this.document );
 		}
 
 		// There are no attributes to break and text nodes breaking is not forced.
@@ -1571,40 +1572,41 @@ export default class DowncastWriter {
 			const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
 
 			return this._breakAttributes( newPosition, forceSplitText );
-		} else
-		// <p>foo<b><u>{}bar</u></b></p>
-		// <p>foo<b>[]<u>bar</u></b></p>
-		// <p>foo{}<b><u>bar</u></b></p>
-		if ( positionOffset === 0 ) {
-			const newPosition = new Position( positionParent.parent, positionParent.index );
+		} else {
+			// <p>foo<b><u>{}bar</u></b></p>
+			// <p>foo<b>[]<u>bar</u></b></p>
+			// <p>foo{}<b><u>bar</u></b></p>
+			if ( positionOffset === 0 ) {
+				const newPosition = new Position( positionParent.parent, positionParent.index );
 
-			return this._breakAttributes( newPosition, forceSplitText );
-		}
-		// <p>foo<b><u>b{}ar</u></b></p>
-		// <p>foo<b><u>b[]ar</u></b></p>
-		// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
-		// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
-		else {
-			const offsetAfter = positionParent.index + 1;
+				return this._breakAttributes( newPosition, forceSplitText );
+			}
+			// <p>foo<b><u>b{}ar</u></b></p>
+			// <p>foo<b><u>b[]ar</u></b></p>
+			// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
+			// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
+			else {
+				const offsetAfter = positionParent.index + 1;
 
-			// Break element.
-			const clonedNode = positionParent._clone();
+				// Break element.
+				const clonedNode = positionParent._clone();
 
-			// Insert cloned node to position's parent node.
-			positionParent.parent._insertChild( offsetAfter, clonedNode );
-			this._addToClonedElementsGroup( clonedNode );
+				// Insert cloned node to position's parent node.
+				positionParent.parent._insertChild( offsetAfter, clonedNode );
+				this._addToClonedElementsGroup( clonedNode );
 
-			// Get nodes to move.
-			const count = positionParent.childCount - positionOffset;
-			const nodesToMove = positionParent._removeChildren( positionOffset, count );
+				// Get nodes to move.
+				const count = positionParent.childCount - positionOffset;
+				const nodesToMove = positionParent._removeChildren( positionOffset, count );
 
-			// Move nodes to cloned node.
-			clonedNode._appendChild( nodesToMove );
+				// Move nodes to cloned node.
+				clonedNode._appendChild( nodesToMove );
 
-			// Create new position to work on.
-			const newPosition = new Position( positionParent.parent, offsetAfter );
+				// Create new position to work on.
+				const newPosition = new Position( positionParent.parent, offsetAfter );
 
-			return this._breakAttributes( newPosition, forceSplitText );
+				return this._breakAttributes( newPosition, forceSplitText );
+			}
 		}
 	}
 
@@ -1825,7 +1827,8 @@ function mergeTextNodes( t1, t2 ) {
 //
 // @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
 // |module:engine/view/containerelement~ContainerElement> nodes
-function validateNodesToInsert( nodes ) {
+// @param {Object} errorContext
+function validateNodesToInsert( nodes, errorContext ) {
 	for ( const node of nodes ) {
 		if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
 			/**
@@ -1836,11 +1839,11 @@ function validateNodesToInsert( nodes ) {
 			 *
 			 * @error view-writer-insert-invalid-node
 			 */
-			throw new CKEditorError( 'view-writer-insert-invalid-node' );
+			throw new CKEditorError( 'view-writer-insert-invalid-node', errorContext );
 		}
 
 		if ( !node.is( 'text' ) ) {
-			validateNodesToInsert( node.getChildren() );
+			validateNodesToInsert( node.getChildren(), errorContext );
 		}
 	}
 }
@@ -1860,7 +1863,8 @@ function isContainerOrFragment( node ) {
 // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
 //
 // @param {module:engine/view/range~Range} range
-function validateRangeContainer( range ) {
+// @param {Object} errorContext
+function validateRangeContainer( range, errorContext ) {
 	const startContainer = getParentContainer( range.start );
 	const endContainer = getParentContainer( range.end );
 
@@ -1872,7 +1876,8 @@ function validateRangeContainer( range ) {
 		 *
 		 * @error view-writer-invalid-range-container
 		 */
-		throw new CKEditorError( 'view-writer-invalid-range-container' );
+
+		throw new CKEditorError( 'view-writer-invalid-range-container', errorContext );
 	}
 }
 

+ 1 - 1
packages/ckeditor5-engine/src/view/editableelement.js

@@ -104,7 +104,7 @@ export default class EditableElement extends ContainerElement {
 			 *
 			 * @error view-editableelement-document-already-set
 			 */
-			throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' );
+			throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.', this );
 		}
 
 		this._setCustomProperty( documentSymbol, document );

+ 4 - 1
packages/ckeditor5-engine/src/view/emptyelement.js

@@ -68,7 +68,10 @@ export default class EmptyElement extends Element {
 			 *
 			 * @error view-emptyelement-cannot-add
 			 */
-			throw new CKEditorError( 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
+			throw new CKEditorError(
+				'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.',
+				[ this, nodes ]
+			);
 		}
 	}
 }

+ 1 - 1
packages/ckeditor5-engine/src/view/node.js

@@ -62,7 +62,7 @@ export default class Node {
 			 *
 			 * @error view-node-not-found-in-parent
 			 */
-			throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;

+ 44 - 0
packages/ckeditor5-engine/src/view/observer/inputobserver.js

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/observer/inputobserver
+ */
+
+import DomEventObserver from './domeventobserver';
+
+/**
+ * Observer for events connected with data input.
+ *
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
+ *
+ * @extends module:engine/view/observer/domeventobserver~DomEventObserver
+ */
+export default class InputObserver extends DomEventObserver {
+	constructor( view ) {
+		super( view );
+
+		this.domEventType = [ 'beforeinput' ];
+	}
+
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}
+
+/**
+ * Fired before browser inputs (or deletes) some data.
+ *
+ * This event is available only on browsers which support DOM `beforeinput` event.
+ *
+ * Introduced by {@link module:engine/view/observer/inputobserver~InputObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/inputobserver~InputObserver} is attached by the
+ * {@link module:engine/view/view~View} this event is available by default.
+ *
+ * @see module:engine/view/observer/inputobserver~InputObserver
+ * @event module:engine/view/document~Document#event:beforeinput
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */

+ 1 - 1
packages/ckeditor5-engine/src/view/observer/keyobserver.js

@@ -11,7 +11,7 @@ import DomEventObserver from './domeventobserver';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /**
- * {@link module:engine/view/document~Document#event:keydown Key down} event observer.
+ * Observer for events connected with pressing keyboard keys.
  *
  * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *

+ 1 - 3
packages/ckeditor5-engine/src/view/placeholder.js

@@ -140,9 +140,7 @@ export function hidePlaceholder( writer, element ) {
  * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
  * sure the correct element is passed to the helper.
  *
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  * @param {module:engine/view/element~Element} element
- * @param {String} text
  * @returns {Boolean}
  */
 export function needsPlaceholder( element ) {
@@ -176,7 +174,7 @@ export function needsPlaceholder( element ) {
 // Updates all placeholders associated with a document in a post–fixer callback.
 //
 // @private
-// @param { module:engine/model/document~Document} doc
+// @param { module:engine/view/document~Document} doc
 // @param {module:engine/view/downcastwriter~DowncastWriter} writer
 // @returns {Boolean} True if any changes were made to the view document.
 function updateDocumentPlaceholders( doc, writer ) {

+ 8 - 6
packages/ckeditor5-engine/src/view/position.js

@@ -21,9 +21,9 @@ import '@ckeditor/ckeditor5-utils/src/version';
  *
  * In order to create a new position instance use the `createPosition*()` factory methods available in:
  *
- * * {@module:engine/view/view~View}
- * * {@module:engine/view/downcastwriter~DowncastWriter}
- * * {@module:engine/view/upcastwriter~UpcastWriter}
+ * * {@link module:engine/view/view~View}
+ * * {@link module:engine/view/downcastwriter~DowncastWriter}
+ * * {@link module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Position {
 	/**
@@ -341,7 +341,9 @@ export default class Position {
 				 */
 				throw new CKEditorError(
 					'view-createPositionAt-offset-required: ' +
-					'View#createPositionAt() requires the offset when the first parameter is a view item.' );
+					'View#createPositionAt() requires the offset when the first parameter is a view item.',
+					node
+				);
 			}
 
 			return new Position( node, offset );
@@ -368,7 +370,7 @@ export default class Position {
 			 * @error view-position-after-root
 			 * @param {module:engine/view/node~Node} root
 			 */
-			throw new CKEditorError( 'view-position-after-root: You can not make position after root.', { root: item } );
+			throw new CKEditorError( 'view-position-after-root: You can not make position after root.', item, { root: item } );
 		}
 
 		return new Position( item.parent, item.index + 1 );
@@ -394,7 +396,7 @@ export default class Position {
 			 * @error view-position-before-root
 			 * @param {module:engine/view/node~Node} root
 			 */
-			throw new CKEditorError( 'view-position-before-root: You can not make position before root.', { root: item } );
+			throw new CKEditorError( 'view-position-before-root: You can not make position before root.', item, { root: item } );
 		}
 
 		return new Position( item.parent, item.index );

+ 3 - 3
packages/ckeditor5-engine/src/view/range.js

@@ -15,9 +15,9 @@ import TreeWalker from './treewalker';
  *
  * In order to create a new position instance use the `createPosition*()` factory methods available in:
  *
- * * {@module:engine/view/view~View}
- * * {@module:engine/view/downcastwriter~DowncastWriter}
- * * {@module:engine/view/upcastwriter~UpcastWriter}
+ * * {@link module:engine/view/view~View}
+ * * {@link module:engine/view/downcastwriter~DowncastWriter}
+ * * {@link module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Range {
 	/**

+ 2 - 2
packages/ckeditor5-engine/src/view/renderer.js

@@ -152,7 +152,7 @@ export default class Renderer {
 				 *
 				 * @error renderer-unknown-type
 				 */
-				throw new CKEditorError( 'view-renderer-unknown-type: Unknown type passed to Renderer.markToSync.' );
+				throw new CKEditorError( 'view-renderer-unknown-type: Unknown type passed to Renderer.markToSync.', this );
 			}
 		}
 	}
@@ -387,7 +387,7 @@ export default class Renderer {
 			 *
 			 * @error view-renderer-filler-was-lost
 			 */
-			throw new CKEditorError( 'view-renderer-filler-was-lost: The inline filler node was lost.' );
+			throw new CKEditorError( 'view-renderer-filler-was-lost: The inline filler node was lost.', this );
 		}
 
 		if ( isInlineFiller( domFillerNode ) ) {

+ 8 - 4
packages/ckeditor5-engine/src/view/selection.js

@@ -524,7 +524,8 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'view-selection-setTo-required-second-parameter: ' +
-					'selection.setTo requires the second parameter when the first parameter is a node.'
+					'selection.setTo requires the second parameter when the first parameter is a node.',
+					this
 				);
 			} else if ( placeOrOffset == 'in' ) {
 				range = Range._createIn( selectable );
@@ -547,7 +548,7 @@ export default class Selection {
 			 *
 			 * @error view-selection-setTo-not-selectable
 			 */
-			throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.' );
+			throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.', this );
 		}
 
 		this.fire( 'change' );
@@ -572,7 +573,8 @@ export default class Selection {
 			 * @error view-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.',
+				this
 			);
 		}
 
@@ -680,7 +682,8 @@ export default class Selection {
 			 */
 			throw new CKEditorError(
 				'view-selection-add-range-not-range: ' +
-				'Selection range set to an object that is not an instance of view.Range'
+				'Selection range set to an object that is not an instance of view.Range',
+				this
 			);
 		}
 
@@ -709,6 +712,7 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
+					this,
 					{ addedRange: range, intersectingRange: storedRange }
 				);
 			}

+ 2 - 2
packages/ckeditor5-engine/src/view/textproxy.js

@@ -55,7 +55,7 @@ export default class TextProxy {
 			 *
 			 * @error view-textproxy-wrong-offsetintext
 			 */
-			throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
+			throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.', this );
 		}
 
 		if ( length < 0 || offsetInText + length > textNode.data.length ) {
@@ -64,7 +64,7 @@ export default class TextProxy {
 			 *
 			 * @error view-textproxy-wrong-length
 			 */
-			throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.' );
+			throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.', this );
 		}
 
 		/**

+ 5 - 1
packages/ckeditor5-engine/src/view/treewalker.js

@@ -42,12 +42,16 @@ export default class TreeWalker {
 			 *
 			 * @error view-tree-walker-no-start-position
 			 */
-			throw new CKEditorError( 'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+			throw new CKEditorError(
+				'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.',
+				null
+			);
 		}
 
 		if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) {
 			throw new CKEditorError(
 				'view-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
+				options.startPosition,
 				{ direction: options.direction }
 			);
 		}

+ 1 - 1
packages/ckeditor5-engine/src/view/uielement.js

@@ -81,7 +81,7 @@ export default class UIElement extends Element {
 			 *
 			 * @error view-uielement-cannot-add
 			 */
-			throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.' );
+			throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.', this );
 		}
 	}
 

+ 8 - 1
packages/ckeditor5-engine/src/view/view.js

@@ -21,6 +21,7 @@ import FakeSelectionObserver from './observer/fakeselectionobserver';
 import SelectionObserver from './observer/selectionobserver';
 import FocusObserver from './observer/focusobserver';
 import CompositionObserver from './observer/compositionobserver';
+import InputObserver from './observer/inputobserver';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -29,6 +30,7 @@ import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/sc
 import { injectUiElementHandling } from './uielement';
 import { injectQuirksHandling } from './filler';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 /**
  * Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
@@ -173,6 +175,10 @@ export default class View {
 		this.addObserver( FakeSelectionObserver );
 		this.addObserver( CompositionObserver );
 
+		if ( env.isAndroid ) {
+			this.addObserver( InputObserver );
+		}
+
 		// Inject quirks handlers.
 		injectQuirksHandling( this );
 		injectUiElementHandling( this );
@@ -449,7 +455,8 @@ export default class View {
 			throw new CKEditorError(
 				'cannot-change-view-tree: ' +
 				'Attempting to make changes to the view when it is in an incorrect state: rendering or post-fixers are in progress. ' +
-				'This may cause some unexpected behavior and inconsistency between the DOM and the view.'
+				'This may cause some unexpected behavior and inconsistency between the DOM and the view.',
+				this
 			);
 		}
 

+ 31 - 31
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -8,7 +8,6 @@ import ModelRange from '../../src/model/range';
 import ViewRange from '../../src/view/range';
 import DataController from '../../src/controller/datacontroller';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ViewDocumentFragment from '../../src/view/documentfragment';
@@ -20,6 +19,7 @@ import count from '@ckeditor/ckeditor5-utils/src/count';
 
 import UpcastHelpers from '../../src/conversion/upcasthelpers';
 import DowncastHelpers from '../../src/conversion/downcasthelpers';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DataController', () => {
 	let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers;
@@ -173,12 +173,9 @@ describe( 'DataController', () => {
 		it( 'should throw an error when document data is already initialized', () => {
 			data.init( '<p>Foo</p>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( '<p>Bar</p>' );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.'
-			);
+			}, /datacontroller-init-document-not-empty:/, model );
 		} );
 
 		it( 'should set data to default main root', () => {
@@ -230,23 +227,17 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used (single)', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( { nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
-			);
+			}, /^datacontroller-init-non-existent-root:/ );
 		} );
 
 		it( 'should throw an error when non-existent root is used (one of many)', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( { main: 'bar', nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
-			);
+			}, /^datacontroller-init-non-existent-root:/, model );
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( '' );
 		} );
@@ -315,27 +306,39 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used (single)', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.set( { nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
-			);
+			}, /datacontroller-set-non-existent-root:/, model );
 		} );
 
 		it( 'should throw an error when non-existent root is used (one of many) without touching any roots data', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.set( { main: 'bar', nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
-			);
+			}, /datacontroller-set-non-existent-root:/, model );
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
+
+		// https://github.com/ckeditor/ckeditor5-engine/issues/1721.
+		it( 'should not throw when setting the data with markers that already exist in the editor', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+
+			data.set( 'foo' );
+
+			downcastHelpers.markerToElement( { model: 'marker', view: 'marker' } );
+			upcastHelpers.elementToMarker( { view: 'marker', model: 'marker' } );
+
+			model.change( writer => {
+				writer.addMarker( 'marker', { range: writer.createRangeIn( modelDocument.getRoot() ), usingOperation: true } );
+			} );
+
+			expect( () => {
+				data.set( data.get() );
+			} ).not.to.throw();
+		} );
 	} );
 
 	describe( 'get()', () => {
@@ -423,12 +426,9 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.get( { rootName: 'nonexistent' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.'
-			);
+			}, /datacontroller-get-non-existent-root:/ );
 		} );
 	} );
 

+ 8 - 8
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -4,7 +4,6 @@
  */
 
 import Conversion from '../../src/conversion/conversion';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
 
@@ -18,6 +17,7 @@ import Model from '../../src/model/model';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
 import { stringify as modelStringify } from '../../src/dev-utils/model';
 import ConversionHelpers from '../../src/conversion/conversionhelpers';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Conversion', () => {
 	let conversion, downcastDispA, upcastDispaA, downcastDispB;
@@ -34,15 +34,15 @@ describe( 'Conversion', () => {
 
 	describe( 'addAlias()', () => {
 		it( 'should throw when trying to use same group name twice', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.addAlias( 'upcast', upcastDispaA );
-			} ).to.throw( CKEditorError, /conversion-group-exists/ );
+			}, /conversion-group-exists/ );
 		} );
 
 		it( 'should throw when trying to add not registered dispatcher', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.addAlias( 'foo', {} );
-			} ).to.throw( CKEditorError, /conversion-add-alias-dispatcher-not-registered/ );
+			}, /conversion-add-alias-dispatcher-not-registered/ );
 		} );
 	} );
 
@@ -53,9 +53,9 @@ describe( 'Conversion', () => {
 		} );
 
 		it( 'should throw if non-existing group name has been used', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.for( 'foo' );
-			} ).to.throw( CKEditorError, /conversion-for-unknown-group/ );
+			}, /conversion-for-unknown-group/ );
 		} );
 
 		it( 'should return proper helpers for group', () => {
@@ -81,7 +81,7 @@ describe( 'Conversion', () => {
 
 		it( 'should be chainable', () => {
 			const helpers = conversion.for( 'upcast' );
-			const addResult = helpers.add( () => {} );
+			const addResult = helpers.add( () => { } );
 
 			expect( addResult ).to.equal( helpers );
 		} );

+ 11 - 6
packages/ckeditor5-engine/tests/conversion/mapper.js

@@ -643,20 +643,25 @@ describe( 'Mapper', () => {
 			expect( elements ).to.deep.equal( [ viewA, viewB, viewC ] );
 		} );
 
-		it( 'should unbind all elements from a marker name', () => {
+		it( 'should unbind element from a marker name', () => {
 			const viewA = new ViewElement( 'a' );
 			const viewB = new ViewElement( 'b' );
-			const viewC = new ViewElement( 'c' );
 
 			mapper.bindElementToMarker( viewA, 'marker' );
 			mapper.bindElementToMarker( viewB, 'marker' );
-			mapper.bindElementToMarker( viewC, 'marker' );
 
-			mapper.unbindElementsFromMarkerName( 'marker' );
+			mapper.unbindElementFromMarkerName( viewA, 'marker' );
 
-			const elements = mapper.markerNameToElements( 'marker' );
+			expect( Array.from( mapper.markerNameToElements( 'marker' ) ) ).to.deep.equal( [ viewB ] );
+
+			mapper.unbindElementFromMarkerName( viewB, 'marker' );
+
+			expect( mapper.markerNameToElements( 'marker' ) ).to.be.null;
+
+			// Removing an element from non-existing group or non-bound element should not cause a crash.
+			mapper.unbindElementFromMarkerName( viewB, 'marker' );
 
-			expect( elements ).to.be.null;
+			expect( mapper.markerNameToElements( 'marker' ) ).to.be.null;
 		} );
 	} );
 

+ 3 - 3
packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js

@@ -19,7 +19,7 @@ import ModelRange from '../../src/model/range';
 import ModelWriter from '../../src/model/writer';
 
 import first from '@ckeditor/ckeditor5-utils/src/first';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'UpcastDispatcher', () => {
 	let model;
@@ -472,9 +472,9 @@ describe( 'UpcastDispatcher', () => {
 					conversionApi.convertItem( viewArray, data.modelCursor );
 				} );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
-				} ).to.throw( CKEditorError, /^view-conversion-dispatcher-incorrect-result/ );
+				}, /^view-conversion-dispatcher-incorrect-result/, model );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyArray.calledOnce ).to.be.true;

+ 3 - 0
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -558,6 +558,9 @@ describe( 'debug tools', () => {
 			const view = editor.editing.view;
 			const viewDoc = view.document;
 
+			// Reset model document version to ensure it will start at 0.
+			model.document.version = 0;
+
 			model.change( () => {
 				const insert = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
 				model.applyOperation( insert );

+ 70 - 0
packages/ckeditor5-engine/tests/model/differ.js

@@ -1759,6 +1759,76 @@ describe( 'Differ', () => {
 		} );
 	} );
 
+	describe( 'refreshItem()', () => {
+		it( 'should mark given element to be removed and added again', () => {
+			const p = root.getChild( 0 );
+
+			differ.refreshItem( p );
+
+			expectChanges( [
+				{ type: 'remove', name: 'paragraph', length: 1, position: model.createPositionBefore( p ) },
+				{ type: 'insert', name: 'paragraph', length: 1, position: model.createPositionBefore( p ) }
+			], true );
+		} );
+
+		it( 'should mark given text proxy to be removed and added again', () => {
+			const p = root.getChild( 0 );
+			const range = model.createRangeIn( p );
+			const textProxy = [ ...range.getItems() ][ 0 ];
+
+			differ.refreshItem( textProxy );
+
+			expectChanges( [
+				{ type: 'remove', name: '$text', length: 3, position: model.createPositionAt( p, 0 ) },
+				{ type: 'insert', name: '$text', length: 3, position: model.createPositionAt( p, 0 ) }
+			], true );
+		} );
+
+		it( 'inside a new element', () => {
+			// Since the refreshed element is inside a new element, it should not be listed on changes list.
+			model.change( () => {
+				insert( new Element( 'blockQuote', null, new Element( 'paragraph' ) ), new Position( root, [ 2 ] ) );
+
+				differ.refreshItem( root.getChild( 2 ).getChild( 0 ) );
+
+				expectChanges( [
+					{ type: 'insert', name: 'blockQuote', length: 1, position: new Position( root, [ 2 ] ) }
+				] );
+			} );
+		} );
+
+		it( 'markers refreshing', () => {
+			model.change( () => {
+				// Refreshed element contains marker.
+				model.markers._set( 'markerA', new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) );
+
+				// Marker contains refreshed element.
+				model.markers._set( 'markerB', new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ) );
+
+				// Intersecting.
+				model.markers._set( 'markerC', new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) ) );
+
+				// Not intersecting.
+				model.markers._set( 'markerD', new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 1 ] ) ) );
+			} );
+
+			const markersToRefresh = [ 'markerA', 'markerB', 'markerC' ];
+
+			differ.refreshItem( root.getChild( 1 ) );
+
+			expectChanges( [
+				{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) },
+				{ type: 'insert', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) }
+			] );
+
+			const markersToRemove = differ.getMarkersToRemove().map( entry => entry.name );
+			const markersToAdd = differ.getMarkersToAdd().map( entry => entry.name );
+
+			expect( markersToRefresh ).to.deep.equal( markersToRemove );
+			expect( markersToRefresh ).to.deep.equal( markersToAdd );
+		} );
+	} );
+
 	describe( 'getChanges()', () => {
 		let position, p1, rangeAttrChange, range;
 

+ 9 - 13
packages/ckeditor5-engine/tests/model/document.js

@@ -9,8 +9,8 @@ import RootElement from '../../src/model/rootelement';
 import Text from '../../src/model/text';
 import Batch from '../../src/model/batch';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Document', () => {
 	let model, doc;
@@ -44,7 +44,7 @@ describe( 'Document', () => {
 				baseVersion: 0,
 				isDocumentOperation: true,
 				_execute: sinon.stub().returns( data ),
-				_validate: () => {}
+				_validate: () => { }
 			};
 
 			batch = new Batch();
@@ -82,14 +82,12 @@ describe( 'Document', () => {
 			const operation = {
 				baseVersion: 1,
 				isDocumentOperation: true,
-				_execute: () => {}
+				_execute: () => { }
 			};
 
-			expect(
-				() => {
-					model.applyOperation( operation );
-				}
-			).to.throw( CKEditorError, /^model-document-applyOperation-wrong-version/ );
+			expectToThrowCKEditorError( () => {
+				model.applyOperation( operation );
+			}, /^model-document-applyOperation-wrong-version/, model );
 		} );
 	} );
 
@@ -130,11 +128,9 @@ describe( 'Document', () => {
 		it( 'should throw an error when trying to create a second root with the same name', () => {
 			doc.createRoot( '$root', 'rootName' );
 
-			expect(
-				() => {
-					doc.createRoot( '$root', 'rootName' );
-				}
-			).to.throw( CKEditorError, /model-document-createRoot-name-exists/ );
+			expectToThrowCKEditorError( () => {
+				doc.createRoot( '$root', 'rootName' );
+			}, /model-document-createRoot-name-exists/, model );
 		} );
 	} );
 

+ 5 - 5
packages/ckeditor5-engine/tests/model/documentfragment.js

@@ -7,7 +7,7 @@ import Element from '../../src/model/element';
 import Text from '../../src/model/text';
 import TextProxy from '../../src/model/textproxy';
 import DocumentFragment from '../../src/model/documentfragment';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DocumentFragment', () => {
 	describe( 'constructor()', () => {
@@ -111,13 +111,13 @@ describe( 'DocumentFragment', () => {
 		} );
 
 		it( 'should throw if given offset is too high or too low', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				frag.offsetToIndex( -1 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, frag );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				frag.offsetToIndex( 55 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, frag );
 		} );
 
 		it( 'should return length if given offset is equal to maxOffset', () => {

+ 25 - 25
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -15,11 +15,11 @@ import InsertOperation from '../../src/model/operation/insertoperation';
 import MoveOperation from '../../src/model/operation/moveoperation';
 import AttributeOperation from '../../src/model/operation/attributeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { setData, getData } from '../../src/dev-utils/model';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DocumentSelection', () => {
 	let model, doc, root, selection, liveRange, range;
@@ -596,9 +596,9 @@ describe( 'DocumentSelection', () => {
 
 	describe( '_setTo()', () => {
 		it( 'should throw an error when range is invalid', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection._setTo( [ { invalid: 'range' } ] );
-			} ).to.throw( CKEditorError, /model-selection-set-ranges-not-range/ );
+			}, /model-selection-set-ranges-not-range/, model );
 		} );
 
 		it( 'should log a warning when trying to set selection to the graveyard', () => {
@@ -1094,19 +1094,19 @@ describe( 'DocumentSelection', () => {
 			selection._restoreGravity( overrideUid );
 
 			// Wrong UID
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection._restoreGravity( 'foo' );
-			} ).to.throw( CKEditorError, /^document-selection-gravity-wrong-restore/ );
+			}, /^document-selection-gravity-wrong-restore/, model );
 
 			// Already restored
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection._restoreGravity( overrideUid );
-			} ).to.throw( CKEditorError, /^document-selection-gravity-wrong-restore/ );
+			}, /^document-selection-gravity-wrong-restore/, model );
 
 			// No UID
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection._restoreGravity();
-			} ).to.throw( CKEditorError, /^document-selection-gravity-wrong-restore/ );
+			}, /^document-selection-gravity-wrong-restore/, model );
 		} );
 
 		it( 'should revert default gravity when is overridden', () => {
@@ -1601,41 +1601,41 @@ describe( 'DocumentSelection', () => {
 		root._removeChildren( 0, root.childCount );
 		root._appendChild( '\uD83D\uDCA9' );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 	} );
 
 	it( 'should throw if one of ranges starts or ends between base character and combining mark', () => {
 		root._removeChildren( 0, root.childCount );
 		root._appendChild( 'foo̻̐ͩbar' );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 3 ), Position._createAt( root, 9 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 4 ), Position._createAt( root, 9 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 5 ), Position._createAt( root, 9 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 3 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 4 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 5 ) ) );
-		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
+		}, /document-selection-wrong-position/, model );
 	} );
 } );

+ 5 - 5
packages/ckeditor5-engine/tests/model/element.js

@@ -8,7 +8,7 @@ import Element from '../../src/model/element';
 import Text from '../../src/model/text';
 import TextProxy from '../../src/model/textproxy';
 import count from '@ckeditor/ckeditor5-utils/src/count';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Element', () => {
 	describe( 'constructor()', () => {
@@ -301,13 +301,13 @@ describe( 'Element', () => {
 		} );
 
 		it( 'should throw if given offset is too high or too low', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				element.offsetToIndex( -1 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, element );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				element.offsetToIndex( 55 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, element );
 		} );
 
 		it( 'should return length if given offset is equal to maxOffset', () => {

+ 6 - 4
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -10,7 +10,7 @@ import Text from '../../src/model/text';
 import Position from '../../src/model/position';
 import LivePosition from '../../src/model/liveposition';
 import Range from '../../src/model/range';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'LivePosition', () =>
 {
@@ -42,9 +42,11 @@ describe( 'LivePosition', () =>
 	} );
 
 	it( 'should throw if given root is not a RootElement', () => {
-		expect( () => {
-			new LivePosition( new DocumentFragment(), [ 1 ] ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, /model-liveposition-root-not-rootelement/ );
+		const docFrag = new DocumentFragment();
+
+		expectToThrowCKEditorError( () => {
+			new LivePosition( docFrag, [ 1 ] ); // eslint-disable-line no-new
+		}, /model-liveposition-root-not-rootelement/, docFrag );
 	} );
 
 	it( 'should listen to the model applyOperation event', () => {

+ 13 - 13
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -9,7 +9,7 @@ import Range from '../../src/model/range';
 import LiveRange from '../../src/model/liverange';
 import Text from '../../src/model/text';
 import Model from '../../src/model/model';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'MarkerCollection', () => {
 	let markers, range, range2, doc, root;
@@ -207,9 +207,9 @@ describe( 'MarkerCollection', () => {
 		} );
 
 		it( 'should throw if marker does not exist', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				markers._refresh( 'name' );
-			} ).to.throw( CKEditorError, 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.' );
+			}, /^markercollection-refresh-marker-not-exists:/, markers );
 		} );
 	} );
 
@@ -292,25 +292,25 @@ describe( 'Marker', () => {
 
 		model.markers._remove( 'name' );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			marker.getRange();
-		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+		}, /^marker-destroyed/ );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			marker.getStart();
-		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+		}, /^marker-destroyed/ );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			marker.getEnd();
-		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+		}, /^marker-destroyed/ );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			marker.managedUsingOperations;
-		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+		}, /^marker-destroyed/ );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			marker.affectsData;
-		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+		}, /^marker-destroyed/ );
 	} );
 
 	it( 'should attach live range to marker', () => {

+ 9 - 1
packages/ckeditor5-engine/tests/model/model.js

@@ -774,7 +774,15 @@ describe( 'Model', () => {
 
 	describe( 'createBatch()', () => {
 		it( 'should return instance of Batch', () => {
-			expect( model.createBatch() ).to.be.instanceof( Batch );
+			const batch = model.createBatch();
+			expect( batch ).to.be.instanceof( Batch );
+			expect( batch.type ).to.equal( 'default' );
+		} );
+
+		it( 'should allow to define type of Batch', () => {
+			const batch = model.createBatch( 'transparent' );
+			expect( batch ).to.be.instanceof( Batch );
+			expect( batch.type ).to.equal( 'transparent' );
 		} );
 	} );
 

+ 7 - 11
packages/ckeditor5-engine/tests/model/node.js

@@ -8,8 +8,8 @@ import DocumentFragment from '../../src/model/documentfragment';
 import Node from '../../src/model/node';
 import Element from '../../src/model/element';
 import Text from '../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Node', () => {
 	let doc, root, node,
@@ -128,11 +128,9 @@ describe( 'Node', () => {
 		it( 'should throw an error if parent does not contain element', () => {
 			node.parent = new Element( 'parent' );
 
-			expect(
-				() => {
-					node.index;
-				}
-			).to.throw( CKEditorError, /model-node-not-found-in-parent/ );
+			expectToThrowCKEditorError( () => {
+				node.index;
+			}, /model-node-not-found-in-parent/, node.parent );
 		} );
 	} );
 
@@ -188,11 +186,9 @@ describe( 'Node', () => {
 		it( 'should throw an error if parent does not contain element', () => {
 			node.parent = new Element( 'parent' );
 
-			expect(
-				() => {
-					node.startOffset;
-				}
-			).to.throw( CKEditorError, /model-node-not-found-in-parent/ );
+			expectToThrowCKEditorError( () => {
+				node.startOffset;
+			}, /model-node-not-found-in-parent/, node.parent );
 		} );
 	} );
 

+ 12 - 11
packages/ckeditor5-engine/tests/model/nodelist.js

@@ -6,7 +6,8 @@
 import NodeList from '../../src/model/nodelist';
 import Element from '../../src/model/element';
 import Text from '../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'NodeList', () => {
 	let nodes, p, foo, img;
@@ -82,13 +83,13 @@ describe( 'NodeList', () => {
 		} );
 
 		it( 'should throw if given offset is too high or too low', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				nodes.indexToOffset( -1 );
-			} ).to.throw( CKEditorError, /model-nodelist-index-out-of-bounds/ );
+			}, /model-nodelist-index-out-of-bounds/, nodes );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				nodes.indexToOffset( 99 );
-			} ).to.throw( CKEditorError, /model-nodelist-index-out-of-bounds/ );
+			}, /model-nodelist-index-out-of-bounds/, nodes );
 		} );
 
 		it( 'should return length if given offset is equal to maxOffset', () => {
@@ -106,13 +107,13 @@ describe( 'NodeList', () => {
 		} );
 
 		it( 'should throw if given offset is too high or too low', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				nodes.offsetToIndex( -1 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, nodes );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				nodes.offsetToIndex( 55 );
-			} ).to.throw( CKEditorError, /nodelist-offset-out-of-bounds/ );
+			}, /nodelist-offset-out-of-bounds/, nodes );
 		} );
 
 		it( 'should return length if given offset is equal to maxOffset', () => {
@@ -157,9 +158,9 @@ describe( 'NodeList', () => {
 		} );
 
 		it( 'should throw if not a Node is inserted', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				nodes._insertNodes( 0, [ 'foo' ] );
-			} ).to.throw( CKEditorError, /nodelist-insertNodes-not-node/ );
+			}, /nodelist-insertNodes-not-node/, nodes );
 		} );
 	} );
 

+ 8 - 7
packages/ckeditor5-engine/tests/model/operation/attributeoperation.js

@@ -9,8 +9,9 @@ import Element from '../../../src/model/element';
 import AttributeOperation from '../../../src/model/operation/attributeoperation';
 import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import count from '@ckeditor/ckeditor5-utils/src/count';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'AttributeOperation', () => {
 	let model, doc, root;
@@ -202,7 +203,7 @@ describe( 'AttributeOperation', () => {
 		it( 'should throw an error when one try to remove and the attribute does not exists', () => {
 			root._insertChild( 0, new Text( 'x' ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const operation = new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'foo',
@@ -212,13 +213,13 @@ describe( 'AttributeOperation', () => {
 				);
 
 				operation._validate();
-			} ).to.throw( CKEditorError, /attribute-operation-wrong-old-value/ );
+			}, /attribute-operation-wrong-old-value/, model );
 		} );
 
 		it( 'should throw an error when one try to insert and the attribute already exists', () => {
 			root._insertChild( 0, new Text( 'x', { x: 1 } ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const operation = new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'x',
@@ -228,7 +229,7 @@ describe( 'AttributeOperation', () => {
 				);
 
 				operation._validate();
-			} ).to.throw( CKEditorError, /attribute-operation-attribute-exists/ );
+			}, /attribute-operation-attribute-exists/, model );
 		} );
 
 		it( 'should not throw when attribute value is the same', () => {
@@ -253,7 +254,7 @@ describe( 'AttributeOperation', () => {
 				new Element( 'paragraph', null, new Text( 'Bar' ) )
 			] );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const operation = new AttributeOperation(
 					new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 1, 1 ] ) ),
 					'x',
@@ -263,7 +264,7 @@ describe( 'AttributeOperation', () => {
 				);
 
 				operation._validate();
-			} ).to.throw( CKEditorError, /attribute-operation-range-not-flat/ );
+			}, /attribute-operation-range-not-flat/, model );
 		} );
 	} );
 

+ 4 - 3
packages/ckeditor5-engine/tests/model/operation/detachoperation.js

@@ -5,10 +5,11 @@
 
 import Model from '../../../src/model/model';
 import DetachOperation from '../../../src/model/operation/detachoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import Position from '../../../src/model/position';
 import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DetachOperation', () => {
 	let model, doc, docFrag, element;
@@ -43,9 +44,9 @@ describe( 'DetachOperation', () => {
 
 			const op = new DetachOperation( Position._createBefore( element ), 1 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				op._validate();
-			} ).to.throw( CKEditorError, /^detach-operation-on-document-node/ );
+			}, /^detach-operation-on-document-node/, model );
 		} );
 	} );
 

+ 2 - 2
packages/ckeditor5-engine/tests/model/operation/insertoperation.js

@@ -10,7 +10,7 @@ import InsertOperation from '../../../src/model/operation/insertoperation';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
 import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'InsertOperation', () => {
 	let model, doc, root;
@@ -225,7 +225,7 @@ describe( 'InsertOperation', () => {
 			const element = new Element( 'p' );
 			const op = new InsertOperation( new Position( root, [ 4 ] ), element, doc.version );
 
-			expect( () => op._validate() ).to.throw( CKEditorError, /insert-operation-position-invalid/ );
+			expectToThrowCKEditorError( () => op._validate(), /insert-operation-position-invalid/, model );
 		} );
 	} );
 

+ 5 - 5
packages/ckeditor5-engine/tests/model/operation/mergeoperation.js

@@ -9,7 +9,7 @@ import SplitOperation from '../../../src/model/operation/splitoperation';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'MergeOperation', () => {
 	let model, doc, root, gy, gyPos;
@@ -119,7 +119,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-target-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-target-position-invalid/, model );
 		} );
 
 		it( 'should throw an error if source position is in root', () => {
@@ -136,7 +136,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-source-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-source-position-invalid/, model );
 		} );
 
 		it( 'should throw an error if target position is invalid', () => {
@@ -153,7 +153,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-source-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-source-position-invalid/, model );
 		} );
 
 		it( 'should throw an error if number of nodes to move is invalid', () => {
@@ -170,7 +170,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-how-many-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-how-many-invalid/, model );
 		} );
 	} );
 

+ 6 - 5
packages/ckeditor5-engine/tests/model/operation/moveoperation.js

@@ -8,7 +8,8 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'MoveOperation', () => {
 	let model, doc, root, gy;
@@ -153,7 +154,7 @@ describe( 'MoveOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /move-operation-nodes-do-not-exist/ );
+			expectToThrowCKEditorError( () => operation._validate(), /move-operation-nodes-do-not-exist/, model );
 		} );
 
 		it( 'should throw an error if target or source parent-element specified by position does not exist', () => {
@@ -170,7 +171,7 @@ describe( 'MoveOperation', () => {
 
 			root._removeChildren( 1 );
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /move-operation-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /move-operation-position-invalid/, model );
 		} );
 
 		it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', () => {
@@ -183,7 +184,7 @@ describe( 'MoveOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /move-operation-range-into-itself/ );
+			expectToThrowCKEditorError( () => operation._validate(), /move-operation-range-into-itself/, model );
 		} );
 
 		it( 'should throw an error if operation tries to move a range into a sub-tree of a node that is in that range', () => {
@@ -197,7 +198,7 @@ describe( 'MoveOperation', () => {
 				doc.version
 			);
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /move-operation-node-into-itself/ );
+			expectToThrowCKEditorError( () => operation._validate(), /move-operation-node-into-itself/, model );
 		} );
 
 		it( 'should not throw an error if operation move a range into a sibling', () => {

+ 6 - 5
packages/ckeditor5-engine/tests/model/operation/renameoperation.js

@@ -7,7 +7,8 @@ import Model from '../../../src/model/model';
 import Element from '../../../src/model/element';
 import RenameOperation from '../../../src/model/operation/renameoperation';
 import Position from '../../../src/model/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'RenameOperation', () => {
 	const oldName = 'oldName';
@@ -66,17 +67,17 @@ describe( 'RenameOperation', () => {
 		it( 'should throw an error if position is not before an element', () => {
 			const op = new RenameOperation( Position._createAt( root, 'end' ), oldName, newName, doc.version );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				op._validate();
-			} ).to.throw( CKEditorError, /rename-operation-wrong-position/ );
+			}, /rename-operation-wrong-position/, model );
 		} );
 
 		it( 'should throw an error if oldName is different than renamed element name', () => {
 			const op = new RenameOperation( position, 'foo', newName, doc.version );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				op._validate();
-			} ).to.throw( CKEditorError, /rename-operation-wrong-name/ );
+			}, /rename-operation-wrong-name/, model );
 		} );
 
 		it( 'should not throw when new name is the same as previous', () => {

+ 11 - 11
packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js

@@ -7,7 +7,7 @@ import Model from '../../../src/model/model';
 import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import RootAttributeOperation from '../../../src/model/operation/rootattributeoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'RootAttributeOperation', () => {
 	let model, doc, root;
@@ -181,7 +181,7 @@ describe( 'RootAttributeOperation', () => {
 			const parent = new Element( 'p' );
 			parent._appendChild( child );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const op = new RootAttributeOperation(
 					child,
 					'foo',
@@ -191,11 +191,11 @@ describe( 'RootAttributeOperation', () => {
 				);
 
 				op._validate();
-			} ).to.throw( CKEditorError, /rootattribute-operation-not-a-root/ );
+			}, /rootattribute-operation-not-a-root/ );
 		} );
 
 		it( 'should throw an error when trying to change document fragment', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const op = new RootAttributeOperation(
 					new DocumentFragment(),
 					'foo',
@@ -205,11 +205,11 @@ describe( 'RootAttributeOperation', () => {
 				);
 
 				op._validate();
-			} ).to.throw( CKEditorError, /rootattribute-operation-not-a-root/ );
+			}, /rootattribute-operation-not-a-root/ );
 		} );
 
 		it( 'should throw an error when trying to remove an attribute that does not exists', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const op = new RootAttributeOperation(
 					root,
 					'foo',
@@ -219,13 +219,13 @@ describe( 'RootAttributeOperation', () => {
 				);
 
 				op._validate();
-			} ).to.throw( CKEditorError, /rootattribute-operation-wrong-old-value/ );
+			}, /rootattribute-operation-wrong-old-value/, model );
 		} );
 
 		it( 'should throw an error when trying to add an attribute that already exists', () => {
 			root._setAttribute( 'x', 1 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const op = new RootAttributeOperation(
 					root,
 					'x',
@@ -235,7 +235,7 @@ describe( 'RootAttributeOperation', () => {
 				);
 
 				op._validate();
-			} ).to.throw( CKEditorError, /rootattribute-operation-attribute-exists/ );
+			}, /rootattribute-operation-attribute-exists/, model );
 		} );
 	} );
 
@@ -304,9 +304,9 @@ describe( 'RootAttributeOperation', () => {
 
 			serialized.root = 'no-root';
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				RootAttributeOperation.fromJSON( serialized, doc );
-			} ).to.throw( CKEditorError, /rootattribute-operation-fromjson-no-root/ );
+			}, /rootattribute-operation-fromjson-no-root/ );
 		} );
 	} );
 } );

+ 6 - 5
packages/ckeditor5-engine/tests/model/operation/splitoperation.js

@@ -9,7 +9,8 @@ import MergeOperation from '../../../src/model/operation/mergeoperation';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'SplitOperation', () => {
 	let model, doc, root, gy, gyPos;
@@ -125,7 +126,7 @@ describe( 'SplitOperation', () => {
 
 			const operation = new SplitOperation( new Position( root, [ 0, 8 ] ), 3, null, doc.version );
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /split-operation-position-invalid/, model );
 		} );
 
 		it( 'should throw an error if split position is in root', () => {
@@ -136,7 +137,7 @@ describe( 'SplitOperation', () => {
 			const operation = new SplitOperation( new Position( root, [ 0, 0 ] ), 3, null, doc.version );
 			operation.splitPosition = new Position( root, [ 1 ] );
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-split-in-root/ );
+			expectToThrowCKEditorError( () => operation._validate(), /split-operation-split-in-root/, model );
 		} );
 
 		it( 'should throw an error if number of nodes to move is invalid', () => {
@@ -146,7 +147,7 @@ describe( 'SplitOperation', () => {
 
 			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 6, null, doc.version );
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-how-many-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /split-operation-how-many-invalid/, model );
 		} );
 
 		it( 'should throw an error if graveyard position is invalid', () => {
@@ -156,7 +157,7 @@ describe( 'SplitOperation', () => {
 
 			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 4, gyPos, doc.version );
 
-			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-graveyard-position-invalid/ );
+			expectToThrowCKEditorError( () => operation._validate(), /split-operation-graveyard-position-invalid/, model );
 		} );
 	} );
 

+ 5 - 6
packages/ckeditor5-engine/tests/model/operation/utils.js

@@ -12,8 +12,7 @@ import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
 import * as utils from '../../../src/model/operation/utils';
 import { getData } from '../../../src/dev-utils/model';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 let model, doc, root;
 
@@ -81,9 +80,9 @@ describe( 'Operation utils', () => {
 		} );
 
 		it( 'should throw if given range is not flat', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				utils._remove( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1, 2 ] ) ) );
-			} ).to.throw( CKEditorError, /operation-utils-remove-range-not-flat/ );
+			}, /operation-utils-remove-range-not-flat/ );
 		} );
 	} );
 
@@ -103,9 +102,9 @@ describe( 'Operation utils', () => {
 		} );
 
 		it( 'should throw if given range is not flat', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				utils._move( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1, 2 ] ) ), null );
-			} ).to.throw( CKEditorError, /operation-utils-move-range-not-flat/ );
+			}, /operation-utils-move-range-not-flat/ );
 		} );
 	} );
 

+ 21 - 20
packages/ckeditor5-engine/tests/model/position.js

@@ -17,9 +17,10 @@ import MoveOperation from '../../src/model/operation/moveoperation';
 import RenameOperation from '../../src/model/operation/renameoperation';
 import MergeOperation from '../../src/model/operation/mergeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import LivePosition from '../../src/model/liveposition';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Position', () => {
 	let doc, model, root, otherRoot, p, ul, li1, li2, f, o, z, b, a, r, foz, bar;
@@ -107,19 +108,19 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should throw error if given path is incorrect', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new Position( root, {} ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /model-position-path-incorrect/ );
+			}, /model-position-path-incorrect/, model );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new Position( root, [] ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /model-position-path-incorrect/ );
+			}, /model-position-path-incorrect/, model );
 		} );
 
 		it( 'should throw error if given root is invalid', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new Position( new Text( 'a' ) ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /model-position-root-invalid/ );
+			}, /model-position-root-invalid/ );
 
 			expect( () => {
 				new Position(); // eslint-disable-line no-new
@@ -130,7 +131,9 @@ describe( 'Position', () => {
 	describe( 'static creators', () => {
 		describe( '_createAt()', () => {
 			it( 'should throw if no offset is passed', () => {
-				expect( () => Position._createAt( ul ) ).to.throw( CKEditorError, /model-createPositionAt-offset-required/ );
+				expectToThrowCKEditorError( () => {
+					Position._createAt( ul );
+				}, /model-createPositionAt-offset-required/, model );
 			} );
 
 			it( 'should create positions from positions', () => {
@@ -187,9 +190,9 @@ describe( 'Position', () => {
 			} );
 
 			it( 'throws when parent is not an element', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					Position._createAt( b, 0 );
-				} ).to.throw( CKEditorError, /^model-position-parent-incorrect/ );
+				}, /^model-position-parent-incorrect/, model );
 			} );
 
 			it( 'works with a doc frag', () => {
@@ -223,9 +226,9 @@ describe( 'Position', () => {
 			} );
 
 			it( 'should throw error if one try to create positions before root', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					Position._createBefore( root );
-				} ).to.throw( CKEditorError, /model-position-before-root/ );
+				}, /model-position-before-root/, model );
 			} );
 		} );
 
@@ -253,9 +256,9 @@ describe( 'Position', () => {
 			} );
 
 			it( 'should throw error if one try to make positions after root', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					Position._createAfter( root );
-				} ).to.throw( CKEditorError, /model-position-after-root/ );
+				}, /model-position-after-root/, model );
 			} );
 		} );
 	} );
@@ -954,7 +957,7 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should not update path if deletion position parent is a node from that path and ' +
-			'offset is after next node on that path', () => {
+		'offset is after next node on that path', () => {
 			const position = new Position( root, [ 1, 2, 3 ] );
 			const transformed = position._getTransformedByDeletion( new Position( root, [ 1, 3 ] ), 2 );
 
@@ -1088,11 +1091,9 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should throw error when creating object in document that does not have provided root', () => {
-			expect(
-				() => {
-					Position.fromJSON( { root: 'noroot', path: [ 0 ] }, doc );
-				}
-			).to.throw( CKEditorError, /model-position-fromjson-no-root/ );
+			expectToThrowCKEditorError( () => {
+				Position.fromJSON( { root: 'noroot', path: [ 0 ] }, doc );
+			}, /model-position-fromjson-no-root/, model );
 		} );
 	} );
 

+ 4 - 3
packages/ckeditor5-engine/tests/model/range.js

@@ -16,7 +16,8 @@ import MoveOperation from '../../src/model/operation/moveoperation';
 import RenameOperation from '../../src/model/operation/renameoperation';
 import MergeOperation from '../../src/model/operation/mergeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Range', () => {
 	let doc, range, start, end, root, otherRoot, gy, model;
@@ -210,9 +211,9 @@ describe( 'Range', () => {
 			} );
 
 			it( 'should throw if empty array is passed', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					Range._createFromRanges( [] );
-				} ).to.throw( CKEditorError, /^range-create-from-ranges-empty-array/ );
+				}, /^range-create-from-ranges-empty-array/ );
 			} );
 
 			it( 'should return a copy of the range if only one range was passed', () => {

+ 39 - 40
packages/ckeditor5-engine/tests/model/schema.js

@@ -5,8 +5,6 @@
 
 import Schema, { SchemaContext } from '../../src/model/schema';
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 import Model from '../../src/model/model';
 
 import DocumentFragment from '../../src/model/documentfragment';
@@ -19,6 +17,7 @@ import Range from '../../src/model/range';
 import { getData, setData, stringify, parse } from '../../src/dev-utils/model';
 
 import AttributeOperation from '../../src/model/operation/attributeoperation';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Schema', () => {
 	let schema, root1, r1p1, r1p2, r1bQ, r1bQp, root2;
@@ -61,9 +60,9 @@ describe( 'Schema', () => {
 		it( 'throws when trying to register for a single item twice', () => {
 			schema.register( 'foo' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				schema.register( 'foo' );
-			} ).to.throw( CKEditorError, /^schema-cannot-register-item-twice:/ );
+			}, /^schema-cannot-register-item-twice:/, schema );
 		} );
 	} );
 
@@ -90,9 +89,9 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'throws when trying to extend a not yet registered item', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				schema.extend( 'foo' );
-			} ).to.throw( CKEditorError, /^schema-cannot-extend-missing-item:/ );
+			}, /^schema-cannot-extend-missing-item:/, schema );
 		} );
 	} );
 
@@ -846,9 +845,9 @@ describe( 'Schema', () => {
 
 			const position = Position._createBefore( listItem );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				expect( schema.checkMerge( position ) );
-			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+			}, /^schema-check-merge-no-element-before:/, schema );
 		} );
 
 		it( 'throws an error if the node before the position is not the element', () => {
@@ -864,9 +863,9 @@ describe( 'Schema', () => {
 
 			const position = Position._createBefore( listItem );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				expect( schema.checkMerge( position ) );
-			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+			}, /^schema-check-merge-no-element-before:/, schema );
 		} );
 
 		it( 'throws an error if there is no element after the position', () => {
@@ -881,9 +880,9 @@ describe( 'Schema', () => {
 
 			const position = Position._createAfter( listItem );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				expect( schema.checkMerge( position ) );
-			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-after:/ );
+			}, /^schema-check-merge-no-element-after:/, schema );
 		} );
 
 		it( 'throws an error if the node after the position is not the element', () => {
@@ -899,9 +898,9 @@ describe( 'Schema', () => {
 
 			const position = Position._createBefore( listItem );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				expect( schema.checkMerge( position ) );
-			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+			}, /^schema-check-merge-no-element-before:/, schema );
 		} );
 
 		// This is an invalid case by definition – the baseElement should not contain disallowed elements
@@ -1005,16 +1004,16 @@ describe( 'Schema', () => {
 			setData(
 				model,
 				'<div>' +
-					'<section>' +
-						'<article>' +
-							'<paragraph>[foo]</paragraph>' +
-						'</article>' +
-					'</section>' +
-					'<widget>' +
-						'<image>' +
-							'<caption>b[a]r</caption>' +
-						'</image>' +
-					'</widget>' +
+				'<section>' +
+				'<article>' +
+				'<paragraph>[foo]</paragraph>' +
+				'</article>' +
+				'</section>' +
+				'<widget>' +
+				'<image>' +
+				'<caption>b[a]r</caption>' +
+				'</image>' +
+				'</widget>' +
 				'</div>'
 			);
 
@@ -1026,11 +1025,11 @@ describe( 'Schema', () => {
 			setData(
 				model,
 				'<div>' +
-					'<section>' +
-						'<article>' +
-							'<paragraph>[foo]</paragraph>' +
-						'</article>' +
-					'</section>' +
+				'<section>' +
+				'<article>' +
+				'<paragraph>[foo]</paragraph>' +
+				'</article>' +
+				'</section>' +
 				'</div>' +
 				'<section>b[]ar</section>'
 			);
@@ -1042,10 +1041,10 @@ describe( 'Schema', () => {
 			setData(
 				model,
 				'<image>' +
-					'<caption>[Foo</caption>' +
+				'<caption>[Foo</caption>' +
 				'</image>' +
 				'<article>' +
-					'<paragraph>Paragraph in article]</paragraph>' +
+				'<paragraph>Paragraph in article]</paragraph>' +
 				'</article>' +
 				'<paragraph>Paragraph item 1</paragraph>' +
 				'<paragraph>Paragraph [item 2]</paragraph>'
@@ -1060,10 +1059,10 @@ describe( 'Schema', () => {
 				'<paragraph>Paragraph item 1</paragraph>' +
 				'<paragraph>Paragraph [item 2]</paragraph>' +
 				'<image>' +
-					'<caption>[Foo</caption>' +
+				'<caption>[Foo</caption>' +
 				'</image>' +
 				'<article>' +
-					'<paragraph>Paragraph in article]</paragraph>' +
+				'<paragraph>Paragraph in article]</paragraph>' +
 				'</article>'
 			);
 
@@ -1814,12 +1813,12 @@ describe( 'Schema', () => {
 				expect( getData( model, { withoutSelection: true } ) )
 					.to.equal(
 						'<div>' +
-							'<paragraph>' +
-								'<$text b="1">foo</$text>' +
-								'<image b="1"></image>' +
-							'</paragraph>' +
-							'<$text a="1">bar</$text>' +
-							'<image a="1"></image>' +
+						'<paragraph>' +
+						'<$text b="1">foo</$text>' +
+						'<image b="1"></image>' +
+						'</paragraph>' +
+						'<$text a="1">bar</$text>' +
+						'<image a="1"></image>' +
 						'</div>'
 					);
 			} );
@@ -2139,7 +2138,7 @@ describe( 'Schema', () => {
 			} );
 
 			it( 'passes $root>paragraph and $root2>paragraph – where $root2 inherits content of $root' +
-				'and paragraph inherits allowWhere from $block', () => {
+			'and paragraph inherits allowWhere from $block', () => {
 				schema.register( '$root' );
 				schema.register( '$root2', {
 					allowContentOf: '$root'

+ 17 - 17
packages/ckeditor5-engine/tests/model/selection.js

@@ -10,11 +10,11 @@ import Range from '../../src/model/range';
 import Position from '../../src/model/position';
 import LiveRange from '../../src/model/liverange';
 import Selection from '../../src/model/selection';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import { parse, setData } from '../../src/dev-utils/model';
 import Schema from '../../src/model/schema';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Selection', () => {
 	let model, doc, root, selection, liveRange, range, range1, range2, range3;
@@ -107,7 +107,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should throw an error if added ranges intersects', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				// eslint-disable-next-line no-new
 				new Selection( [
 					liveRange,
@@ -116,14 +116,14 @@ describe( 'Selection', () => {
 						new Position( root, [ 1, 2 ] )
 					)
 				] );
-			} ).to.throw( CKEditorError, /model-selection-range-intersects/ );
+			}, /model-selection-range-intersects/, model );
 		} );
 
 		it( 'should throw an error when trying to set selection to not selectable', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				// eslint-disable-next-line no-new
 				new Selection( {} );
-			} ).to.throw( /model-selection-setTo-not-selectable/ );
+			}, /model-selection-setTo-not-selectable/ );
 		} );
 	} );
 
@@ -268,7 +268,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should throw an error if added ranges intersects', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection.setTo( [
 					liveRange,
 					new Range(
@@ -276,19 +276,19 @@ describe( 'Selection', () => {
 						new Position( root, [ 1, 2 ] )
 					)
 				] );
-			} ).to.throw( CKEditorError, /model-selection-range-intersects/ );
+			}, /model-selection-range-intersects/, model );
 		} );
 
 		it( 'should throw an error when trying to set selection to not selectable', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection.setTo( {} );
-			} ).to.throw( /model-selection-setTo-not-selectable/ );
+			}, /model-selection-setTo-not-selectable/ );
 		} );
 
 		it( 'should throw an error when trying to set selection to not selectable #2', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection.setTo();
-			} ).to.throw( /model-selection-setTo-not-selectable/ );
+			}, /model-selection-setTo-not-selectable/ );
 		} );
 
 		it( 'should allow setting selection inside an element', () => {
@@ -352,9 +352,9 @@ describe( 'Selection', () => {
 			} );
 
 			it( 'should throw if second parameter is not passed', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					selection.setTo( root );
-				} ).to.throw( CKEditorError, /model-selection-setTo-required-second-parameter/ );
+				}, /model-selection-setTo-required-second-parameter/, model );
 			} );
 
 			it( 'should set selection at given offset in given parent', () => {
@@ -438,9 +438,9 @@ describe( 'Selection', () => {
 		it( 'throws if there are no ranges in selection', () => {
 			const endPos = Position._createAt( root, 'end' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection.setFocus( endPos );
-			} ).to.throw( CKEditorError, /model-selection-setFocus-no-ranges/ );
+			}, /model-selection-setFocus-no-ranges/, model );
 		} );
 
 		it( 'modifies existing collapsed selection', () => {
@@ -617,9 +617,9 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should throw an error when range is invalid', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				selection._setRanges( [ { invalid: 'range' } ] );
-			} ).to.throw( CKEditorError, /model-selection-set-ranges-not-range/ );
+			}, /model-selection-set-ranges-not-range/, model );
 		} );
 
 		it( 'should remove all ranges and add given ranges', () => {

+ 10 - 9
packages/ckeditor5-engine/tests/model/textproxy.js

@@ -7,7 +7,8 @@ import Element from '../../src/model/element';
 import Text from '../../src/model/text';
 import TextProxy from '../../src/model/textproxy';
 import Model from '../../src/model/model';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'TextProxy', () => {
 	let model, doc, element, textProxy, root, textProxyNoParent, text, textNoParent;
@@ -82,23 +83,23 @@ describe( 'TextProxy', () => {
 	} );
 
 	it( 'should throw if wrong offsetInText is passed', () => {
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			new TextProxy( text, -1, 2 ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, /model-textproxy-wrong-offsetintext/ );
+		}, /model-textproxy-wrong-offsetintext/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			new TextProxy( text, 9, 1 ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, /model-textproxy-wrong-offsetintext/ );
+		}, /model-textproxy-wrong-offsetintext/, model );
 	} );
 
 	it( 'should throw if wrong length is passed', () => {
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			new TextProxy( text, 2, -1 ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, /model-textproxy-wrong-length/ );
+		}, /model-textproxy-wrong-length/, model );
 
-		expect( () => {
+		expectToThrowCKEditorError( () => {
 			new TextProxy( text, 2, 9 ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, /model-textproxy-wrong-length/ );
+		}, /model-textproxy-wrong-length/, model );
 	} );
 
 	describe( 'is', () => {

+ 10 - 9
packages/ckeditor5-engine/tests/model/treewalker.js

@@ -10,7 +10,8 @@ import Text from '../../src/model/text';
 import TreeWalker from '../../src/model/treewalker';
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'TreeWalker', () => {
 	let model, doc, root, img1, paragraph, ba, r, img2, x,
@@ -48,23 +49,23 @@ describe( 'TreeWalker', () => {
 
 	describe( 'constructor()', () => {
 		it( 'should throw if neither boundaries nor starting position is set', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new TreeWalker(); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /^model-tree-walker-no-start-position/ );
+			}, /^model-tree-walker-no-start-position/, null );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new TreeWalker( {} ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /^model-tree-walker-no-start-position/ );
+			}, /^model-tree-walker-no-start-position/, null );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new TreeWalker( { singleCharacters: true } ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /^model-tree-walker-no-start-position/ );
+			}, /^model-tree-walker-no-start-position/, null );
 		} );
 
 		it( 'should throw if walking direction is unknown', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				new TreeWalker( { startPosition: rootBeginning, direction: 'unknown' } ); // eslint-disable-line no-new
-			} ).to.throw( CKEditorError, /^model-tree-walker-unknown-direction/ );
+			}, /^model-tree-walker-unknown-direction/, model );
 		} );
 	} );
 

+ 37 - 0
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -1254,6 +1254,43 @@ describe( 'DataController utils', () => {
 			expect( getData( model ) ).to.equal( '<limit>ab[]foo</limit>' );
 			expect( stringify( root, affectedRange ) ).to.equal( '<limit>[ab]foo</limit>' );
 		} );
+
+		describe( 'when allowed element is above limit element in document tree', () => {
+			// $root > table > tableRow > tableCell > paragraph
+			// After inserting new table ( allowed in root ), empty paragraph shouldn't be removed from current tableCell.
+			beforeEach( () => {
+				const schema = model.schema;
+
+				schema.register( 'wrapper', {
+					isLimit: true,
+					isBlock: true,
+					isObject: true,
+					allowWhere: '$block'
+				} );
+
+				schema.extend( 'paragraph', { allowIn: 'limit' } );
+				schema.extend( 'limit', { allowIn: 'wrapper' } );
+			} );
+
+			it( 'should not remove empty elements when not-allowed element is paste', () => {
+				setData( model, '<wrapper><limit><paragraph>[]</paragraph></limit></wrapper>' );
+
+				// pasted content is forbidden in current selection
+				const affectedRange = insertHelper( '<wrapper><limit><paragraph>foo</paragraph></limit></wrapper>' );
+
+				expect( getData( model ) ).to.equal( '<wrapper><limit>[]<paragraph></paragraph></limit></wrapper>' );
+				expect( stringify( root, affectedRange ) ).to.equal( '<wrapper><limit><paragraph>[]</paragraph></limit></wrapper>' );
+			} );
+
+			it( 'should correctly paste allowed nodes', () => {
+				setData( model, '<wrapper><limit><paragraph>[]</paragraph></limit></wrapper>' );
+
+				const affectedRange = insertHelper( '<paragraph>foo</paragraph>' );
+
+				expect( getData( model ) ).to.equal( '<wrapper><limit><paragraph>foo</paragraph>[]</limit></wrapper>' );
+				expect( stringify( root, affectedRange ) ).to.equal( '<wrapper><limit>[<paragraph>foo</paragraph>]</limit></wrapper>' );
+			} );
+		} );
 	} );
 
 	// Helper function that parses given content and inserts it at the cursor position.

+ 168 - 113
packages/ckeditor5-engine/tests/model/writer.js

@@ -15,9 +15,10 @@ import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 
 import count from '@ckeditor/ckeditor5-utils/src/count';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import { getNodesAndText } from '../../tests/model/_utils/utils';
 import DocumentSelection from '../../src/model/documentselection';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Writer', () => {
 	let model, doc, batch;
@@ -300,17 +301,17 @@ describe( 'Writer', () => {
 
 			insert( node, root );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				insert( node, docFrag );
-			} ).to.throw();
+			}, /^model-writer-insert-forbidden-move/, model );
 		} );
 
 		it( 'should transfer markers from given DocumentFragment', () => {
 			const root = doc.createRoot();
-
 			const docFrag = createDocumentFragment();
 
 			appendText( 'abcd', root );
+
 			appendElement( 'p', docFrag );
 			insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );
 
@@ -331,14 +332,47 @@ describe( 'Writer', () => {
 			expect( modelMarker.affectsData ).to.be.true;
 		} );
 
+		// https://github.com/ckeditor/ckeditor5-engine/issues/1721.
+		it( 'should update a marker if DocumentFragment has a marker that is already in the model (markers have the same name)', () => {
+			const root = doc.createRoot();
+			const docFrag = createDocumentFragment();
+
+			// <root><p></p><p>[ab]cd</p></root>.
+			appendText( 'abcd', root );
+
+			// <docFrag><p>f[oo b]ar</p></docFrag>.
+			appendElement( 'p', docFrag );
+			insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );
+
+			model.change( writer => {
+				const range = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 2 ] ) );
+
+				writer.addMarker( 'marker', { range, usingOperation: true } );
+			} );
+
+			docFrag.markers.set( 'marker', new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 5 ] ) ) );
+
+			insert( docFrag, new Position( root, [ 2 ] ) );
+
+			expect( Array.from( model.markers ).length ).to.equal( 1 );
+
+			const modelMarker = model.markers.get( 'marker' );
+			const range = modelMarker.getRange();
+			expect( range.root ).to.equal( root );
+			expect( range.start.path ).to.deep.equal( [ 2, 1 ] );
+			expect( range.end.path ).to.deep.equal( [ 2, 5 ] );
+			expect( modelMarker.managedUsingOperations ).to.be.true;
+			expect( modelMarker.affectsData ).to.be.true;
+		} );
+
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 			const root = doc.createRoot();
 			const node = createText( 'foo' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insert( node, root );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -462,9 +496,9 @@ describe( 'Writer', () => {
 			const writer = new Writer( model, batch );
 			const parent = createDocumentFragment();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insertText( 'foo', parent );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -584,9 +618,9 @@ describe( 'Writer', () => {
 			const writer = new Writer( model, batch );
 			const child = createElement( 'child' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insertElement( 'foo', child, 'after' );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -714,9 +748,9 @@ describe( 'Writer', () => {
 
 			insert( node, root );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				append( node, docFrag );
-			} ).to.throw();
+			}, /^model-writer-insert-forbidden-move/, model );
 		} );
 	} );
 
@@ -770,9 +804,9 @@ describe( 'Writer', () => {
 			const writer = new Writer( model, batch );
 			const parent = createDocumentFragment();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.appendText( 'foo', parent );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -824,9 +858,9 @@ describe( 'Writer', () => {
 			const writer = new Writer( model, batch );
 			const parent = createDocumentFragment();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.appendElement( 'foo', parent );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -884,9 +918,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.setAttribute( 'a', 1, node );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 
@@ -911,9 +945,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.removeAttribute( 'b', node );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 		} );
@@ -1056,9 +1090,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.setAttribute( 'a', 1, getRange( 0, 20 ) );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 
@@ -1132,9 +1166,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.removeAttribute( 'a', getRange( 3, 15 ) );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 		} );
@@ -1191,9 +1225,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.setAttribute( 'a', 1, p );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 
@@ -1214,9 +1248,9 @@ describe( 'Writer', () => {
 				it( 'should throw when trying to use detached writer', () => {
 					const writer = new Writer( model, batch );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.removeAttribute( 'b', root );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 
@@ -1278,9 +1312,9 @@ describe( 'Writer', () => {
 					const writer = new Writer( model, batch );
 					const element = createElement( 'x' );
 
-					expect( () => {
+					expectToThrowCKEditorError( () => {
 						writer.clearAttributes( element );
-					} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+					}, /^writer-incorrect-use/, model );
 				} );
 			} );
 		} );
@@ -1384,9 +1418,9 @@ describe( 'Writer', () => {
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), item );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -1425,29 +1459,50 @@ describe( 'Writer', () => {
 			expect( docFrag.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
 		} );
 
-		it( 'should create a marker operation if a marker was affected', () => {
-			const markerRange = new Range( Position._createAt( p2, 0 ), Position._createAt( p2, 0 ) );
+		describe( 'should create a marker operation if a marker was affected', () => {
+			it( '<p>Foo[</p><p>Bar]</p>', () => {
+				test( p1, 'end', p2, 0 );
+			} );
 
-			addMarker( 'name', {
-				range: markerRange,
-				usingOperation: true
+			it( '<p>[Foo</p><p>]Bar</p>', () => {
+				test( p1, 0, p2, 0 );
 			} );
 
-			const documentVersion = model.document.version;
+			it( '<p>[Foo</p>]<p>Bar</p>', () => {
+				test( p1, 0, root, 1 );
+			} );
 
-			merge( Position._createAfter( p1 ) );
+			it( '<p>Foo</p>[<p>Bar]</p>', () => {
+				test( root, 1, p2, 'end' );
+			} );
 
-			const history = model.document.history;
+			function test( startElement, startOffset, endElement, endOffset ) {
+				const markerRange = new Range(
+					Position._createAt( startElement, startOffset ),
+					Position._createAt( endElement, endOffset )
+				);
 
-			const lastOperation = history._operations[ history._operations.length - 1 ];
-			const secondLastOperation = history._operations[ history._operations.length - 2 ];
+				addMarker( 'name', {
+					range: markerRange,
+					usingOperation: true
+				} );
 
-			expect( secondLastOperation.type ).to.equal( 'marker' );
-			expect( secondLastOperation.oldRange.isEqual( markerRange ) );
-			expect( secondLastOperation.newRange.isEqual( markerRange ) );
+				const documentVersion = model.document.version;
 
-			expect( lastOperation.type ).to.equal( 'merge' );
-			expect( model.document.version ).to.equal( documentVersion + 2 );
+				merge( Position._createAfter( p1 ) );
+
+				const history = model.document.history;
+
+				const lastOperation = history._operations[ history._operations.length - 1 ];
+				const secondLastOperation = history._operations[ history._operations.length - 2 ];
+
+				expect( secondLastOperation.type ).to.equal( 'marker' );
+				expect( secondLastOperation.oldRange.isEqual( markerRange ) );
+				expect( secondLastOperation.newRange.isEqual( markerRange ) );
+
+				expect( lastOperation.type ).to.equal( 'merge' );
+				expect( model.document.version ).to.equal( documentVersion + 2 );
+			}
 		} );
 
 		it( 'should not create a marker operation if affected marker was not using operations', () => {
@@ -1471,23 +1526,23 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if there is no element after', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				merge( new Position( root, [ 2 ] ) );
-			} ).to.throw( CKEditorError, /^writer-merge-no-element-after/ );
+			}, /^writer-merge-no-element-after/, model );
 		} );
 
 		it( 'should throw if there is no element before', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				merge( new Position( root, [ 0, 2 ] ) );
-			} ).to.throw( CKEditorError, /^writer-merge-no-element-before/ );
+			}, /^writer-merge-no-element-before/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.merge( new Position( root, [ 1 ] ) );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -1561,33 +1616,33 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if object to move is not a range', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				move( root.getChild( 0 ), new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^writer-move-invalid-range/ );
+			}, /^writer-move-invalid-range/, model );
 		} );
 
 		it( 'should throw if given range is not flat', () => {
 			const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				move( notFlatRange, new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^writer-move-range-not-flat/ );
+			}, /^writer-move-range-not-flat/, model );
 		} );
 
 		it( 'should throw if range is going to be moved to the other document', () => {
 			const docFrag = createDocumentFragment();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				move( range, docFrag, 0 );
-			} ).to.throw( CKEditorError, /^writer-move-different-document/ );
+			}, /^writer-move-different-document/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.move( range, new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -1706,9 +1761,9 @@ describe( 'Writer', () => {
 			it( 'should throw when trying to use detached writer', () => {
 				const writer = new Writer( model, batch );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.remove( range );
-				} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+				}, /^writer-incorrect-use/, model );
 			} );
 		} );
 
@@ -1763,9 +1818,9 @@ describe( 'Writer', () => {
 			it( 'should throw when trying to use detached writer', () => {
 				const writer = new Writer( model, batch );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.remove( range );
-				} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+				}, /^writer-incorrect-use/, model );
 			} );
 		} );
 	} );
@@ -1796,18 +1851,18 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if not an Element instance is passed', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				rename( new Text( 'abc' ), 'h' );
-			} ).to.throw( CKEditorError, /^writer-rename-not-element-instance/ );
+			}, /^writer-rename-not-element-instance/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 			const p = new Element( 'p', null, new Text( 'abc' ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.rename( p, 'h' );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -1875,25 +1930,25 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if we try to split a root', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				split( new Position( root, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^writer-split-element-no-parent/ );
+			}, /^writer-split-element-no-parent/, model );
 		} );
 
 		it( 'should throw if we try to split an element with no parent', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const element = createElement( 'p' );
 
 				split( new Position( element, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^writer-split-element-no-parent/ );
+			}, /^writer-split-element-no-parent/, model );
 		} );
 
 		it( 'should throw if we try to split a document fragment', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				const documentFragment = createDocumentFragment();
 
 				split( new Position( documentFragment, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^writer-split-element-no-parent/ );
+			}, /^writer-split-element-no-parent/, model );
 		} );
 
 		it( 'should split elements to limitElement', () => {
@@ -1927,17 +1982,17 @@ describe( 'Writer', () => {
 			root._insertChild( 0, div );
 			root._insertChild( 1, section );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				split( new Position( p, [ 3 ] ), section );
-			} ).to.throw( CKEditorError, /^writer-split-invalid-limit-element/ );
+			}, /^writer-split-invalid-limit-element/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.split( new Position( root, [ 0, 3 ] ) );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -1987,34 +2042,34 @@ describe( 'Writer', () => {
 			root._insertChild( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] );
 			const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				wrap( notFlatRange, 'p' );
-			} ).to.throw( CKEditorError, /^writer-wrap-range-not-flat/ );
+			}, /^writer-wrap-range-not-flat/, model );
 		} );
 
 		it( 'should throw if element to wrap with has children #1', () => {
 			const p = new Element( 'p', [], new Text( 'a' ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				wrap( range, p );
-			} ).to.throw( CKEditorError, /^writer-wrap-element-not-empty/ );
+			}, /^writer-wrap-element-not-empty/, model );
 		} );
 
 		it( 'should throw if element to wrap with has children #2', () => {
 			const p = new Element( 'p' );
 			root._insertChild( 0, p );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				wrap( range, p );
-			} ).to.throw( CKEditorError, /^writer-wrap-element-attached/ );
+			}, /^writer-wrap-element-attached/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.wrap( range, 'p' );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -2047,17 +2102,17 @@ describe( 'Writer', () => {
 		it( 'should throw if element to unwrap has no parent', () => {
 			const element = new Element( 'p' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				unwrap( element );
-			} ).to.throw( CKEditorError, /^writer-unwrap-element-no-parent/ );
+			}, /^writer-unwrap-element-no-parent/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( p );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -2071,15 +2126,15 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if options.usingOperation is not defined', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name' );
-			} ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperation/ );
+			}, /^writer-addMarker-no-usingOperation/, model );
 		} );
 
 		it( 'should throw if name and range is defined but options.usingOperation is not defined', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name', { range } );
-			} ).to.throw( CKEditorError, /^writer-addMarker-no-usingOperation/ );
+			}, /^writer-addMarker-no-usingOperation/, model );
 		} );
 
 		it( 'should add marker to the document marker collection', () => {
@@ -2129,9 +2184,9 @@ describe( 'Writer', () => {
 
 			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name', { range: range2, usingOperation: false } );
-			} ).to.throw( CKEditorError, /^writer-addMarker-marker-exists/ );
+			}, /^writer-addMarker-marker-exists/, model );
 		} );
 
 		it( 'should use operations when having set usingOperation to true', () => {
@@ -2149,24 +2204,24 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw if marker with given name does not exist and range is not passed', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name', { usingOperation: true } );
-			} ).to.throw( CKEditorError, /^writer-addMarker-no-range/ );
+			}, /^writer-addMarker-no-range/, model );
 		} );
 
 		it( 'should throw if marker is set directly and range is not passed', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name', { usingOperation: false } );
-			} ).to.throw( CKEditorError, /^writer-addMarker-no-range/ );
+			}, /^writer-addMarker-no-range/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const marker = addMarker( 'name', { range, usingOperation: false } );
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.addMarker( marker, null, { usingOperation: true } );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -2400,16 +2455,16 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw when range and usingOperations were not provided', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				addMarker( 'name', { range, usingOperation: false } );
 				updateMarker( 'name', {} );
-			} ).to.throw( CKEditorError, /^writer-updateMarker-wrong-options/ );
+			}, /^writer-updateMarker-wrong-options/, model );
 		} );
 
 		it( 'should throw when marker provided by name does not exists', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				updateMarker( 'name', { usingOperation: false } );
-			} ).to.throw( CKEditorError, /^writer-updateMarker-marker-not-exists/ );
+			}, /^writer-updateMarker-marker-not-exists/, model );
 		} );
 
 		it( 'should only refresh the marker when there is no provided options to update', () => {
@@ -2431,9 +2486,9 @@ describe( 'Writer', () => {
 			const marker = addMarker( 'name', { range, usingOperation: false } );
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.updateMarker( marker, { usingOperation: true } );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 	} );
 
@@ -2454,17 +2509,17 @@ describe( 'Writer', () => {
 		} );
 
 		it( 'should throw when trying to remove non existing marker', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				removeMarker( 'name' );
-			} ).to.throw( CKEditorError, /^writer-removeMarker-no-marker/ );
+			}, /^writer-removeMarker-no-marker/, model );
 		} );
 
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.removeMarker( 'name' );
-			} ).to.throw( CKEditorError, /^writer-incorrect-use/ );
+			}, /^writer-incorrect-use/, model );
 		} );
 
 		it( 'should accept marker instance', () => {

+ 4 - 3
packages/ckeditor5-engine/tests/view/attributeelement.js

@@ -6,7 +6,8 @@
 import AttributeElement from '../../src/view/attributeelement';
 import Element from '../../src/view/element';
 import { parse } from '../../src/dev-utils/view';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'AttributeElement', () => {
 	describe( 'constructor()', () => {
@@ -128,9 +129,9 @@ describe( 'AttributeElement', () => {
 		it( 'should throw if attribute element has no id', () => {
 			const attribute = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				attribute.getElementsWithSameId();
-			} ).to.throw( CKEditorError, /attribute-element-get-elements-with-same-id-no-id/ );
+			}, /attribute-element-get-elements-with-same-id-no-id/ );
 		} );
 	} );
 

+ 19 - 19
packages/ckeditor5-engine/tests/view/documentselection.js

@@ -10,11 +10,11 @@ import Document from '../../src/view/document';
 import Element from '../../src/view/element';
 import Text from '../../src/view/text';
 import Position from '../../src/view/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import createViewRoot from './_utils/createroot';
 import { parse } from '../../src/dev-utils/view';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DocumentSelection', () => {
 	let documentSelection, el, range1, range2, range3;
@@ -113,27 +113,27 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should throw an error when range is invalid', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				// eslint-disable-next-line no-new
 				new DocumentSelection( [ { invalid: 'range' } ] );
-			} ).to.throw( CKEditorError, /view-selection-add-range-not-range/ );
+			}, /view-selection-add-range-not-range/ );
 		} );
 
 		it( 'should throw an error when ranges intersects', () => {
 			const text = el.getChild( 0 );
 			const range2 = Range._createFromParentsAndOffsets( text, 7, text, 15 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				// eslint-disable-next-line no-new
 				new DocumentSelection( [ range1, range2 ] );
-			} ).to.throw( CKEditorError, 'view-selection-range-intersects' );
+			}, 'view-selection-range-intersects' );
 		} );
 
 		it( 'should throw an error when trying to set to not selectable', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				// eslint-disable-next-line no-new
 				new DocumentSelection( {} );
-			} ).to.throw( /view-selection-setTo-not-selectable/ );
+			}, /view-selection-setTo-not-selectable/ );
 		} );
 	} );
 
@@ -203,9 +203,9 @@ describe( 'DocumentSelection', () => {
 		it( 'throws if there are no ranges in selection', () => {
 			const endPos = Position._createAt( el, 'end' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				documentSelection._setFocus( endPos );
-			} ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ );
+			}, /view-selection-setFocus-no-ranges/, documentSelection );
 		} );
 
 		it( 'modifies existing collapsed selection', () => {
@@ -808,17 +808,17 @@ describe( 'DocumentSelection', () => {
 			it( 'should throw an error when trying to set to not selectable', () => {
 				const otherSelection = new DocumentSelection();
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					otherSelection._setTo( {} );
-				} ).to.throw( /view-selection-setTo-not-selectable/ );
+				}, /view-selection-setTo-not-selectable/ );
 			} );
 
 			it( 'should throw an error when trying to set to not selectable #2', () => {
 				const otherSelection = new DocumentSelection();
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					otherSelection._setTo();
-				} ).to.throw( /view-selection-setTo-not-selectable/ );
+				}, /view-selection-setTo-not-selectable/ );
 			} );
 		} );
 
@@ -860,9 +860,9 @@ describe( 'DocumentSelection', () => {
 			it( 'should throw an error when the second parameter is not passed and first is an item', () => {
 				const foo = new Text( 'foo' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					documentSelection._setTo( foo );
-				} ).to.throw( CKEditorError, /view-selection-setTo-required-second-parameter/ );
+				}, /view-selection-setTo-required-second-parameter/, documentSelection );
 			} );
 
 			it( 'should collapse selection at node and flag', () => {
@@ -1007,18 +1007,18 @@ describe( 'DocumentSelection', () => {
 
 		describe( 'throwing errors', () => {
 			it( 'should throw an error when range is invalid', () => {
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					documentSelection._setTo( [ { invalid: 'range' } ] );
-				} ).to.throw( CKEditorError, /view-selection-add-range-not-range/ );
+				}, /view-selection-add-range-not-range/, documentSelection );
 			} );
 
 			it( 'should throw when range is intersecting with already added range', () => {
 				const text = el.getChild( 0 );
 				const range2 = Range._createFromParentsAndOffsets( text, 7, text, 15 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					documentSelection._setTo( [ range1, range2 ] );
-				} ).to.throw( CKEditorError, 'view-selection-range-intersects' );
+				}, 'view-selection-range-intersects', documentSelection );
 			} );
 		} );
 

+ 14 - 19
packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

@@ -451,7 +451,7 @@ describe( 'DomConverter', () => {
 			// At the end.
 			test( 'x_', 'x ' );
 			test( 'x _', 'x  ' );
-			test( 'x_ _', 'x   ' );
+			test( 'x __', 'x   ' );
 			test( 'x _ _', 'x    ' );
 
 			// In the middle.
@@ -464,7 +464,7 @@ describe( 'DomConverter', () => {
 			test( '_x_', ' x ' );
 			test( '_ x _x _', '  x  x  ' );
 			test( '_ _x x _', '   x x  ' );
-			test( '_ _x x_ _', '   x x   ' );
+			test( '_ _x x __', '   x x   ' );
 			test( '_ _x _ _x_', '   x    x ' );
 			test( '_', ' ' );
 
@@ -477,10 +477,6 @@ describe( 'DomConverter', () => {
 			test( 'x_', 'x ' );
 			test( 'x__', 'x_ ' );
 			test( 'x___', 'x__ ' );
-			// This is an edge case, but it's impossible to write elegant and compact algorithm that is also
-			// 100% correct. We might assume that expected result is `x  _` but it will be converted to `x   `
-			// by the algorithm. This is acceptable, though.
-			test( 'x __', 'x   ' );
 
 			test( 'x_x', 'x_x' );
 			test( 'x___x', 'x___x' );
@@ -496,26 +492,25 @@ describe( 'DomConverter', () => {
 			test( [ 'x', 'y' ], 'xy' );
 			test( [ 'x ', 'y' ], 'x y' );
 			test( [ 'x _', 'y' ], 'x  y' );
-			test( [ 'x _ ', 'y' ], 'x   y' );
+			test( [ 'x __', 'y' ], 'x   y' );
 			test( [ 'x _  _', 'y' ], 'x    y' );
 
 			test( [ 'x', ' y' ], 'x y' );
-			test( [ 'x ', '_y' ], 'x  y' );
-			test( [ 'x_ ', '_y' ], 'x   y' );
-			test( [ 'x _ ', '_y' ], 'x    y' );
-			test( [ 'x_ _ ', '_y' ], 'x     y' );
+			test( [ 'x_', ' y' ], 'x  y' );
+			test( [ 'x _', ' y' ], 'x   y' );
+			test( [ 'x __', ' y' ], 'x    y' );
+			test( [ 'x _ _', ' y' ], 'x     y' );
 
 			test( [ 'x', ' _y' ], 'x  y' );
-			test( [ 'x ', '_ y' ], 'x   y' );
-			test( [ 'x_ ', '_ y' ], 'x    y' );
-			test( [ 'x _ ', '_ y' ], 'x     y' );
-			test( [ 'x_ _ ', '_ y' ], 'x      y' );
+			test( [ 'x_', ' _y' ], 'x   y' );
+			test( [ 'x _', ' _y' ], 'x    y' );
+			test( [ 'x __', ' _y' ], 'x     y' );
+			test( [ 'x _ _', ' _y' ], 'x      y' );
 
 			// Some tests with hard &nbsp;
 			test( [ 'x', '_y' ], 'x_y' );
 			test( [ 'x_', 'y' ], 'x_y' );
-			test( [ 'x_', ' y' ], 'x_ y' );
-			test( [ 'x__', ' y' ], 'x__ y' );
+			test( [ 'x__', ' y' ], 'x_  y' );
 			test( [ 'x_ _', ' y' ], 'x_   y' );
 
 			it( 'not in preformatted blocks', () => {
@@ -605,9 +600,9 @@ describe( 'DomConverter', () => {
 				expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' );
 			} );
 
-			it( 'not before a <br> (nbsp+space)', () => {
+			it( 'not before a <br> (space+nbsp)', () => {
 				const domP = createElement( document, 'p', {}, [
-					document.createTextNode( 'foo\u00a0 ' ),
+					document.createTextNode( 'foo \u00a0' ),
 					createElement( document, 'br' )
 				] );
 

+ 35 - 34
packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js

@@ -212,7 +212,7 @@ describe( 'DomConverter', () => {
 
 				const domDiv = converter.viewToDom( viewDiv, document );
 
-				expect( domDiv.innerHTML ).to.equal( 'x &nbsp;x &nbsp; x x <b>&nbsp;x </b><i><b><u>&nbsp;x</u></b></i>' );
+				expect( domDiv.innerHTML ).to.equal( 'x &nbsp;x &nbsp; x x&nbsp;<b> x&nbsp;</b><i><b><u> x</u></b></i>' );
 			} );
 
 			it( 'all together', () => {
@@ -232,7 +232,7 @@ describe( 'DomConverter', () => {
 				const domDiv = converter.viewToDom( viewDiv, document );
 
 				expect( domDiv.innerHTML ).to.equal(
-					'<p>&nbsp;x &nbsp;x &nbsp; x x <b>&nbsp;x </b><i><b><u>&nbsp;x&nbsp;</u></b></i></p><p>&nbsp; x &nbsp;</p>'
+					'<p>&nbsp;x &nbsp;x &nbsp; x x&nbsp;<b> x&nbsp;</b><i><b><u> x&nbsp;</u></b></i></p><p>&nbsp; x &nbsp;</p>'
 				);
 			} );
 
@@ -321,86 +321,87 @@ describe( 'DomConverter', () => {
 			test( 'x _ x', 'x _ x' );
 			test( 'x  _  x', 'x __ _x' );
 
-			// Two text nodes.
 			test( [ 'x', 'y' ], 'xy' );
 			test( [ 'x ', 'y' ], 'x y' );
 			test( [ 'x  ', 'y' ], 'x _y' );
-			test( [ 'x   ', 'y' ], 'x _ y' );
+			test( [ 'x   ', 'y' ], 'x __y' );
 			test( [ 'x    ', 'y' ], 'x _ _y' );
 
 			test( [ 'x', ' y' ], 'x y' );
-			test( [ 'x ', ' y' ], 'x _y' );
+			test( [ 'x ', ' y' ], 'x_ y' );
 			test( [ 'x  ', ' y' ], 'x _ y' );
-			test( [ 'x   ', ' y' ], 'x _ _y' );
+			test( [ 'x   ', ' y' ], 'x __ y' );
 			test( [ 'x    ', ' y' ], 'x _ _ y' );
 
 			test( [ 'x', '_y' ], 'x_y' );
 			test( [ 'x ', '_y' ], 'x _y' );
 			test( [ 'x  ', '_y' ], 'x __y' );
-			test( [ 'x   ', '_y' ], 'x _ _y' );
+
+			// Two text nodes.
+			test( [ 'x   ', '_y' ], 'x ___y' );
 			test( [ 'x    ', '_y' ], 'x _ __y' );
 
 			test( [ 'x', '  y' ], 'x _y' );
-			test( [ 'x ', '  y' ], 'x _ y' );
+			test( [ 'x ', '  y' ], 'x_ _y' );
 			test( [ 'x  ', '  y' ], 'x _ _y' );
-			test( [ 'x   ', '  y' ], 'x _ _ y' );
+			test( [ 'x   ', '  y' ], 'x __ _y' );
 			test( [ 'x    ', '  y' ], 'x _ _ _y' );
 
 			test( [ 'x', '   y' ], 'x _ y' );
-			test( [ 'x ', '   y' ], 'x _ _y' );
+			test( [ 'x ', '   y' ], 'x_ _ y' );
 			test( [ 'x  ', '   y' ], 'x _ _ y' );
-			test( [ 'x   ', '   y' ], 'x _ _ _y' );
+			test( [ 'x   ', '   y' ], 'x __ _ y' );
 			test( [ 'x    ', '   y' ], 'x _ _ _ y' );
 
-			// "Non-empty" + "empty" text nodes.
 			test( [ 'x',	' '		], 'x_' );
 			test( [ 'x',	'  '	], 'x _' );
 			test( [ 'x',	'   '	], 'x __' );
-			test( [ 'x ',	' '		], 'x _' );
-			test( [ 'x ',	'  '	], 'x __' );
-			test( [ 'x ',	'   '	], 'x _ _' );
+			test( [ 'x ',	' '		], 'x__' );
+			test( [ 'x ',	'  '	], 'x_ _' );
+			test( [ 'x ',	'   '	], 'x_ __' );
 			test( [ 'x  ',	' '		], 'x __' );
 			test( [ 'x  ',	'  '	], 'x _ _' );
 			test( [ 'x  ',	'   '	], 'x _ __' );
-			test( [ 'x   ',	' '		], 'x _ _' );
-			test( [ 'x   ',	'  '	], 'x _ __' );
-			test( [ 'x   ',	'   '	], 'x _ _ _' );
+			test( [ 'x   ',	' '		], 'x ___' );
+			test( [ 'x   ',	'  '	], 'x __ _' );
+			test( [ 'x   ',	'   '	], 'x __ __' );
 
 			test( [ ' ',	'x'		], '_x' );
 			test( [ '  ',	'x'		], '_ x' );
 			test( [ '   ',	'x'		], '_ _x' );
 			test( [ ' ',	' x'	], '_ x' );
-			test( [ '  ',	' x'	], '_ _x' );
+			test( [ '  ',	' x'	], '__ x' );
 			test( [ '   ',	' x'	], '_ _ x' );
 			test( [ ' ',	'  x'	], '_ _x' );
-			test( [ '  ',	'  x'	], '_ _ x' );
+			test( [ '  ',	'  x'	], '__ _x' );
 			test( [ '   ',	'  x'	], '_ _ _x' );
 			test( [ ' ',	'   x'	], '_ _ x' );
-			test( [ '  ',	'   x'	], '_ _ _x' );
+			test( [ '  ',	'   x'	], '__ _ x' );
 			test( [ '   ',	'   x'	], '_ _ _ x' );
 
+			// "Non-empty" + "empty" text nodes.
 			test( [ 'x',	' ',		'x'		],	'x x' );
-			test( [ 'x',	' ',		' x'	],	'x _x' );
+			test( [ 'x',	' ',		' x'	],	'x_ x' );
 			test( [ 'x',	'  ',		' x'	],	'x _ x' );
-			test( [ 'x',	'   ',		'  x'	],	'x _ _ x' );
-			test( [ 'x ',	' ',		' x'	],	'x _ x' );
-			test( [ 'x ',	'  ',		' x'	],	'x _ _x' );
-			test( [ 'x ',	'   ',		'  x'	],	'x _ _ _x' );
-			test( [ 'x  ',	' ',		' x'	],	'x _ _x' );
+			test( [ 'x',	'   ',		'  x'	],	'x __ _x' );
+			test( [ 'x ',	' ',		' x'	],	'x__ x' );
+			test( [ 'x ',	'  ',		' x'	],	'x_ _ x' );
+			test( [ 'x ',	'   ',		'  x'	],	'x_ __ _x' );
+			test( [ 'x  ',	' ',		' x'	],	'x __ x' );
 			test( [ 'x  ',	'  ',		' x'	],	'x _ _ x' );
-			test( [ 'x  ',	'   ',		'  x'	],	'x _ _ _ x' );
-			test( [ 'x   ',	' ',		' x'	],	'x _ _ x' );
-			test( [ 'x   ',	'  ',		' x'	],	'x _ _ _x' );
-			test( [ 'x   ',	'   ',		'  x'	],	'x _ _ _ _x' );
+			test( [ 'x  ',	'   ',		'  x'	],	'x _ __ _x' );
+			test( [ 'x   ',	' ',		' x'	],	'x ___ x' );
+			test( [ 'x   ',	'  ',		' x'	],	'x __ _ x' );
+			test( [ 'x   ',	'   ',		'  x'	],	'x __ __ _x' );
 
 			// "Empty" + "empty" text nodes.
 			test( [ ' ', ' ' ], '__' );
-			test( [ '  ', ' ' ], '_ _' );
+			test( [ '  ', ' ' ], '___' );
 			test( [ '   ', ' ' ], '_ __' );
 			test( [ ' ', '  ' ], '_ _' );
 			test( [ ' ', '   ' ], '_ __' );
-			test( [ '  ', '  ' ], '_ __' );
-			test( [ '  ', '   ' ], '_ _ _' );
+			test( [ '  ', '  ' ], '__ _' );
+			test( [ '  ', '   ' ], '__ __' );
 			test( [ '   ', '  ' ], '_ _ _' );
 			test( [ '   ', '   ' ], '_ _ __' );
 

+ 5 - 14
packages/ckeditor5-engine/tests/view/domconverter/whitespace-handling-integration.js

@@ -165,20 +165,20 @@ describe( 'DomConverter – whitespace handling – integration', () => {
 			return editor.destroy();
 		} );
 
-		it( 'single spaces around <br>', () => {
+		it( 'single spaces around <br> #1', () => {
 			editor.setData( '<p>foo&nbsp;<br>&nbsp;bar</p>' );
 
 			expect( getData( editor.model, { withoutSelection: true } ) )
-				.to.equal( '<paragraph>foo <softBreak></softBreak> bar</paragraph>' );
+				.to.equal( '<paragraph>foo\u00A0<softBreak></softBreak> bar</paragraph>' );
 
 			expect( editor.getData() ).to.equal( '<p>foo&nbsp;<br>&nbsp;bar</p>' );
 		} );
 
-		it( 'single spaces around <br> (normalization)', () => {
+		it( 'single spaces around <br> #2', () => {
 			editor.setData( '<p>foo&nbsp;<br> bar</p>' );
 
 			expect( getData( editor.model, { withoutSelection: true } ) )
-				.to.equal( '<paragraph>foo <softBreak></softBreak>bar</paragraph>' );
+				.to.equal( '<paragraph>foo\u00A0<softBreak></softBreak>bar</paragraph>' );
 
 			expect( editor.getData() ).to.equal( '<p>foo&nbsp;<br>bar</p>' );
 		} );
@@ -192,16 +192,7 @@ describe( 'DomConverter – whitespace handling – integration', () => {
 			expect( editor.getData() ).to.equal( '<p>foo &nbsp;<br>bar</p>' );
 		} );
 
-		it( 'two spaces before a <br> (normalization)', () => {
-			editor.setData( '<p>foo&nbsp; <br>bar</p>' );
-
-			expect( getData( editor.model, { withoutSelection: true } ) )
-				.to.equal( '<paragraph>foo  <softBreak></softBreak>bar</paragraph>' );
-
-			expect( editor.getData() ).to.equal( '<p>foo &nbsp;<br>bar</p>' );
-		} );
-
-		it( 'two spaces before a <br> (normalization to a model nbsp)', () => {
+		it( 'two nbsps before a <br>', () => {
 			editor.setData( '<p>foo&nbsp;&nbsp;<br>bar</p>' );
 
 			expect( getData( editor.model, { withoutSelection: true } ) )

+ 18 - 16
packages/ckeditor5-engine/tests/view/downcastwriter/breakattributes.js

@@ -12,14 +12,16 @@ import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
 import Range from '../../../src/view/range';
 import Position from '../../../src/view/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'breakAttributes()', () => {
-		let writer;
+		let writer, document;
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		describe( 'break position', () => {
@@ -153,17 +155,17 @@ describe( 'DowncastWriter', () => {
 				const p1 = new ContainerElement( 'p' );
 				const p2 = new ContainerElement( 'p' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( Range._createFromParentsAndOffsets( p1, 0, p2, 0 ) );
-				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+				}, 'view-writer-invalid-range-container', document );
 			} );
 
 			it( 'should throw when range has no parent container', () => {
 				const el = new AttributeElement( 'b' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( Range._createFromParentsAndOffsets( el, 0, el, 0 ) );
-				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+				}, 'view-writer-invalid-range-container', document );
 			} );
 
 			it( 'should not break text nodes if they are not in attribute elements', () => {
@@ -243,9 +245,9 @@ describe( 'DowncastWriter', () => {
 				new ContainerElement( 'p', null, img ); // eslint-disable-line no-new
 				const position = new Position( img, 0 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( position );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+				}, 'view-writer-cannot-break-empty-element', writer );
 			} );
 
 			it( 'should throw if breaking inside EmptyElement #2', () => {
@@ -254,9 +256,9 @@ describe( 'DowncastWriter', () => {
 				new ContainerElement( 'p', null, [ img, b ] ); // eslint-disable-line no-new
 				const range = Range._createFromParentsAndOffsets( img, 0, b, 0 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( range );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+				}, 'view-writer-cannot-break-empty-element', writer );
 			} );
 
 			it( 'should throw if breaking inside UIElement #1', () => {
@@ -264,9 +266,9 @@ describe( 'DowncastWriter', () => {
 				new ContainerElement( 'p', null, span ); // eslint-disable-line no-new
 				const position = new Position( span, 0 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( position );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+				}, 'view-writer-cannot-break-ui-element', writer );
 			} );
 
 			it( 'should throw if breaking inside UIElement #2', () => {
@@ -275,9 +277,9 @@ describe( 'DowncastWriter', () => {
 				new ContainerElement( 'p', null, [ span, b ] ); // eslint-disable-line no-new
 				const range = Range._createFromParentsAndOffsets( span, 0, b, 0 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.breakAttributes( range );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+				}, 'view-writer-cannot-break-ui-element', writer );
 			} );
 		} );
 	} );

+ 6 - 5
packages/ckeditor5-engine/tests/view/downcastwriter/breakcontainer.js

@@ -5,10 +5,11 @@
 
 import DowncastWriter from '../../../src/view/downcastwriter';
 import { stringify, parse } from '../../../src/dev-utils/view';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import ContainerElement from '../../../src/view/containerelement';
 import Position from '../../../src/view/position';
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'breakContainer()', () => {
@@ -66,18 +67,18 @@ describe( 'DowncastWriter', () => {
 		it( 'should throw if position parent is not container', () => {
 			const { selection } = parse( '<container:div>foo{}bar</container:div>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.breakContainer( selection.getFirstPosition() );
-			} ).to.throw( CKEditorError, /view-writer-break-non-container-element/ );
+			}, /view-writer-break-non-container-element/, writer );
 		} );
 
 		it( 'should throw if position parent is root', () => {
 			const element = new ContainerElement( 'div' );
 			const position = Position._createAt( element, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.breakContainer( position );
-			} ).to.throw( CKEditorError, /view-writer-break-root/ );
+			}, /view-writer-break-root/, writer );
 		} );
 	} );
 } );

+ 10 - 8
packages/ckeditor5-engine/tests/view/downcastwriter/clear.js

@@ -10,12 +10,13 @@ import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'clear()', () => {
-		let writer;
+		let writer, document;
 
 		// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
 		//
@@ -30,25 +31,26 @@ describe( 'DowncastWriter', () => {
 			expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
 		}
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		it( 'should throw when range placed in two containers', () => {
 			const p1 = new ContainerElement( 'p' );
 			const p2 = new ContainerElement( 'p' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.clear( Range._createFromParentsAndOffsets( p1, 0, p2, 0 ) );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should throw when range has no parent container', () => {
 			const el = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.clear( Range._createFromParentsAndOffsets( el, 0, el, 0 ) );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should remove matched element from range', () => {

+ 17 - 14
packages/ckeditor5-engine/tests/view/downcastwriter/insert.js

@@ -9,14 +9,15 @@ import Element from '../../../src/view/element';
 import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import { stringify, parse } from '../../../src/dev-utils/view';
 import AttributeElement from '../../../src/view/attributeelement';
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'insert()', () => {
-		let writer;
+		let writer, document;
 
 		// Executes test using `parse` and `stringify` utils functions.
 		//
@@ -31,8 +32,9 @@ describe( 'DowncastWriter', () => {
 			expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
 		}
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		it( 'should return collapsed range in insertion position when using empty array', () => {
@@ -155,9 +157,10 @@ describe( 'DowncastWriter', () => {
 			const element = new Element( 'b' );
 			const container = new ContainerElement( 'p' );
 			const position = new Position( container, 0 );
-			expect( () => {
+
+			expectToThrowCKEditorError( () => {
 				writer.insert( position, element );
-			} ).to.throw( CKEditorError, 'view-writer-insert-invalid-node' );
+			}, 'view-writer-insert-invalid-node', document );
 		} );
 
 		it( 'should throw when Element is inserted as child node', () => {
@@ -166,9 +169,9 @@ describe( 'DowncastWriter', () => {
 			const container = new ContainerElement( 'p' );
 			const position = new Position( container, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insert( position, root );
-			} ).to.throw( CKEditorError, 'view-writer-insert-invalid-node' );
+			}, 'view-writer-insert-invalid-node', document );
 		} );
 
 		it( 'should throw when position is not placed inside container', () => {
@@ -176,9 +179,9 @@ describe( 'DowncastWriter', () => {
 			const position = new Position( element, 0 );
 			const attributeElement = new AttributeElement( 'i' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insert( position, attributeElement );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-position-container' );
+			}, 'view-writer-invalid-position-container', document );
 		} );
 
 		it( 'should allow to insert EmptyElement into container', () => {
@@ -195,9 +198,9 @@ describe( 'DowncastWriter', () => {
 			const position = new Position( emptyElement, 0 );
 			const attributeElement = new AttributeElement( 'i' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insert( position, attributeElement );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+			}, 'view-writer-cannot-break-empty-element', document );
 		} );
 
 		it( 'should throw if trying to insert inside UIElement', () => {
@@ -206,9 +209,9 @@ describe( 'DowncastWriter', () => {
 			const position = new Position( uiElement, 0 );
 			const attributeElement = new AttributeElement( 'i' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.insert( position, attributeElement );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			}, 'view-writer-cannot-break-ui-element', document );
 		} );
 	} );
 } );

+ 10 - 9
packages/ckeditor5-engine/tests/view/downcastwriter/mergecontainers.js

@@ -5,8 +5,9 @@
 
 import DowncastWriter from '../../../src/view/downcastwriter';
 import { stringify, parse } from '../../../src/dev-utils/view';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'mergeContainers()', () => {
@@ -58,33 +59,33 @@ describe( 'DowncastWriter', () => {
 		it( 'should throw if there is no element before position', () => {
 			const { selection } = parse( '[]<container:div>foobar</container:div>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.mergeContainers( selection.getFirstPosition() );
-			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+			}, /view-writer-merge-containers-invalid-position/, writer );
 		} );
 
 		it( 'should throw if there is no element after position', () => {
 			const { selection } = parse( '<container:div>foobar</container:div>[]' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.mergeContainers( selection.getFirstPosition() );
-			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+			}, /view-writer-merge-containers-invalid-position/, writer );
 		} );
 
 		it( 'should throw if element before position is not a container element', () => {
 			const { selection } = parse( '<attribute:u>foo</attribute:u>[]<container:div>bar</container:div>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.mergeContainers( selection.getFirstPosition() );
-			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+			}, /view-writer-merge-containers-invalid-position/, writer );
 		} );
 
 		it( 'should throw if element after position is not a container element', () => {
 			const { selection } = parse( '<container:div>foo</container:div>[]<attribute:u>bar</attribute:u>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.mergeContainers( selection.getFirstPosition() );
-			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+			}, /view-writer-merge-containers-invalid-position/, writer );
 		} );
 	} );
 } );

+ 6 - 5
packages/ckeditor5-engine/tests/view/downcastwriter/move.js

@@ -12,9 +12,10 @@ import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
 import Range from '../../../src/view/range';
 import Position from '../../../src/view/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import Document from '../../../src/view/document';
 import Mapper from '../../../src/conversion/mapper';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'move()', () => {
@@ -155,9 +156,9 @@ describe( 'DowncastWriter', () => {
 			new ContainerElement( 'p', null, dstEmpty ); // eslint-disable-line no-new
 			const dstPosition = new Position( dstEmpty, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.move( srcRange, dstPosition );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+			}, 'view-writer-cannot-break-empty-element', writer );
 		} );
 
 		it( 'should move UIElement', () => {
@@ -178,9 +179,9 @@ describe( 'DowncastWriter', () => {
 			new ContainerElement( 'p', null, dstUI ); // eslint-disable-line no-new
 			const dstPosition = new Position( dstUI, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.move( srcRange, dstPosition );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			}, 'view-writer-cannot-break-ui-element', writer );
 		} );
 
 		it( 'should not break marker mappings if marker element was split and the original element was removed', () => {

+ 16 - 14
packages/ckeditor5-engine/tests/view/downcastwriter/remove.js

@@ -11,12 +11,13 @@ import { stringify, parse } from '../../../src/dev-utils/view';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'remove()', () => {
-		let writer;
+		let writer, document;
 
 		// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
 		// test ranges.
@@ -36,25 +37,26 @@ describe( 'DowncastWriter', () => {
 			expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
 		}
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		it( 'should throw when range placed in two containers', () => {
 			const p1 = new ContainerElement( 'p' );
 			const p2 = new ContainerElement( 'p' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.remove( Range._createFromParentsAndOffsets( p1, 0, p2, 0 ) );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should throw when range has no parent container', () => {
 			const el = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.remove( Range._createFromParentsAndOffsets( el, 0, el, 0 ) );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should return empty DocumentFragment when range is collapsed', () => {
@@ -133,9 +135,9 @@ describe( 'DowncastWriter', () => {
 			new ContainerElement( 'p', null, [ emptyElement, attributeElement ] ); // eslint-disable-line no-new
 			const range = Range._createFromParentsAndOffsets( emptyElement, 0, attributeElement, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.remove( range );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+			}, 'view-writer-cannot-break-empty-element', document );
 		} );
 
 		it( 'should remove UIElement', () => {
@@ -152,9 +154,9 @@ describe( 'DowncastWriter', () => {
 			new ContainerElement( 'p', null, [ uiElement, attributeElement ] ); // eslint-disable-line no-new
 			const range = Range._createFromParentsAndOffsets( uiElement, 0, attributeElement, 0 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.remove( range );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			}, 'view-writer-cannot-break-ui-element', document );
 		} );
 
 		it( 'should remove single text node (as item)', () => {
@@ -201,9 +203,9 @@ describe( 'DowncastWriter', () => {
 		it( 'should throw when item has no parent container', () => {
 			const el = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.remove( el );
-			} ).to.throw( CKEditorError, 'view-position-before-root' );
+			}, 'view-position-before-root' );
 		} );
 	} );
 } );

+ 16 - 14
packages/ckeditor5-engine/tests/view/downcastwriter/unwrap.js

@@ -12,13 +12,14 @@ import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import Range from '../../../src/view/range';
 import Text from '../../../src/view/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import { stringify, parse } from '../../../src/dev-utils/view';
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'unwrap()', () => {
-		let writer;
+		let writer, document;
 
 		// Executes test using `parse` and `stringify` utils functions.
 		//
@@ -32,8 +33,9 @@ describe( 'DowncastWriter', () => {
 			expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
 		}
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		it( 'should do nothing on collapsed ranges', () => {
@@ -60,9 +62,9 @@ describe( 'DowncastWriter', () => {
 			);
 			const b = new Element( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( range, b );
-			} ).to.throw( CKEditorError, 'view-writer-unwrap-invalid-attribute' );
+			}, 'view-writer-unwrap-invalid-attribute', document );
 		} );
 
 		it( 'should throw error when range placed in two containers', () => {
@@ -74,18 +76,18 @@ describe( 'DowncastWriter', () => {
 			);
 			const b = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( range, b );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should throw when range has no parent container', () => {
 			const el = new AttributeElement( 'b' );
 			const b = new AttributeElement( 'b' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( Range._createFromParentsAndOffsets( el, 0, el, 0 ), b );
-			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+			}, 'view-writer-invalid-range-container', document );
 		} );
 
 		it( 'should unwrap single node', () => {
@@ -399,9 +401,9 @@ describe( 'DowncastWriter', () => {
 			const container = new ContainerElement( 'p', null, [ empty, attribute ] );
 			const range = Range._createFromParentsAndOffsets( empty, 0, container, 2 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( range, attribute );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+			}, 'view-writer-cannot-break-empty-element', document );
 		} );
 
 		it( 'should unwrap UIElement', () => {
@@ -418,9 +420,9 @@ describe( 'DowncastWriter', () => {
 			const container = new ContainerElement( 'p', null, [ uiElement, attribute ] );
 			const range = Range._createFromParentsAndOffsets( uiElement, 0, container, 2 );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				writer.unwrap( range, attribute );
-			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			}, 'view-writer-cannot-break-ui-element', document );
 		} );
 
 		it( 'should unwrap if both elements have same id', () => {

+ 18 - 16
packages/ckeditor5-engine/tests/view/downcastwriter/wrap.js

@@ -14,17 +14,19 @@ import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import Range from '../../../src/view/range';
 import Text from '../../../src/view/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import { stringify, parse } from '../../../src/dev-utils/view';
 import createViewRoot from '../_utils/createroot';
 import Document from '../../../src/view/document';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastWriter', () => {
 	describe( 'wrap()', () => {
-		let writer;
+		let writer, document;
 
-		before( () => {
-			writer = new DowncastWriter( new Document() );
+		beforeEach( () => {
+			document = new Document();
+			writer = new DowncastWriter( document );
 		} );
 
 		describe( 'non-collapsed range', () => {
@@ -67,9 +69,9 @@ describe( 'DowncastWriter', () => {
 				);
 				const b = new Element( 'b' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( range, b );
-				} ).to.throw( CKEditorError, 'view-writer-wrap-invalid-attribute' );
+				}, 'view-writer-wrap-invalid-attribute', document );
 			} );
 
 			it( 'should throw error when range placed in two containers', () => {
@@ -81,18 +83,18 @@ describe( 'DowncastWriter', () => {
 				);
 				const b = new AttributeElement( 'b' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( range, b );
-				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+				}, 'view-writer-invalid-range-container', document );
 			} );
 
 			it( 'should throw when range has no parent container', () => {
 				const el = new AttributeElement( 'b' );
 				const b = new AttributeElement( 'b' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( Range._createFromParentsAndOffsets( el, 0, el, 0 ), b );
-				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+				}, 'view-writer-invalid-range-container', document );
 			} );
 
 			it( 'wraps part of a single text node #1', () => {
@@ -396,9 +398,9 @@ describe( 'DowncastWriter', () => {
 				const container = new ContainerElement( 'p', null, emptyElement );
 				const range = Range._createFromParentsAndOffsets( emptyElement, 0, container, 1 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( range, new AttributeElement( 'b' ) );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
+				}, 'view-writer-cannot-break-empty-element', document );
 			} );
 
 			it( 'should wrap UIElement', () => {
@@ -414,9 +416,9 @@ describe( 'DowncastWriter', () => {
 				const container = new ContainerElement( 'p', null, uiElement );
 				const range = Range._createFromParentsAndOffsets( uiElement, 0, container, 1 );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( range, new AttributeElement( 'b' ) );
-				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+				}, 'view-writer-cannot-break-ui-element', document );
 			} );
 
 			it( 'should keep stable hierarchy when wrapping with attribute with same priority', () => {
@@ -551,9 +553,9 @@ describe( 'DowncastWriter', () => {
 				const position = new Position( container, 0 );
 				const b = new Element( 'b' );
 
-				expect( () => {
+				expectToThrowCKEditorError( () => {
 					writer.wrap( new Range( position ), b );
-				} ).to.throw( CKEditorError, 'view-writer-wrap-invalid-attribute' );
+				}, 'view-writer-wrap-invalid-attribute', document );
 			} );
 
 			it( 'should wrap position at the beginning of text node', () => {

+ 4 - 3
packages/ckeditor5-engine/tests/view/editableelement.js

@@ -4,9 +4,10 @@
  */
 
 import createDocumentMock from '../../tests/view/_utils/createdocumentmock';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 import RootEditableElement from '../../src/view/rooteditableelement';
 import Range from '../../src/view/range';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'EditableElement', () => {
 	describe( 'document', () => {
@@ -31,9 +32,9 @@ describe( 'EditableElement', () => {
 			element._document = docMock;
 			const newDoc = createDocumentMock();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				element._document = newDoc;
-			} ).to.throw( CKEditorError, 'view-editableelement-document-already-set: View document is already set.' );
+			}, 'view-editableelement-document-already-set: View document is already set.', docMock );
 		} );
 
 		it( 'should be cloned properly', () => {

+ 11 - 8
packages/ckeditor5-engine/tests/view/emptyelement.js

@@ -5,7 +5,8 @@
 
 import EmptyElement from '../../src/view/emptyelement';
 import Element from '../../src/view/element';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'EmptyElement', () => {
 	let element, emptyElement;
@@ -49,24 +50,26 @@ describe( 'EmptyElement', () => {
 	} );
 
 	it( 'should throw if child elements are passed to constructor', () => {
-		expect( () => {
-			new EmptyElement( 'img', null, [ new Element( 'i' ) ] ); // eslint-disable-line no-new
-		} ).to.throw( CKEditorError, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
+		const el = new Element( 'i' );
+
+		expectToThrowCKEditorError( () => {
+			new EmptyElement( 'img', null, [ el ] ); // eslint-disable-line no-new
+		}, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.', el );
 	} );
 
 	describe( '_appendChild', () => {
 		it( 'should throw when try to append new child element', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				emptyElement._appendChild( element );
-			} ).to.throw( CKEditorError, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
+			}, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.', element );
 		} );
 	} );
 
 	describe( '_insertChild', () => {
 		it( 'should throw when try to insert new child element', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				emptyElement._insertChild( 0, element );
-			} ).to.throw( CKEditorError, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
+			}, 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.', element );
 		} );
 	} );
 

Some files were not shown because too many files changed in this diff