contextwatchdog.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  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 for promises', 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', 'editor2' ] );
  234. await watchdog.destroy();
  235. } );
  236. it( 'should not change the input items', async () => {
  237. watchdog = new ContextWatchdog( Context );
  238. watchdog.create();
  239. watchdog.add( Object.freeze( [ {
  240. id: 'editor1',
  241. type: 'editor',
  242. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  243. sourceElementOrData: element1,
  244. config: {}
  245. } ] ) );
  246. await watchdog._restart();
  247. await watchdog.destroy();
  248. } );
  249. it( 'should return the created items instances with ContextWatchdog#get( itemId )', async () => {
  250. watchdog = new ContextWatchdog( Context );
  251. watchdog.create();
  252. await watchdog.add( [ {
  253. id: 'editor1',
  254. type: 'editor',
  255. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  256. sourceElementOrData: element1,
  257. config: {}
  258. }, {
  259. id: 'editor2',
  260. type: 'editor',
  261. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  262. sourceElementOrData: element2,
  263. config: {}
  264. } ] );
  265. expect( watchdog.get( 'editor1' ) ).to.be.instanceOf( ClassicTestEditor );
  266. expect( watchdog.get( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  267. await watchdog.remove( 'editor1' );
  268. expect( () => {
  269. watchdog.get( 'editor1' );
  270. } ).to.throw( /Item with the given id was not registered: editor1\./ );
  271. expect( watchdog.get( 'editor2' ) ).to.be.instanceOf( ClassicTestEditor );
  272. await watchdog.destroy();
  273. } );
  274. describe( 'in case of error handling', () => {
  275. it( 'should restart the whole structure of editors if an error happens inside the `Context`', async () => {
  276. watchdog = new ContextWatchdog( Context );
  277. await watchdog.create();
  278. await watchdog.add( [ {
  279. id: 'editor1',
  280. type: 'editor',
  281. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  282. sourceElementOrData: element1,
  283. config: {}
  284. } ] );
  285. const oldContext = watchdog.context;
  286. const restartSpy = sinon.spy();
  287. watchdog.on( 'restart', restartSpy );
  288. setTimeout( () => throwCKEditorError( 'foo', watchdog.context ) );
  289. await waitCycle();
  290. sinon.assert.calledOnce( restartSpy );
  291. expect( watchdog.getState( 'editor1' ) ).to.equal( 'ready' );
  292. expect( watchdog.context ).to.not.equal( oldContext );
  293. await watchdog.destroy();
  294. } );
  295. it( 'should restart only the editor if an error happens inside the editor', async () => {
  296. watchdog = new ContextWatchdog( Context );
  297. await watchdog.create();
  298. await watchdog.add( {
  299. id: 'editor1',
  300. type: 'editor',
  301. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  302. sourceElementOrData: element1,
  303. config: {}
  304. } );
  305. const oldContext = watchdog.context;
  306. const restartSpy = sinon.spy();
  307. const oldEditor = watchdog.get( 'editor1' );
  308. watchdog.on( 'restart', restartSpy );
  309. setTimeout( () => throwCKEditorError( 'foo', oldEditor ) );
  310. await waitCycle();
  311. sinon.assert.notCalled( restartSpy );
  312. expect( watchdog.context ).to.equal( oldContext );
  313. expect( watchdog.get( 'editor1' ) ).to.not.equal( oldEditor );
  314. await watchdog.destroy();
  315. } );
  316. it( 'should restart only the editor if an error happens inside one of the editors', async () => {
  317. watchdog = new ContextWatchdog( Context );
  318. await watchdog.create();
  319. await watchdog.add( [ {
  320. id: 'editor1',
  321. type: 'editor',
  322. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  323. sourceElementOrData: element1,
  324. config: {}
  325. }, {
  326. id: 'editor2',
  327. type: 'editor',
  328. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  329. sourceElementOrData: element2,
  330. config: {}
  331. } ] );
  332. const oldContext = watchdog.context;
  333. const editorWatchdog1 = watchdog._watchdogs.get( 'editor1' );
  334. const editorWatchdog2 = watchdog._watchdogs.get( 'editor2' );
  335. const oldEditor1 = watchdog.get( 'editor1' );
  336. const oldEditor2 = watchdog.get( 'editor2' );
  337. const mainWatchdogRestartSpy = sinon.spy();
  338. const editorWatchdog1RestartSpy = sinon.spy();
  339. const editorWatchdog2RestartSpy = sinon.spy();
  340. watchdog.on( 'restart', mainWatchdogRestartSpy );
  341. editorWatchdog1.on( 'restart', editorWatchdog1RestartSpy );
  342. editorWatchdog2.on( 'restart', editorWatchdog2RestartSpy );
  343. setTimeout( () => throwCKEditorError( 'foo', editorWatchdog1.editor ) );
  344. await waitCycle();
  345. sinon.assert.calledOnce( editorWatchdog1RestartSpy );
  346. sinon.assert.notCalled( mainWatchdogRestartSpy );
  347. sinon.assert.notCalled( editorWatchdog2RestartSpy );
  348. expect( watchdog.getState( 'editor1' ) ).to.equal( 'ready' );
  349. expect( watchdog.getState( 'editor2' ) ).to.equal( 'ready' );
  350. expect( watchdog.state ).to.equal( 'ready' );
  351. expect( oldEditor1 ).to.not.equal( editorWatchdog1.editor );
  352. expect( oldEditor2 ).to.equal( editorWatchdog2.editor );
  353. expect( watchdog.context ).to.equal( oldContext );
  354. await watchdog.destroy();
  355. } );
  356. it( 'should handle removing and restarting at the same time', async () => {
  357. watchdog = new ContextWatchdog( Context );
  358. await watchdog.create();
  359. await watchdog.add( [ {
  360. id: 'editor1',
  361. type: 'editor',
  362. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  363. sourceElementOrData: element1,
  364. config: {}
  365. }, {
  366. id: 'editor2',
  367. type: 'editor',
  368. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  369. sourceElementOrData: element2,
  370. config: {}
  371. } ] );
  372. const editor1 = watchdog.get( 'editor1' );
  373. const removePromise = watchdog.remove( 'editor1' );
  374. setTimeout( () => throwCKEditorError( 'foo', editor1 ) );
  375. await waitCycle();
  376. await removePromise;
  377. expect( [ ...watchdog._watchdogs.keys() ] ).to.include( 'editor2' );
  378. expect( [ ...watchdog._watchdogs.keys() ] ).to.not.include( 'editor1' );
  379. await watchdog.destroy();
  380. } );
  381. it( 'should handle restarting the item instance many times', async () => {
  382. watchdog = new ContextWatchdog( Context );
  383. await watchdog.create();
  384. await watchdog.add( [ {
  385. id: 'editor1',
  386. type: 'editor',
  387. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  388. sourceElementOrData: element1,
  389. config: {}
  390. }, {
  391. id: 'editor2',
  392. type: 'editor',
  393. creator: ( el, config ) => ClassicTestEditor.create( el, config ),
  394. sourceElementOrData: element2,
  395. config: {}
  396. } ] );
  397. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  398. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  399. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  400. setTimeout( () => throwCKEditorError( 'foo', watchdog.get( 'editor1' ) ) );
  401. await waitCycle();
  402. expect( watchdog.getState( 'editor1' ) ).to.equal( 'crashedPermanently' );
  403. expect( watchdog.state ).to.equal( 'ready' );
  404. await watchdog.destroy();
  405. } );
  406. } );
  407. } );
  408. } );
  409. function throwCKEditorError( name, context ) {
  410. throw new CKEditorError( name, context );
  411. }
  412. function waitCycle() {
  413. return new Promise( res => setTimeout( res ) );
  414. }