watchdog.js 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073
  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. describe( 'multi-root editors', () => {
  769. it( 'should support multi-root editors', () => {
  770. class MultiRootEditor extends Editor {
  771. constructor( sourceElements, config ) {
  772. super( config );
  773. this.data.processor = new HtmlDataProcessor();
  774. // Create a root for each source element.
  775. for ( const rootName of Object.keys( sourceElements ) ) {
  776. this.model.document.createRoot( '$root', rootName );
  777. }
  778. }
  779. static async create( sourceElements, config ) {
  780. const editor = new this( sourceElements, config );
  781. await editor.initPlugins();
  782. await editor.data.init( config.initialData );
  783. editor.fire( 'ready' );
  784. return editor;
  785. }
  786. }
  787. const watchdog = Watchdog.for( MultiRootEditor );
  788. // sinon.stub( window, 'onerror' ).value( undefined ); and similar do not work.
  789. const originalErrorHandler = window.onerror;
  790. window.onerror = undefined;
  791. return watchdog
  792. .create( {
  793. header: element
  794. }, {
  795. initialData: {
  796. header: '<p>Foo</p>'
  797. },
  798. plugins: [ Paragraph ]
  799. } )
  800. .then( () => {
  801. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  802. setTimeout( () => throwCKEditorError( 'foo', watchdog.editor ) );
  803. return new Promise( res => {
  804. window.onerror = originalErrorHandler;
  805. expect( watchdog.editor.data.get( { rootName: 'header' } ) ).to.equal( '<p>Foo</p>' );
  806. res();
  807. } );
  808. } ).then( () => {
  809. return watchdog.destroy();
  810. } );
  811. } );
  812. } );
  813. } );
  814. function throwCKEditorError( name, context ) {
  815. throw new CKEditorError( name, context );
  816. }
  817. // Feature detection works as a race condition - if the `unhandledrejection` event
  818. // is supported then the listener should be called first. Otherwise the timeout will be reached.
  819. function isUnhandledRejectionEventSupported() {
  820. return new Promise( res => {
  821. window.addEventListener( 'unhandledrejection', function listener() {
  822. res( true );
  823. window.removeEventListener( 'unhandledrejection', listener );
  824. } );
  825. Promise.resolve().then( () => Promise.reject( new Error() ) );
  826. setTimeout( () => res( false ) );
  827. } );
  828. }