8
0

autosave.js 20 KB

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