8
0

autosave.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  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 document, window */
  6. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  7. import Autosave from '../src/autosave';
  8. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  9. import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
  10. describe( 'Autosave', () => {
  11. let editor, element, autosave;
  12. beforeEach( () => {
  13. sinon.useFakeTimers( { now: Date.now() } );
  14. } );
  15. afterEach( () => {
  16. sinon.restore();
  17. } );
  18. it( 'should have static pluginName property', () => {
  19. expect( Autosave.pluginName ).to.equal( 'Autosave' );
  20. } );
  21. describe( 'initialization', () => {
  22. beforeEach( () => {
  23. element = document.createElement( 'div' );
  24. document.body.appendChild( element );
  25. return ClassicTestEditor
  26. .create( element, {
  27. plugins: [ Autosave, Paragraph ]
  28. } )
  29. .then( _editor => {
  30. const data = '<p>paragraph1</p><p>paragraph2</p>';
  31. editor = _editor;
  32. editor.setData( data );
  33. autosave = editor.plugins.get( Autosave );
  34. } );
  35. } );
  36. afterEach( () => {
  37. document.body.removeChild( element );
  38. return editor.destroy();
  39. } );
  40. it( 'should initialize adapter with an undefined value', () => {
  41. expect( autosave.adapter ).to.be.undefined;
  42. } );
  43. it( 'should allow plugin to work without defined adapter and without its config', () => {
  44. expect( () => {
  45. editor.model.change( writer => {
  46. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  47. editor.model.insertContent( writer.createText( 'foo' ) );
  48. } );
  49. sinon.clock.tick( 1000 );
  50. } ).to.not.throw();
  51. } );
  52. it( 'should start with the `synchronized` state', () => {
  53. expect( autosave.state ).to.equal( 'synchronized' );
  54. } );
  55. } );
  56. describe( 'config.autosave.save', () => {
  57. beforeEach( () => {
  58. element = document.createElement( 'div' );
  59. document.body.appendChild( element );
  60. return ClassicTestEditor
  61. .create( element, {
  62. plugins: [ Autosave, Paragraph ],
  63. autosave: {
  64. save: sinon.spy()
  65. }
  66. } )
  67. .then( _editor => {
  68. editor = _editor;
  69. autosave = editor.plugins.get( Autosave );
  70. const data = '<p>paragraph1</p><p>paragraph2</p>';
  71. editor.setData( data );
  72. } );
  73. } );
  74. afterEach( () => {
  75. document.body.removeChild( element );
  76. return editor.destroy();
  77. } );
  78. it( 'should enable providing callback via the config', () => {
  79. editor.config.get( 'autosave' ).save.resetHistory();
  80. editor.model.change( writer => {
  81. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  82. editor.model.insertContent( writer.createText( 'foo' ) );
  83. } );
  84. sinon.clock.tick( 1000 );
  85. return Promise.resolve().then( () => {
  86. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  87. } );
  88. } );
  89. it( 'config callback and adapter callback should be called if both are provided', () => {
  90. editor.config.get( 'autosave' ).save.resetHistory();
  91. autosave.adapter = {
  92. save: sinon.spy()
  93. };
  94. editor.model.change( writer => {
  95. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  96. editor.model.insertContent( writer.createText( 'foo' ) );
  97. } );
  98. sinon.clock.tick( 1000 );
  99. return Promise.resolve().then( () => {
  100. sinon.assert.calledOnce( autosave.adapter.save );
  101. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  102. } );
  103. } );
  104. it( 'config callback and adapter callback should be called with the editor as an argument', () => {
  105. editor.config.get( 'autosave' ).save.resetHistory();
  106. autosave.adapter = {
  107. save: sinon.spy()
  108. };
  109. editor.model.change( writer => {
  110. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  111. editor.model.insertContent( writer.createText( 'foo' ) );
  112. } );
  113. sinon.clock.tick( 1000 );
  114. return Promise.resolve().then( () => {
  115. sinon.assert.calledWithExactly( autosave.adapter.save, editor );
  116. sinon.assert.calledWithExactly( editor.config.get( 'autosave' ).save, editor );
  117. } );
  118. } );
  119. } );
  120. describe( 'config.autosave.waitingTime', () => {
  121. afterEach( () => {
  122. document.body.removeChild( element );
  123. return editor.destroy();
  124. } );
  125. it( 'should specify the time of waiting on the next use action before saving', () => {
  126. element = document.createElement( 'div' );
  127. document.body.appendChild( element );
  128. return ClassicTestEditor
  129. .create( element, {
  130. plugins: [ Autosave, Paragraph ],
  131. autosave: {
  132. save: sinon.spy(),
  133. waitingTime: 500
  134. }
  135. } )
  136. .then( _editor => {
  137. editor = _editor;
  138. const data = '<p>paragraph1</p><p>paragraph2</p>';
  139. editor.setData( data );
  140. editor.model.change( writer => {
  141. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  142. editor.model.insertContent( writer.createText( 'foo' ) );
  143. } );
  144. sinon.clock.tick( 499 );
  145. return Promise.resolve().then( () => {
  146. sinon.assert.notCalled( editor.config.get( 'autosave' ).save );
  147. sinon.clock.tick( 1 );
  148. return Promise.resolve();
  149. } ).then( () => {
  150. // Callback should be called exactly after 500ms.
  151. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  152. } );
  153. } );
  154. } );
  155. it( 'should be default to 1000', () => {
  156. element = document.createElement( 'div' );
  157. document.body.appendChild( element );
  158. return ClassicTestEditor
  159. .create( element, {
  160. plugins: [ Autosave, Paragraph ],
  161. autosave: {
  162. save: sinon.spy()
  163. }
  164. } )
  165. .then( _editor => {
  166. editor = _editor;
  167. const data = '<p>paragraph1</p><p>paragraph2</p>';
  168. editor.setData( data );
  169. editor.model.change( writer => {
  170. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  171. editor.model.insertContent( writer.createText( 'foo' ) );
  172. } );
  173. sinon.clock.tick( 999 );
  174. return Promise.resolve().then( () => {
  175. sinon.assert.notCalled( editor.config.get( 'autosave' ).save );
  176. sinon.clock.tick( 1 );
  177. return Promise.resolve();
  178. } ).then( () => {
  179. // Callback should be called exactly after 1000ms by default.
  180. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  181. } );
  182. } );
  183. } );
  184. } );
  185. describe( 'autosaving', () => {
  186. beforeEach( () => {
  187. element = document.createElement( 'div' );
  188. document.body.appendChild( element );
  189. return ClassicTestEditor
  190. .create( element, {
  191. plugins: [ Autosave, Paragraph ]
  192. } )
  193. .then( _editor => {
  194. const data = '<p>paragraph1</p><p>paragraph2</p>';
  195. editor = _editor;
  196. editor.setData( data );
  197. autosave = editor.plugins.get( Autosave );
  198. } );
  199. } );
  200. afterEach( () => {
  201. document.body.removeChild( element );
  202. return editor.destroy();
  203. } );
  204. it( 'should run adapter\'s save method when the editor\'s change event is fired', () => {
  205. autosave.adapter = {
  206. save: sinon.spy()
  207. };
  208. editor.model.change( writer => {
  209. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  210. editor.model.insertContent( writer.createText( 'foo' ) );
  211. } );
  212. sinon.clock.tick( 1000 );
  213. return Promise.resolve().then( () => {
  214. sinon.assert.calledOnce( autosave.adapter.save );
  215. } );
  216. } );
  217. it( 'should debounce editor\'s change event', () => {
  218. const spy = sinon.spy();
  219. const savedStates = [];
  220. autosave.adapter = {
  221. save() {
  222. spy();
  223. savedStates.push( editor.getData() );
  224. }
  225. };
  226. editor.model.change( writer => {
  227. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) ) );
  228. editor.model.insertContent( writer.createText( 'foo' ) );
  229. } );
  230. sinon.clock.tick( 1000 );
  231. editor.model.change( writer => {
  232. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) ) );
  233. editor.model.insertContent( writer.createText( 'bar' ) );
  234. } );
  235. sinon.clock.tick( 1000 );
  236. editor.model.change( writer => {
  237. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) ) );
  238. editor.model.insertContent( writer.createText( 'biz' ) );
  239. } );
  240. sinon.assert.notCalled( spy );
  241. sinon.clock.tick( 1000 );
  242. return Promise.resolve().then( () => {
  243. sinon.assert.calledOnce( spy );
  244. expect( savedStates ).to.deep.equal( [
  245. '<p>paragraph1</p><p>biz</p>'
  246. ] );
  247. } );
  248. } );
  249. it( 'should add a pending action after a change and wait on the server response', () => {
  250. const pendingActions = editor.plugins.get( PendingActions );
  251. const serverActionSpy = sinon.spy();
  252. const serverActionStub = sinon.stub();
  253. serverActionStub.callsFake( () => wait( 1000 ).then( serverActionSpy ) );
  254. autosave.adapter = {
  255. save: serverActionStub
  256. };
  257. editor.model.change( writer => {
  258. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  259. editor.model.insertContent( writer.createText( 'foo' ) );
  260. } );
  261. sinon.assert.notCalled( serverActionSpy );
  262. expect( pendingActions.hasAny ).to.be.true;
  263. expect( pendingActions.first.message ).to.equal( 'Saving changes' );
  264. sinon.clock.tick( 1000 );
  265. sinon.assert.notCalled( serverActionSpy );
  266. expect( pendingActions.hasAny ).to.be.true;
  267. expect( pendingActions.first.message ).to.equal( 'Saving changes' );
  268. return Promise.resolve().then( () => {
  269. sinon.clock.tick( 1000 );
  270. return runPromiseCycles().then( () => {
  271. sinon.assert.calledOnce( serverActionSpy );
  272. expect( pendingActions.hasAny ).to.be.false;
  273. } );
  274. } );
  275. } );
  276. it( 'should add a pending action during the saving #2.', () => {
  277. const serverActionSpy = sinon.spy();
  278. const pendingActions = editor.plugins.get( PendingActions );
  279. autosave.adapter = {
  280. save: serverActionSpy
  281. };
  282. expect( pendingActions.hasAny ).to.be.false;
  283. editor.model.change( writer => {
  284. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  285. editor.model.insertContent( writer.createText( 'foo' ) );
  286. } );
  287. expect( pendingActions.hasAny ).to.be.true;
  288. sinon.clock.tick( 1000 );
  289. return runPromiseCycles().then( () => {
  290. sinon.assert.calledOnce( serverActionSpy );
  291. expect( pendingActions.hasAny ).to.be.false;
  292. } );
  293. } );
  294. it( 'should be in correct states during the saving', () => {
  295. const pendingActions = editor.plugins.get( PendingActions );
  296. const serverActionSpy = sinon.spy();
  297. const serverActionStub = sinon.stub();
  298. serverActionStub.callsFake( () => wait( 1000 ).then( serverActionSpy ) );
  299. autosave.adapter = {
  300. save: serverActionStub
  301. };
  302. editor.model.change( writer => {
  303. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  304. editor.model.insertContent( writer.createText( 'foo' ) );
  305. } );
  306. sinon.assert.notCalled( serverActionSpy );
  307. expect( pendingActions.hasAny ).to.be.true;
  308. expect( pendingActions.first.message ).to.equal( 'Saving changes' );
  309. sinon.clock.tick( 1000 );
  310. expect( autosave.state ).to.equal( 'saving' );
  311. return Promise.resolve().then( () => {
  312. // Add new change before the response from the server.
  313. editor.model.change( writer => {
  314. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  315. editor.model.insertContent( writer.createText( 'foo' ) );
  316. } );
  317. sinon.clock.tick( 1000 );
  318. return runPromiseCycles();
  319. } ).then( () => {
  320. // Now there should come the first server response.
  321. expect( autosave.state ).to.equal( 'waiting' );
  322. expect( pendingActions.hasAny ).to.be.true;
  323. sinon.assert.calledOnce( serverActionSpy );
  324. sinon.clock.tick( 1000 );
  325. return runPromiseCycles();
  326. } ).then( () => {
  327. expect( autosave.state ).to.equal( 'saving' );
  328. expect( pendingActions.hasAny ).to.be.true;
  329. sinon.assert.calledOnce( serverActionSpy );
  330. // Wait for the second server response.
  331. sinon.clock.tick( 1000 );
  332. return runPromiseCycles();
  333. } ).then( () => {
  334. expect( pendingActions.hasAny ).to.be.false;
  335. expect( autosave.state ).to.equal( 'synchronized' );
  336. sinon.assert.calledTwice( serverActionSpy );
  337. } );
  338. } );
  339. it( 'should filter out selection changes', () => {
  340. autosave.adapter = {
  341. save: sinon.spy()
  342. };
  343. editor.model.change( writer => {
  344. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  345. } );
  346. sinon.clock.tick( 1000 );
  347. return runPromiseCycles().then( () => {
  348. sinon.assert.notCalled( autosave.adapter.save );
  349. } );
  350. } );
  351. it( 'should filter out markers that does not affect the data', () => {
  352. autosave.adapter = {
  353. save: sinon.spy()
  354. };
  355. editor.model.change( writer => {
  356. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) );
  357. writer.addMarker( 'name', { usingOperation: true, range } );
  358. } );
  359. sinon.clock.tick( 1000 );
  360. editor.model.change( writer => {
  361. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) );
  362. writer.updateMarker( 'name', { range } );
  363. } );
  364. sinon.clock.tick( 1000 );
  365. return runPromiseCycles().then( () => {
  366. sinon.assert.notCalled( autosave.adapter.save );
  367. } );
  368. } );
  369. it( 'should filter out markers that does not affect the data #2', () => {
  370. autosave.adapter = {
  371. save: sinon.spy()
  372. };
  373. editor.model.change( writer => {
  374. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) );
  375. writer.addMarker( 'name', { usingOperation: false, range } );
  376. } );
  377. sinon.clock.tick( 1000 );
  378. editor.model.change( writer => {
  379. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) );
  380. writer.updateMarker( 'name', { range } );
  381. } );
  382. sinon.clock.tick( 1000 );
  383. return runPromiseCycles().then( () => {
  384. sinon.assert.notCalled( autosave.adapter.save );
  385. } );
  386. } );
  387. it( 'should call the save method when some marker affects the data', () => {
  388. autosave.adapter = {
  389. save: sinon.spy()
  390. };
  391. editor.model.change( writer => {
  392. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) );
  393. writer.addMarker( 'name', { usingOperation: true, affectsData: true, range } );
  394. } );
  395. sinon.clock.tick( 1000 );
  396. return runPromiseCycles().then( () => {
  397. sinon.assert.calledOnce( autosave.adapter.save );
  398. } );
  399. } );
  400. it( 'should call the save method when some marker affects the data #2', () => {
  401. autosave.adapter = {
  402. save: sinon.spy()
  403. };
  404. editor.model.change( writer => {
  405. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) );
  406. writer.addMarker( 'name', { usingOperation: false, affectsData: true, range } );
  407. } );
  408. sinon.clock.tick( 1000 );
  409. return runPromiseCycles().then( () => {
  410. sinon.assert.calledOnce( autosave.adapter.save );
  411. editor.model.change( writer => {
  412. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) );
  413. writer.updateMarker( 'name', { range } );
  414. } );
  415. sinon.clock.tick( 1000 );
  416. return runPromiseCycles().then( () => {
  417. sinon.assert.calledTwice( autosave.adapter.save );
  418. } );
  419. } );
  420. } );
  421. it( 'should call the save method when some marker affects the data #3', () => {
  422. autosave.adapter = {
  423. save: sinon.spy()
  424. };
  425. editor.model.change( writer => {
  426. const range = writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) );
  427. writer.addMarker( 'marker-not-affecting-data', { usingOperation: false, affectsData: true, range } );
  428. writer.addMarker( 'marker-affecting-data', { usingOperation: false, affectsData: false, range } );
  429. } );
  430. sinon.clock.tick( 1000 );
  431. return runPromiseCycles().then( () => {
  432. sinon.assert.calledOnce( autosave.adapter.save );
  433. } );
  434. } );
  435. it( 'should flush remaining call after editor\'s destroy', () => {
  436. const spy = sinon.spy();
  437. const savedStates = [];
  438. autosave.adapter = {
  439. save() {
  440. spy();
  441. savedStates.push( editor.getData() );
  442. }
  443. };
  444. editor.model.change( writer => {
  445. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  446. editor.model.insertContent( writer.createText( 'foo' ) );
  447. } );
  448. editor.model.change( writer => {
  449. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 1 ) ) );
  450. editor.model.insertContent( writer.createText( 'bar' ) );
  451. } );
  452. sinon.assert.notCalled( spy );
  453. return editor.destroy().then( () => {
  454. sinon.assert.calledOnce( spy );
  455. expect( savedStates ).to.deep.equal( [
  456. '<p>foo</p><p>bar</p>'
  457. ] );
  458. } );
  459. } );
  460. it( 'should work after editor\'s destroy with long server\'s response time', () => {
  461. const pendingActions = editor.plugins.get( PendingActions );
  462. const serverActionSpy = sinon.spy();
  463. const serverActionStub = sinon.stub();
  464. serverActionStub.onCall( 0 ).resolves( wait( 1000 ).then( serverActionSpy ) );
  465. autosave.adapter = {
  466. save: serverActionStub
  467. };
  468. editor.model.change( writer => {
  469. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  470. editor.model.insertContent( writer.createText( 'foo' ) );
  471. } );
  472. return editor.destroy()
  473. .then( () => {
  474. expect( pendingActions.hasAny ).to.be.true;
  475. sinon.clock.tick( 1000 );
  476. } )
  477. .then( runPromiseCycles )
  478. .then( () => {
  479. expect( pendingActions.hasAny ).to.be.false;
  480. sinon.assert.calledOnce( serverActionSpy );
  481. sinon.assert.calledOnce( serverActionStub );
  482. } );
  483. } );
  484. it( 'should handle a situration when the save callback throws an error', () => {
  485. const pendingActions = editor.plugins.get( PendingActions );
  486. const successServerActionSpy = sinon.spy();
  487. const serverActionStub = sinon.stub();
  488. serverActionStub.onFirstCall()
  489. .rejects( new Error( 'foo' ) );
  490. serverActionStub.onSecondCall()
  491. .callsFake( successServerActionSpy );
  492. autosave.adapter = {
  493. save: serverActionStub
  494. };
  495. editor.model.change( writer => {
  496. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  497. editor.model.insertContent( writer.createText( 'foo' ) );
  498. } );
  499. return editor.destroy()
  500. .then( () => {
  501. expect( pendingActions.hasAny ).to.be.true;
  502. } )
  503. .then( runPromiseCycles )
  504. .then( () => {
  505. expect( pendingActions.hasAny ).to.be.true;
  506. sinon.assert.calledOnce( serverActionStub );
  507. sinon.assert.notCalled( successServerActionSpy );
  508. } )
  509. .then( () => sinon.clock.tick( 1000 ) )
  510. .then( runPromiseCycles )
  511. .then( () => {
  512. expect( pendingActions.hasAny ).to.be.false;
  513. sinon.assert.calledTwice( serverActionStub );
  514. sinon.assert.calledOnce( successServerActionSpy );
  515. } );
  516. } );
  517. } );
  518. it( 'should run callbacks until the editor is in the ready state', () => {
  519. element = document.createElement( 'div' );
  520. document.body.appendChild( element );
  521. editor = null;
  522. class AsyncPlugin {
  523. constructor( editor ) {
  524. this.editor = editor;
  525. }
  526. init() {
  527. this.editor.data.once( 'ready', () => {
  528. const editor = this.editor;
  529. editor.model.change( writer => {
  530. writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) );
  531. editor.model.insertContent( writer.createText( 'bar' ) );
  532. } );
  533. sinon.clock.tick( 10 );
  534. } );
  535. }
  536. }
  537. return ClassicTestEditor
  538. .create( element, {
  539. plugins: [ Autosave, Paragraph, AsyncPlugin ],
  540. autosave: {
  541. save: sinon.spy(),
  542. waitingTime: 5
  543. }
  544. } )
  545. .then( _editor => {
  546. editor = _editor;
  547. const spy = editor.config.get( 'autosave' ).save;
  548. expect( editor.getData() ).to.equal( '<p>bar</p>' );
  549. sinon.assert.notCalled( spy );
  550. } )
  551. .then( () => {
  552. document.body.removeChild( element );
  553. return editor.destroy();
  554. } );
  555. } );
  556. } );
  557. function wait( time ) {
  558. return new Promise( res => {
  559. window.setTimeout( res, time );
  560. } );
  561. }
  562. function runPromiseCycles() {
  563. return Promise.resolve()
  564. .then( () => Promise.resolve() )
  565. .then( () => Promise.resolve() )
  566. .then( () => Promise.resolve() )
  567. .then( () => Promise.resolve() );
  568. }