8
0

watchdog.js 32 KB

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