diff --git a/packages/ckeditor5-editor-balloon/tests/ballooneditorui.js b/packages/ckeditor5-editor-balloon/tests/ballooneditorui.js index 28d9488779e..ee8f0cec686 100644 --- a/packages/ckeditor5-editor-balloon/tests/ballooneditorui.js +++ b/packages/ckeditor5-editor-balloon/tests/ballooneditorui.js @@ -189,6 +189,13 @@ describe( 'BalloonEditorUI', () => { sinon.assert.callOrder( parentDestroySpy, viewDestroySpy ); } ); + + it( 'should not crash if called twice', async () => { + const newEditor = await VirtualBalloonTestEditor.create( '' ); + + await newEditor.destroy(); + await newEditor.destroy(); + } ); } ); describe( 'element()', () => { diff --git a/packages/ckeditor5-editor-classic/tests/classiceditorui.js b/packages/ckeditor5-editor-classic/tests/classiceditorui.js index 210626a8a1e..2fbfa63404c 100644 --- a/packages/ckeditor5-editor-classic/tests/classiceditorui.js +++ b/packages/ckeditor5-editor-classic/tests/classiceditorui.js @@ -694,6 +694,13 @@ describe( 'ClassicEditorUI', () => { sinon.assert.callOrder( parentDestroySpy, viewDestroySpy ); } ); + + it( 'should not crash if called twice', async () => { + const newEditor = await VirtualClassicTestEditor.create( '' ); + + await newEditor.destroy(); + await newEditor.destroy(); // Should not throw. + } ); } ); describe( 'view()', () => { diff --git a/packages/ckeditor5-editor-decoupled/tests/decouplededitorui.js b/packages/ckeditor5-editor-decoupled/tests/decouplededitorui.js index 5563d9dc512..85cef70964b 100644 --- a/packages/ckeditor5-editor-decoupled/tests/decouplededitorui.js +++ b/packages/ckeditor5-editor-decoupled/tests/decouplededitorui.js @@ -243,6 +243,13 @@ describe( 'DecoupledEditorUI', () => { sinon.assert.callOrder( parentDestroySpy, viewDestroySpy ); } ); + + it( 'should not crash if called twice', async () => { + const newEditor = await VirtualDecoupledTestEditor.create( '' ); + + await newEditor.destroy(); + await newEditor.destroy(); + } ); } ); describe( 'element()', () => { diff --git a/packages/ckeditor5-editor-inline/src/inlineeditorui.ts b/packages/ckeditor5-editor-inline/src/inlineeditorui.ts index d11f04e7efe..3e21c216761 100644 --- a/packages/ckeditor5-editor-inline/src/inlineeditorui.ts +++ b/packages/ckeditor5-editor-inline/src/inlineeditorui.ts @@ -114,7 +114,10 @@ export default class InlineEditorUI extends EditorUI { const view = this.view; const editingView = this.editor.editing.view; - editingView.detachDomRoot( view.editable.name! ); + if ( editingView.getDomRoot( view.editable.name! ) ) { + editingView.detachDomRoot( view.editable.name! ); + } + view.destroy(); } diff --git a/packages/ckeditor5-editor-inline/tests/inlineeditorui.js b/packages/ckeditor5-editor-inline/tests/inlineeditorui.js index 3842d42ff08..96db3b25bdc 100644 --- a/packages/ckeditor5-editor-inline/tests/inlineeditorui.js +++ b/packages/ckeditor5-editor-inline/tests/inlineeditorui.js @@ -41,7 +41,9 @@ describe( 'InlineEditorUI', () => { } ); afterEach( async () => { - await editor.destroy(); + if ( editor ) { + await editor.destroy(); + } } ); describe( 'constructor()', () => { @@ -328,6 +330,20 @@ describe( 'InlineEditorUI', () => { sinon.assert.callOrder( parentDestroySpy, viewDestroySpy ); } ); + + it( 'should not crash if the editable element is not present', async () => { + editor.editing.view.detachDomRoot( editor.ui.view.editable.name ); + + await editor.destroy(); + editor = null; + } ); + + it( 'should not crash if called twice', async () => { + const editor = await VirtualInlineTestEditor.create( '' ); + + await editor.destroy(); + await editor.destroy(); + } ); } ); describe( 'element()', () => { diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts index 6d0c2edbae1..9948298c9de 100644 --- a/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts +++ b/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts @@ -157,7 +157,12 @@ export default class MultiRootEditorUI extends EditorUI { * @param editable Editable to remove from the editor UI. */ public removeEditable( editable: InlineEditableUIView ): void { - this.editor.editing.view.detachDomRoot( editable.name! ); + const editingView = this.editor.editing.view; + + if ( editingView.getDomRoot( editable.name! ) ) { + editingView.detachDomRoot( editable.name! ); + } + editable.unbind( 'isFocused' ); this.removeEditableElement( editable.name! ); } diff --git a/packages/ckeditor5-editor-multi-root/tests/multirooteditorui.js b/packages/ckeditor5-editor-multi-root/tests/multirooteditorui.js index cc8df837cdb..22efb56fbac 100644 --- a/packages/ckeditor5-editor-multi-root/tests/multirooteditorui.js +++ b/packages/ckeditor5-editor-multi-root/tests/multirooteditorui.js @@ -423,5 +423,29 @@ describe( 'MultiRootEditorUI', () => { sinon.assert.callOrder( parentDestroySpy, viewDestroySpy ); } ); + + // Some of integrations might detach the DOM editing view *before* destroying the editor. + // It happens quite often in the strict mode of the React integration. In such case, the editor + // component is being unmounted after editable component is detached from the DOM. In such scenario, + // the root doesn't contain the DOM editable anymore. This test ensures that the editor does not throw. + // Issue: https://github.com/ckeditor/ckeditor5/issues/16561 + it( 'should not throw when trying to detach a DOM root that was not attached to editing view', async () => { + const newEditor = await MultiRootEditor.create( { foo: '', bar: '' } ); + const editingView = newEditor.editing.view; + + // Simulate unmounting the editable child component before the editor component. + editingView.detachDomRoot( 'foo' ); + + // This should not throw + await newEditor.destroy(); + } ); + + // Issue: https://github.com/ckeditor/ckeditor5/issues/16561 + it( 'should not throw error when it was called twice', async () => { + const newEditor = await MultiRootEditor.create( { foo: '', bar: '' } ); + + await newEditor.destroy(); + await newEditor.destroy(); // This should not throw + } ); } ); } );