From 1e79addaa13b690734ba2d292e337474eb18009b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Mon, 18 Sep 2023 15:52:16 +0200 Subject: [PATCH 1/9] Update watchdog demo to present crashes. --- src/app/watchdog-demo/watchdog-demo.html | 19 +++++++++++++++---- src/app/watchdog-demo/watchdog-demo.ts | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/app/watchdog-demo/watchdog-demo.html b/src/app/watchdog-demo/watchdog-demo.html index 755c716..94d7970 100644 --- a/src/app/watchdog-demo/watchdog-demo.html +++ b/src/app/watchdog-demo/watchdog-demo.html @@ -1,7 +1,18 @@

Watchdog demo

- + - - + + + +

Type '1' or '2' to crash the editor.

+ + + + +
+ + +

An error has occurred, please contast us at: support@company.com to resolve the situation.

+
diff --git a/src/app/watchdog-demo/watchdog-demo.ts b/src/app/watchdog-demo/watchdog-demo.ts index 71a207f..3ab8a64 100644 --- a/src/app/watchdog-demo/watchdog-demo.ts +++ b/src/app/watchdog-demo/watchdog-demo.ts @@ -18,9 +18,26 @@ export class WatchdogDemoComponent { public ready = false; public isDisabled = false; + public errorOccurred = false; public onReady( editor: AngularEditor ): void { console.log( editor ); + + const inputCommand = editor.commands.get( 'input' )!; + + inputCommand.on( 'execute', ( evt, data ) => { + const commandArgs = data[ 0 ]; + + if ( commandArgs.text === '1' ) { + // Simulate an error. + throw new Error( 'a-custom-editor-error' ); + } + + if ( commandArgs.text === '2' ) { + // Simulate an error. + throw 'foobar'; + } + } ); } public ngOnInit(): void { @@ -45,4 +62,8 @@ export class WatchdogDemoComponent { public toggle(): void { this.isDisabled = !this.isDisabled; } + + public onError(): void { + this.errorOccurred = true; + } } From 929692a3e60da1307522dff457411306596cf413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Tue, 19 Sep 2023 12:02:38 +0200 Subject: [PATCH 2/9] Add handling editor initialization errors with demo --- src/app/app.component.html | 1 + src/app/app.module.ts | 7 ++- .../initialization-crash.component.css | 0 .../initialization-crash.component.html | 30 +++++++++ .../initialization-crash.component.ts | 62 +++++++++++++++++++ src/app/watchdog-demo/watchdog-demo.ts | 5 +- src/ckeditor/ckeditor.component.ts | 14 +++-- 7 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 src/app/initialization-crash/initialization-crash.component.css create mode 100644 src/app/initialization-crash/initialization-crash.component.html create mode 100644 src/app/initialization-crash/initialization-crash.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 60b25a8..dfaa590 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -7,6 +7,7 @@

CKEditor 5 integration with Angular

  • Integration with reactive forms (formControlName)
  • Integration with CKEditor Watchdog
  • Integration with CKEditor Context
  • +
  • Catching error when editor crashes during initialization
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 29bb669..5d194a5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { DemoFormComponent } from './demo-form/demo-form.component'; import { DemoReactiveFormComponent } from './demo-reactive-form/demo-reactive-form.component'; import { ContextDemoComponent } from './context-demo/context-demo'; import { WatchdogDemoComponent } from './watchdog-demo/watchdog-demo'; +import { InitializationCrashComponent } from './initialization-crash/initialization-crash.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/simple-usage', pathMatch: 'full' }, @@ -18,7 +19,8 @@ const appRoutes: Routes = [ { path: 'forms', component: DemoFormComponent }, { path: 'reactive-forms', component: DemoReactiveFormComponent }, { path: 'watchdog', component: WatchdogDemoComponent }, - { path: 'simple-usage', component: SimpleUsageComponent } + { path: 'simple-usage', component: SimpleUsageComponent }, + { path: 'initialization', component: InitializationCrashComponent } ]; @NgModule( { @@ -35,7 +37,8 @@ const appRoutes: Routes = [ DemoFormComponent, DemoReactiveFormComponent, SimpleUsageComponent, - WatchdogDemoComponent + WatchdogDemoComponent, + InitializationCrashComponent ], providers: [], bootstrap: [ AppComponent ] diff --git a/src/app/initialization-crash/initialization-crash.component.css b/src/app/initialization-crash/initialization-crash.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/initialization-crash/initialization-crash.component.html b/src/app/initialization-crash/initialization-crash.component.html new file mode 100644 index 0000000..569a1f1 --- /dev/null +++ b/src/app/initialization-crash/initialization-crash.component.html @@ -0,0 +1,30 @@ +

    Initialization crash demo

    +

    Without watchdog

    + + + + + + + + + + +

    An error has occurred, please contast us at: support@company.com to resolve the situation.

    +
    + +

    With watchdog

    + + + + + + + + + + +

    An error has occurred, please contast us at: support@company.com to resolve the situation.

    +
    diff --git a/src/app/initialization-crash/initialization-crash.component.ts b/src/app/initialization-crash/initialization-crash.component.ts new file mode 100644 index 0000000..276c3e0 --- /dev/null +++ b/src/app/initialization-crash/initialization-crash.component.ts @@ -0,0 +1,62 @@ +import { Component, ViewChild } from '@angular/core'; +import { CKEditorComponent } from 'src/ckeditor'; +import AngularEditor from 'ckeditor/build/ckeditor'; +import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; + +@Component( { + selector: 'app-initialization-crash', + templateUrl: './initialization-crash.component.html', + styleUrls: [ './initialization-crash.component.css' ] +} ) +export class InitializationCrashComponent { + public Editor = AngularEditor; + public EditorWatchdog = AngularEditor; + + public constructor() { + ( this.Editor as any ).create = () => { + throw 'init failure'; + }; + ( this.EditorWatchdog as any ).create = () => { + throw 'init failure'; + }; + } + + @ViewChild( CKEditorComponent ) public ckeditor?: CKEditorComponent; + + public config: any; + public ready = false; + + public errorOccurred = false; + public errorOccurredWatchdog = false; + + public watchdog?: ContextWatchdog; + + public ngOnInit(): void { + const contextConfig: any = { + foo: 'bar' + }; + + this.config = { + collaboration: { + channelId: 'foobar-baz' + } + }; + + this.watchdog = new AngularEditor.ContextWatchdog( AngularEditor.Context ); + + this.watchdog.create( contextConfig ) + .then( () => { + this.ready = true; + } ); + } + + public onError( error: any ): void { + console.error( 'Editor without watchdog crashed - catched', error ); + this.errorOccurred = true; + } + + public onErrorWatchdog( error: any ): void { + console.error( 'Editor with watchdog crashed - catched', error ); + this.errorOccurredWatchdog = true; + } +} diff --git a/src/app/watchdog-demo/watchdog-demo.ts b/src/app/watchdog-demo/watchdog-demo.ts index 3ab8a64..0952d93 100644 --- a/src/app/watchdog-demo/watchdog-demo.ts +++ b/src/app/watchdog-demo/watchdog-demo.ts @@ -1,5 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { CKEditorComponent } from '../../ckeditor/ckeditor.component'; +import { Component } from '@angular/core'; import AngularEditor from '../../../ckeditor/build/ckeditor'; import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; @@ -11,8 +10,6 @@ import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; export class WatchdogDemoComponent { public Editor = AngularEditor; - @ViewChild( CKEditorComponent ) public ckeditor?: ElementRef; - public config: any; public watchdog?: ContextWatchdog; public ready = false; diff --git a/src/ckeditor/ckeditor.component.ts b/src/ckeditor/ckeditor.component.ts index 9b8808b..4d80804 100644 --- a/src/ckeditor/ckeditor.component.ts +++ b/src/ckeditor/ckeditor.component.ts @@ -164,7 +164,7 @@ export class CKEditorComponent implements After /** * Fires when the editor component crashes. */ - @Output() public error = new EventEmitter(); + @Output() public error = new EventEmitter(); /** * The instance of the editor created by this component. @@ -367,16 +367,15 @@ export class CKEditorComponent implements After this.elementRef.nativeElement.removeChild( this.editorElement! ); }; - const emitError = () => { + const emitError = ( e?: unknown ) => { // Do not run change detection by re-entering the Angular zone if the `error` // emitter doesn't have any subscribers. // Subscribers are pushed onto the list whenever `error` is listened inside the template: // ``. if ( hasObservers( this.error ) ) { - this.ngZone.run( () => this.error.emit() ); + this.ngZone.run( () => this.error.emit( e ) ); } }; - const element = document.createElement( this.tagName ); const config = this.getConfig(); @@ -392,6 +391,8 @@ export class CKEditorComponent implements After destructor, sourceElementOrData: element, config + } ).catch( e => { + emitError( e ); } ); this.watchdog.on( 'itemError', ( _, { itemId } ) => { @@ -410,11 +411,12 @@ export class CKEditorComponent implements After editorWatchdog.on( 'error', emitError ); this.editorWatchdog = editorWatchdog; - this.ngZone.runOutsideAngular( () => { // Note: must be called outside of the Angular zone too because `create` is calling // `_startErrorHandling` within a microtask which sets up `error` listener on the window. - editorWatchdog.create( element, config ); + editorWatchdog.create( element, config ).catch( e => { + emitError( e ); + } ); } ); } } From 17a5b4324770be6d873543a8054f17ef42451b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Thu, 21 Sep 2023 09:54:08 +0200 Subject: [PATCH 3/9] Fix typo. --- .../initialization-crash/initialization-crash.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/initialization-crash/initialization-crash.component.html b/src/app/initialization-crash/initialization-crash.component.html index 569a1f1..b30ad53 100644 --- a/src/app/initialization-crash/initialization-crash.component.html +++ b/src/app/initialization-crash/initialization-crash.component.html @@ -11,7 +11,7 @@

    Without watchdog

    -

    An error has occurred, please contast us at: support@company.com to resolve the situation.

    +

    An error has occurred, please contact us at: support@company.com to resolve the situation.

    With watchdog

    @@ -26,5 +26,5 @@

    With watchdog

    -

    An error has occurred, please contast us at: support@company.com to resolve the situation.

    +

    An error has occurred, please contact us at: support@company.com to resolve the situation.

    From 80b521f8c249270b4f8e3bd75bd417193fbe2279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Thu, 21 Sep 2023 13:05:17 +0200 Subject: [PATCH 4/9] Add tests for coverage. --- src/ckeditor/ckeditor.component.spec.ts | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/ckeditor/ckeditor.component.spec.ts b/src/ckeditor/ckeditor.component.spec.ts index a4bbd22..60df5dc 100644 --- a/src/ckeditor/ckeditor.component.spec.ts +++ b/src/ckeditor/ckeditor.component.spec.ts @@ -491,6 +491,62 @@ describe( 'CKEditorComponent', () => { } ); } ); } ); + describe( 'initialization errors are catched', () => { + const CrashyEditor = AngularEditor; + let originalCreate: any; + + beforeEach( () => { + originalCreate = AngularEditor.create; + CrashyEditor.create = () => { + throw 'init failure'; + }; + } ); + + afterEach( () => { + AngularEditor.create = originalCreate; + } ); + + it( 'when internal watchgod is created', async () => { + fixture.detectChanges(); + await waitCycle(); + + fixture = TestBed.createComponent( CKEditorComponent ); + const errorSpy = jasmine.createSpy( 'errorSpy' ); + component.error.subscribe( errorSpy ); + component.editor = CrashyEditor; + component.ngAfterViewInit(); + + fixture.detectChanges(); + await waitCycle(); + + expect( errorSpy ).toHaveBeenCalledTimes( 1 ); + + fixture.destroy(); + } ); + + it( 'when external watchdog is provided', async () => { + fixture.detectChanges(); + await waitCycle(); + + fixture = TestBed.createComponent( CKEditorComponent ); + const errorSpy = jasmine.createSpy( 'errorSpy' ); + component.error.subscribe( errorSpy ); + const contextWatchdog = new AngularEditor.ContextWatchdog( AngularEditor.Context ); + + await contextWatchdog.create(); + + component.watchdog = contextWatchdog; + component.editor = CrashyEditor; + component.ngAfterViewInit(); + + fixture.detectChanges(); + await waitCycle(); + + expect( errorSpy ).toHaveBeenCalledTimes( 1 ); + + fixture.destroy(); + } ); + } ); } ); describe( 'change detection', () => { From 74b9bbb8dfb4396f1aad11cfc8b592ba49e3361d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Fri, 22 Sep 2023 09:32:57 +0200 Subject: [PATCH 5/9] Fix tests. --- src/ckeditor/ckeditor.component.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ckeditor/ckeditor.component.spec.ts b/src/ckeditor/ckeditor.component.spec.ts index 60df5dc..25b36d0 100644 --- a/src/ckeditor/ckeditor.component.spec.ts +++ b/src/ckeditor/ckeditor.component.spec.ts @@ -507,9 +507,6 @@ describe( 'CKEditorComponent', () => { } ); it( 'when internal watchgod is created', async () => { - fixture.detectChanges(); - await waitCycle(); - fixture = TestBed.createComponent( CKEditorComponent ); const errorSpy = jasmine.createSpy( 'errorSpy' ); component.error.subscribe( errorSpy ); @@ -525,9 +522,6 @@ describe( 'CKEditorComponent', () => { } ); it( 'when external watchdog is provided', async () => { - fixture.detectChanges(); - await waitCycle(); - fixture = TestBed.createComponent( CKEditorComponent ); const errorSpy = jasmine.createSpy( 'errorSpy' ); component.error.subscribe( errorSpy ); From e1a7c8c10fb38ddc76232e3297e5066d6aea7564 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Mon, 9 Oct 2023 15:17:44 +0200 Subject: [PATCH 6/9] Use a plugin that would crash the editor. --- .../initialization-crash.component.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/initialization-crash/initialization-crash.component.ts b/src/app/initialization-crash/initialization-crash.component.ts index 276c3e0..42a3aa0 100644 --- a/src/app/initialization-crash/initialization-crash.component.ts +++ b/src/app/initialization-crash/initialization-crash.component.ts @@ -2,6 +2,7 @@ import { Component, ViewChild } from '@angular/core'; import { CKEditorComponent } from 'src/ckeditor'; import AngularEditor from 'ckeditor/build/ckeditor'; import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; @Component( { selector: 'app-initialization-crash', @@ -12,15 +13,6 @@ export class InitializationCrashComponent { public Editor = AngularEditor; public EditorWatchdog = AngularEditor; - public constructor() { - ( this.Editor as any ).create = () => { - throw 'init failure'; - }; - ( this.EditorWatchdog as any ).create = () => { - throw 'init failure'; - }; - } - @ViewChild( CKEditorComponent ) public ckeditor?: CKEditorComponent; public config: any; @@ -37,6 +29,14 @@ export class InitializationCrashComponent { }; this.config = { + extraPlugins: [ + function( editor: any ) { + editor.data.on( 'init', () => { + // eslint-disable-next-line + throw new CKEditorError( 'example-error', editor ); + } ); + } + ], collaboration: { channelId: 'foobar-baz' } @@ -51,12 +51,12 @@ export class InitializationCrashComponent { } public onError( error: any ): void { - console.error( 'Editor without watchdog crashed - catched', error ); + console.error( 'Editor without watchdog threw an error which was caught', error ); this.errorOccurred = true; } public onErrorWatchdog( error: any ): void { - console.error( 'Editor with watchdog crashed - catched', error ); + console.error( 'Editor with watchdog threw an error which was caught', error ); this.errorOccurredWatchdog = true; } } From 5b2c9fc61f792afb53a22e130b3b6fc7fc3ef9ac Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Mon, 9 Oct 2023 15:28:55 +0200 Subject: [PATCH 7/9] Proposed better route path. --- src/app/app.component.html | 2 +- src/app/app.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index dfaa590..ab4868b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -7,7 +7,7 @@

    CKEditor 5 integration with Angular

  • Integration with reactive forms (formControlName)
  • Integration with CKEditor Watchdog
  • Integration with CKEditor Context
  • -
  • Catching error when editor crashes during initialization
  • +
  • Catching error when editor crashes during initialization
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5d194a5..20c19e6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -20,7 +20,7 @@ const appRoutes: Routes = [ { path: 'reactive-forms', component: DemoReactiveFormComponent }, { path: 'watchdog', component: WatchdogDemoComponent }, { path: 'simple-usage', component: SimpleUsageComponent }, - { path: 'initialization', component: InitializationCrashComponent } + { path: 'init-crash', component: InitializationCrashComponent } ]; @NgModule( { From 3b239458aa1d839011f76adbeab25fafc8045d35 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Mon, 9 Oct 2023 16:03:07 +0200 Subject: [PATCH 8/9] Cause the error instead of throwing it directly. --- .../initialization-crash.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/initialization-crash/initialization-crash.component.ts b/src/app/initialization-crash/initialization-crash.component.ts index 42a3aa0..a63be5e 100644 --- a/src/app/initialization-crash/initialization-crash.component.ts +++ b/src/app/initialization-crash/initialization-crash.component.ts @@ -2,7 +2,6 @@ import { Component, ViewChild } from '@angular/core'; import { CKEditorComponent } from 'src/ckeditor'; import AngularEditor from 'ckeditor/build/ckeditor'; import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; @Component( { selector: 'app-initialization-crash', @@ -32,8 +31,11 @@ export class InitializationCrashComponent { extraPlugins: [ function( editor: any ) { editor.data.on( 'init', () => { - // eslint-disable-next-line - throw new CKEditorError( 'example-error', editor ); + // Simulate an error. + // Create a non-existing position, then try to get its parent. + const position = editor.model.createPositionFromPath( editor.model.document.getRoot(), [ 1, 2, 3 ] ); + + return position.parent; } ); } ], From ef1a387baf5234337f6f53a465f15e03d03e744b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Remiszewski?= Date: Wed, 11 Oct 2023 13:30:15 +0200 Subject: [PATCH 9/9] Fix tests. --- src/ckeditor/ckeditor.component.spec.ts | 38 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/ckeditor/ckeditor.component.spec.ts b/src/ckeditor/ckeditor.component.spec.ts index 25b36d0..ce5b8b3 100644 --- a/src/ckeditor/ckeditor.component.spec.ts +++ b/src/ckeditor/ckeditor.component.spec.ts @@ -491,27 +491,36 @@ describe( 'CKEditorComponent', () => { } ); } ); } ); + describe( 'initialization errors are catched', () => { - const CrashyEditor = AngularEditor; - let originalCreate: any; + let config: any; beforeEach( () => { - originalCreate = AngularEditor.create; - CrashyEditor.create = () => { - throw 'init failure'; + config = { + extraPlugins: [ + function( editor: any ) { + editor.data.on( 'init', () => { + // Simulate an error. + // Create a non-existing position, then try to get its parent. + const position = editor.model.createPositionFromPath( editor.model.document.getRoot(), [ 1, 2, 3 ] ); + + return position.parent; + } ); + } + ], + collaboration: { + channelId: 'foobar-baz' + } }; } ); - afterEach( () => { - AngularEditor.create = originalCreate; - } ); - - it( 'when internal watchgod is created', async () => { + it( 'when internal watchdog is created', async () => { fixture = TestBed.createComponent( CKEditorComponent ); + const component = fixture.componentInstance; const errorSpy = jasmine.createSpy( 'errorSpy' ); component.error.subscribe( errorSpy ); - component.editor = CrashyEditor; - component.ngAfterViewInit(); + component.editor = AngularEditor; + component.config = config; fixture.detectChanges(); await waitCycle(); @@ -523,6 +532,7 @@ describe( 'CKEditorComponent', () => { it( 'when external watchdog is provided', async () => { fixture = TestBed.createComponent( CKEditorComponent ); + const component = fixture.componentInstance; const errorSpy = jasmine.createSpy( 'errorSpy' ); component.error.subscribe( errorSpy ); const contextWatchdog = new AngularEditor.ContextWatchdog( AngularEditor.Context ); @@ -530,8 +540,8 @@ describe( 'CKEditorComponent', () => { await contextWatchdog.create(); component.watchdog = contextWatchdog; - component.editor = CrashyEditor; - component.ngAfterViewInit(); + component.editor = AngularEditor; + component.config = config; fixture.detectChanges(); await waitCycle();