Parcourir la source

Merge pull request #2 from ckeditor/t/1

Feature: Introduce Watchdog. Closes #1.
Piotr Jasiun il y a 6 ans
Parent
commit
7838310c56

+ 12 - 0
packages/ckeditor5-watchdog/.editorconfig

@@ -0,0 +1,12 @@
+# Configurations to normalize the IDE behavior.
+# http://editorconfig.org/
+
+root = true
+
+[*]
+indent_style = tab
+tab_width = 4
+charset = utf-8
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 12 - 0
packages/ckeditor5-watchdog/.eslintrc.js

@@ -0,0 +1,12 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* eslint-env node */
+
+'use strict';
+
+module.exports = {
+	extends: 'ckeditor5'
+};

+ 18 - 0
packages/ckeditor5-watchdog/.gitattributes

@@ -0,0 +1,18 @@
+*			text=auto
+
+*.htaccess	eol=lf
+*.cgi		eol=lf
+*.sh		eol=lf
+
+*.css		text
+*.htm		text
+*.html		text
+*.js		text
+*.json		text
+*.php		text
+*.txt		text
+*.md		text
+
+*.png		-text
+*.gif		-text
+*.jpg		-text

+ 9 - 0
packages/ckeditor5-watchdog/.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,9 @@
+### Suggested merge commit message ([convention](https://github.com/ckeditor/ckeditor5-design/wiki/Git-commit-message-convention))
+
+Type: Message. Closes #000.
+
+---
+
+### Additional information
+
+*For example – encountered issues, assumptions you had to make, other affected tickets, etc.*

+ 1 - 0
packages/ckeditor5-watchdog/.gitignore

@@ -0,0 +1 @@
+node_modules/

+ 28 - 0
packages/ckeditor5-watchdog/.travis.yml

@@ -0,0 +1,28 @@
+sudo: required
+dist: trusty
+addons:
+  chrome: stable
+  firefox: latest
+language: node_js
+node_js:
+- '8'
+cache:
+  yarn: true
+branches:
+  except:
+  - stable
+before_install:
+- export START_TIME=$( date +%s )
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
+- npm i -g yarn
+install:
+- yarn add @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
+script:
+- ckeditor5-dev-tests-travis
+after_success:
+- ckeditor5-dev-tests-save-revision
+after_script:
+- export END_TIME=$( date +%s )
+- ckeditor5-dev-tests-notify-travis-status

+ 4 - 0
packages/ckeditor5-watchdog/CONTRIBUTING.md

