contextwatchdog.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  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 destroying', 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 destroying the context or items.',
  87. err
  88. );
  89. mainWatchdog.setDestructor( editor => editor.destroy() );
  90. await mainWatchdog.destroy();
  91. } );
  92. it( 'should handle the Watchdog configuration', async () => {
  93. watchdog = new ContextWatchdog( Context, {
  94. crashNumberLimit: 0
  95. } );
  96. await watchdog.create();
  97. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  98. await waitCycle();
  99. expect( watchdog.state ).to.equal( 'crashedPermanently' );
  100. await watchdog.destroy();
  101. } );
  102. describe( 'in case of error handling', () => {
  103. it( 'should restart the `Context`', async () => {
  104. watchdog = new ContextWatchdog( Context );
  105. const errorSpy = sinon.spy();
  106. await watchdog.create();
  107. const oldContext = watchdog.context;
  108. watchdog.on( 'restart', errorSpy );
  109. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  110. await waitCycle();
  111. sinon.assert.calledOnce( errorSpy );
  112. expect( watchdog.context ).to.not.equal( oldContext );
  113. } );
  114. } );
  115. } );
  116. describe( 'for multiple items scenario', () => {
  117. it( 'should allow adding multiple items without waiting for promises', async () => {
  118. watchdog = new ContextWatchdog( Context );
  119. watchdog.create();
  120. watchdog.add( {
  121. id: 'editor1',
  122. type: 'editor',
  123. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  124. sourceElementOrData: element1,
  125. config: {}
  126. }, {
  127. id: 'editor2',
  128. type: 'editor',
  129. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  130. sourceElementOrData: element2,
  131. config: {}
  132. } );
  133. await watchdog.destroy();
  134. } );
  135. it( 'should throw when multiple items with the same id are added', async () => {
  136. watchdog = new ContextWatchdog( Context );
  137. await watchdog.create();
  138. const editorItemConfig = {
  139. id: 'editor1',
  140. type: 'editor',
  141. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  142. sourceElementOrData: element1,
  143. config: {}
  144. };
  145. const editorCreationPromise1 = watchdog.add( editorItemConfig );
  146. const editorCreationPromise2 = watchdog.add( editorItemConfig );
  147. let err;
  148. try {
  149. await editorCreationPromise1;
  150. await editorCreationPromise2;
  151. } catch ( _err ) {
  152. err = _err;
  153. }
  154. await watchdog.destroy();
  155. expect( err ).to.be.instanceOf( Error );
  156. expect( err.message ).to.match( /Item with the given id is already added: 'editor1'./ );
  157. } );
  158. it( 'should throw when not added item is removed', async () => {
  159. watchdog = new ContextWatchdog( Context );
  160. await watchdog.create();
  161. let err;
  162. try {
  163. await watchdog.remove( 'foo' );
  164. } catch ( _err ) {
  165. err = _err;
  166. }
  167. await watchdog.destroy();
  168. expect( err ).to.be.instanceOf( Error );
  169. expect( err.message ).to.match( /Item with the given id was not registered: foo\./ );
  170. } );
  171. it( 'should throw when the item is added before the context is created', async () => {
  172. const mainWatchdog = new ContextWatchdog( Context );
  173. let err;
  174. try {
  175. await mainWatchdog.add( {} );
  176. } catch ( _err ) {
  177. err = _err;
  178. }
  179. expect( err ).to.be.instanceOf( Error );
  180. expect( err.message ).to.match(
  181. /Context was not created yet\. You should call the `ContextWatchdog#create\(\)` method first\./
  182. );
  183. } );
  184. it( 'should allow setting editor custom destructors', async () => {
  185. watchdog = new ContextWatchdog( Context );
  186. watchdog.create();
  187. const destructorSpy = sinon.spy( editor => editor.destroy() );
  188. watchdog.add( {
  189. id: 'editor1',
  190. type: 'editor',
  191. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  192. destructor: destructorSpy,
  193. sourceElementOrData: element1,
  194. config: {},
  195. } );
  196. await watchdog.destroy();
  197. sinon.assert.calledOnce( destructorSpy );
  198. } );
  199. it( 'should throw when the item is of not known type', async () => {
  200. watchdog = new ContextWatchdog( Context );
  201. await watchdog.create();
  202. let err;
  203. try {
  204. await watchdog.add( {
  205. id: 'editor1',
  206. type: 'foo',
  207. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  208. sourceElementOrData: element1,
  209. config: {}
  210. } );
  211. } catch ( _err ) {
  212. watchdog._stopErrorHandling();
  213. err = _err;
  214. }
  215. await watchdog.destroy();
  216. expect( err ).to.be.instanceOf( Error );
  217. expect( err.message ).to.match( /Not supported item type: 'foo'\./ );
  218. } );
  219. it( 'should allow adding and removing items without waiting for promises', async () => {
  220. watchdog = new ContextWatchdog( Context );
  221. watchdog.create();
  222. watchdog.add( [ {
  223. id: 'editor1',
  224. type: 'editor',
  225. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  226. sourceElementOrData: element1,
  227. config: {}
  228. }, {
  229. id: 'editor2',
  230. type: 'editor',
  231. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  232. sourceElementOrData: element2,
  233. config: {}
  234. } ] );
  235. watchdog.remove( [ 'editor1', 'editor2' ] );
  236. await watchdog.destroy();
  237. } );
  238. it( 'should not change the input items', async () => {
  239. watchdog = new ContextWatchdog( Context );
  240. watchdog.create();
  241. watchdog.add( Object.freeze( [ {
  242. id: 'editor1',
  243. type: 'editor',
  244. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  245. sourceElementOrData: element1,
  246. config: {}
  247. } ] ) );
  248. await watchdog._restart();
  249. await watchdog.destroy();
  250. } );
  251. it( 'should return the created items instances with ContextWatchdog#getItem( itemId )', async () => {
  252. watchdog = new ContextWatchdog( Context );
  253. watchdog.create();
  254. await watchdog.add( [ {
  255. id: 'editor1',
  256. type: 'editor',
  257. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  258. sourceElementOrData: element1,
  259. config: {}
  260. }, {
  261. id: 'editor2',
  262. type: 'editor',
  263. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  264. sourceElementOrData: element2,
  265. config: {}
  266. } ] );
  267. expect( watchdog.getItem( 'editor1' ) ).to.be.instanceOf( ClassicTestEditor );
  268. expect( watchdog.getItem( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  269. await watchdog.remove( 'editor1' );
  270. expect( () => {
  271. watchdog.getItem( 'editor1' );
  272. } ).to.throw( /Item with the given id was not registered: editor1\./ );
  273. expect( watchdog.getItem( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  274. await watchdog.destroy();
  275. } );
  276. describe( 'in case of error handling', () => {
  277. it( 'should restart the whole structure of editors if an error happens inside the `Context`', async () => {
  278. watchdog = new ContextWatchdog( Context );
  279. await watchdog.create();
  280. await watchdog.add( [ {
  281. id: 'editor1',
  282. type: 'editor',
  283. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  284. sourceElementOrData: element1,
  285. config: {}
  286. } ] );
  287. const oldContext = watchdog.context;
  288. const restartSpy = sinon.spy();
  289. watchdog.on( 'restart', restartSpy );
  290. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  291. await waitCycle();
  292. sinon.assert.calledOnce( restartSpy );
  293. expect( watchdog.getItemState( 'editor1' ) ).to.equal( 'ready' );
  294. expect( watchdog.context ).to.not.equal( oldContext );
  295. await watchdog.destroy();
  296. } );
  297. it( 'should restart only the editor if an error happens inside the editor', async () => {
  298. watchdog = new ContextWatchdog( Context );
  299. await watchdog.create();
  300. await watchdog.add( {
  301. id: 'editor1',
  302. type: 'editor',
  303. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  304. sourceElementOrData: element1,
  305. config: {}
  306. } );
  307. const oldContext = watchdog.context;
  308. const restartSpy = sinon.spy();
  309. const oldEditor = watchdog.getItem( 'editor1' );
  310. watchdog.on( 'restart', restartSpy );
  311. setTimeout( () => throwCKEditorError( 'foo', oldEditor ) );
  312. await waitCycle();
  313. sinon.assert.notCalled( restartSpy );
  314. expect( watchdog.context ).to.equal( oldContext );
  315. expect( watchdog.getItem( 'editor1' ) ).to.not.equal( oldEditor );
  316. await watchdog.destroy();
  317. } );
  318. it( 'should restart only the editor if an error happens inside one of the editors', async () => {
  319. watchdog = new ContextWatchdog( Context );
  320. await watchdog.create();
  321. await watchdog.add( [ {
  322. id: 'editor1',
  323. type: 'editor',
  324. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  325. sourceElementOrData: element1,
  326. config: {}
  327. }, {
  328. id: 'editor2',
  329. type: 'editor',
  330. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  331. sourceElementOrData: element2,
  332. config: {}
  333. } ] );
  334. const oldContext = watchdog.context;
  335. const editorWatchdog1 = watchdog._getWatchdog( 'editor1' );
  336. const editorWatchdog2 = watchdog._getWatchdog( 'editor2' );
  337. const oldEditor1 = watchdog.getItem( 'editor1' );
  338. const oldEditor2 = watchdog.getItem( 'editor2' );
  339. const mainWatchdogRestartSpy = sinon.spy();
  340. const editorWatchdog1RestartSpy = sinon.spy();
  341. const editorWatchdog2RestartSpy = sinon.spy();
  342. watchdog.on( 'restart', mainWatchdogRestartSpy );
  343. editorWatchdog1.on( 'restart', editorWatchdog1RestartSpy );
  344. editorWatchdog2.on( 'restart', editorWatchdog2RestartSpy );
  345. setTimeout( () => throwCKEditorError( 'foo', editorWatchdog1.editor ) );
  346. await waitCycle();
  347. sinon.assert.calledOnce( editorWatchdog1RestartSpy );
  348. sinon.assert.notCalled( mainWatchdogRestartSpy );
  349. sinon.assert.notCalled( editorWatchdog2RestartSpy );
  350. expect( watchdog.getItemState( 'editor1' ) ).to.equal( 'ready' );
  351. expect( watchdog.getItemState( 'editor2' ) ).to.equal( 'ready' );
  352. expect( watchdog.state ).to.equal( 'ready' );
  353. expect( oldEditor1 ).to.not.equal( editorWatchdog1.editor );
  354. expect( oldEditor2 ).to.equal( editorWatchdog2.editor );
  355. expect( watchdog.context ).to.equal( oldContext );
  356. await watchdog.destroy();
  357. } );
  358. it( 'should handle removing and restarting at the same time', async () => {
  359. watchdog = new ContextWatchdog( Context );
  360. await watchdog.create();
  361. await watchdog.add( [ {
  362. id: 'editor1',
  363. type: 'editor',
  364. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  365. sourceElementOrData: element1,
  366. config: {}
  367. }, {
  368. id: 'editor2',
  369. type: 'editor',
  370. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  371. sourceElementOrData: element2,
  372. config: {}
  373. } ] );
  374. const editor1 = watchdog.getItem( 'editor1' );
  375. const removePromise = watchdog.remove( 'editor1' );
  376. setTimeout( () => throwCKEditorError( 'foo', editor1 ) );
  377. await waitCycle();
  378. await removePromise;
  379. expect( Array.from( watchdog._watchdogs.keys() ) ).to.include( 'editor2' );
  380. expect( Array.from( watchdog._watchdogs.keys() ) ).to.not.include( 'editor1' );
  381. await watchdog.destroy();
  382. } );
  383. it( 'should handle restarting the item instance many times', async () => {
  384. watchdog = new ContextWatchdog( Context );
  385. await watchdog.create();
  386. await watchdog.add( [ {
  387. id: 'editor1',
  388. type: 'editor',
  389. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  390. sourceElementOrData: element1,
  391. config: {}
  392. }, {
  393. id: 'editor2',
  394. type: 'editor',
  395. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  396. sourceElementOrData: element2,
  397. config: {}
  398. } ] );
  399. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  400. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  401. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  402. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  403. await waitCycle();
  404. expect( watchdog.getItemState( 'editor1' ) ).to.equal( 'crashedPermanently' );
  405. expect( watchdog.state ).to.equal( 'ready' );
  406. await watchdog.destroy();
  407. } );
  408. it( 'should rethrow item `error` events as `itemError` events', async () => {
  409. watchdog = new ContextWatchdog( Context );
  410. watchdog.create();
  411. await watchdog.add( [ {
  412. id: 'editor1',
  413. type: 'editor',
  414. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  415. sourceElementOrData: element1,
  416. config: {}
  417. } ] );
  418. watchdog.on(
  419. 'itemError',
  420. sinon.mock()
  421. .once()
  422. .withArgs( sinon.match.any, sinon.match( data => {
  423. return data.itemId === 'editor1';
  424. } ) )
  425. .callsFake( () => {
  426. expect( watchdog.getItemState( 'editor1' ) ).to.equal( 'crashed' );
  427. } )
  428. );
  429. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  430. await waitCycle();
  431. sinon.verify();
  432. await watchdog.destroy();
  433. } );
  434. it( 'should rethrow item `restart` events as `itemRestart` events', async () => {
  435. watchdog = new ContextWatchdog( Context );
  436. watchdog.create();
  437. await watchdog.add( [ {
  438. id: 'editor1',
  439. type: 'editor',
  440. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  441. sourceElementOrData: element1,
  442. config: {}
  443. } ] );
  444. watchdog.on(
  445. 'itemRestart',
  446. sinon.mock()
  447. .once()
  448. .withArgs( sinon.match.any, sinon.match( data => {
  449. return data.itemId === 'editor1';
  450. } ) )
  451. .callsFake( () => {
  452. expect( watchdog.getItemState( 'editor1' ) ).to.equal( 'ready' );
  453. } )
  454. );
  455. setTimeout( () => throwCKEditorError( 'foo', watchdog.getItem( 'editor1' ) ) );
  456. await waitCycle();
  457. sinon.verify();
  458. await watchdog.destroy();
  459. } );
  460. } );
  461. } );
  462. } );
  463. function throwCKEditorError( name, context ) {
  464. throw new CKEditorError( name, context );
  465. }
  466. function waitCycle() {
  467. return new Promise( res => setTimeout( res ) );
  468. }