plugincollection.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. const modules = bender.amd.require(
  7. 'core/editor',
  8. 'core/plugincollection',
  9. 'core/plugin',
  10. 'core/creator',
  11. 'core/ckeditorerror',
  12. 'core/log'
  13. );
  14. let PluginCollection, Plugin, Editor, Creator, CKEditorError, log;
  15. let editor;
  16. let PluginA, PluginB, PluginC, PluginD, PluginE, PluginF, PluginG;
  17. class TestError extends Error {}
  18. bender.tools.createSinonSandbox();
  19. before( () => {
  20. PluginCollection = modules[ 'core/plugincollection' ];
  21. Editor = modules[ 'core/editor' ];
  22. Plugin = modules[ 'core/plugin' ];
  23. Creator = modules[ 'core/creator' ];
  24. CKEditorError = modules[ 'core/ckeditorerror' ];
  25. log = modules[ 'core/log' ];
  26. PluginA = createPlugin( 'A' );
  27. PluginB = createPlugin( 'B' );
  28. PluginC = createPlugin( 'C' );
  29. PluginD = createPlugin( 'D' );
  30. PluginE = createPlugin( 'E' );
  31. PluginF = createPlugin( 'F' );
  32. PluginG = createPlugin( 'G', Creator );
  33. PluginC.requires = [ PluginB ];
  34. PluginD.requires = [ PluginA, PluginC ];
  35. PluginF.requires = [ PluginE ];
  36. PluginE.requires = [ PluginF ];
  37. editor = new Editor( document.body.appendChild( document.createElement( 'div' ) ) );
  38. } );
  39. // Create fake plugins that will be used on tests.
  40. bender.amd.define( 'A', () => {
  41. return PluginA;
  42. } );
  43. bender.amd.define( 'B', () => {
  44. return PluginB;
  45. } );
  46. bender.amd.define( 'C', [ 'core/plugin', 'B' ], () => {
  47. return PluginC;
  48. } );
  49. bender.amd.define( 'D', [ 'core/plugin', 'A', 'C' ], () => {
  50. return PluginD;
  51. } );
  52. bender.amd.define( 'E', [ 'core/plugin', 'F' ], () => {
  53. return PluginE;
  54. } );
  55. bender.amd.define( 'F', [ 'core/plugin', 'E' ], () => {
  56. return PluginF;
  57. } );
  58. bender.amd.define( 'G', () => {
  59. return PluginG;
  60. } );
  61. // Erroneous cases.
  62. bender.amd.define( 'X', () => {
  63. throw new TestError( 'Some error inside a plugin' );
  64. } );
  65. bender.amd.define( 'Y', () => {
  66. return class {};
  67. } );
  68. /////////////
  69. describe( 'load', () => {
  70. it( 'should not fail when trying to load 0 plugins (empty array)', () => {
  71. let plugins = new PluginCollection( editor );
  72. return plugins.load( [] )
  73. .then( () => {
  74. expect( getPlugins( plugins ) ).to.be.empty();
  75. } );
  76. } );
  77. it( 'should add collection items for loaded plugins', () => {
  78. let plugins = new PluginCollection( editor );
  79. return plugins.load( [ 'A', 'B' ] )
  80. .then( () => {
  81. expect( getPlugins( plugins ).length ).to.equal( 2 );
  82. expect( plugins.get( PluginA ) ).to.be.an.instanceof( PluginA );
  83. expect( plugins.get( PluginB ) ).to.be.an.instanceof( PluginB );
  84. expect( plugins.get( 'A' ) ).to.be.an.instanceof( PluginA );
  85. expect( plugins.get( 'B' ) ).to.be.an.instanceof( PluginB );
  86. } );
  87. } );
  88. it( 'should load dependency plugins', () => {
  89. let plugins = new PluginCollection( editor );
  90. let spy = sinon.spy( plugins, 'set' );
  91. return plugins.load( [ 'A', 'C' ] )
  92. .then( ( loadedPlugins ) => {
  93. expect( getPlugins( plugins ).length ).to.equal( 3 );
  94. expect( getPluginNames( getPluginsFromSpy( spy ) ) )
  95. .to.deep.equal( [ 'A', 'B', 'C' ], 'order by plugins.set()' );
  96. expect( getPluginNames( loadedPlugins ) )
  97. .to.deep.equal( [ 'A', 'B', 'C' ], 'order by returned value' );
  98. } );
  99. } );
  100. it( 'should be ok when dependencies are loaded first', () => {
  101. let plugins = new PluginCollection( editor );
  102. let spy = sinon.spy( plugins, 'set' );
  103. return plugins.load( [ 'A', 'B', 'C' ] )
  104. .then( ( loadedPlugins ) => {
  105. expect( getPlugins( plugins ).length ).to.equal( 3 );
  106. expect( getPluginNames( getPluginsFromSpy( spy ) ) )
  107. .to.deep.equal( [ 'A', 'B', 'C' ], 'order by plugins.set()' );
  108. expect( getPluginNames( loadedPlugins ) )
  109. .to.deep.equal( [ 'A', 'B', 'C' ], 'order by returned value' );
  110. } );
  111. } );
  112. it( 'should load deep dependency plugins', () => {
  113. let plugins = new PluginCollection( editor );
  114. let spy = sinon.spy( plugins, 'set' );
  115. return plugins.load( [ 'D' ] )
  116. .then( ( loadedPlugins ) => {
  117. expect( getPlugins( plugins ).length ).to.equal( 4 );
  118. // The order must have dependencies first.
  119. expect( getPluginNames( getPluginsFromSpy( spy ) ) )
  120. .to.deep.equal( [ 'A', 'B', 'C', 'D' ], 'order by plugins.set()' );
  121. expect( getPluginNames( loadedPlugins ) )
  122. .to.deep.equal( [ 'A', 'B', 'C', 'D' ], 'order by returned value' );
  123. } );
  124. } );
  125. it( 'should handle cross dependency plugins', () => {
  126. let plugins = new PluginCollection( editor );
  127. let spy = sinon.spy( plugins, 'set' );
  128. return plugins.load( [ 'A', 'E' ] )
  129. .then( ( loadedPlugins ) => {
  130. expect( getPlugins( plugins ).length ).to.equal( 3 );
  131. // The order must have dependencies first.
  132. expect( getPluginNames( getPluginsFromSpy( spy ) ) )
  133. .to.deep.equal( [ 'A', 'F', 'E' ], 'order by plugins.set()' );
  134. expect( getPluginNames( loadedPlugins ) )
  135. .to.deep.equal( [ 'A', 'F', 'E' ], 'order by returned value' );
  136. } );
  137. } );
  138. it( 'should load grand child classes', () => {
  139. let plugins = new PluginCollection( editor );
  140. return plugins.load( [ 'G' ] )
  141. .then( () => {
  142. expect( getPlugins( plugins ).length ).to.equal( 1 );
  143. } );
  144. } );
  145. it( 'should set the `editor` property on loaded plugins', () => {
  146. let plugins = new PluginCollection( editor );
  147. return plugins.load( [ 'A', 'B' ] )
  148. .then( () => {
  149. expect( plugins.get( 'A' ).editor ).to.equal( editor );
  150. expect( plugins.get( 'B' ).editor ).to.equal( editor );
  151. } );
  152. } );
  153. it( 'should reject on invalid plugin names (forward require.js loading error)', () => {
  154. let logSpy = bender.sinon.stub( log, 'error' );
  155. let plugins = new PluginCollection( editor );
  156. return plugins.load( [ 'A', 'BAD', 'B' ] )
  157. // Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
  158. .then( () => {
  159. throw new Error( 'Test error: this promise should not be resolved successfully' );
  160. } )
  161. .catch( ( err ) => {
  162. expect( err ).to.be.an.instanceof( Error );
  163. // Make sure it's the Require.JS error, not the one thrown above.
  164. expect( err.message ).to.match( /^Script error for/ );
  165. sinon.assert.calledOnce( logSpy );
  166. expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
  167. } );
  168. } );
  169. it( 'should reject on broken plugins (forward the error thrown in a plugin)', () => {
  170. let logSpy = bender.sinon.stub( log, 'error' );
  171. let plugins = new PluginCollection( editor );
  172. return plugins.load( [ 'A', 'X', 'B' ] )
  173. // Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
  174. .then( () => {
  175. throw new Error( 'Test error: this promise should not be resolved successfully' );
  176. } )
  177. .catch( ( err ) => {
  178. expect( err ).to.be.an.instanceof( TestError );
  179. expect( err ).to.have.property( 'message', 'Some error inside a plugin' );
  180. sinon.assert.calledOnce( logSpy );
  181. expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
  182. } );
  183. } );
  184. it( 'should reject when loading a module which is not a plugin', () => {
  185. let logSpy = bender.sinon.stub( log, 'error' );
  186. let plugins = new PluginCollection( editor );
  187. return plugins.load( [ 'Y' ] )
  188. // Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
  189. .then( () => {
  190. throw new Error( 'Test error: this promise should not be resolved successfully' );
  191. } )
  192. .catch( ( err ) => {
  193. expect( err ).to.be.an.instanceof( CKEditorError );
  194. expect( err.message ).to.match( /^plugincollection-instance/ );
  195. sinon.assert.calledOnce( logSpy );
  196. expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
  197. } );
  198. } );
  199. } );
  200. function createPlugin( name, baseClass ) {
  201. const P = class extends ( baseClass || Plugin ) {
  202. constructor( editor ) {
  203. super( editor );
  204. this._pluginName = name;
  205. }
  206. };
  207. P._pluginName = name;
  208. return P;
  209. }
  210. function getPlugins( pluginCollection ) {
  211. const plugins = [];
  212. for ( let entry of pluginCollection.entries() ) {
  213. if ( typeof entry[ 0 ] == 'function' ) {
  214. plugins.push( entry[ 1 ] );
  215. }
  216. }
  217. return plugins;
  218. }
  219. function getPluginsFromSpy( addSpy ) {
  220. return addSpy.args
  221. .map( ( arg ) => arg[ 0 ] )
  222. // Entries may be kept twice in the plugins map - once as a pluginName => plugin, once as pluginClass => plugin.
  223. // Return only pluginClass => plugin entries as these will always represent all plugins.
  224. .filter( ( plugin ) => typeof plugin == 'function' );
  225. }
  226. function getPluginNames( plugins ) {
  227. return plugins.map( ( plugin ) => plugin._pluginName );
  228. }