@@ -0,0 +1,4 @@
+Contributing
+========================================
+
+See the [official contributors' guide to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html) to learn more.

+ 21 - 0
packages/ckeditor5-watchdog/LICENSE.md

@@ -0,0 +1,21 @@
+Software License Agreement
+==========================
+
+**CKEditor 5 watchdog feature** – https://github.com/ckeditor/ckeditor5-watchdog <br>
+Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
+
+Sources of Intellectual Property Included in CKEditor
+-----------------------------------------------------
+
+Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
+
+The following libraries are included in CKEditor under the [MIT license](https://opensource.org/licenses/MIT):
+
+* Lo-Dash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
+
+Trademarks
+----------
+
+**CKEditor** is a trademark of [CKSource](http://cksource.com) Frederico Knabben. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.

+ 20 - 1
packages/ckeditor5-watchdog/README.md

@@ -1,2 +1,21 @@
-CKEditor 5 watchdog
+CKEditor 5 watchdog feature
 ========================================
+
+[![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-watchdog.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-watchdog)
+[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-watchdog.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-watchdog)
+[![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-watchdog/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-watchdog?branch=master)
+<br>
+[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-watchdog/status.svg)](https://david-dm.org/ckeditor/ckeditor5-watchdog)
+[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-watchdog/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-watchdog?type=dev)
+
+This package implements the watchdog feature for CKEditor 5.
+
+## Documentation
+
+See the [`@ckeditor/ckeditor5-watchdog` package](https://ckeditor.com/docs/ckeditor5/latest/api/watchdog.html) page in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).
+
+## 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 or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 30 - 0
packages/ckeditor5-watchdog/docs/api/watchdog.md

@@ -0,0 +1,30 @@
+---
+category: api-reference
+---
+
+# CKEditor 5 watchdog feature
+
+[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-watchdog.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-watchdog)
+
+This package implements the watchdog feature for CKEditor 5.
+
+## Documentation
+
+See the {@link features/watchdog Watchdog feature} guide and the {@link module:watchdog/watchdog~Watchdog} plugin documentation.
+
+## Installation
+
+```bash
+npm install --save @ckeditor/ckeditor5-watchdog
+```
+
+## Contribute
+
+The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-watchdog.
+
+## External links
+
+* [`@ckeditor/ckeditor5-watchdog` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-watchdog)
+* [`ckeditor/ckeditor5-watchdog` on GitHub](https://github.com/ckeditor/ckeditor5-watchdog)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5-watchdog/issues)
+* [Changelog](https://github.com/ckeditor/ckeditor5-watchdog/blob/master/CHANGELOG.md)

+ 35 - 0
packages/ckeditor5-watchdog/docs/features/watchdog.md

@@ -0,0 +1,35 @@
+---
+title: Watchdog
+category: features
+---
+
+# Watchdog
+
+The {@link module:watchdog/watchdog~Watchdog} feature allows you to create a wrapper for the editor that will ensure that the editor instance is running. If a {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` error} is thrown by the editor, it tries to restart the editor to the state before the crash. All other errors are transparent to the watchdog. By looking at the error context, the Watchdog restarts only the editor which crashed.
+
+**Note**: The watchdog does not handle errors during editor initialization (`Editor.create()`) and editor destruction (`editor.destroy()`). Errors at these stages mean that there is a serious problem in the code integrating the editor and such problem cannot be easily fixed restarting the editor.
+
+**Note**: A new editor instance is created each time the watchdog is restarted. Thus the editor instance should not be kept internally. Use the `watchdog.editor` each time you need to access the editor. It also means that you should not execute any actions on `editor.create` because these actions will not be executed when the editor restarts. Use `watchdog.create` instead, add a plugin and add your code in the `plugin#init`, or listen on `watchdog#restart` to handle restarts.
+
+## Basic implementation
+
+```js
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Watchdog from '@ckeditor/ckeditor5-watchdog/src/watchdog';
+
+import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+
+const watchdog = Watchdog.for( ClassicEditor );
+
+watchdog.create( document.querySelector( '#editor' ), {
+	plugins: [ Essentials, Paragraph, Bold, Italic ],
+	toolbar: [ 'bold', 'italic', 'alignment' ]
+} )
+	.then( () => {
+		const editor = watchdog.editor;
+	} );
+```
+

+ 56 - 0
packages/ckeditor5-watchdog/package.json

@@ -0,0 +1,56 @@
+{
+  "name": "@ckeditor/ckeditor5-watchdog",
+  "version": "0.1.0",
+  "description": "A watchdog feature for CKEditor 5 editors. It keeps a CKEditor 5 editor instance running",
+  "keywords": [
+    "ckeditor",
+    "ckeditor5",
+    "ckeditor 5",
+    "ckeditor5-lib"
+  ],
+  "dependencies": {
+    "@ckeditor/ckeditor5-utils": "^12.1.1",
+    "lodash-es": "^4.17.10"
+  },
+  "devDependencies": {
+    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
+    "eslint": "^5.5.0",
+    "eslint-config-ckeditor5": "^1.0.11",
+    "husky": "^2.4.1",
+    "lint-staged": "^8.2.1"
+  },
+  "engines": {
+    "node": ">=8.0.0",
+    "npm": ">=5.7.1"
+  },
+  "author": "CKSource (http://cksource.com/)",
+  "license": "GPL-2.0-or-later",
+  "homepage": "https://ckeditor.com/ckeditor-5",
+  "bugs": "https://github.com/ckeditor/ckeditor5-utils/issues",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/ckeditor/ckeditor5-utils.git"
+  },
+  "files": [
+    "lang",
+    "src",
+    "theme"
+  ],
+  "scripts": {
+    "lint": "eslint --quiet '**/*.js'"
+  },
+  "lint-staged": {
+    "**/*.js": [
+      "eslint --quiet"
+    ]
+  },
+  "eslintIgnore": [
+    "src/lib/**",
+    "packages/**"
+  ],
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  }
+}

+ 415 - 0
packages/ckeditor5-watchdog/src/watchdog.js

@@ -0,0 +1,415 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module watchdog/watchdog
+ */
+
+/* globals console, window */
+
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import { throttle, cloneDeepWith, isElement } from 'lodash-es';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import areConnectedThroughProperties from '@ckeditor/ckeditor5-utils/src/areconnectedthroughproperties';
+
+/**
+ * A watchdog for CKEditor 5 editors.
+ *
+ * It keeps an {@link module:core/editor/editor~Editor editor} instance running.
+ * If a {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` error}
+ * is thrown by the editor, it tries to restart the editor to the state before the crash. All other errors
+ * are transparent to the watchdog. By looking at the error context, the Watchdog restarts only the editor which crashed.
+ *
+ * Note that the watchdog does not handle errors during editor initialization (`Editor.create()`)
+ * and editor destruction (`editor.destroy()`). Errors at these stages mean that there is a serious
+ * problem in the code integrating the editor and such problem cannot be easily fixed restarting the editor.
+ *
+ * A new editor instance is created each time the watchdog is restarted. Thus the editor instance should not be kept
+ * internally. Use the `watchdog.editor` each time you need to access the editor.
+ *
+ * Basic usage:
+ *
+ * 		const watchdog = Watchdog.for( ClassicEditor );
+ *
+ * 		watchdog.create( elementOrData, editorConfig ).then( editor => {} );
+ *
+ * Full usage:
+ *
+ *		const watchdog = new Watchdog();
+ *
+ *		watchdog.setCreator( ( elementOrData, editorConfig ) => ClassicEditor.create( elementOrData, editorConfig ) );
+ *		watchdog.setDestructor( editor => editor.destroy() );
+ *
+ *		watchdog.create( elementOrData, editorConfig ).then( editor => {} );
+ *
+ * Other important APIs:
+ *
+ *		watchdog.on( 'error', () => { console.log( 'Editor crashed.' ) } );
+ *		watchdog.on( 'restart', () => { console.log( 'Editor was restarted.' ) } );
+ *
+ *		watchdog.restart(); // Restarts the editor.
+ *		watchdog.destroy(); // Destroys the editor and global error event listeners.
+ *
+ * 		watchdog.editor; // Current editor instance.
+ *
+ * 		watchdog.crashes.forEach( crashInfo => console.log( crashInfo ) );
+ */
+export default class Watchdog {
+	/**
+	 * @param {Object} [config] The watchdog plugin configuration.
+	 * @param {Number} [config.crashNumberLimit=3] A threshold specifying the number of crashes
+	 * when the watchdog stops restarting the editor in case of errors.
+	 * @param {Number} [config.waitingTime=5000] A minimum amount of milliseconds between saving editor data internally.
+	 */
+	constructor( { crashNumberLimit, waitingTime } = {} ) {
+		/**
+		 * An array of crashes saved as an object with the following props:
+		 * * message: String,
+		 * * source: String,
+		 * * lineno: String,
+		 * * colno: String
+		 *
+		 * @public
+		 * @readonly
+		 * @type {Array.<Object>}
+		 */
+		this.crashes = [];
+
+		/**
+		 * Crash number limit (default to 3). After the limit is reached the editor is not restarted by the watchdog.
+		 * This is to prevent an infinite crash loop.
+		 *
+		 * @private
+		 * @type {Number}
+		 */
+		this._crashNumberLimit = crashNumberLimit || 3;
+
+		/**
+		 * Checks if the event error comes from the editor that is handled by the watchdog (by checking the error context)
+		 * and restarts the editor.
+		 *
+		 * @private
+		 * @type {Function}
+		 */
+		this._boundErrorHandler = this._handleGlobalErrorEvent.bind( this );
+
+		/**
+		 * Throttled save method. The `save()` method is called the specified `waitingTime` after `throttledSave()` is called,
+		 * unless a new action happens in the meantime.
+		 *
+		 * @private
+		 * @type {Function}
+		 */
+		this._throttledSave = throttle( this._save.bind( this ), waitingTime || 5000 );
+
+		/**
+		 * The current editor.
+		 *
+		 * @private
+		 * @type {module:core/editor/editor~Editor}
+		 */
+		this._editor = null;
+
+		/**
+		 * The editor creation method.
+		 *
+		 * @private
+		 * @member {Function} #_creator
+		 * @see #setCreator
+		 */
+
+		/**
+		 * The editor destruction method.
+		 *
+		 * @private
+		 * @member {Function} #_destructor
+		 * @see #setDestructor
+		 */
+
+		/**
+		 * The latest saved editor data.
+		 *
+		 * @private
+		 * @member {String} #_data
+		 */
+
+		/**
+		 * The last document version.
+		 *
+		 * @private
+		 * @member {Number} #_lastDocumentVersion
+		 */
+
+		/**
+		* The editor source element or data.
+		*
+		* @private
+		* @member {HTMLElement|String} #_elementOrData
+		*/
+
+		/**
+		* The editor configuration.
+		*
+		* @private
+		* @member {Object|undefined} #_config
+		*/
+	}
+
+	/**
+	 * The current editor instance.
+	 *
+	 * @readonly
+	 * @type {module:core/editor/editor~Editor}
+	 */
+	get editor() {
+		return this._editor;
+	}
+
+	/**
+	 * Sets the function that is responsible for editor creation.
+	 * It expects a function that should return a promise.
+	 *
+	 * 		watchdog.setCreator( ( el, config ) => ClassicEditor.create( el, config ) );
+	 *
+	 * @param {Function} creator
+	 */
+	setCreator( creator ) {
+		this._creator = creator;
+	}
+
+	/**
+	 * Sets the function that is responsible for editor destruction.
+	 * It expects a function that should return a promise or `undefined`.
+	 *
+	 *		watchdog.setDestructor( editor => editor.destroy() );
+	 *
+	 * @param {Function} destructor
+	 */
+	setDestructor( destructor ) {
+		this._destructor = destructor;
+	}
+
+	/**
+	 * Creates a watched editor instance using the creator passed to the {@link #setCreator} method or
+	 * {@link module:watchdog/watchdog~Watchdog.for} helper.
+	 *
+	 * @param {HTMLElement|String} elementOrData
+	 * @param {module:core/editor/editorconfig~EditorConfig} [config]
+	 *
+	 * @returns {Promise.<module:watchdog/watchdog~Watchdog>}
+	 */
+	create( elementOrData, config ) {
+		if ( !this._creator ) {
+			/**
+			 * @error watchdog-creator-not-defined
+			 *
+			 * The watchdog creator is not defined, define it using `watchdog.setCreator()` or `Watchdog.for` helper.
+			 */
+			throw new CKEditorError(
+				'watchdog-creator-not-defined: The watchdog creator is not defined, define it using `watchdog.setCreator()` ' +
+				'or `Watchdog.for` helper.',
+				null
+			);
+		}
+
+		if ( !this._destructor ) {
+			/**
+			 * @error watchdog-destructor-not-defined
+			 *
+			 * The watchdog destructor is not defined, define it using `watchdog.setDestructor()` or `Watchdog.for` helper.
+			 */
+			throw new CKEditorError(
+				'watchdog-destructor-not-defined: The watchdog destructor is not defined, define it using `watchdog.setDestructor()` ' +
+				'or `Watchdog.for` helper.',
+				null
+			);
+		}
+
+		this._elementOrData = elementOrData;
+
+		// Clone config because it might be shared within multiple watchdog instances. Otherwise
+		// when an error occurs in one of these editors the watchdog will restart all of them.
+		this._config = cloneDeepWith( config, value => {
+			// Leave DOM references.
+			return isElement( value ) ? value : undefined;
+		} );
+
+		return Promise.resolve()
+			.then( () => this._creator( elementOrData, this._config ) )
+			.then( editor => {
+				this._editor = editor;
+
+				window.addEventListener( 'error', this._boundErrorHandler );
+				this.listenTo( editor.model.document, 'change:data', this._throttledSave );
+
+				this._lastDocumentVersion = editor.model.document.version;
+				this._data = editor.data.get();
+
+				return this;
+			} );
+	}
+
+	/**
+	 * Restarts the editor instance. This method is also called whenever an editor error occurs.
+	 * It fires the `restart` event.
+	 *
+	 * @fires restart
+	 * @returns {Promise}
+	 */
+	restart() {
+		this._throttledSave.flush();
+
+		return Promise.resolve()
+			.then( () => this.destroy() )
+			.catch( err => console.error( 'An error happened during the editor destructing.', err ) )
+			.then( () => {
+				if ( typeof this._elementOrData === 'string' ) {
+					return this.create( this._data, this._config );
+				}
+
+				const updatedConfig = Object.assign( {}, this._config, {
+					initialData: this._data
+				} );
+
+				return this.create( this._elementOrData, updatedConfig );
+			} )
+			.then( () => {
+				this.fire( 'restart' );
+			} );
+	}
+
+	/**
+	 * Destroys the current editor using the destructor passed to {@link #setDestructor} method.
+	 *
+	 * @returns {Promise.<module:watchdog/watchdog~Watchdog>}
+	 */
+	destroy() {
+		window.removeEventListener( 'error', this._boundErrorHandler );
+		this.stopListening( this._editor.model.document, 'change:data', this._throttledSave );
+
+		return Promise.resolve()
+			.then( () => this._destructor( this._editor ) )
+			.then( () => {
+				this._editor = null;
+
+				return this;
+			} );
+	}
+
+	/**
+	 * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
+	 * the moment of a crash.
+	 *
+	 * @private
+	 */
+	_save() {
+		const version = this._editor.model.document.version;
+
+		// Change may not produce an operation, so the document's version
+		// can be the same after that change.
+		if ( version === this._lastDocumentVersion ) {
+			return;
+		}
+
+		try {
+			this._data = this._editor.data.get();
+			this._lastDocumentVersion = version;
+		} catch ( err ) {
+			console.error(
+				err,
+				'An error happened during restoring editor data. ' +
+				'Editor will be restored from the previously saved data.'
+			);
+		}
+	}
+
+	/**
+	 * Checks if the event error comes from the editor that is handled by the watchdog (by checking the error context) and
+	 * restarts the editor. It handles {@link module:utils/ckeditorerror~CKEditorError CKEditorError errors} only.
+	 *
+	 * @private
+	 * @fires error
+	 * @param {Event} evt Error event.
+	 */
+	_handleGlobalErrorEvent( evt ) {
+		if ( !evt.error.is || !evt.error.is( 'CKEditorError' ) ) {
+			return;
+		}
+
+		if ( evt.error.context === undefined ) {
+			console.error( 'The error is missing its context and Watchdog cannot restart the proper editor.' );
+
+			return;
+		}
+
+		// In some cases the editor should not be restarted - e.g. in case of the editor initialization.
+		// That's why the `null` was introduced as a correct error context which does cause restarting.
+		if ( evt.error.context === null ) {
+			return;
+		}
+
+		if ( this._isErrorComingFromThisEditor( evt.error ) ) {
+			this.crashes.push( {
+				message: evt.error.message,
+				source: evt.source,
+				lineno: evt.lineno,
+				colno: evt.colno
+			} );
+
+			this.fire( 'error' );
+
+			if ( this.crashes.length <= this._crashNumberLimit ) {
+				this.restart();
+			}
+		}
+	}
+
+	/**
+	 * Traverses both structures to find out whether the error context is connected
+	 * with the current editor.
+	 *
+	 * @private
+	 * @param {module:utils/ckeditorerror~CKEditorError} error
+	 */
+	_isErrorComingFromThisEditor( error ) {
+		return areConnectedThroughProperties( this._editor, error.context );
+	}
+
+	/**
+	 * A shortcut method for creating the Watchdog. For the full usage see the
+	 * {@link ~Watchdog Watchdog class description}.
+	 *
+	 * Usage:
+	 *
+	 * 		const watchdog = Watchdog.for( ClassicEditor );
+	 *
+	 * 		watchdog.create( elementOrData, config ).then( editor => {} );
+	 *
+	 * @param {*} Editor The editor class.
+	 */
+	static for( Editor ) {
+		const watchdog = new Watchdog();
+
+		watchdog.setCreator( ( elementOrData, config ) => Editor.create( elementOrData, config ) );
+		watchdog.setDestructor( editor => editor.destroy() );
+
+		return watchdog;
+	}
+
+	/**
+	 * Fired when an error occurs and the watchdog will be restarting the editor.
+	 *
+	 * @event error
+	 */
+
+	/**
+	 * Fired after the watchdog restarts the error in case of a crash or when the `restart()` method was called explicitly.
+	 *
+	 * @event restart
+	 */
+}
+
+mix( Watchdog, EmitterMixin );

+ 13 - 0
packages/ckeditor5-watchdog/tests/manual/watchdog.html

@@ -0,0 +1,13 @@
+<button id="random-error">Simulate a random `Error`</button>
+
+<button id="restart">Restart both editors</button>
+
+<div id="editor-1">
+	<h2>Heading 1</h2>
+	<p>Paragraph</p>
+</div>
+
+<div id="editor-2">
+	<h2>Heading 1</h2>
+	<p>Paragraph</p>
+</div>

+ 94 - 0
packages/ckeditor5-watchdog/tests/manual/watchdog.js

@@ -0,0 +1,94 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document, console, window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import Watchdog from '../../src/watchdog';
+
+const firstEditorElement = document.getElementById( 'editor-1' );
+const secondEditorElement = document.getElementById( 'editor-2' );
+
+const restartButton = document.getElementById( 'restart' );
+const randomErrorButton = document.getElementById( 'random-error' );
+
+class TypingError {
+	constructor( editor ) {
+		this.editor = editor;
+	}
+
+	init() {
+		const inputCommand = this.editor.commands.get( 'input' );
+
+		inputCommand.on( 'execute', ( evt, data ) => {
+			const commandArgs = data[ 0 ];
+
+			if ( commandArgs.text === '1' ) {
+				throw new CKEditorError( 'Fake error - input command executed with value `1`', this );
+			}
+		} );
+	}
+}
+
+const editorConfig = {
+	plugins: [
+		ArticlePluginSet, TypingError
+	],
+	toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote',
+		'insertTable', 'mediaEmbed', 'undo', 'redo' ],
+	table: {
+		contentToolbar: [
+			'tableColumn',
+			'tableRow',
+			'mergeTableCells'
+		]
+	}
+};
+
+// Watchdog 1
+
+const watchdog1 = Watchdog.for( ClassicEditor );
+window.watchdog1 = watchdog1;
+
+watchdog1.create( firstEditorElement, editorConfig ).then( () => {
+	console.log( 'First editor created.' );
+} );
+
+watchdog1.on( 'error', () => {
+	console.log( 'First editor crashed!' );
+} );
+
+watchdog1.on( 'restart', () => {
+	console.log( 'First editor restarted.' );
+} );
+
+// Watchdog 2
+
+const watchdog2 = Watchdog.for( ClassicEditor );
+window.watchdog2 = watchdog2;
+
+watchdog2.create( secondEditorElement, editorConfig ).then( () => {
+	console.log( 'Second editor created.' );
+} );
+
+watchdog2.on( 'error', () => {
+	console.log( 'Second editor crashed!' );
+} );
+
+watchdog2.on( 'restart', () => {
+	console.log( 'Second editor restarted.' );
+} );
+
+restartButton.addEventListener( 'click', () => {
+	watchdog1.restart();
+	watchdog2.restart();
+} );
+
+randomErrorButton.addEventListener( 'click', () => {
+	throw new Error( 'foo' );
+} );

+ 9 - 0
packages/ckeditor5-watchdog/tests/manual/watchdog.md

@@ -0,0 +1,9 @@
+1. Type `1` in the first editor. Only the first editor should crash and be restarted. The error should be logged in the console.
+
+1. Type `1` in the second editor. Only the second editor should crash and be restarted. The error should be logged in the console.
+
+1. Click `Simulate a random error` No editor should be restarted.
+
+1. Refresh page and type `1` in the first editor 4 times. The last time the editor should not be restarted.
+
+1. Run `restart both editor`. Both editors should be restarted.

+ 749 - 0
packages/ckeditor5-watchdog/tests/watchdog.js

@@ -0,0 +1,749 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals setTimeout, window, console, document */
+
+import Watchdog from '../src/watchdog';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+describe( 'Watchdog', () => {
+	let element;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+	} );
+
+	afterEach( () => {
+		element.remove();
+		sinon.restore();
+	} );
+
+	describe( 'create()', () => {
+		it( 'should create an editor instance', () => {
+			const watchdog = new Watchdog();
+
+			const editorCreateSpy = sinon.spy( ClassicTestEditor, 'create' );
+			const editorDestroySpy = sinon.spy( ClassicTestEditor.prototype, 'destroy' );
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			return watchdog.create( element, {} )
+				.then( () => {
+					sinon.assert.calledOnce( editorCreateSpy );
+					sinon.assert.notCalled( editorDestroySpy );
+
+					return watchdog.destroy();
+				} )
+				.then( () => {
+					sinon.assert.calledOnce( editorCreateSpy );
+					sinon.assert.calledOnce( editorDestroySpy );
+				} );
+		} );
+
+		it( 'should throw an error when the creator is not defined', () => {
+			const watchdog = new Watchdog();
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			expectToThrowCKEditorError(
+				() => watchdog.create(),
+				/^watchdog-creator-not-defined/,
+				null
+			);
+		} );
+
+		it( 'should throw an error when the destructor is not defined', () => {
+			const watchdog = new Watchdog();
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+
+			expectToThrowCKEditorError(
+				() => watchdog.create(),
+				/^watchdog-destructor-not-defined/,
+				null
+			);
+		} );
+
+		it( 'should properly copy the config', () => {
+			const watchdog = new Watchdog();
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			const config = {
+				foo: [],
+				bar: document.createElement( 'div' )
+			};
+
+			return watchdog.create( element, config ).then( () => {
+				expect( watchdog.editor.config._config.foo ).to.not.equal( config.foo );
+				expect( watchdog.editor.config._config.bar ).to.equal( config.bar );
+			} );
+		} );
+	} );
+
+	describe( 'restart()', () => {
+		it( 'should restart the editor', () => {
+			const watchdog = new Watchdog();
+
+			const editorCreateSpy = sinon.spy( ClassicTestEditor, 'create' );
+			const editorDestroySpy = sinon.spy( ClassicTestEditor.prototype, 'destroy' );
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			return watchdog.create( element, {} )
+				.then( () => {
+					sinon.assert.calledOnce( editorCreateSpy );
+					sinon.assert.notCalled( editorDestroySpy );
+
+					return watchdog.restart();
+				} )
+				.then( () => {
+					sinon.assert.calledTwice( editorCreateSpy );
+					sinon.assert.calledOnce( editorDestroySpy );
+
+					return watchdog.destroy();
+				} );
+		} );
+
+		it( 'should restart the editor with the same data', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} )
+				.then( () => {
+					expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return watchdog.restart();
+				} )
+				.then( () => {
+					expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return watchdog.destroy();
+				} );
+		} );
+
+		it( 'should support editor data passed as the `Editor.create()` as the first argument', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( data, config ) => ClassicTestEditor.create( data, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			return watchdog.create( '<p>foo</p>', { plugins: [ Paragraph ] } )
+				.then( () => {
+					expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return watchdog.restart();
+				} )
+				.then( () => {
+					expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return watchdog.destroy();
+				} );
+		} );
+	} );
+
+	describe( 'editor', () => {
+		it( 'should be the current editor instance', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			expect( watchdog.editor ).to.be.null;
+
+			let oldEditor;
+
+			return watchdog.create( element, {} )
+				.then( () => {
+					oldEditor = watchdog.editor;
+					expect( watchdog.editor ).to.be.instanceOf( ClassicTestEditor );
+
+					return watchdog.restart();
+				} )
+				.then( () => {
+					expect( watchdog.editor ).to.be.instanceOf( ClassicTestEditor );
+					expect( watchdog.editor ).to.not.equal( oldEditor );
+
+					return watchdog.destroy();
+				} )
+				.then( () => {
+					expect( watchdog.editor ).to.be.null;
+				} );
+		} );
+	} );
+
+	describe( 'error handling', () => {
+		it( 'Watchdog should not restart editor during the initialization', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( el =>
+				ClassicTestEditor.create( el )
+					.then( () => Promise.reject( new Error( 'foo' ) ) )
+			);
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			return watchdog.create( element ).then(
+				() => { throw new Error( '`watchdog.create()` should throw an error.' ); },
+				err => {
+					expect( err ).to.be.instanceOf( Error );
+					expect( err.message ).to.equal( 'foo' );
+				}
+			);
+		} );
+
+		it( 'Watchdog should not restart editor during the destroy', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( el => ClassicTestEditor.create( el ) );
+			watchdog.setDestructor( () => Promise.reject( new Error( 'foo' ) ) );
+
+			return Promise.resolve()
+				.then( () => watchdog.create( element ) )
+				.then( () => watchdog.destroy() )
+				.then(
+					() => { throw new Error( '`watchdog.create()` should throw an error.' ); },
+					err => {
+						expect( err ).to.be.instanceOf( Error );
+						expect( err.message ).to.equal( 'foo' );
+					}
+				);
+		} );
+
+		it( 'Watchdog should not hide intercepted errors', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			const windowErrorSpy = sinon.spy();
+			window.onerror = windowErrorSpy;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						sinon.assert.calledOnce( windowErrorSpy );
+						expect( windowErrorSpy.getCall( 0 ).args[ 0 ] ).to.equal( 'Uncaught CKEditorError: foo' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should intercept editor errors and restart the editor during the runtime', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should not intercept non-editor errors', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			const editorErrorSpy = sinon.spy();
+			watchdog.on( 'error', editorErrorSpy );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => {
+					throw new Error( 'foo' );
+				} );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						sinon.assert.notCalled( editorErrorSpy );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should not intercept other editor errors', () => {
+			const watchdog1 = Watchdog.for( ClassicTestEditor );
+			const watchdog2 = Watchdog.for( ClassicTestEditor );
+
+			const config = {
+				plugins: []
+			};
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return Promise.all( [
+				watchdog1.create( element, config ),
+				watchdog2.create( element, config )
+			] ).then( () => {
+				return new Promise( res => {
+					const watchdog1ErrorSpy = sinon.spy();
+					const watchdog2ErrorSpy = sinon.spy();
+
+					watchdog1.on( 'restart', watchdog1ErrorSpy );
+					watchdog2.on( 'restart', watchdog2ErrorSpy );
+
+					setTimeout( () => throwCKEditorError( 'foo', watchdog2.editor ) );
+
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						sinon.assert.notCalled( watchdog1ErrorSpy );
+						sinon.assert.calledOnce( watchdog2ErrorSpy );
+
+						Promise.all( [ watchdog1.destroy(), watchdog2.destroy() ] )
+							.then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor.model.document ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context #2', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', {
+					foo: [ 1, 2, 3, {
+						bar: new Set( [
+							new Map( [
+								[ 'foo', 'bar' ],
+								[ 0, watchdog.editor ]
+							] )
+						] )
+					} ]
+				} ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted up to 3 times by default', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			const errorSpy = sinon.spy();
+			watchdog.on( 'error', errorSpy );
+
+			const restartSpy = sinon.spy();
+			watchdog.on( 'restart', restartSpy );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						expect( errorSpy.callCount ).to.equal( 4 );
+						expect( watchdog.crashes.length ).to.equal( 4 );
+						expect( restartSpy.callCount ).to.equal( 3 );
+
+						window.onerror = originalErrorHandler;
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted up to `crashNumberLimit` times if the option is set', () => {
+			const watchdog = new Watchdog( { crashNumberLimit: 2 } );
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			const errorSpy = sinon.spy();
+			watchdog.on( 'error', errorSpy );
+
+			const restartSpy = sinon.spy();
+			watchdog.on( 'restart', restartSpy );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						expect( errorSpy.callCount ).to.equal( 4 );
+						expect( watchdog.crashes.length ).to.equal( 4 );
+						expect( restartSpy.callCount ).to.equal( 2 );
+
+						window.onerror = originalErrorHandler;
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should warn if the CKEditorError missing its context', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			sinon.stub( console, 'error' );
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo' ) );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.crashes ).to.deep.equal( [] );
+
+						sinon.assert.calledWithExactly(
+							console.error,
+							'The error is missing its context and Watchdog cannot restart the proper editor.'
+						);
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should omit error if the CKEditorError context is equal to null', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			sinon.stub( console, 'error' );
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', null ) );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.crashes ).to.deep.equal( [] );
+						sinon.assert.notCalled( console.error );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted with the data before the crash #1', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted with the data before the crash #2', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} ).then( () => {
+				const doc = watchdog.editor.model.document;
+
+				watchdog.editor.model.change( writer => {
+					writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
+				} );
+
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted with the data of the latest document version before the crash', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} ).then( () => {
+				const model = watchdog.editor.model;
+				const doc = model.document;
+
+				// Decrement the document version to simulate a situation when an operation
+				// don't produce new document version.
+				doc.version--;
+
+				model.change( writer => {
+					writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
+				} );
+
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted with the latest available data before the crash', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			sinon.stub( console, 'error' );
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} ).then( () => {
+				const editorGetDataError = new Error( 'Some error' );
+				const getDataStub = sinon.stub( watchdog.editor.data, 'get' )
+					.throwsException( editorGetDataError );
+
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					const doc = watchdog.editor.model.document;
+
+					watchdog.editor.model.change( writer => {
+						writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
+					} );
+
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						// It is called second time by during the default editor destruction
+						// to update the source element.
+						sinon.assert.calledTwice( getDataStub );
+
+						expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+						sinon.assert.calledWith(
+							console.error,
+							editorGetDataError,
+							'An error happened during restoring editor data. Editor will be restored from the previously saved data.'
+						);
+
+						sinon.assert.calledWith(
+							console.error,
+							'An error happened during the editor destructing.'
+						);
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'static for()', () => {
+		it( 'should be a shortcut method for creating the watchdog', () => {
+			const watchdog = Watchdog.for( ClassicTestEditor );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element, {
+				initialData: '<p>foo</p>',
+				plugins: [ Paragraph ]
+			} ).then( () => {
+				const oldEditor = watchdog.editor;
+				expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
+
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
+						expect( watchdog.editor ).to.not.equal( oldEditor );
+						expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'crashes', () => {
+		it( 'should be an array of caught errors by the watchdog', () => {
+			const watchdog = new Watchdog();
+
+			watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( element ).then( () => {
+				setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
+				setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
+
+				return new Promise( res => {
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.crashes[ 0 ].message ).to.equal( 'foo' );
+						expect( watchdog.crashes[ 1 ].message ).to.equal( 'bar' );
+
+						watchdog.destroy().then( res );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );
+
+function throwCKEditorError( name, context ) {
+	throw new CKEditorError( name, context );
+}