autosave.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals document, window */
  6. import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
  7. import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
  8. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  9. import Autosave from '../src/autosave';
  10. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  11. import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
  12. describe( 'Autosave', () => {
  13. const sandbox = sinon.sandbox.create( { useFakeTimers: true } );
  14. let editor, element, autosave;
  15. afterEach( () => {
  16. sandbox.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. // Clean autosave's state after setting data.
  35. autosave._flush();
  36. } );
  37. } );
  38. afterEach( () => {
  39. document.body.removeChild( element );
  40. return editor.destroy();
  41. } );
  42. it( 'should initialize adapter with an undefined value', () => {
  43. expect( autosave.adapter ).to.be.undefined;
  44. } );
  45. it( 'should allow plugin to work without defined adapter and without its config', () => {
  46. editor.model.change( writer => {
  47. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  48. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  49. } );
  50. autosave._flush();
  51. } );
  52. } );
  53. describe( 'config', () => {
  54. beforeEach( () => {
  55. element = document.createElement( 'div' );
  56. document.body.appendChild( element );
  57. return ClassicTestEditor
  58. .create( element, {
  59. plugins: [ Autosave, Paragraph ],
  60. autosave: {
  61. save: sinon.spy()
  62. }
  63. } )
  64. .then( _editor => {
  65. const data = '<p>paragraph1</p><p>paragraph2</p>';
  66. editor = _editor;
  67. editor.setData( data );
  68. autosave = editor.plugins.get( Autosave );
  69. // Clean autosave's state after setting data.
  70. autosave._flush();
  71. editor.config.get( 'autosave' ).save.resetHistory();
  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.model.change( writer => {
  80. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  81. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  82. } );
  83. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  84. } );
  85. it( 'its callback and adapter callback should be called if both are provided', () => {
  86. autosave.adapter = {
  87. save: sinon.spy()
  88. };
  89. editor.model.change( writer => {
  90. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  91. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  92. } );
  93. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  94. sinon.assert.calledOnce( autosave.adapter.save );
  95. } );
  96. } );
  97. describe( 'autosaving', () => {
  98. beforeEach( () => {
  99. element = document.createElement( 'div' );
  100. document.body.appendChild( element );
  101. return ClassicTestEditor
  102. .create( element, {
  103. plugins: [ Autosave, Paragraph ]
  104. } )
  105. .then( _editor => {
  106. const data = '<p>paragraph1</p><p>paragraph2</p>';
  107. editor = _editor;
  108. editor.setData( data );
  109. autosave = editor.plugins.get( Autosave );
  110. // Clean autosave's state after setting data.
  111. autosave._flush();
  112. } );
  113. } );
  114. afterEach( () => {
  115. document.body.removeChild( element );
  116. return editor.destroy();
  117. } );
  118. it( 'should run adapter\'s save method when the editor\'s change event is fired', () => {
  119. autosave.adapter = {
  120. save: sinon.spy()
  121. };
  122. editor.model.change( writer => {
  123. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  124. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  125. } );
  126. sinon.assert.calledOnce( autosave.adapter.save );
  127. } );
  128. it( 'should throttle editor\'s change event', () => {
  129. const spy = sinon.spy();
  130. const savedStates = [];
  131. autosave.adapter = {
  132. save() {
  133. spy();
  134. savedStates.push( editor.getData() );
  135. }
  136. };
  137. // Leading (will fire change).
  138. editor.model.change( writer => {
  139. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  140. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  141. } );
  142. // Throttled (won't fire change).
  143. editor.model.change( writer => {
  144. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  145. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  146. } );
  147. // Flushed (will fire change).
  148. editor.model.change( writer => {
  149. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  150. editor.model.insertContent( new ModelText( 'biz' ), editor.model.document.selection );
  151. } );
  152. autosave._flush();
  153. expect( spy.callCount ).to.equal( 2 );
  154. expect( savedStates ).to.deep.equal( [
  155. '<p>paragraph1</p><p>foo</p>',
  156. '<p>paragraph1</p><p>biz</p>'
  157. ] );
  158. } );
  159. it( 'should add a pending action during the saving.', () => {
  160. sandbox.useFakeTimers();
  161. const pendingActions = editor.plugins.get( PendingActions );
  162. const serverActionSpy = sinon.spy();
  163. const serverActionStub = sinon.stub();
  164. serverActionStub.onCall( 0 ).resolves( wait( 500 ).then( serverActionSpy ) );
  165. autosave.adapter = {
  166. save: serverActionStub
  167. };
  168. editor.model.change( writer => {
  169. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  170. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  171. } );
  172. sinon.assert.notCalled( serverActionSpy );
  173. expect( pendingActions.isPending ).to.be.true;
  174. expect( pendingActions.first.message ).to.equal( 'Saving in progress.' );
  175. sandbox.clock.tick( 500 );
  176. return Promise.resolve().then( () => Promise.resolve() ).then( () => {
  177. sinon.assert.calledOnce( serverActionSpy );
  178. expect( pendingActions.isPending ).to.be.false;
  179. } );
  180. } );
  181. it( 'should add a pending action during the saving #2.', () => {
  182. const serverActionSpy = sinon.spy();
  183. const pendingActions = editor.plugins.get( PendingActions );
  184. autosave.adapter = {
  185. save: serverActionSpy
  186. };
  187. expect( pendingActions.isPending ).to.be.false;
  188. editor.model.change( writer => {
  189. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  190. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  191. } );
  192. expect( pendingActions.isPending ).to.be.true;
  193. // Server action needs to wait at least a cycle.
  194. return wait().then( () => {
  195. sinon.assert.calledOnce( serverActionSpy );
  196. expect( pendingActions.isPending ).to.be.false;
  197. } );
  198. } );
  199. it( 'should handle correctly throttled save action and preserve pending action until both save actions finish', () => {
  200. sandbox.useFakeTimers();
  201. const serverActionSpy = sinon.spy();
  202. const pendingActions = editor.plugins.get( PendingActions );
  203. // Create a fake server that responses after 500ms for the first call and after 1000ms for the second call.
  204. const serverActionStub = sinon.stub();
  205. serverActionStub.onCall( 0 ).resolves( wait( 500 ).then( serverActionSpy ) );
  206. serverActionStub.onCall( 1 ).resolves( wait( 1000 ).then( serverActionSpy ) );
  207. autosave.adapter = {
  208. save: serverActionStub
  209. };
  210. expect( pendingActions.isPending ).to.be.false;
  211. editor.model.change( writer => {
  212. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  213. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  214. } );
  215. expect( pendingActions.isPending ).to.be.true;
  216. editor.model.change( writer => {
  217. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  218. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  219. } );
  220. autosave._flush();
  221. expect( pendingActions.isPending ).to.be.true;
  222. sandbox.clock.tick( 500 );
  223. return Promise.resolve().then( () => {
  224. expect( pendingActions.isPending ).to.be.true;
  225. sinon.assert.calledOnce( serverActionSpy );
  226. // Wait another 500ms and a promise cycle for the second server action.
  227. sandbox.clock.tick( 500 );
  228. } )
  229. .then( () => Promise.resolve() )
  230. .then( () => {
  231. expect( pendingActions.isPending ).to.be.false;
  232. sinon.assert.calledTwice( serverActionSpy );
  233. } );
  234. } );
  235. it( 'should handle correctly throttled save action and preserve pending action until both save actions finish #2', () => {
  236. const serverActionSpy = sinon.spy();
  237. const pendingActions = editor.plugins.get( PendingActions );
  238. autosave.adapter = {
  239. save: serverActionSpy
  240. };
  241. expect( pendingActions.isPending ).to.be.false;
  242. editor.model.change( writer => {
  243. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  244. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  245. } );
  246. expect( pendingActions.isPending ).to.be.true;
  247. editor.model.change( writer => {
  248. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  249. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  250. } );
  251. expect( pendingActions.isPending ).to.be.true;
  252. // Server action needs to wait at least a cycle.
  253. return wait().then( () => {
  254. sinon.assert.calledOnce( serverActionSpy );
  255. expect( pendingActions.isPending ).to.be.true;
  256. autosave._flush();
  257. // Wait another promise cycle.
  258. return wait().then( () => {
  259. sinon.assert.calledTwice( serverActionSpy );
  260. expect( pendingActions.isPending ).to.be.false;
  261. } );
  262. } );
  263. } );
  264. it( 'should filter out changes in the selection', () => {
  265. autosave.adapter = {
  266. save: sandbox.spy()
  267. };
  268. editor.model.change( writer => {
  269. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  270. } );
  271. autosave._flush();
  272. sinon.assert.notCalled( autosave.adapter.save );
  273. } );
  274. it( 'should filter out markers that does not affect the data model', () => {
  275. autosave.adapter = {
  276. save: sandbox.spy()
  277. };
  278. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  279. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  280. editor.model.change( writer => {
  281. writer.addMarker( 'name', { usingOperation: true, range } );
  282. } );
  283. autosave._flush();
  284. editor.model.change( writer => {
  285. writer.updateMarker( 'name', { range: range2 } );
  286. } );
  287. autosave._flush();
  288. sinon.assert.notCalled( autosave.adapter.save );
  289. } );
  290. it( 'should filter out markers that does not affect the data model #2', () => {
  291. autosave.adapter = {
  292. save: sandbox.spy()
  293. };
  294. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  295. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  296. editor.model.change( writer => {
  297. writer.addMarker( 'name', { usingOperation: false, range } );
  298. } );
  299. autosave._flush();
  300. editor.model.change( writer => {
  301. writer.updateMarker( 'name', { range: range2 } );
  302. } );
  303. autosave._flush();
  304. sinon.assert.notCalled( autosave.adapter.save );
  305. } );
  306. it( 'should call the save method when some marker affects the data model', () => {
  307. autosave.adapter = {
  308. save: sandbox.spy()
  309. };
  310. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  311. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  312. editor.model.change( writer => {
  313. writer.addMarker( 'name', { usingOperation: true, affectsData: true, range } );
  314. } );
  315. autosave._flush();
  316. editor.model.change( writer => {
  317. writer.updateMarker( 'name', { range: range2 } );
  318. } );
  319. autosave._flush();
  320. sinon.assert.calledTwice( autosave.adapter.save );
  321. } );
  322. it( 'should call the save method when some marker affects the data model #2', () => {
  323. autosave.adapter = {
  324. save: sandbox.spy()
  325. };
  326. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  327. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  328. editor.model.change( writer => {
  329. writer.addMarker( 'name', { usingOperation: false, affectsData: true, range } );
  330. } );
  331. autosave._flush();
  332. sinon.assert.calledOnce( autosave.adapter.save );
  333. editor.model.change( writer => {
  334. writer.updateMarker( 'name', { range: range2 } );
  335. } );
  336. autosave._flush();
  337. sinon.assert.calledTwice( autosave.adapter.save );
  338. } );
  339. it( 'should call the save method when some marker affects the data model #3', () => {
  340. autosave.adapter = {
  341. save: sandbox.spy()
  342. };
  343. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  344. editor.model.change( writer => {
  345. writer.addMarker( 'marker-not-affecting-data', { usingOperation: false, affectsData: true, range } );
  346. writer.addMarker( 'marker-affecting-data', { usingOperation: false, affectsData: false, range } );
  347. } );
  348. sinon.assert.calledOnce( autosave.adapter.save );
  349. } );
  350. it( 'should flush remaining calls after editor\'s destroy', () => {
  351. const spy = sandbox.spy();
  352. const savedStates = [];
  353. autosave.adapter = {
  354. save() {
  355. spy();
  356. savedStates.push( editor.getData() );
  357. }
  358. };
  359. editor.model.change( writer => {
  360. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  361. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  362. } );
  363. editor.model.change( writer => {
  364. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  365. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  366. } );
  367. return editor.destroy().then( () => {
  368. expect( spy.callCount ).to.equal( 2 );
  369. expect( savedStates ).to.deep.equal( [
  370. '<p>foo</p><p>paragraph2</p>',
  371. '<p>foo</p><p>bar</p>',
  372. ] );
  373. } );
  374. } );
  375. it( 'should work after editor\'s destroy with long server\'s response time', () => {
  376. sandbox.useFakeTimers();
  377. const pendingActions = editor.plugins.get( PendingActions );
  378. const serverActionSpy = sinon.spy();
  379. const serverActionStub = sinon.stub();
  380. serverActionStub.onCall( 0 ).resolves( wait( 500 ).then( serverActionSpy ) );
  381. autosave.adapter = {
  382. save: serverActionStub
  383. };
  384. editor.model.change( writer => {
  385. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  386. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  387. } );
  388. return editor.destroy()
  389. .then( () => {
  390. expect( pendingActions.isPending ).to.be.true;
  391. sandbox.clock.tick( 500 );
  392. } )
  393. .then( () => Promise.resolve() )
  394. .then( () => {
  395. expect( pendingActions.isPending ).to.be.false;
  396. sinon.assert.calledOnce( serverActionSpy );
  397. sinon.assert.calledOnce( serverActionStub );
  398. } );
  399. } );
  400. } );
  401. } );
  402. function wait( time ) {
  403. return new Promise( res => {
  404. window.setTimeout( res, time );
  405. } );
  406. }