diff --git a/src/editor/editor.js b/src/editor/editor.js index 44ede824..83eb502f 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -150,7 +150,7 @@ export default class Editor { this.commands.destroy(); - return Promise.resolve() + return this.plugins.destroy() .then( () => { this.document.destroy(); this.data.destroy(); diff --git a/src/plugin.js b/src/plugin.js index 344c779b..323b7f5a 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -34,6 +34,7 @@ export default class Plugin { * @inheritDoc */ destroy() { + this.stopListening(); } } diff --git a/src/plugincollection.js b/src/plugincollection.js index a894d41c..df3a5567 100644 --- a/src/plugincollection.js +++ b/src/plugincollection.js @@ -207,6 +207,20 @@ export default class PluginCollection { } } + /** + * Destroys all loaded plugins. + * + * @returns {Promise} + */ + destroy() { + const promises = Array.from( this ) + .map( ( [ , pluginInstance ] ) => pluginInstance ) + .filter( pluginInstance => typeof pluginInstance.destroy == 'function' ) + .map( pluginInstance => pluginInstance.destroy() ); + + return Promise.all( promises ); + } + /** * Adds the plugin to the collection. Exposed mainly for testing purposes. * diff --git a/tests/_utils/classictesteditor.js b/tests/_utils/classictesteditor.js index d8c864ec..271edad2 100644 --- a/tests/_utils/classictesteditor.js +++ b/tests/_utils/classictesteditor.js @@ -43,8 +43,8 @@ export default class ClassicTestEditor extends StandardEditor { destroy() { this._elementReplacer.restore(); - return this.ui.destroy() - .then( () => super.destroy() ); + return super.destroy() + .then( () => this.ui.destroy() ); } /** diff --git a/tests/editor/editor.js b/tests/editor/editor.js index 94caa778..f25ad82b 100644 --- a/tests/editor/editor.js +++ b/tests/editor/editor.js @@ -175,11 +175,13 @@ describe( 'Editor', () => { const spy1 = sinon.spy( editor.data, 'destroy' ); const spy2 = sinon.spy( editor.document, 'destroy' ); + const spy3 = sinon.spy( editor.plugins, 'destroy' ); return editor.destroy() .then( () => { expect( spy1.calledOnce ).to.be.true; expect( spy2.calledOnce ).to.be.true; + expect( spy3.calledOnce ).to.be.true; } ); } ); } ); diff --git a/tests/plugin.js b/tests/plugin.js index d2fa73df..88f68216 100644 --- a/tests/plugin.js +++ b/tests/plugin.js @@ -25,5 +25,14 @@ describe( 'constructor()', () => { expect( plugin.destroy ).to.be.a( 'function' ); } ); + + it( 'should stop listening', () => { + const plugin = new Plugin( editor ); + const stopListeningSpy = sinon.spy( plugin, 'stopListening' ); + + plugin.destroy(); + + expect( stopListeningSpy.calledOnce ).to.equal( true ); + } ); } ); } ); diff --git a/tests/plugincollection.js b/tests/plugincollection.js index 74c63663..9064d40f 100644 --- a/tests/plugincollection.js +++ b/tests/plugincollection.js @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md. */ +/* globals setTimeout */ + import testUtils from '../tests/_utils/utils'; import Editor from '../src/editor/editor'; import PluginCollection from '../src/plugincollection'; @@ -381,6 +383,81 @@ describe( 'PluginCollection', () => { } ); } ); + describe( 'destroy()', () => { + it( 'calls Plugin#destroy() method on every loaded plugin', () => { + let destroySpyForPluginA, destroySpyForPluginB; + + const plugins = new PluginCollection( editor, [] ); + + return plugins.load( [ PluginA, PluginB ] ) + .then( () => { + destroySpyForPluginA = sinon.spy( plugins.get( PluginA ), 'destroy' ); + destroySpyForPluginB = sinon.spy( plugins.get( PluginB ), 'destroy' ); + + return plugins.destroy(); + } ) + .then( () => { + expect( destroySpyForPluginA.calledOnce ).to.equal( true ); + expect( destroySpyForPluginB.calledOnce ).to.equal( true ); + } ); + } ); + + it( 'waits until all plugins are destroyed', () => { + const destroyedPlugins = []; + + class AsynchronousPluginA extends Plugin { + destroy() { + return new Promise( resolve => { + setTimeout( () => { + super.destroy(); + + destroyedPlugins.push( 'AsynchronousPluginA.destroy()' ); + resolve(); + } ); + } ); + } + } + + class AsynchronousPluginB extends Plugin { + destroy() { + return new Promise( resolve => { + setTimeout( () => { + super.destroy(); + + destroyedPlugins.push( 'AsynchronousPluginB.destroy()' ); + resolve(); + } ); + } ); + } + } + + const plugins = new PluginCollection( editor, [] ); + + return plugins.load( [ AsynchronousPluginA, AsynchronousPluginB ] ) + .then( () => plugins.destroy() ) + .then( () => { + expect( destroyedPlugins ).to.contain( 'AsynchronousPluginB.destroy()' ); + expect( destroyedPlugins ).to.contain( 'AsynchronousPluginA.destroy()' ); + } ); + } ); + + it( 'does not execute Plugin#destroy() for plugins which do not have this method', () => { + class FooPlugin { + constructor( editor ) { + this.editor = editor; + } + } + + const plugins = new PluginCollection( editor, [] ); + + return plugins.load( [ PluginA, FooPlugin ] ) + .then( () => plugins.destroy() ) + .then( destroyedPlugins => { + expect( destroyedPlugins.length ).to.equal( 1 ); + } ); + } ); + } ); + describe( 'iterator', () => { it( 'exists', () => { const plugins = new PluginCollection( editor, availablePlugins );