watchdog.js 32 KB

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