8
0
Просмотр исходного кода

Added advanced error handling tests.

Maciej Bukowski 6 лет назад
Родитель
Сommit
150e6c9714

+ 7 - 7
packages/ckeditor5-watchdog/src/watchdog.js

@@ -5,7 +5,7 @@
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import { debounce } from 'lodash-es';
+import { throttle } from 'lodash-es';
 
 /**
  * A Watchdog for CKEditor 5 editors.
@@ -42,13 +42,13 @@ export default class Watchdog {
 		this._boundErrorWatcher = this._watchForEditorErrors.bind( this );
 
 		/**
-		 * Debounced save method. The `save()` method is called the specified `waitingTime` after `debouncedSave()` is called,
+		 * 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._debouncedSave = debounce( this._save.bind( this ), waitingTime || 5000 );
+		this._throttledSave = throttle( this._save.bind( this ), waitingTime || 5000 );
 
 		/**
 		 * @private
@@ -166,7 +166,7 @@ export default class Watchdog {
 				this._editor = editor;
 
 				window.addEventListener( 'error', this._boundErrorWatcher );
-				this.listenTo( editor.model.document, 'change:data', this._debouncedSave );
+				this.listenTo( editor.model.document, 'change:data', this._throttledSave );
 
 				this._lastDocumentVersion = editor.model.document.version;
 				this._data = editor.getData();
@@ -182,7 +182,7 @@ export default class Watchdog {
 	 */
 	destroy() {
 		window.removeEventListener( 'error', this._boundErrorWatcher );
-		this.stopListening( this._editor.model.document, 'change:data', this._debouncedSave );
+		this.stopListening( this._editor.model.document, 'change:data', this._throttledSave );
 
 		return Promise.resolve()
 			.then( () => this._destructor( this._editor ) )
@@ -198,7 +198,7 @@ export default class Watchdog {
 		// Change may not produce an operation, so the document's version
 		// can be the same after that change.
 		if ( version < this._lastDocumentVersion ) {
-			this._debouncedSave.cancel();
+			this._throttledSave.cancel();
 
 			return;
 		}
@@ -258,7 +258,7 @@ function areElementsConnected( from, searchedElement ) {
 	const nodes = [ from ];
 
 	// Elements are stored to prevent infinite looping.
-	const storedElements = new WeakSet( nodes );
+	const storedElements = new WeakSet();
 
 	while ( nodes.length > 0 ) {
 		// BFS should be faster.

+ 309 - 1
packages/ckeditor5-watchdog/tests/watchdog.js

@@ -6,10 +6,19 @@
 import Watchdog from '../src/watchdog';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'Watchdog', () => {
 	testUtils.createSinonSandbox();
 
+	beforeEach( () => {
+
+	} );
+
+	afterEach( () => {
+
+	} );
+
 	describe( 'create()', () => {
 		it( 'should create an editor instance', () => {
 			const watchdog = new Watchdog();
@@ -63,6 +72,7 @@ describe( 'Watchdog', () => {
 			const watchdog = new Watchdog();
 
 			// VirtualTestEditor doesn't handle the `config.initialData` properly.
+			// See https://github.com/ckeditor/ckeditor5-core/issues/180.
 			const FakeEditor = getFakeEditor();
 
 			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
@@ -145,6 +155,300 @@ describe( 'Watchdog', () => {
 					}
 				);
 		} );
+
+		it( 'Watchdog should catch editor errors and restart the editor during the runtime', done => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { watchdog.editor.throwEditorError() } );
+
+				watchdog.on( 'restart', () => {
+					window.onerror = originalErrorHandler;
+					done();
+				} )
+			} );
+		} );
+
+		it( 'Watchdog should catch editor errors and restart the editor during the runtime', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo', undefined, watchdog.editor ); } );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+						res();
+					} )
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should not catch other editor errors', () => {
+			const watchdog1 = new Watchdog();
+			const watchdog2 = new Watchdog();
+			const FakeEditor1 = getFakeEditor();
+			const FakeEditor2 = getFakeEditor();
+
+			watchdog1.setCreator( ( el, config ) => FakeEditor1.create( el, config ) );
+			watchdog1.setDestructor( editor => editor.destroy() );
+
+			watchdog2.setCreator( ( el, config ) => FakeEditor2.create( el, config ) );
+			watchdog2.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return Promise.all( [
+				watchdog1.create( document.createElement( 'div' ) ),
+				watchdog2.create( document.createElement( 'div' ) )
+			] ).then( () => {
+				return new Promise( res => {
+					let watchdog1ErrorSpy = sinon.spy();
+					let watchdog2ErrorSpy = sinon.spy();
+
+					watchdog1.on( 'restart', watchdog1ErrorSpy );
+					watchdog2.on( 'restart', watchdog2ErrorSpy );
+
+					setTimeout( () => {
+						throw new CKEditorError( 'foo', undefined, watchdog2.editor );
+					} );
+
+					// TODO - timing.
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						sinon.assert.notCalled( watchdog1ErrorSpy );
+						sinon.assert.calledOnce( watchdog2ErrorSpy );
+						res();
+					}, 5 );
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should catch editor errors and restart the editor during the runtime', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo', undefined, watchdog.editor ); } );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+						res();
+					} )
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should catch editor errors and restart the editor during the runtime if the editor can be found from the ctx', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo', undefined, watchdog.editor.model.document ); } );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+						res();
+					} )
+				} );
+			} );
+		} );
+
+		it( 'Watchdog should catch editor errors and restart the editor during the runtime if the editor can be found from the ctx #2', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo', undefined, { foo: [ 1, 2, 3, { bar: watchdog.editor } ] } ); } );
+
+				return new Promise( res => {
+					watchdog.on( 'restart', () => {
+						window.onerror = originalErrorHandler;
+						res();
+					} )
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted maximum 3 times by default', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.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 don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo1', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo2', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo3', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo4', undefined, 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;
+						res();
+					}, 5 );
+				} );
+			} );
+		} );
+
+		it( 'editor should be restarted maximum of `crashNumberLimit` if the option is set', () => {
+			const watchdog = new Watchdog( { crashNumberLimit: 2 } );
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.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 don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo1', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo2', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo3', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo4', undefined, 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;
+						res();
+					}, 5 );
+				} );
+			} );
+		} );
+
+		it( 'editor should be reinitialized with the last data ', () => {
+			const watchdog = new Watchdog( { crashNumberLimit: 2 } );
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.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 don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo1', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo2', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo3', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'foo4', undefined, 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;
+						res();
+					}, 5 );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'crashes', () => {
+		it( 'should be an array of caught errors by the Watchdog', () => {
+			const watchdog = new Watchdog();
+			const FakeEditor = getFakeEditor();
+
+			watchdog.setCreator( ( el, config ) => FakeEditor.create( el, config ) );
+			watchdog.setDestructor( editor => editor.destroy() );
+
+			// sinon.stub( window, 'onerror' ).value( undefined ); and similar don't work.
+			const originalErrorHandler = window.onerror;
+			window.onerror = undefined;
+
+			return watchdog.create( document.createElement( 'div' ) ).then( () => {
+				setTimeout( () => { throw new CKEditorError( 'foo', undefined, watchdog.editor ); } );
+				setTimeout( () => { throw new CKEditorError( 'bar', undefined, watchdog.editor ); } );
+
+				return new Promise( res => {
+					// TODO - timing.
+					setTimeout( () => {
+						window.onerror = originalErrorHandler;
+
+						expect( watchdog.crashes[ 0 ].message ).to.equal( 'Uncaught CKEditorError: foo' );
+						expect( watchdog.crashes[ 1 ].message ).to.equal( 'Uncaught CKEditorError: bar' );
+						res();
+					}, 5 );
+				} );
+			} );
+		} );
 	} );
 } );
 
@@ -157,7 +461,7 @@ function getFakeEditor() {
 				.then( () => editor.create( el, config ) );
 		}
 
-		create( el, config ) {
+		create( el, config = {} ) {
 			this.el = el;
 			this.config = config;
 			this._data = config.initialData || '';
@@ -182,5 +486,9 @@ function getFakeEditor() {
 
 			return Promise.resolve();
 		}
+
+		throwEditorError() {
+			throw new CKEditorError( 'foo', undefined, this );
+		}
 	}
 }