8
0

editorwatchdog.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  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 setTimeout, window, console, document */
  6. import EditorWatchdog from '../src/editorwatchdog';
  7. import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
  8. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  11. import { assertCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  12. import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
  13. // The error handling testing with mocha & chai is quite broken and hard to test.
  14. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  15. //
  16. describe( 'EditorWatchdog', () => {
  17. let element;
  18. beforeEach( () => {
  19. element = document.createElement( 'div' );
  20. document.body.appendChild( element );
  21. } );
  22. afterEach( () => {
  23. element.remove();
  24. sinon.restore();
  25. } );
  26. describe( 'create()', () => {
  27. it( 'should create an editor instance', async () => {
  28. const watchdog = new EditorWatchdog();
  29. const editorCreateSpy = sinon.spy( ClassicTestEditor, 'create' );
  30. const editorDestroySpy = sinon.spy( ClassicTestEditor.prototype, 'destroy' );
  31. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  32. await watchdog.create( element, {} );
  33. sinon.assert.calledOnce( editorCreateSpy );
  34. sinon.assert.notCalled( editorDestroySpy );
  35. await watchdog.destroy();
  36. sinon.assert.calledOnce( editorCreateSpy );
  37. sinon.assert.calledOnce( editorDestroySpy );
  38. } );
  39. it( 'should throw an error when the creator is not defined', async () => {
  40. const watchdog = new EditorWatchdog();
  41. try {
  42. await watchdog.create();
  43. } catch ( err ) {
  44. assertCKEditorError( err, /^watchdog-creator-not-defined/, null );
  45. }
  46. } );
  47. it( 'should properly copy the config', async () => {
  48. const watchdog = new EditorWatchdog();
  49. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  50. const config = {
  51. foo: [],
  52. bar: document.createElement( 'div' )
  53. };
  54. await watchdog.create( element, config );
  55. expect( watchdog.editor.config._config.foo ).to.not.equal( config.foo );
  56. expect( watchdog.editor.config._config.bar ).to.equal( config.bar );
  57. await watchdog.destroy();
  58. } );
  59. it( 'should support editor data passed as the first argument', async () => {
  60. const watchdog = new EditorWatchdog();
  61. watchdog.setCreator( ( data, config ) => ClassicTestEditor.create( data, config ) );
  62. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  63. const originalErrorHandler = window.onerror;
  64. const windowErrorSpy = sinon.spy();
  65. window.onerror = windowErrorSpy;
  66. await watchdog.create( '<p>foo</p>', { plugins: [ Paragraph ] } );
  67. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  68. await new Promise( res => {
  69. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  70. watchdog.on( 'restart', () => {
  71. window.onerror = originalErrorHandler;
  72. res();
  73. } );
  74. } );
  75. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  76. await watchdog.destroy();
  77. } );
  78. } );
  79. describe( 'editor', () => {
  80. it( 'should be the current editor instance', () => {
  81. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  82. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  83. const originalErrorHandler = window.onerror;
  84. const windowErrorSpy = sinon.spy();
  85. window.onerror = windowErrorSpy;
  86. expect( watchdog.editor ).to.be.null;
  87. let oldEditor;
  88. return watchdog.create( element, {} )
  89. .then( () => {
  90. oldEditor = watchdog.editor;
  91. expect( watchdog.editor ).to.be.instanceOf( ClassicTestEditor );
  92. return new Promise( res => {
  93. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  94. watchdog.on( 'restart', () => {
  95. window.onerror = originalErrorHandler;
  96. res();
  97. } );
  98. } );
  99. } )
  100. .then( () => {
  101. expect( watchdog.editor ).to.be.instanceOf( ClassicTestEditor );
  102. expect( watchdog.editor ).to.not.equal( oldEditor );
  103. return watchdog.destroy();
  104. } )
  105. .then( () => {
  106. expect( watchdog.editor ).to.be.null;
  107. } );
  108. } );
  109. } );
  110. describe( 'error handling', () => {
  111. it( 'Watchdog should not restart editor during the initialization', () => {
  112. const watchdog = new EditorWatchdog();
  113. let editor;
  114. watchdog.setCreator( async el => {
  115. editor = await ClassicTestEditor.create( el );
  116. await Promise.reject( new Error( 'foo' ) );
  117. } );
  118. return watchdog.create( element ).then(
  119. () => { throw new Error( '`watchdog.create()` should throw an error.' ); },
  120. err => {
  121. expect( err ).to.be.instanceOf( Error );
  122. expect( err.message ).to.equal( 'foo' );
  123. return editor.destroy();
  124. }
  125. );
  126. } );
  127. it( 'Watchdog should not restart editor during the destroy', async () => {
  128. const watchdog = new EditorWatchdog();
  129. watchdog.setCreator( el => ClassicTestEditor.create( el ) );
  130. watchdog.setDestructor( () => Promise.reject( new Error( 'foo' ) ) );
  131. await watchdog.create( element );
  132. let caughtError = false;
  133. const editor = watchdog.editor;
  134. try {
  135. await watchdog.destroy();
  136. } catch ( err ) {
  137. caughtError = true;
  138. expect( err ).to.be.instanceOf( Error );
  139. expect( err.message ).to.equal( 'foo' );
  140. await editor.destroy();
  141. }
  142. if ( !caughtError ) {
  143. throw new Error( '`watchdog.create()` should throw an error.' );
  144. }
  145. } );
  146. it( 'Watchdog should not hide intercepted errors', () => {
  147. const watchdog = new EditorWatchdog();
  148. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  149. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  150. const originalErrorHandler = window.onerror;
  151. const windowErrorSpy = sinon.spy();
  152. window.onerror = windowErrorSpy;
  153. return watchdog.create( element ).then( () => {
  154. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  155. return new Promise( res => {
  156. watchdog.on( 'restart', () => {
  157. window.onerror = originalErrorHandler;
  158. sinon.assert.calledOnce( windowErrorSpy );
  159. // Various browsers will display the error slightly differently.
  160. expect( windowErrorSpy.getCall( 0 ).args[ 0 ] ).to.match( /foo/ );
  161. watchdog.destroy().then( res );
  162. } );
  163. } );
  164. } );
  165. } );
  166. it( 'Watchdog should intercept editor errors and restart the editor during the runtime', () => {
  167. const watchdog = new EditorWatchdog();
  168. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  169. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  170. const originalErrorHandler = window.onerror;
  171. window.onerror = undefined;
  172. return watchdog.create( element ).then( () => {
  173. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  174. return new Promise( res => {
  175. watchdog.on( 'restart', () => {
  176. window.onerror = originalErrorHandler;
  177. watchdog.destroy().then( res );
  178. } );
  179. } );
  180. } );
  181. } );
  182. it( 'Watchdog should not intercept non-editor errors', () => {
  183. const watchdog = new EditorWatchdog();
  184. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  185. const editorErrorSpy = sinon.spy();
  186. watchdog.on( 'error', editorErrorSpy );
  187. const watchdogErrorHandlerSpy = sinon.spy( watchdog, '_handleError' );
  188. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  189. const originalErrorHandler = window.onerror;
  190. window.onerror = undefined;
  191. return watchdog.create( element ).then( () => {
  192. const error = new Error( 'foo' );
  193. setTimeout( () => {
  194. throw error;
  195. } );
  196. setTimeout( () => {
  197. throw 'bar';
  198. } );
  199. setTimeout( () => {
  200. throw null;
  201. } );
  202. return new Promise( res => {
  203. setTimeout( () => {
  204. window.onerror = originalErrorHandler;
  205. sinon.assert.notCalled( editorErrorSpy );
  206. // Assert that only instances of the `Error` class will be checked deeper.
  207. sinon.assert.calledOnce( watchdogErrorHandlerSpy );
  208. expect( watchdogErrorHandlerSpy.getCall( 0 ).args[ 0 ] ).to.equal( error );
  209. watchdog.destroy().then( res );
  210. } );
  211. } );
  212. } );
  213. } );
  214. it( 'Watchdog should not intercept other editor errors', () => {
  215. const watchdog1 = EditorWatchdog.for( ClassicTestEditor );
  216. const watchdog2 = EditorWatchdog.for( ClassicTestEditor );
  217. const config = {
  218. plugins: []
  219. };
  220. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  221. const originalErrorHandler = window.onerror;
  222. window.onerror = undefined;
  223. return Promise.all( [
  224. watchdog1.create( element, config ),
  225. watchdog2.create( element, config )
  226. ] ).then( () => {
  227. return new Promise( res => {
  228. const watchdog1ErrorSpy = sinon.spy();
  229. const watchdog2ErrorSpy = sinon.spy();
  230. watchdog1.on( 'restart', watchdog1ErrorSpy );
  231. watchdog2.on( 'restart', watchdog2ErrorSpy );
  232. setTimeout( () => throwCKEditorError( 'foo', watchdog2.editor ) );
  233. setTimeout( () => {
  234. window.onerror = originalErrorHandler;
  235. sinon.assert.notCalled( watchdog1ErrorSpy );
  236. sinon.assert.calledOnce( watchdog2ErrorSpy );
  237. Promise.all( [ watchdog1.destroy(), watchdog2.destroy() ] )
  238. .then( res );
  239. } );
  240. } );
  241. } );
  242. } );
  243. it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context', async () => {
  244. const watchdog = new EditorWatchdog();
  245. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  246. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  247. const originalErrorHandler = window.onerror;
  248. window.onerror = undefined;
  249. await watchdog.create( element );
  250. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor.model.document ) );
  251. await new Promise( res => {
  252. watchdog.on( 'restart', () => {
  253. window.onerror = originalErrorHandler;
  254. watchdog.destroy().then( res );
  255. } );
  256. } );
  257. } );
  258. it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context #2', async () => {
  259. const watchdog = new EditorWatchdog();
  260. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  261. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  262. const originalErrorHandler = window.onerror;
  263. window.onerror = undefined;
  264. await watchdog.create( element );
  265. setTimeout( () => throwCKEditorError( 'foo', {
  266. foo: [ 1, 2, 3, {
  267. bar: new Set( [
  268. new Map( /** @type any */( [
  269. [ 'foo', 'bar' ],
  270. [ 0, watchdog.editor ]
  271. ] ) )
  272. ] )
  273. } ]
  274. } ) );
  275. await new Promise( res => {
  276. watchdog.on( 'restart', () => {
  277. window.onerror = originalErrorHandler;
  278. watchdog.destroy().then( res );
  279. } );
  280. } );
  281. } );
  282. it( 'Watchdog should crash permanently if the `crashNumberLimit` is reached' +
  283. ' and the average time between errors is lower than `minimumNonErrorTimePeriod` (default values)', async () => {
  284. const watchdog = new EditorWatchdog();
  285. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  286. const errorSpy = sinon.spy();
  287. watchdog.on( 'error', errorSpy );
  288. const restartSpy = sinon.spy();
  289. watchdog.on( 'restart', restartSpy );
  290. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  291. const originalErrorHandler = window.onerror;
  292. window.onerror = undefined;
  293. await watchdog.create( element );
  294. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
  295. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
  296. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
  297. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
  298. await waitCycle();
  299. expect( errorSpy.callCount ).to.equal( 4 );
  300. expect( watchdog.crashes.length ).to.equal( 4 );
  301. expect( restartSpy.callCount ).to.equal( 3 );
  302. window.onerror = originalErrorHandler;
  303. await watchdog.destroy();
  304. } );
  305. it( 'Watchdog should crash permanently if the `crashNumberLimit` is reached' +
  306. ' and the average time between errors is lower than `minimumNonErrorTimePeriod` (custom values)', async () => {
  307. const watchdog = new EditorWatchdog( { crashNumberLimit: 2, minimumNonErrorTimePeriod: 1000 } );
  308. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  309. const errorSpy = sinon.spy();
  310. watchdog.on( 'error', errorSpy );
  311. const restartSpy = sinon.spy();
  312. watchdog.on( 'restart', restartSpy );
  313. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  314. const originalErrorHandler = window.onerror;
  315. window.onerror = undefined;
  316. await watchdog.create( element );
  317. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
  318. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
  319. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
  320. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
  321. await waitCycle();
  322. expect( errorSpy.callCount ).to.equal( 3 );
  323. expect( watchdog.crashes.length ).to.equal( 3 );
  324. expect( restartSpy.callCount ).to.equal( 2 );
  325. window.onerror = originalErrorHandler;
  326. await watchdog.destroy();
  327. } );
  328. it( 'Watchdog should not crash permanently when average time between errors' +
  329. ' is longer than `minimumNonErrorTimePeriod`', async () => {
  330. const watchdog = new EditorWatchdog( { crashNumberLimit: 2, minimumNonErrorTimePeriod: 0 } );
  331. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  332. const errorSpy = sinon.spy();
  333. watchdog.on( 'error', errorSpy );
  334. const restartSpy = sinon.spy();
  335. watchdog.on( 'restart', restartSpy );
  336. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  337. const originalErrorHandler = window.onerror;
  338. window.onerror = undefined;
  339. await watchdog.create( element );
  340. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ), 5 );
  341. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ), 10 );
  342. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ), 15 );
  343. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ), 20 );
  344. await new Promise( res => {
  345. setTimeout( res, 20 );
  346. } );
  347. expect( errorSpy.callCount ).to.equal( 4 );
  348. expect( watchdog.crashes.length ).to.equal( 4 );
  349. expect( restartSpy.callCount ).to.equal( 4 );
  350. window.onerror = originalErrorHandler;
  351. await watchdog.destroy();
  352. } );
  353. it( 'Watchdog should warn if the CKEditorError missing its context', async () => {
  354. const watchdog = new EditorWatchdog();
  355. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  356. watchdog.setDestructor( editor => editor.destroy() );
  357. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  358. const originalErrorHandler = window.onerror;
  359. window.onerror = undefined;
  360. sinon.stub( console, 'warn' );
  361. await watchdog.create( element );
  362. setTimeout( () => throwCKEditorError( 'foo' ) );
  363. await waitCycle();
  364. window.onerror = originalErrorHandler;
  365. expect( watchdog.crashes ).to.deep.equal( [] );
  366. sinon.assert.calledWithExactly(
  367. console.warn,
  368. 'The error is missing its context and Watchdog cannot restart the proper instance.'
  369. );
  370. await watchdog.destroy();
  371. } );
  372. it( 'Watchdog should omit error if the CKEditorError context is equal to null', async () => {
  373. const watchdog = new EditorWatchdog();
  374. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  375. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  376. const originalErrorHandler = window.onerror;
  377. window.onerror = undefined;
  378. await watchdog.create( element );
  379. setTimeout( () => throwCKEditorError( 'foo', null ) );
  380. await waitCycle();
  381. window.onerror = originalErrorHandler;
  382. expect( watchdog.crashes ).to.deep.equal( [] );
  383. await watchdog.destroy();
  384. } );
  385. it( 'editor should be restarted with the data before the crash #1', () => {
  386. const watchdog = new EditorWatchdog();
  387. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  388. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  389. const originalErrorHandler = window.onerror;
  390. window.onerror = undefined;
  391. return watchdog.create( element, {
  392. initialData: '<p>foo</p>',
  393. plugins: [ Paragraph ]
  394. } ).then( () => {
  395. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  396. return new Promise( res => {
  397. watchdog.on( 'restart', () => {
  398. window.onerror = originalErrorHandler;
  399. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  400. watchdog.destroy().then( res );
  401. } );
  402. } );
  403. } );
  404. } );
  405. it( 'editor should be restarted with the data before the crash #2', () => {
  406. const watchdog = new EditorWatchdog();
  407. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  408. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  409. const originalErrorHandler = window.onerror;
  410. window.onerror = undefined;
  411. return watchdog.create( element, {
  412. initialData: '<p>foo</p>',
  413. plugins: [ Paragraph ]
  414. } ).then( () => {
  415. const doc = watchdog.editor.model.document;
  416. watchdog.editor.model.change( writer => {
  417. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  418. } );
  419. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  420. return new Promise( res => {
  421. watchdog.on( 'restart', () => {
  422. window.onerror = originalErrorHandler;
  423. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
  424. watchdog.destroy().then( res );
  425. } );
  426. } );
  427. } );
  428. } );
  429. it( 'editor should be restarted with the data of the latest document version before the crash', () => {
  430. const watchdog = new EditorWatchdog();
  431. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  432. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  433. const originalErrorHandler = window.onerror;
  434. window.onerror = sinon.spy();
  435. return watchdog.create( element, {
  436. initialData: '<p>foo</p>',
  437. plugins: [ Paragraph ]
  438. } ).then( () => {
  439. const model = watchdog.editor.model;
  440. const doc = model.document;
  441. // Decrement the document version to simulate a situation when an operation
  442. // don't produce new document version.
  443. doc.version--;
  444. model.change( writer => {
  445. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  446. } );
  447. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  448. return new Promise( res => {
  449. watchdog.on( 'restart', () => {
  450. window.onerror = originalErrorHandler;
  451. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  452. watchdog.destroy().then( res );
  453. } );
  454. } );
  455. } );
  456. } );
  457. it( 'editor should be restarted with the latest available data before the crash', async () => {
  458. const watchdog = new EditorWatchdog();
  459. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  460. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  461. const originalErrorHandler = window.onerror;
  462. window.onerror = undefined;
  463. sinon.stub( console, 'error' );
  464. await watchdog.create( element, {
  465. initialData: '<p>foo</p>',
  466. plugins: [ Paragraph ]
  467. } );
  468. const editorGetDataError = new Error( 'Some error' );
  469. const getDataStub = sinon.stub( watchdog.editor.data, 'get' )
  470. .throwsException( editorGetDataError );
  471. // Keep the reference to cleanly destroy it at in the end, as during the TC it
  472. // throws an exception during destruction.
  473. const firstEditor = watchdog.editor;
  474. await new Promise( res => {
  475. const doc = watchdog.editor.model.document;
  476. watchdog.editor.model.change( writer => {
  477. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  478. } );
  479. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  480. watchdog.on( 'restart', async () => {
  481. window.onerror = originalErrorHandler;
  482. // It is called second time by during the default editor destruction
  483. // to update the source element.
  484. sinon.assert.calledTwice( getDataStub );
  485. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  486. sinon.assert.calledWith(
  487. console.error,
  488. editorGetDataError,
  489. 'An error happened during restoring editor data. Editor will be restored from the previously saved data.'
  490. );
  491. sinon.assert.calledWith(
  492. console.error,
  493. 'An error happened during the editor destructing.'
  494. );
  495. await watchdog.destroy();
  496. getDataStub.restore();
  497. await firstEditor.destroy();
  498. res();
  499. } );
  500. } );
  501. } );
  502. it( 'should use the custom destructor if passed', () => {
  503. const watchdog = new EditorWatchdog();
  504. const destructionSpy = sinon.spy();
  505. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  506. watchdog.setDestructor( editor => {
  507. destructionSpy();
  508. return editor.destroy();
  509. } );
  510. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  511. const originalErrorHandler = window.onerror;
  512. window.onerror = undefined;
  513. return watchdog.create( element ).then( () => {
  514. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  515. return new Promise( res => {
  516. watchdog.on( 'restart', () => {
  517. window.onerror = originalErrorHandler;
  518. sinon.assert.calledOnce( destructionSpy );
  519. watchdog.destroy().then( res );
  520. } );
  521. } );
  522. } );
  523. } );
  524. } );
  525. describe( 'async error handling', () => {
  526. let unhandledRejectionEventSupported;
  527. before( () => {
  528. return isUnhandledRejectionEventSupported()
  529. .then( val => {
  530. unhandledRejectionEventSupported = val;
  531. } );
  532. } );
  533. it( 'Watchdog should handle async CKEditorError errors', () => {
  534. if ( !unhandledRejectionEventSupported ) {
  535. return;
  536. }
  537. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  538. const originalErrorHandler = window.onerror;
  539. window.onerror = undefined;
  540. return watchdog.create( element, {
  541. initialData: '<p>foo</p>',
  542. plugins: [ Paragraph ]
  543. } ).then( () => {
  544. const oldEditor = watchdog.editor;
  545. Promise.resolve().then( () => throwCKEditorError( 'foo', watchdog.editor ) );
  546. return new Promise( res => {
  547. watchdog.on( 'restart', () => {
  548. window.onerror = originalErrorHandler;
  549. expect( watchdog.editor ).to.not.equal( oldEditor );
  550. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  551. watchdog.destroy().then( res );
  552. } );
  553. } );
  554. } );
  555. } );
  556. it( 'Watchdog should not react to non-editor async errors', () => {
  557. if ( !unhandledRejectionEventSupported ) {
  558. return;
  559. }
  560. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  561. const originalErrorHandler = window.onerror;
  562. const editorErrorSpy = sinon.spy();
  563. window.onerror = undefined;
  564. return watchdog.create( element, {
  565. initialData: '<p>foo</p>',
  566. plugins: [ Paragraph ]
  567. } ).then( () => {
  568. watchdog.on( 'error', editorErrorSpy );
  569. Promise.resolve().then( () => Promise.reject( 'foo' ) );
  570. Promise.resolve().then( () => Promise.reject( new Error( 'bar' ) ) );
  571. // Wait a cycle.
  572. return new Promise( res => setTimeout( res ) );
  573. } ).then( () => {
  574. window.onerror = originalErrorHandler;
  575. sinon.assert.notCalled( editorErrorSpy );
  576. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  577. return watchdog.destroy();
  578. } );
  579. } );
  580. } );
  581. describe( 'destroy()', () => {
  582. // See #19.
  583. it( 'should clean internal stuff', () => {
  584. // 30ms should be enough to make the two data changes split into two data save actions.
  585. // This will ensure that the second data save action will be put off in time.
  586. const SAVE_INTERVAL = 30;
  587. const watchdog = EditorWatchdog.for( ClassicTestEditor, {
  588. saveInterval: SAVE_INTERVAL,
  589. } );
  590. return watchdog.create( element, {
  591. initialData: '<p>foo</p>',
  592. plugins: [ Paragraph ]
  593. } ).then( () => {
  594. const doc = watchdog.editor.model.document;
  595. watchdog.editor.model.change( writer => {
  596. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  597. } );
  598. watchdog.editor.model.change( writer => {
  599. writer.insertText( 'foo', writer.createPositionAt( doc.getRoot(), 1 ) );
  600. } );
  601. return watchdog.destroy();
  602. } ).then( () => {
  603. // Wait to ensure that the throttled save is cleared and won't be executed
  604. // on the non-existing editor.
  605. return new Promise( res => setTimeout( res, SAVE_INTERVAL ) );
  606. } ).then( () => {
  607. expect( watchdog.editor ).to.equal( null );
  608. expect( watchdog.state ).to.equal( 'destroyed' );
  609. expect( watchdog.crashes ).to.deep.equal( [] );
  610. } );
  611. } );
  612. } );
  613. describe( 'static for()', () => {
  614. it( 'should be a shortcut method for creating the watchdog', () => {
  615. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  616. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  617. const originalErrorHandler = window.onerror;
  618. window.onerror = undefined;
  619. return watchdog.create( element, {
  620. initialData: '<p>foo</p>',
  621. plugins: [ Paragraph ]
  622. } ).then( () => {
  623. const oldEditor = watchdog.editor;
  624. expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
  625. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  626. return new Promise( res => {
  627. watchdog.on( 'restart', () => {
  628. window.onerror = originalErrorHandler;
  629. expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
  630. expect( watchdog.editor ).to.not.equal( oldEditor );
  631. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  632. watchdog.destroy().then( res );
  633. } );
  634. } );
  635. } );
  636. } );
  637. } );
  638. describe( 'crashes', () => {
  639. it( 'should be an array of caught errors by the watchdog', () => {
  640. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  641. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  642. const originalErrorHandler = window.onerror;
  643. window.onerror = undefined;
  644. return watchdog.create( element ).then( () => {
  645. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  646. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  647. return new Promise( res => {
  648. setTimeout( () => {
  649. window.onerror = originalErrorHandler;
  650. expect( watchdog.crashes[ 0 ].message ).to.equal( 'foo' );
  651. expect( watchdog.crashes[ 0 ].stack ).to.be.a( 'string' );
  652. expect( watchdog.crashes[ 0 ].date ).to.be.a( 'number' );
  653. expect( watchdog.crashes[ 0 ].filename ).to.be.a( 'string' );
  654. expect( watchdog.crashes[ 0 ].lineno ).to.be.a( 'number' );
  655. expect( watchdog.crashes[ 0 ].colno ).to.be.a( 'number' );
  656. expect( watchdog.crashes[ 1 ].message ).to.equal( 'bar' );
  657. watchdog.destroy().then( res );
  658. } );
  659. } );
  660. } );
  661. } );
  662. it( 'should include async errors', () => {
  663. return isUnhandledRejectionEventSupported().then( isSupported => {
  664. if ( !isSupported ) {
  665. return;
  666. }
  667. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  668. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  669. const originalErrorHandler = window.onerror;
  670. window.onerror = undefined;
  671. return watchdog.create( element ).then( () => {
  672. Promise.resolve().then( () => throwCKEditorError( 'foo', watchdog.editor ) );
  673. return new Promise( res => {
  674. // This `setTimeout` needs to have a timer defined because Firefox calls the code in random order
  675. // and causes the test failed.
  676. setTimeout( () => {
  677. window.onerror = originalErrorHandler;
  678. expect( watchdog.crashes[ 0 ].message ).to.equal( 'foo' );
  679. expect( watchdog.crashes[ 0 ].stack ).to.be.a( 'string' );
  680. expect( watchdog.crashes[ 0 ].date ).to.be.a( 'number' );
  681. expect( watchdog.crashes[ 0 ].filename ).to.be.an( 'undefined' );
  682. expect( watchdog.crashes[ 0 ].lineno ).to.be.an( 'undefined' );
  683. expect( watchdog.crashes[ 0 ].colno ).to.be.an( 'undefined' );
  684. watchdog.destroy().then( res );
  685. }, 10 );
  686. } );
  687. } );
  688. } );
  689. } );
  690. } );
  691. describe( 'state', () => {
  692. let orphanEditors = [];
  693. afterEach( () => {
  694. return Promise.all( orphanEditors.map( editor => editor.destroy() ) )
  695. .then( () => {
  696. orphanEditors = [];
  697. } );
  698. } );
  699. it( 'should reflect the state of the watchdog', async () => {
  700. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  701. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  702. const originalErrorHandler = window.onerror;
  703. window.onerror = undefined;
  704. expect( watchdog.state ).to.equal( 'initializing' );
  705. await watchdog.create( element );
  706. orphanEditors.push( watchdog.editor );
  707. expect( watchdog.state ).to.equal( 'ready' );
  708. await watchdog.create( element );
  709. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  710. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  711. await waitCycle();
  712. window.onerror = originalErrorHandler;
  713. expect( watchdog.state ).to.equal( 'ready' );
  714. await watchdog.destroy();
  715. expect( watchdog.state ).to.equal( 'destroyed' );
  716. } );
  717. it( 'should be observable', async () => {
  718. const watchdog = EditorWatchdog.for( ClassicTestEditor );
  719. const states = [];
  720. watchdog.on( 'change:state', ( evt, propName, newValue ) => {
  721. states.push( newValue );
  722. } );
  723. const originalErrorHandler = window.onerror;
  724. window.onerror = undefined;
  725. return watchdog.create( element ).then( () => {
  726. orphanEditors.push( watchdog.editor );
  727. return watchdog.create( element ).then( () => {
  728. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  729. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  730. setTimeout( () => throwCKEditorError( 'baz', watchdog.editor ) );
  731. setTimeout( () => throwCKEditorError( 'biz', watchdog.editor ) );
  732. return new Promise( res => {
  733. setTimeout( () => {
  734. window.onerror = originalErrorHandler;
  735. watchdog.destroy().then( () => {
  736. expect( states ).to.deep.equal( [
  737. 'ready',
  738. 'crashed',
  739. 'initializing',
  740. 'ready',
  741. 'crashed',
  742. 'initializing',
  743. 'ready',
  744. 'crashed',
  745. 'initializing',
  746. 'ready',
  747. 'crashed',
  748. 'crashedPermanently',
  749. 'destroyed'
  750. ] );
  751. res();
  752. } );
  753. } );
  754. } );
  755. } );
  756. } );
  757. } );
  758. } );
  759. describe( 'multi-root editors', () => {
  760. it( 'should support multi-root editors', async () => {
  761. class MultiRootEditor extends Editor {
  762. constructor( sourceElements, config ) {
  763. super( config );
  764. this.data.processor = new HtmlDataProcessor();
  765. // Create a root for each source element.
  766. for ( const rootName of Object.keys( sourceElements ) ) {
  767. this.model.document.createRoot( '$root', rootName );
  768. }
  769. }
  770. static async create( sourceElements, config ) {
  771. const editor = new this( sourceElements, config );
  772. await editor.initPlugins();
  773. await editor.data.init( config.initialData );
  774. editor.fire( 'ready' );
  775. return editor;
  776. }
  777. }
  778. const watchdog = EditorWatchdog.for( MultiRootEditor );
  779. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  780. const originalErrorHandler = window.onerror;
  781. window.onerror = undefined;
  782. await watchdog.create( {
  783. header: element
  784. }, {
  785. initialData: {
  786. header: '<p>Foo</p>'
  787. },
  788. plugins: [ Paragraph ]
  789. } );
  790. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  791. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  792. await waitCycle();
  793. window.onerror = originalErrorHandler;
  794. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  795. await watchdog.destroy();
  796. } );
  797. } );
  798. } );
  799. function throwCKEditorError( name, context ) {
  800. throw new CKEditorError( name, context );
  801. }
  802. // Feature detection works as a race condition - if the `unhandledrejection` event
  803. // is supported then the listener should be called first. Otherwise the timeout will be reached.
  804. function isUnhandledRejectionEventSupported() {
  805. return new Promise( res => {
  806. window.addEventListener( 'unhandledrejection', function listener() {
  807. res( true );
  808. window.removeEventListener( 'unhandledrejection', listener );
  809. } );
  810. Promise.resolve().then( () => Promise.reject( new Error() ) );
  811. setTimeout( () => res( false ) );
  812. } );
  813. }
  814. function waitCycle() {
  815. return new Promise( res => setTimeout( res ) );
  816. }