watchdog.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  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. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  192. const originalErrorHandler = window.onerror;
  193. window.onerror = undefined;
  194. return watchdog.create( element ).then( () => {
  195. setTimeout( () => {
  196. throw new Error( 'foo' );
  197. } );
  198. setTimeout( () => {
  199. throw 'bar';
  200. } );
  201. return new Promise( res => {
  202. setTimeout( () => {
  203. window.onerror = originalErrorHandler;
  204. sinon.assert.notCalled( editorErrorSpy );
  205. watchdog.destroy().then( res );
  206. } );
  207. } );
  208. } );
  209. } );
  210. it( 'Watchdog should not intercept other editor errors', () => {
  211. const watchdog1 = Watchdog.for( ClassicTestEditor );
  212. const watchdog2 = Watchdog.for( ClassicTestEditor );
  213. const config = {
  214. plugins: []
  215. };
  216. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  217. const originalErrorHandler = window.onerror;
  218. window.onerror = undefined;
  219. return Promise.all( [
  220. watchdog1.create( element, config ),
  221. watchdog2.create( element, config )
  222. ] ).then( () => {
  223. return new Promise( res => {
  224. const watchdog1ErrorSpy = sinon.spy();
  225. const watchdog2ErrorSpy = sinon.spy();
  226. watchdog1.on( 'restart', watchdog1ErrorSpy );
  227. watchdog2.on( 'restart', watchdog2ErrorSpy );
  228. setTimeout( () => throwCKEditorError( 'foo', watchdog2.editor ) );
  229. setTimeout( () => {
  230. window.onerror = originalErrorHandler;
  231. sinon.assert.notCalled( watchdog1ErrorSpy );
  232. sinon.assert.calledOnce( watchdog2ErrorSpy );
  233. Promise.all( [ watchdog1.destroy(), watchdog2.destroy() ] )
  234. .then( res );
  235. } );
  236. } );
  237. } );
  238. } );
  239. it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context', () => {
  240. const watchdog = new Watchdog();
  241. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  242. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  243. const originalErrorHandler = window.onerror;
  244. window.onerror = undefined;
  245. return watchdog.create( element ).then( () => {
  246. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor.model.document ) );
  247. return new Promise( res => {
  248. watchdog.on( 'restart', () => {
  249. window.onerror = originalErrorHandler;
  250. watchdog.destroy().then( res );
  251. } );
  252. } );
  253. } );
  254. } );
  255. it( 'Watchdog should intercept editor errors and restart the editor if the editor can be found from the context #2', () => {
  256. const watchdog = new Watchdog();
  257. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  258. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  259. const originalErrorHandler = window.onerror;
  260. window.onerror = undefined;
  261. return watchdog.create( element ).then( () => {
  262. setTimeout( () => throwCKEditorError( 'foo', {
  263. foo: [ 1, 2, 3, {
  264. bar: new Set( [
  265. new Map( [
  266. [ 'foo', 'bar' ],
  267. [ 0, watchdog.editor ]
  268. ] )
  269. ] )
  270. } ]
  271. } ) );
  272. return new Promise( res => {
  273. watchdog.on( 'restart', () => {
  274. window.onerror = originalErrorHandler;
  275. watchdog.destroy().then( res );
  276. } );
  277. } );
  278. } );
  279. } );
  280. it( 'Watchdog should crash permanently if the `crashNumberLimit` is reached' +
  281. ' and the average time between errors is lower than `minimumNonErrorTimePeriod` (default values)', () => {
  282. const watchdog = new Watchdog();
  283. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  284. const errorSpy = sinon.spy();
  285. watchdog.on( 'error', errorSpy );
  286. const restartSpy = sinon.spy();
  287. watchdog.on( 'restart', restartSpy );
  288. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  289. const originalErrorHandler = window.onerror;
  290. window.onerror = undefined;
  291. return watchdog.create( element ).then( () => {
  292. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
  293. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
  294. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
  295. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
  296. return new Promise( res => {
  297. setTimeout( () => {
  298. expect( errorSpy.callCount ).to.equal( 4 );
  299. expect( watchdog.crashes.length ).to.equal( 4 );
  300. expect( restartSpy.callCount ).to.equal( 3 );
  301. window.onerror = originalErrorHandler;
  302. watchdog.destroy().then( res );
  303. } );
  304. } );
  305. } );
  306. } );
  307. it( 'Watchdog should crash permanently if the `crashNumberLimit` is reached' +
  308. ' and the average time between errors is lower than `minimumNonErrorTimePeriod` (custom values)', () => {
  309. const watchdog = new Watchdog( { crashNumberLimit: 2, minimumNonErrorTimePeriod: 1000 } );
  310. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  311. const errorSpy = sinon.spy();
  312. watchdog.on( 'error', errorSpy );
  313. const restartSpy = sinon.spy();
  314. watchdog.on( 'restart', restartSpy );
  315. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  316. const originalErrorHandler = window.onerror;
  317. window.onerror = undefined;
  318. return watchdog.create( element ).then( () => {
  319. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ) );
  320. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ) );
  321. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ) );
  322. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ) );
  323. return new Promise( res => {
  324. setTimeout( () => {
  325. expect( errorSpy.callCount ).to.equal( 3 );
  326. expect( watchdog.crashes.length ).to.equal( 3 );
  327. expect( restartSpy.callCount ).to.equal( 2 );
  328. window.onerror = originalErrorHandler;
  329. watchdog.destroy().then( res );
  330. } );
  331. } );
  332. } );
  333. } );
  334. it( 'Watchdog should not crash permantently when average time between errors is longer than `minimumNonErrorTimePeriod`', () => {
  335. const watchdog = new Watchdog( { crashNumberLimit: 2, minimumNonErrorTimePeriod: 0 } );
  336. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  337. const errorSpy = sinon.spy();
  338. watchdog.on( 'error', errorSpy );
  339. const restartSpy = sinon.spy();
  340. watchdog.on( 'restart', restartSpy );
  341. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  342. const originalErrorHandler = window.onerror;
  343. window.onerror = undefined;
  344. return watchdog.create( element ).then( () => {
  345. setTimeout( () => throwCKEditorError( 'foo1', watchdog.editor ), 5 );
  346. setTimeout( () => throwCKEditorError( 'foo2', watchdog.editor ), 10 );
  347. setTimeout( () => throwCKEditorError( 'foo3', watchdog.editor ), 15 );
  348. setTimeout( () => throwCKEditorError( 'foo4', watchdog.editor ), 20 );
  349. return new Promise( res => {
  350. setTimeout( () => {
  351. expect( errorSpy.callCount ).to.equal( 4 );
  352. expect( watchdog.crashes.length ).to.equal( 4 );
  353. expect( restartSpy.callCount ).to.equal( 4 );
  354. window.onerror = originalErrorHandler;
  355. watchdog.destroy().then( res );
  356. }, 20 );
  357. } );
  358. } );
  359. } );
  360. it( 'Watchdog should omit error if the CKEditorError context is equal to null', () => {
  361. const watchdog = new Watchdog();
  362. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  363. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  364. const originalErrorHandler = window.onerror;
  365. window.onerror = undefined;
  366. return watchdog.create( element ).then( () => {
  367. setTimeout( () => throwCKEditorError( 'foo', null ) );
  368. return new Promise( res => {
  369. setTimeout( () => {
  370. window.onerror = originalErrorHandler;
  371. expect( watchdog.crashes ).to.deep.equal( [] );
  372. watchdog.destroy().then( res );
  373. } );
  374. } );
  375. } );
  376. } );
  377. it( 'editor should be restarted with the data before the crash #1', () => {
  378. const watchdog = new Watchdog();
  379. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  380. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  381. const originalErrorHandler = window.onerror;
  382. window.onerror = undefined;
  383. return watchdog.create( element, {
  384. initialData: '<p>foo</p>',
  385. plugins: [ Paragraph ]
  386. } ).then( () => {
  387. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  388. return new Promise( res => {
  389. watchdog.on( 'restart', () => {
  390. window.onerror = originalErrorHandler;
  391. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  392. watchdog.destroy().then( res );
  393. } );
  394. } );
  395. } );
  396. } );
  397. it( 'editor should be restarted with the data before the crash #2', () => {
  398. const watchdog = new Watchdog();
  399. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  400. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  401. const originalErrorHandler = window.onerror;
  402. window.onerror = undefined;
  403. return watchdog.create( element, {
  404. initialData: '<p>foo</p>',
  405. plugins: [ Paragraph ]
  406. } ).then( () => {
  407. const doc = watchdog.editor.model.document;
  408. watchdog.editor.model.change( writer => {
  409. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  410. } );
  411. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  412. return new Promise( res => {
  413. watchdog.on( 'restart', () => {
  414. window.onerror = originalErrorHandler;
  415. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
  416. watchdog.destroy().then( res );
  417. } );
  418. } );
  419. } );
  420. } );
  421. it( 'editor should be restarted with the data of the latest document version before the crash', () => {
  422. const watchdog = new Watchdog();
  423. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  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. const model = watchdog.editor.model;
  432. const doc = model.document;
  433. // Decrement the document version to simulate a situation when an operation
  434. // don't produce new document version.
  435. doc.version--;
  436. model.change( writer => {
  437. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  438. } );
  439. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  440. return new Promise( res => {
  441. watchdog.on( 'restart', () => {
  442. window.onerror = originalErrorHandler;
  443. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  444. watchdog.destroy().then( res );
  445. } );
  446. } );
  447. } );
  448. } );
  449. it( 'editor should be restarted with the latest available data before the crash', () => {
  450. const watchdog = new Watchdog();
  451. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  452. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  453. const originalErrorHandler = window.onerror;
  454. window.onerror = undefined;
  455. sinon.stub( console, 'error' );
  456. return watchdog.create( element, {
  457. initialData: '<p>foo</p>',
  458. plugins: [ Paragraph ]
  459. } ).then( () => {
  460. const editorGetDataError = new Error( 'Some error' );
  461. const getDataStub = sinon.stub( watchdog.editor.data, 'get' )
  462. .throwsException( editorGetDataError );
  463. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  464. return new Promise( res => {
  465. const doc = watchdog.editor.model.document;
  466. watchdog.editor.model.change( writer => {
  467. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  468. } );
  469. watchdog.on( 'restart', () => {
  470. window.onerror = originalErrorHandler;
  471. // It is called second time by during the default editor destruction
  472. // to update the source element.
  473. sinon.assert.calledTwice( getDataStub );
  474. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  475. sinon.assert.calledWith(
  476. console.error,
  477. editorGetDataError,
  478. 'An error happened during restoring editor data. Editor will be restored from the previously saved data.'
  479. );
  480. sinon.assert.calledWith(
  481. console.error,
  482. 'An error happened during the editor destructing.'
  483. );
  484. watchdog.destroy().then( res );
  485. } );
  486. } );
  487. } );
  488. } );
  489. it( 'should use the custom destructor if passed', () => {
  490. const watchdog = new Watchdog();
  491. const destructionSpy = sinon.spy();
  492. watchdog.setCreator( ( el, config ) => ClassicTestEditor.create( el, config ) );
  493. watchdog.setDestructor( editor => {
  494. destructionSpy();
  495. return editor.destroy();
  496. } );
  497. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  498. const originalErrorHandler = window.onerror;
  499. window.onerror = undefined;
  500. return watchdog.create( element ).then( () => {
  501. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  502. return new Promise( res => {
  503. watchdog.on( 'restart', () => {
  504. window.onerror = originalErrorHandler;
  505. sinon.assert.calledOnce( destructionSpy );
  506. watchdog.destroy().then( res );
  507. } );
  508. } );
  509. } );
  510. } );
  511. } );
  512. describe( 'async error handling', () => {
  513. let unhandledRejectionEventSupported;
  514. before( () => {
  515. return isUnhandledRejectionEventSupported()
  516. .then( val => {
  517. unhandledRejectionEventSupported = val;
  518. } );
  519. } );
  520. it( 'Watchdog should handle async CKEditorError errors', () => {
  521. if ( !unhandledRejectionEventSupported ) {
  522. return;
  523. }
  524. const watchdog = Watchdog.for( ClassicTestEditor );
  525. const originalErrorHandler = window.onerror;
  526. window.onerror = undefined;
  527. return watchdog.create( element, {
  528. initialData: '<p>foo</p>',
  529. plugins: [ Paragraph ]
  530. } ).then( () => {
  531. const oldEditor = watchdog.editor;
  532. Promise.resolve().then( () => throwCKEditorError( 'foo', watchdog.editor ) );
  533. return new Promise( res => {
  534. watchdog.on( 'restart', () => {
  535. window.onerror = originalErrorHandler;
  536. expect( watchdog.editor ).to.not.equal( oldEditor );
  537. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  538. watchdog.destroy().then( res );
  539. } );
  540. } );
  541. } );
  542. } );
  543. it( 'Watchdog should not react to non-editor async errors', () => {
  544. if ( !unhandledRejectionEventSupported ) {
  545. return;
  546. }
  547. const watchdog = Watchdog.for( ClassicTestEditor );
  548. const originalErrorHandler = window.onerror;
  549. const editorErrorSpy = sinon.spy();
  550. window.onerror = undefined;
  551. return watchdog.create( element, {
  552. initialData: '<p>foo</p>',
  553. plugins: [ Paragraph ]
  554. } ).then( () => {
  555. watchdog.on( 'error', editorErrorSpy );
  556. Promise.resolve().then( () => Promise.reject( 'foo' ) );
  557. Promise.resolve().then( () => Promise.reject( new Error( 'bar' ) ) );
  558. // Wait a cycle.
  559. return new Promise( res => setTimeout( res ) );
  560. } ).then( () => {
  561. window.onerror = originalErrorHandler;
  562. sinon.assert.notCalled( editorErrorSpy );
  563. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  564. return watchdog.destroy();
  565. } );
  566. } );
  567. } );
  568. describe( 'destroy()', () => {
  569. // See #19.
  570. it( 'should clean internal stuff', () => {
  571. // 30ms should be enough to make the two data changes split into two data save actions.
  572. // This will ensure that the second data save action will be put off in time.
  573. const SAVE_INTERVAL = 30;
  574. const watchdog = Watchdog.for( ClassicTestEditor, {
  575. saveInterval: SAVE_INTERVAL,
  576. } );
  577. return watchdog.create( element, {
  578. initialData: '<p>foo</p>',
  579. plugins: [ Paragraph ]
  580. } ).then( () => {
  581. const doc = watchdog.editor.model.document;
  582. watchdog.editor.model.change( writer => {
  583. writer.insertText( 'bar', writer.createPositionAt( doc.getRoot(), 1 ) );
  584. } );
  585. watchdog.editor.model.change( writer => {
  586. writer.insertText( 'foo', writer.createPositionAt( doc.getRoot(), 1 ) );
  587. } );
  588. return watchdog.destroy();
  589. } ).then( () => {
  590. // Wait to ensure that the throttled save is cleared and won't be executed
  591. // on the non-existing editor.
  592. return new Promise( res => setTimeout( res, SAVE_INTERVAL ) );
  593. } ).then( () => {
  594. expect( watchdog.editor ).to.equal( null );
  595. expect( watchdog.state ).to.equal( 'destroyed' );
  596. expect( watchdog.crashes ).to.deep.equal( [] );
  597. } );
  598. } );
  599. } );
  600. describe( 'static for()', () => {
  601. it( 'should be a shortcut method for creating the watchdog', () => {
  602. const watchdog = Watchdog.for( ClassicTestEditor );
  603. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  604. const originalErrorHandler = window.onerror;
  605. window.onerror = undefined;
  606. return watchdog.create( element, {
  607. initialData: '<p>foo</p>',
  608. plugins: [ Paragraph ]
  609. } ).then( () => {
  610. const oldEditor = watchdog.editor;
  611. expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
  612. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  613. return new Promise( res => {
  614. watchdog.on( 'restart', () => {
  615. window.onerror = originalErrorHandler;
  616. expect( watchdog.editor ).to.be.an.instanceOf( ClassicTestEditor );
  617. expect( watchdog.editor ).to.not.equal( oldEditor );
  618. expect( watchdog.editor.getData() ).to.equal( '<p>foo</p>' );
  619. watchdog.destroy().then( res );
  620. } );
  621. } );
  622. } );
  623. } );
  624. } );
  625. describe( 'crashes', () => {
  626. it( 'should be an array of caught errors by 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 ).then( () => {
  632. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  633. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  634. return new Promise( res => {
  635. setTimeout( () => {
  636. window.onerror = originalErrorHandler;
  637. expect( watchdog.crashes[ 0 ].message ).to.equal( 'foo' );
  638. expect( watchdog.crashes[ 0 ].stack ).to.be.a( 'string' );
  639. expect( watchdog.crashes[ 0 ].date ).to.be.a( 'number' );
  640. expect( watchdog.crashes[ 0 ].filename ).to.be.a( 'string' );
  641. expect( watchdog.crashes[ 0 ].lineno ).to.be.a( 'number' );
  642. expect( watchdog.crashes[ 0 ].colno ).to.be.a( 'number' );
  643. expect( watchdog.crashes[ 1 ].message ).to.equal( 'bar' );
  644. watchdog.destroy().then( res );
  645. } );
  646. } );
  647. } );
  648. } );
  649. it( 'should include async errors', () => {
  650. return isUnhandledRejectionEventSupported().then( isSupported => {
  651. if ( !isSupported ) {
  652. return;
  653. }
  654. const watchdog = Watchdog.for( ClassicTestEditor );
  655. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  656. const originalErrorHandler = window.onerror;
  657. window.onerror = undefined;
  658. return watchdog.create( element ).then( () => {
  659. Promise.resolve().then( () => throwCKEditorError( 'foo', watchdog.editor ) );
  660. return new Promise( res => {
  661. // This `setTimeout` needs to have a timer defined because Firefox calls the code in random order
  662. // and causes the test failed.
  663. setTimeout( () => {
  664. window.onerror = originalErrorHandler;
  665. expect( watchdog.crashes[ 0 ].message ).to.equal( 'foo' );
  666. expect( watchdog.crashes[ 0 ].stack ).to.be.a( 'string' );
  667. expect( watchdog.crashes[ 0 ].date ).to.be.a( 'number' );
  668. expect( watchdog.crashes[ 0 ].filename ).to.be.an( 'undefined' );
  669. expect( watchdog.crashes[ 0 ].lineno ).to.be.an( 'undefined' );
  670. expect( watchdog.crashes[ 0 ].colno ).to.be.an( 'undefined' );
  671. watchdog.destroy().then( res );
  672. }, 10 );
  673. } );
  674. } );
  675. } );
  676. } );
  677. } );
  678. describe( 'state', () => {
  679. it( 'should reflect the state of the watchdog', () => {
  680. const watchdog = Watchdog.for( ClassicTestEditor );
  681. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  682. const originalErrorHandler = window.onerror;
  683. window.onerror = undefined;
  684. expect( watchdog.state ).to.equal( 'initializing' );
  685. return watchdog.create( element ).then( () => {
  686. expect( watchdog.state ).to.equal( 'ready' );
  687. return watchdog.create( element ).then( () => {
  688. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  689. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  690. return new Promise( res => {
  691. setTimeout( () => {
  692. window.onerror = originalErrorHandler;
  693. expect( watchdog.state ).to.equal( 'ready' );
  694. watchdog.destroy().then( () => {
  695. expect( watchdog.state ).to.equal( 'destroyed' );
  696. res();
  697. } );
  698. } );
  699. } );
  700. } );
  701. } );
  702. } );
  703. it( 'should be observable', () => {
  704. const watchdog = Watchdog.for( ClassicTestEditor );
  705. const states = [];
  706. watchdog.on( 'change:state', ( evt, propName, newValue ) => {
  707. states.push( newValue );
  708. } );
  709. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  710. const originalErrorHandler = window.onerror;
  711. window.onerror = undefined;
  712. return watchdog.create( element ).then( () => {
  713. return watchdog.create( element ).then( () => {
  714. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  715. setTimeout( () => throwCKEditorError( 'bar', watchdog.editor ) );
  716. setTimeout( () => throwCKEditorError( 'baz', watchdog.editor ) );
  717. setTimeout( () => throwCKEditorError( 'biz', watchdog.editor ) );
  718. return new Promise( res => {
  719. setTimeout( () => {
  720. window.onerror = originalErrorHandler;
  721. watchdog.destroy().then( () => {
  722. expect( states ).to.deep.equal( [
  723. 'ready',
  724. 'crashed',
  725. 'initializing',
  726. 'ready',
  727. 'crashed',
  728. 'initializing',
  729. 'ready',
  730. 'crashed',
  731. 'initializing',
  732. 'ready',
  733. 'crashed',
  734. 'crashedPermanently',
  735. 'destroyed'
  736. ] );
  737. res();
  738. } );
  739. } );
  740. } );
  741. } );
  742. } );
  743. } );
  744. } );
  745. describe( 'multi-root editors', () => {
  746. it( 'should support multi-root editors', () => {
  747. class MultiRootEditor extends Editor {
  748. constructor( sourceElements, config ) {
  749. super( config );
  750. this.data.processor = new HtmlDataProcessor();
  751. // Create a root for each source element.
  752. for ( const rootName of Object.keys( sourceElements ) ) {
  753. this.model.document.createRoot( '$root', rootName );
  754. }
  755. }
  756. static async create( sourceElements, config ) {
  757. const editor = new this( sourceElements, config );
  758. await editor.initPlugins();
  759. await editor.data.init( config.initialData );
  760. editor.fire( 'ready' );
  761. return editor;
  762. }
  763. }
  764. const watchdog = Watchdog.for( MultiRootEditor );
  765. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  766. const originalErrorHandler = window.onerror;
  767. window.onerror = undefined;
  768. return watchdog
  769. .create( {
  770. header: element
  771. }, {
  772. initialData: {
  773. header: '<p>Foo</p>'
  774. },
  775. plugins: [ Paragraph ]
  776. } )
  777. .then( () => {
  778. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  779. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  780. return new Promise( res => {
  781. window.onerror = originalErrorHandler;
  782. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  783. res();
  784. } );
  785. } ).then( () => {
  786. return watchdog.destroy();
  787. } );
  788. } );
  789. } );
  790. } );
  791. function throwCKEditorError( name, context ) {
  792. throw new CKEditorError( name, context );
  793. }
  794. // Feature detection works as a race condition - if the `unhandledrejection` event
  795. // is supported then the listener should be called first. Otherwise the timeout will be reached.
  796. function isUnhandledRejectionEventSupported() {
  797. return new Promise( res => {
  798. window.addEventListener( 'unhandledrejection', function listener() {
  799. res( true );
  800. window.removeEventListener( 'unhandledrejection', listener );
  801. } );
  802. Promise.resolve().then( () => Promise.reject( new Error() ) );
  803. setTimeout( () => res( false ) );
  804. } );
  805. }