contextwatchdog.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document, setTimeout, window, console */
  6. import ContextWatchdog from '../src/contextwatchdog';
  7. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  8. import Context from '@ckeditor/ckeditor5-core/src/context';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. describe( 'ContextWatchdog', () => {
  11. let element1, element2;
  12. let watchdog;
  13. let originalErrorHandler;
  14. beforeEach( () => {
  15. element1 = document.createElement( 'div' );
  16. element2 = document.createElement( 'div' );
  17. document.body.appendChild( element1 );
  18. document.body.appendChild( element2 );
  19. originalErrorHandler = window.onerror;
  20. window.onerror = sinon.spy();
  21. } );
  22. afterEach( () => {
  23. window.onerror = originalErrorHandler;
  24. element1.remove();
  25. element2.remove();
  26. sinon.restore();
  27. } );
  28. it( 'should disable adding items once the ContextWatchdog is destroyed', async () => {
  29. watchdog = new ContextWatchdog( Context );
  30. watchdog.create();
  31. await watchdog.destroy();
  32. let err;
  33. try {
  34. await watchdog.add( [ {
  35. id: 'editor1',
  36. type: 'editor',
  37. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  38. sourceElementOrData: element1,
  39. config: {}
  40. } ] );
  41. } catch ( _err ) {
  42. err = _err;
  43. }
  44. expect( err ).to.be.instanceOf( Error );
  45. expect( err.message ).to.match( /Cannot add items to destroyed watchdog\./ );
  46. } );
  47. describe( 'for scenario with no items', () => {
  48. it( 'should create only context', async () => {
  49. watchdog = new ContextWatchdog( Context );
  50. await watchdog.create();
  51. expect( watchdog.context ).to.be.instanceOf( Context );
  52. await watchdog.destroy();
  53. } );
  54. it( 'should have proper states', async () => {
  55. watchdog = new ContextWatchdog( Context );
  56. const initializationPromise = watchdog.create();
  57. expect( watchdog.state ).to.equal( 'initializing' );
  58. await initializationPromise;
  59. expect( watchdog.state ).to.equal( 'ready' );
  60. await watchdog.destroy();
  61. expect( watchdog.state ).to.equal( 'destroyed' );
  62. } );
  63. it( 'should set custom creator and destructor if provided', async () => {
  64. const mainWatchdog = new ContextWatchdog( Context );
  65. const customCreator = sinon.spy( config => Context.create( config ) );
  66. const customDestructor = sinon.spy( context => context.destroy() );
  67. mainWatchdog.setCreator( customCreator );
  68. mainWatchdog.setDestructor( customDestructor );
  69. await mainWatchdog.create();
  70. sinon.assert.calledOnce( customCreator );
  71. await mainWatchdog.destroy();
  72. sinon.assert.calledOnce( customDestructor );
  73. } );
  74. it( 'should log if an error happens during the component destructing', async () => {
  75. const mainWatchdog = new ContextWatchdog( Context );
  76. const consoleErrorStub = sinon.stub( console, 'error' );
  77. const err = new Error( 'foo' );
  78. mainWatchdog.setDestructor( async editor => {
  79. await editor.destroy();
  80. throw err;
  81. } );
  82. await mainWatchdog.create();
  83. await mainWatchdog._restart();
  84. sinon.assert.calledWith(
  85. consoleErrorStub,
  86. 'An error happened during destructing.',
  87. err
  88. );
  89. mainWatchdog.setDestructor( editor => editor.destroy() );
  90. await mainWatchdog.destroy();
  91. } );
  92. it( 'should handle the Watchdog configuration', async () => {
  93. // TODO
  94. const mainWatchdog = new ContextWatchdog( Context, {
  95. crashNumberLimit: Infinity
  96. } );
  97. await mainWatchdog.create();
  98. await mainWatchdog.destroy();
  99. } );
  100. describe( 'in case of error handling', () => {
  101. it( 'should restart the `Context`', async () => {
  102. watchdog = new ContextWatchdog( Context );
  103. const errorSpy = sinon.spy();
  104. await watchdog.create();
  105. const oldContext = watchdog.context;
  106. watchdog.on( 'restart', errorSpy );
  107. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  108. await waitCycle();
  109. sinon.assert.calledOnce( errorSpy );
  110. expect( watchdog.context ).to.not.equal( oldContext );
  111. } );
  112. } );
  113. } );
  114. describe( 'for multiple items scenario', () => {
  115. it( 'should allow adding multiple items without waiting for promises', async () => {
  116. watchdog = new ContextWatchdog( Context );
  117. watchdog.create();
  118. watchdog.add( {
  119. id: 'editor1',
  120. type: 'editor',
  121. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  122. sourceElementOrData: element1,
  123. config: {}
  124. }, {
  125. id: 'editor2',
  126. type: 'editor',
  127. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  128. sourceElementOrData: element2,
  129. config: {}
  130. } );
  131. await watchdog.destroy();
  132. } );
  133. it( 'should throw when multiple items with the same id are added', async () => {
  134. watchdog = new ContextWatchdog( Context );
  135. await watchdog.create();
  136. const editorItemConfig = {
  137. id: 'editor1',
  138. type: 'editor',
  139. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  140. sourceElementOrData: element1,
  141. config: {}
  142. };
  143. const editorCreationPromise1 = watchdog.add( editorItemConfig );
  144. const editorCreationPromise2 = watchdog.add( editorItemConfig );
  145. let err;
  146. try {
  147. await editorCreationPromise1;
  148. await editorCreationPromise2;
  149. } catch ( _err ) {
  150. err = _err;
  151. }
  152. await watchdog.destroy();
  153. expect( err ).to.be.instanceOf( Error );
  154. expect( err.message ).to.match( /Item with the given id is already added: 'editor1'./ );
  155. } );
  156. it( 'should throw when not added item is removed', async () => {
  157. watchdog = new ContextWatchdog( Context );
  158. await watchdog.create();
  159. let err;
  160. try {
  161. await watchdog.remove( [ 'foo' ] );
  162. } catch ( _err ) {
  163. err = _err;
  164. }
  165. await watchdog.destroy();
  166. expect( err ).to.be.instanceOf( Error );
  167. expect( err.message ).to.match( /Item with the given id was not registered: foo\./ );
  168. } );
  169. it( 'should throw when the item is added before the context is created', async () => {
  170. const mainWatchdog = new ContextWatchdog( Context );
  171. let err;
  172. try {
  173. await mainWatchdog.add( {} );
  174. } catch ( _err ) {
  175. err = _err;
  176. }
  177. expect( err ).to.be.instanceOf( Error );
  178. expect( err.message ).to.match(
  179. /Context was not created yet\. You should call the `ContextWatchdog#create\(\)` method first\./
  180. );
  181. } );
  182. it( 'should allow setting editor custom destructors', async () => {
  183. watchdog = new ContextWatchdog( Context );
  184. watchdog.create();
  185. const destructorSpy = sinon.spy( editor => editor.destroy() );
  186. watchdog.add( {
  187. id: 'editor1',
  188. type: 'editor',
  189. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  190. destructor: destructorSpy,
  191. sourceElementOrData: element1,
  192. config: {},
  193. } );
  194. await watchdog.destroy();
  195. sinon.assert.calledOnce( destructorSpy );
  196. } );
  197. it( 'should throw when the item is of not known type', async () => {
  198. watchdog = new ContextWatchdog( Context );
  199. await watchdog.create();
  200. let err;
  201. try {
  202. await watchdog.add( {
  203. id: 'editor1',
  204. type: 'foo',
  205. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  206. sourceElementOrData: element1,
  207. config: {}
  208. } );
  209. } catch ( _err ) {
  210. watchdog._stopErrorHandling();
  211. err = _err;
  212. }
  213. await watchdog.destroy();
  214. expect( err ).to.be.instanceOf( Error );
  215. expect( err.message ).to.match( /Not supported item type: 'foo'\./ );
  216. } );
  217. it( 'should allow adding and removing items without waiting', async () => {
  218. watchdog = new ContextWatchdog( Context );
  219. watchdog.create();
  220. watchdog.add( [ {
  221. id: 'editor1',
  222. type: 'editor',
  223. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  224. sourceElementOrData: element1,
  225. config: {}
  226. }, {
  227. id: 'editor2',
  228. type: 'editor',
  229. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  230. sourceElementOrData: element2,
  231. config: {}
  232. } ] );
  233. watchdog.remove( [ 'editor1' ] );
  234. watchdog.remove( [ 'editor2' ] );
  235. await watchdog.destroy();
  236. } );
  237. it( 'should not change the input items', async () => {
  238. watchdog = new ContextWatchdog( Context );
  239. watchdog.create();
  240. watchdog.add( Object.freeze( [ {
  241. id: 'editor1',
  242. type: 'editor',
  243. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  244. sourceElementOrData: element1,
  245. config: {}
  246. } ] ) );
  247. await watchdog._restart();
  248. await watchdog.destroy();
  249. } );
  250. it( 'should return the created items instances with ContextWatchdog#get( itemId )', async () => {
  251. watchdog = new ContextWatchdog( Context );
  252. watchdog.create();
  253. await watchdog.add( [ {
  254. id: 'editor1',
  255. type: 'editor',
  256. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  257. sourceElementOrData: element1,
  258. config: {}
  259. }, {
  260. id: 'editor2',
  261. type: 'editor',
  262. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  263. sourceElementOrData: element2,
  264. config: {}
  265. } ] );
  266. expect( watchdog.get( 'editor1' ) ).to.be.instanceOf( ClassicTestEditor );
  267. expect( watchdog.get( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  268. await watchdog.remove( [ 'editor1' ] );
  269. expect( () => {
  270. watchdog.get( 'editor1' );
  271. } ).to.throw( /Item with the given id was not registered: editor1\./ );
  272. expect( watchdog.get( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  273. await watchdog.destroy();
  274. } );
  275. describe( 'in case of error handling', () => {
  276. it( 'should restart the whole structure of editors if an error happens inside the `Context`', async () => {
  277. watchdog = new ContextWatchdog( Context );
  278. await watchdog.create();
  279. await watchdog.add( [ {
  280. id: 'editor1',
  281. type: 'editor',
  282. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  283. sourceElementOrData: element1,
  284. config: {}
  285. } ] );
  286. const oldContext = watchdog.context;
  287. const restartSpy = sinon.spy();
  288. watchdog.on( 'restart', restartSpy );
  289. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  290. await waitCycle();
  291. sinon.assert.calledOnce( restartSpy );
  292. expect( watchdog.getState( 'editor1' ) ).to.equal( 'ready' );
  293. expect( watchdog.context ).to.not.equal( oldContext );
  294. await watchdog.destroy();
  295. } );
  296. it( 'should restart only the editor if an error happens inside the editor', async () => {
  297. watchdog = new ContextWatchdog( Context );
  298. await watchdog.create();
  299. await watchdog.add( {
  300. id: 'editor1',
  301. type: 'editor',
  302. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  303. sourceElementOrData: element1,
  304. config: {}
  305. } );
  306. const oldContext = watchdog.context;
  307. const restartSpy = sinon.spy();
  308. const oldEditor = watchdog.get( 'editor1' );
  309. watchdog.on( 'restart', restartSpy );
  310. setTimeout( () => throwCKEditorError( 'foo', oldEditor ) );
  311. await waitCycle();
  312. sinon.assert.notCalled( restartSpy );
  313. expect( watchdog.context ).to.equal( oldContext );
  314. expect( watchdog.get( 'editor1' ) ).to.not.equal( oldEditor );
  315. await watchdog.destroy();
  316. } );
  317. it( 'should restart only the editor if an error happens inside one of the editors', async () => {
  318. watchdog = new ContextWatchdog( Context );
  319. await watchdog.create();
  320. await watchdog.add( [ {
  321. id: 'editor1',
  322. type: 'editor',
  323. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  324. sourceElementOrData: element1,
  325. config: {}
  326. }, {
  327. id: 'editor2',
  328. type: 'editor',
  329. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  330. sourceElementOrData: element2,
  331. config: {}
  332. } ] );
  333. const oldContext = watchdog.context;
  334. const editorWatchdog1 = watchdog._watchdogs.get( 'editor1' );
  335. const editorWatchdog2 = watchdog._watchdogs.get( 'editor2' );
  336. const oldEditor1 = watchdog.get( 'editor1' );
  337. const oldEditor2 = watchdog.get( 'editor2' );
  338. const mainWatchdogRestartSpy = sinon.spy();
  339. const editorWatchdog1RestartSpy = sinon.spy();
  340. const editorWatchdog2RestartSpy = sinon.spy();
  341. watchdog.on( 'restart', mainWatchdogRestartSpy );
  342. editorWatchdog1.on( 'restart', editorWatchdog1RestartSpy );
  343. editorWatchdog2.on( 'restart', editorWatchdog2RestartSpy );
  344. setTimeout( () => throwCKEditorError( 'foo', editorWatchdog1.editor ) );
  345. await waitCycle();
  346. sinon.assert.calledOnce( editorWatchdog1RestartSpy );
  347. sinon.assert.notCalled( mainWatchdogRestartSpy );
  348. sinon.assert.notCalled( editorWatchdog2RestartSpy );
  349. expect( watchdog.getState( 'editor1' ) ).to.equal( 'ready' );
  350. expect( watchdog.getState( 'editor2' ) ).to.equal( 'ready' );
  351. expect( watchdog.state ).to.equal( 'ready' );
  352. expect( oldEditor1 ).to.not.equal( editorWatchdog1.editor );
  353. expect( oldEditor2 ).to.equal( editorWatchdog2.editor );
  354. expect( watchdog.context ).to.equal( oldContext );
  355. await watchdog.destroy();
  356. } );
  357. it( 'should handle removing and restarting at the same time', async () => {
  358. watchdog = new ContextWatchdog( Context );
  359. await watchdog.create();
  360. await watchdog.add( [ {
  361. id: 'editor1',
  362. type: 'editor',
  363. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  364. sourceElementOrData: element1,
  365. config: {}
  366. }, {
  367. id: 'editor2',
  368. type: 'editor',
  369. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  370. sourceElementOrData: element2,
  371. config: {}
  372. } ] );
  373. const editor1 = watchdog.get( 'editor1' );
  374. const removePromise = watchdog.remove( [ 'editor1' ] );
  375. setTimeout( () => throwCKEditorError( 'foo', editor1 ) );
  376. await waitCycle();
  377. await removePromise;
  378. expect( [ ...watchdog._watchdogs.keys() ] ).to.include( 'editor2' );
  379. expect( [ ...watchdog._watchdogs.keys() ] ).to.not.include( 'editor1' );
  380. await watchdog.destroy();
  381. } );
  382. it( 'should handle restarting the item instance many times', async () => {
  383. watchdog = new ContextWatchdog( Context );
  384. await watchdog.create();
  385. await watchdog.add( [ {
  386. id: 'editor1',
  387. type: 'editor',
  388. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  389. sourceElementOrData: element1,
  390. config: {}
  391. }, {
  392. id: 'editor2',
  393. type: 'editor',
  394. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  395. sourceElementOrData: element2,
  396. config: {}
  397. } ] );
  398. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  399. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  400. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  401. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  402. await waitCycle();
  403. expect( watchdog.getState( 'editor1' ) ).to.equal( 'crashedPermanently' );
  404. expect( watchdog.state ).to.equal( 'ready' );
  405. await watchdog.destroy();
  406. } );
  407. } );
  408. } );
  409. } );
  410. function throwCKEditorError( name, context ) {
  411. throw new CKEditorError( name, context );
  412. }
  413. function waitCycle() {
  414. return new Promise( res => setTimeout( res ) );
  415. }