8
0

autosave.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /**
  2. * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved.
  3. */
  4. /* globals document, window */
  5. import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
  6. import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
  7. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  8. import Autosave from '../src/autosave';
  9. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  10. import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
  11. describe( 'Autosave', () => {
  12. const sandbox = sinon.sandbox.create();
  13. let editor, element, autosave;
  14. beforeEach( () => {
  15. element = document.createElement( 'div' );
  16. document.body.appendChild( element );
  17. return ClassicTestEditor
  18. .create( element, {
  19. plugins: [ Autosave, Paragraph ]
  20. } )
  21. .then( _editor => {
  22. const data = '<p>paragraph1</p><p>paragraph2</p>';
  23. editor = _editor;
  24. editor.setData( data );
  25. autosave = editor.plugins.get( Autosave );
  26. // Clean autosave's state after setting data.
  27. autosave._throttledSave.cancel();
  28. } );
  29. } );
  30. afterEach( () => {
  31. document.body.removeChild( element );
  32. sandbox.restore();
  33. return editor.destroy();
  34. } );
  35. it( 'should have static pluginName property', () => {
  36. expect( Autosave.pluginName ).to.equal( 'Autosave' );
  37. } );
  38. describe( 'initialization', () => {
  39. it( 'should initialize provider with an undefined value', () => {
  40. expect( autosave.provider ).to.be.undefined;
  41. } );
  42. it( 'should allow plugin to work without any defined provider', () => {
  43. editor.model.change( writer => {
  44. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  45. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  46. } );
  47. // Go to the next cycle to because synchronization of CS documentVersion is async.
  48. return Promise.resolve().then( () => {
  49. autosave._flush();
  50. } );
  51. } );
  52. } );
  53. describe( 'autosaving', () => {
  54. it( 'should run provider\'s save method when the editor\'s change event is fired', () => {
  55. autosave.provider = {
  56. save: sinon.spy()
  57. };
  58. editor.model.change( writer => {
  59. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  60. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  61. } );
  62. // Go to the next cycle to because synchronization of CS documentVersion is async.
  63. return Promise.resolve()
  64. .then( () => autosave._flush() )
  65. .then( () => {
  66. sinon.assert.calledOnce( autosave.provider.save );
  67. } );
  68. } );
  69. it( 'should throttle editor\'s change event', () => {
  70. const spy = sinon.spy();
  71. const savedStates = [];
  72. autosave.provider = {
  73. save() {
  74. spy();
  75. savedStates.push( editor.getData() );
  76. }
  77. };
  78. // Leading (will fire change).
  79. const change1 = () => {
  80. editor.model.change( writer => {
  81. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  82. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  83. } );
  84. };
  85. const change2 = () => {
  86. // Throttled (won't fire change).
  87. editor.model.change( writer => {
  88. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  89. editor.model.insertContent( new ModelText( 'bar' ), editor.model.document.selection );
  90. } );
  91. };
  92. const change3 = () => {
  93. // Flushed (will fire change).
  94. editor.model.change( writer => {
  95. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 1 ) ) );
  96. editor.model.insertContent( new ModelText( 'biz' ), editor.model.document.selection );
  97. } );
  98. };
  99. // Provider.save is done asynchronously, so all changes are run in different cycles.
  100. return Promise.resolve()
  101. .then( () => change1() )
  102. .then( () => change2() )
  103. .then( () => change3() )
  104. .then( () => autosave._flush() )
  105. .then( () => {
  106. expect( spy.callCount ).to.equal( 2 );
  107. expect( savedStates ).to.deep.equal( [
  108. '<p>paragraph1</p><p>foo</p>',
  109. '<p>paragraph1</p><p>biz</p>'
  110. ] );
  111. } );
  112. } );
  113. it( 'should add a pending action during the saving.', () => {
  114. const wait5msSpy = sinon.spy();
  115. const pendingActions = editor.plugins.get( PendingActions );
  116. autosave.provider = {
  117. save() {
  118. return wait5ms().then( wait5ms );
  119. }
  120. };
  121. editor.model.change( writer => {
  122. writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
  123. editor.model.insertContent( new ModelText( 'foo' ), editor.model.document.selection );
  124. } );
  125. // Go to the next cycle to because synchronization of CS documentVersion is async.
  126. return Promise.resolve()
  127. .then( () => autosave._flush() )
  128. .then( () => {
  129. sinon.assert.notCalled( wait5msSpy );
  130. expect( pendingActions.isPending ).to.be.true;
  131. expect( pendingActions.first.message ).to.equal( 'Saving in progress.' );
  132. } )
  133. .then( wait5ms )
  134. .then( () => {
  135. } );
  136. } );
  137. } );
  138. function wait5ms() {
  139. return new Promise( res => {
  140. window.setTimeout( res, 5 );
  141. } );
  142. }
  143. } );