8
0

editorwatchdog.js 33 KB

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