autosave.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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. } );
  72. } );
  73. afterEach( () => {
  74. document.body.removeChild( element );
  75. return editor.destroy();
  76. } );
  77. it( 'should enable providing callback via the config', () => {
  78. editor.config.get( 'autosave' ).save.resetHistory();
  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. return wait().then( () => {
  84. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  85. } );
  86. } );
  87. it( 'its callback and adapter callback should be called if both are provided', () => {
  88. editor.config.get( 'autosave' ).save.resetHistory();
  89. autosave.adapter = {
  90. save: sinon.spy()
  91. };
  92. editor.model.change( writer => {
  93. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  94. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  95. } );
  96. return wait().then( () => {
  97. sinon.assert.calledOnce( autosave.adapter.save );
  98. sinon.assert.calledOnce( editor.config.get( 'autosave' ).save );
  99. } );
  100. } );
  101. it( 'config callback and adapter callback should be called with the editor as an argument', () => {
  102. editor.config.get( 'autosave' ).save.resetHistory();
  103. autosave.adapter = {
  104. save: sinon.spy()
  105. };
  106. editor.model.change( writer => {
  107. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  108. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  109. } );
  110. return wait().then( () => {
  111. sinon.assert.calledWithExactly( autosave.adapter.save, editor );
  112. sinon.assert.calledWithExactly( editor.config.get( 'autosave' ).save, editor );
  113. } );
  114. } );
  115. } );
  116. describe( 'autosaving', () => {
  117. beforeEach( () => {
  118. element = document.createElement( 'div' );
  119. document.body.appendChild( element );
  120. return ClassicTestEditor
  121. .create( element, {
  122. plugins: [ Autosave, Paragraph ]
  123. } )
  124. .then( _editor => {
  125. const data = '<p>paragraph1</p><p>paragraph2</p>';
  126. editor = _editor;
  127. editor.setData( data );
  128. autosave = editor.plugins.get( Autosave );
  129. // Clean autosave's state after setting data.
  130. autosave._flush();
  131. } );
  132. } );
  133. afterEach( () => {
  134. document.body.removeChild( element );
  135. return editor.destroy();
  136. } );
  137. it( 'should run adapter\'s save method when the editor\'s change event is fired', () => {
  138. autosave.adapter = {
  139. save: sinon.spy()
  140. };
  141. editor.model.change( writer => {
  142. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  143. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  144. } );
  145. return wait().then( () => {
  146. sinon.assert.calledOnce( autosave.adapter.save );
  147. } );
  148. } );
  149. it( 'should throttle editor\'s change event', () => {
  150. const spy = sinon.spy();
  151. const savedStates = [];
  152. autosave.adapter = {
  153. save() {
  154. spy();
  155. savedStates.push( editor.getData() );
  156. }
  157. };
  158. // Leading (will fire change).
  159. editor.model.change( writer => {
  160. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  161. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  162. } );
  163. return wait().then( () => {
  164. // Throttled (won't fire change).
  165. editor.model.change( writer => {
  166. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  167. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  168. } );
  169. return wait();
  170. } ).then( () => {
  171. // Flushed (will fire change).
  172. editor.model.change( writer => {
  173. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  174. editor.model.insertContent( new ModelText( 'biz' ), editor.model.document.selection );
  175. } );
  176. autosave._flush();
  177. return wait();
  178. } ).then( () => {
  179. expect( spy.callCount ).to.equal( 2 );
  180. expect( savedStates ).to.deep.equal( [
  181. '<p>paragraph1</p><p>foo</p>',
  182. '<p>paragraph1</p><p>biz</p>'
  183. ] );
  184. } );
  185. } );
  186. it( 'should add a pending action during the saving.', () => {
  187. sandbox.useFakeTimers();
  188. const pendingActions = editor.plugins.get( PendingActions );
  189. const serverActionSpy = sinon.spy();
  190. const serverActionStub = sinon.stub();
  191. serverActionStub.onCall( 0 ).resolves( wait( 1000 ).then( serverActionSpy ) );
  192. autosave.adapter = {
  193. save: serverActionStub
  194. };
  195. editor.model.change( writer => {
  196. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  197. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  198. } );
  199. sinon.assert.notCalled( serverActionSpy );
  200. expect( pendingActions.isPending ).to.be.true;
  201. expect( pendingActions.first.message ).to.equal( 'Saving changes' );
  202. sandbox.clock.tick( 1000 );
  203. return Promise.resolve().then( () => Promise.resolve() ).then( () => {
  204. sinon.assert.calledOnce( serverActionSpy );
  205. expect( pendingActions.isPending ).to.be.false;
  206. } );
  207. } );
  208. it( 'should add a pending action during the saving #2.', () => {
  209. const serverActionSpy = sinon.spy();
  210. const pendingActions = editor.plugins.get( PendingActions );
  211. autosave.adapter = {
  212. save: serverActionSpy
  213. };
  214. expect( pendingActions.isPending ).to.be.false;
  215. editor.model.change( writer => {
  216. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  217. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  218. } );
  219. expect( pendingActions.isPending ).to.be.true;
  220. // Server action needs to wait at least a cycle.
  221. return wait().then( () => {
  222. sinon.assert.calledOnce( serverActionSpy );
  223. expect( pendingActions.isPending ).to.be.false;
  224. } );
  225. } );
  226. it( 'should handle correctly throttled save action and preserve pending action until both save actions finish', () => {
  227. sandbox.useFakeTimers();
  228. const serverActionSpy = sinon.spy();
  229. const pendingActions = editor.plugins.get( PendingActions );
  230. // Create a fake server that responses after 1000ms for the first call and after 1000ms for the second call.
  231. const serverActionStub = sinon.stub();
  232. serverActionStub.onCall( 0 ).resolves( wait( 1000 ).then( serverActionSpy ) );
  233. serverActionStub.onCall( 1 ).resolves( wait( 2000 ).then( serverActionSpy ) );
  234. autosave.adapter = {
  235. save: serverActionStub
  236. };
  237. expect( pendingActions.isPending ).to.be.false;
  238. editor.model.change( writer => {
  239. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  240. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  241. } );
  242. expect( pendingActions.isPending ).to.be.true;
  243. editor.model.change( writer => {
  244. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  245. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  246. } );
  247. autosave._flush();
  248. expect( pendingActions.isPending ).to.be.true;
  249. sandbox.clock.tick( 1000 );
  250. return Promise.resolve().then( () => {
  251. expect( pendingActions.isPending ).to.be.true;
  252. sinon.assert.calledOnce( serverActionSpy );
  253. // Wait another 1000ms and a promise cycle for the second server action.
  254. sandbox.clock.tick( 1000 );
  255. } )
  256. .then( () => Promise.resolve() )
  257. .then( () => {
  258. expect( pendingActions.isPending ).to.be.false;
  259. sinon.assert.calledTwice( serverActionSpy );
  260. } );
  261. } );
  262. it( 'should handle correctly throttled save action and preserve pending action until both save actions finish #2', () => {
  263. const serverActionSpy = sinon.spy();
  264. const pendingActions = editor.plugins.get( PendingActions );
  265. autosave.adapter = {
  266. save: serverActionSpy
  267. };
  268. expect( pendingActions.isPending ).to.be.false;
  269. editor.model.change( writer => {
  270. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  271. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  272. } );
  273. expect( pendingActions.isPending ).to.be.true;
  274. editor.model.change( writer => {
  275. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  276. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  277. } );
  278. expect( pendingActions.isPending ).to.be.true;
  279. // Server action needs to wait at least a cycle.
  280. return wait().then( () => {
  281. sinon.assert.calledOnce( serverActionSpy );
  282. expect( pendingActions.isPending ).to.be.true;
  283. autosave._flush();
  284. // Wait another promise cycle.
  285. return wait().then( () => {
  286. sinon.assert.calledTwice( serverActionSpy );
  287. expect( pendingActions.isPending ).to.be.false;
  288. } );
  289. } );
  290. } );
  291. it( 'should filter out changes in the selection', () => {
  292. autosave.adapter = {
  293. save: sandbox.spy()
  294. };
  295. editor.model.change( writer => {
  296. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  297. } );
  298. autosave._flush();
  299. sinon.assert.notCalled( autosave.adapter.save );
  300. } );
  301. it( 'should filter out markers that does not affect the data model', () => {
  302. autosave.adapter = {
  303. save: sandbox.spy()
  304. };
  305. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  306. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  307. editor.model.change( writer => {
  308. writer.addMarker( 'name', { usingOperation: true, range } );
  309. } );
  310. autosave._flush();
  311. editor.model.change( writer => {
  312. writer.updateMarker( 'name', { range: range2 } );
  313. } );
  314. autosave._flush();
  315. sinon.assert.notCalled( autosave.adapter.save );
  316. } );
  317. it( 'should filter out markers that does not affect the data model #2', () => {
  318. autosave.adapter = {
  319. save: sandbox.spy()
  320. };
  321. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  322. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  323. editor.model.change( writer => {
  324. writer.addMarker( 'name', { usingOperation: false, range } );
  325. } );
  326. autosave._flush();
  327. editor.model.change( writer => {
  328. writer.updateMarker( 'name', { range: range2 } );
  329. } );
  330. autosave._flush();
  331. sinon.assert.notCalled( autosave.adapter.save );
  332. } );
  333. it( 'should call the save method when some marker affects the data model', () => {
  334. autosave.adapter = {
  335. save: sandbox.spy()
  336. };
  337. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  338. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  339. editor.model.change( writer => {
  340. writer.addMarker( 'name', { usingOperation: true, affectsData: true, range } );
  341. } );
  342. autosave._flush();
  343. return wait().then( () => {
  344. editor.model.change( writer => {
  345. writer.updateMarker( 'name', { range: range2 } );
  346. } );
  347. autosave._flush();
  348. return wait().then( () => {
  349. sinon.assert.calledTwice( autosave.adapter.save );
  350. } );
  351. } );
  352. } );
  353. it( 'should call the save method when some marker affects the data model #2', () => {
  354. autosave.adapter = {
  355. save: sandbox.spy()
  356. };
  357. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  358. const range2 = ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) );
  359. editor.model.change( writer => {
  360. writer.addMarker( 'name', { usingOperation: false, affectsData: true, range } );
  361. } );
  362. autosave._flush();
  363. return wait().then( () => {
  364. sinon.assert.calledOnce( autosave.adapter.save );
  365. editor.model.change( writer => {
  366. writer.updateMarker( 'name', { range: range2 } );
  367. } );
  368. autosave._flush();
  369. return wait().then( () => {
  370. sinon.assert.calledTwice( autosave.adapter.save );
  371. } );
  372. } );
  373. } );
  374. it( 'should call the save method when some marker affects the data model #3', () => {
  375. autosave.adapter = {
  376. save: sandbox.spy()
  377. };
  378. const range = ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) );
  379. editor.model.change( writer => {
  380. writer.addMarker( 'marker-not-affecting-data', { usingOperation: false, affectsData: true, range } );
  381. writer.addMarker( 'marker-affecting-data', { usingOperation: false, affectsData: false, range } );
  382. } );
  383. return wait().then( () => {
  384. sinon.assert.calledOnce( autosave.adapter.save );
  385. } );
  386. } );
  387. it( 'should flush remaining calls after editor\'s destroy', () => {
  388. const spy = sandbox.spy();
  389. const savedStates = [];
  390. autosave.adapter = {
  391. save() {
  392. spy();
  393. savedStates.push( editor.getData() );
  394. }
  395. };
  396. editor.model.change( writer => {
  397. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  398. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  399. } );
  400. return wait().then( () => {
  401. editor.model.change( writer => {
  402. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  403. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  404. } );
  405. return editor.destroy().then( () => {
  406. expect( spy.callCount ).to.equal( 2 );
  407. expect( savedStates ).to.deep.equal( [
  408. '<p>foo</p><p>paragraph2</p>',
  409. '<p>foo</p><p>bar</p>',
  410. ] );
  411. } );
  412. } );
  413. } );
  414. it( 'should work after editor\'s destroy with long server\'s response time', () => {
  415. sandbox.useFakeTimers();
  416. const pendingActions = editor.plugins.get( PendingActions );
  417. const serverActionSpy = sinon.spy();
  418. const serverActionStub = sinon.stub();
  419. serverActionStub.onCall( 0 ).resolves( wait( 1000 ).then( serverActionSpy ) );
  420. autosave.adapter = {
  421. save: serverActionStub
  422. };
  423. editor.model.change( writer => {
  424. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  425. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  426. } );
  427. return editor.destroy()
  428. .then( () => {
  429. expect( pendingActions.isPending ).to.be.true;
  430. sandbox.clock.tick( 1000 );
  431. } )
  432. .then( () => Promise.resolve() )
  433. .then( () => {
  434. expect( pendingActions.isPending ).to.be.false;
  435. sinon.assert.calledOnce( serverActionSpy );
  436. sinon.assert.calledOnce( serverActionStub );
  437. } );
  438. } );
  439. } );
  440. it( 'should wait on editor initialization', () => {
  441. element = document.createElement( 'div' );
  442. document.body.appendChild( element );
  443. editor = null;
  444. class AsyncPlugin {
  445. constructor( editor ) {
  446. this.editor = editor;
  447. }
  448. init() {
  449. this.editor.once( 'ready', () => {
  450. const editor = this.editor;
  451. editor.model.change( writer => {
  452. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  453. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  454. } );
  455. } );
  456. return Promise.resolve().then( () => Promise.resolve() );
  457. }
  458. }
  459. return ClassicTestEditor
  460. .create( element, {
  461. plugins: [ Autosave, Paragraph, AsyncPlugin ],
  462. autosave: {
  463. save: sinon.spy( () => {
  464. expect( editor ).to.not.be.null;
  465. } )
  466. }
  467. } )
  468. .then( _editor => {
  469. editor = _editor;
  470. autosave = editor.plugins.get( Autosave );
  471. const spy = editor.config.get( 'autosave' ).save;
  472. expect( editor.getData() ).to.equal( '<p>bar</p>' );
  473. sinon.assert.calledOnce( spy );
  474. } )
  475. .then( () => {
  476. document.body.removeChild( element );
  477. return editor.destroy();
  478. } );
  479. } );
  480. } );
  481. function wait( time ) {
  482. return new Promise( res => {
  483. window.setTimeout( res, time );
  484. } );
  485. }