imageuploadprogress.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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 window */
  6. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  7. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  8. import ImageEditing from '../../src/image/imageediting';
  9. import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
  10. import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';
  11. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  12. import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
  13. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  14. import { createNativeFileMock, NativeFileReaderMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
  15. import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  16. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  17. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  18. import svgPlaceholder from '../../theme/icons/image_placeholder.svg';
  19. describe( 'ImageUploadProgress', () => {
  20. const imagePlaceholder = encodeURIComponent( svgPlaceholder );
  21. // eslint-disable-next-line max-len
  22. const base64Sample = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
  23. let editor, model, doc, fileRepository, view, nativeReaderMock, loader, adapterMock;
  24. class UploadAdapterPluginMock extends Plugin {
  25. init() {
  26. fileRepository = this.editor.plugins.get( FileRepository );
  27. fileRepository.createUploadAdapter = newLoader => {
  28. loader = newLoader;
  29. adapterMock = new UploadAdapterMock( loader );
  30. return adapterMock;
  31. };
  32. }
  33. }
  34. testUtils.createSinonSandbox();
  35. beforeEach( () => {
  36. testUtils.sinon.stub( window, 'FileReader' ).callsFake( () => {
  37. nativeReaderMock = new NativeFileReaderMock();
  38. return nativeReaderMock;
  39. } );
  40. return VirtualTestEditor
  41. .create( {
  42. plugins: [ ImageEditing, Paragraph, ImageUploadEditing, ImageUploadProgress, UploadAdapterPluginMock, Clipboard ]
  43. } )
  44. .then( newEditor => {
  45. editor = newEditor;
  46. model = editor.model;
  47. doc = model.document;
  48. view = editor.editing.view;
  49. fileRepository = editor.plugins.get( FileRepository );
  50. fileRepository.createUploadAdapter = newLoader => {
  51. loader = newLoader;
  52. adapterMock = new UploadAdapterMock( loader );
  53. return adapterMock;
  54. };
  55. } );
  56. } );
  57. it( 'should convert image\'s "reading" uploadStatus attribute', () => {
  58. setModelData( model, '<paragraph>[]foo</paragraph>' );
  59. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  60. expect( getViewData( view ) ).to.equal(
  61. '[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
  62. `<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
  63. '<div class="ck-upload-placeholder-loader"></div>' +
  64. '</figure>]<p>foo</p>'
  65. );
  66. } );
  67. it( 'should convert image\'s "uploading" uploadStatus attribute', done => {
  68. setModelData( model, '<paragraph>[]foo</paragraph>' );
  69. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  70. model.document.once( 'change', () => {
  71. try {
  72. expect( getViewData( view ) ).to.equal(
  73. '[<figure class="ck-appear ck-widget image" contenteditable="false">' +
  74. `<img src="${ base64Sample }"></img>` +
  75. '<div class="ck-progress-bar"></div>' +
  76. '</figure>]<p>foo</p>'
  77. );
  78. done();
  79. } catch ( err ) {
  80. done( err );
  81. }
  82. }, { priority: 'lowest' } );
  83. loader.file.then( () => nativeReaderMock.mockSuccess( base64Sample ) );
  84. } );
  85. // See https://github.com/ckeditor/ckeditor5/issues/1985.
  86. // Might be obsolete after changes in table refreshing (now it refreshes siblings of an image and not its parent).
  87. it( 'should work if image parent is refreshed by the differ', function( done ) {
  88. model.schema.register( 'outerBlock', {
  89. allowWhere: '$block',
  90. isBlock: true
  91. } );
  92. model.schema.register( 'innerBlock', {
  93. allowIn: 'outerBlock',
  94. isLimit: true
  95. } );
  96. model.schema.extend( '$block', { allowIn: 'innerBlock' } );
  97. editor.conversion.elementToElement( { model: 'outerBlock', view: 'outerBlock' } );
  98. editor.conversion.elementToElement( { model: 'innerBlock', view: 'innerBlock' } );
  99. model.document.registerPostFixer( () => {
  100. for ( const change of doc.differ.getChanges() ) {
  101. // The differ.refreshItem() simulates remove and insert of and image parent thus preventing image from proper work.
  102. if ( change.type == 'insert' && change.name == 'image' ) {
  103. doc.differ.refreshItem( change.position.parent );
  104. return false; // Refreshing item should not trigger calling post-fixer again.
  105. }
  106. }
  107. } );
  108. setModelData( model, '<outerBlock><innerBlock><paragraph>[]</paragraph></innerBlock></outerBlock>' );
  109. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  110. model.document.once( 'change', () => {
  111. try {
  112. expect( getViewData( view ) ).to.equal(
  113. '<outerBlock>' +
  114. '<innerBlock>' +
  115. '[<figure class="ck-appear ck-widget image" contenteditable="false">' +
  116. `<img src="${ base64Sample }"></img>` +
  117. '<div class="ck-progress-bar"></div>' +
  118. '</figure>]' +
  119. '</innerBlock>' +
  120. '</outerBlock>'
  121. );
  122. done();
  123. } catch ( err ) {
  124. done( err );
  125. }
  126. }, { priority: 'lowest' } );
  127. loader.file.then( () => nativeReaderMock.mockSuccess( base64Sample ) );
  128. } );
  129. it( 'should work correctly when there is no "reading" status and go straight to "uploading"', () => {
  130. const fileRepository = editor.plugins.get( FileRepository );
  131. const file = createNativeFileMock();
  132. const loader = fileRepository.createLoader( file );
  133. setModelData( model, '<image></image>' );
  134. const image = doc.getRoot().getChild( 0 );
  135. // Set attributes directly on image to simulate instant "uploading" status.
  136. model.change( writer => {
  137. writer.setAttribute( 'uploadStatus', 'uploading', image );
  138. writer.setAttribute( 'uploadId', loader.id, image );
  139. writer.setAttribute( 'src', 'image.png', image );
  140. } );
  141. expect( getViewData( view ) ).to.equal(
  142. '[<figure class="ck-appear ck-widget image" contenteditable="false">' +
  143. '<img src="image.png"></img>' +
  144. '<div class="ck-progress-bar"></div>' +
  145. '</figure>]'
  146. );
  147. } );
  148. it( 'should work correctly when there is no "reading" status and go straight to "uploading" - external changes', () => {
  149. setModelData( model, '<image></image>' );
  150. const image = doc.getRoot().getChild( 0 );
  151. // Set attributes directly on image to simulate instant "uploading" status.
  152. model.change( writer => {
  153. writer.setAttribute( 'uploadStatus', 'uploading', image );
  154. writer.setAttribute( 'uploadId', '12345', image );
  155. } );
  156. expect( getViewData( view ) ).to.equal(
  157. '[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
  158. `<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
  159. '<div class="ck-upload-placeholder-loader"></div>' +
  160. '</figure>]'
  161. );
  162. } );
  163. it( 'should "clear" image when uploadId changes to null', () => {
  164. setModelData( model, '<image></image>' );
  165. const image = doc.getRoot().getChild( 0 );
  166. // Set attributes directly on image to simulate instant "uploading" status.
  167. model.change( writer => {
  168. writer.setAttribute( 'uploadStatus', 'uploading', image );
  169. writer.setAttribute( 'uploadId', '12345', image );
  170. } );
  171. model.change( writer => {
  172. writer.setAttribute( 'uploadStatus', null, image );
  173. writer.setAttribute( 'uploadId', null, image );
  174. } );
  175. expect( getViewData( view ) ).to.equal(
  176. '[<figure class="ck-widget image" contenteditable="false">' +
  177. `<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
  178. '</figure>]'
  179. );
  180. } );
  181. it( 'should update progressbar width on progress', done => {
  182. setModelData( model, '<paragraph>[]foo</paragraph>' );
  183. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  184. model.document.once( 'change', () => {
  185. adapterMock.mockProgress( 40, 100 );
  186. try {
  187. expect( getViewData( view ) ).to.equal(
  188. '[<figure class="ck-appear ck-widget image" contenteditable="false">' +
  189. `<img src="${ base64Sample }"></img>` +
  190. '<div class="ck-progress-bar" style="width:40%"></div>' +
  191. '</figure>]<p>foo</p>'
  192. );
  193. done();
  194. } catch ( err ) {
  195. done( err );
  196. }
  197. }, { priority: 'lowest' } );
  198. loader.file.then( () => nativeReaderMock.mockSuccess( base64Sample ) );
  199. } );
  200. it( 'should convert image\'s "complete" uploadStatus attribute and display temporary icon', done => {
  201. const clock = testUtils.sinon.useFakeTimers();
  202. setModelData( model, '<paragraph>[]foo</paragraph>' );
  203. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  204. model.document.once( 'change', () => {
  205. model.document.once( 'change', () => {
  206. try {
  207. expect( getViewData( view ) ).to.equal(
  208. '[<figure class="ck-widget image" contenteditable="false">' +
  209. '<img src="image.png"></img>' +
  210. '<div class="ck-image-upload-complete-icon"></div>' +
  211. '</figure>]<p>foo</p>'
  212. );
  213. clock.tick( 3000 );
  214. expect( getViewData( view ) ).to.equal(
  215. '[<figure class="ck-widget image" contenteditable="false">' +
  216. '<img src="image.png"></img>' +
  217. '</figure>]<p>foo</p>'
  218. );
  219. done();
  220. } catch ( err ) {
  221. done( err );
  222. }
  223. }, { priority: 'lowest' } );
  224. loader.file.then( () => adapterMock.mockSuccess( { default: 'image.png' } ) );
  225. } );
  226. loader.file.then( () => nativeReaderMock.mockSuccess( base64Sample ) );
  227. } );
  228. it( 'should allow to customize placeholder image', () => {
  229. const uploadProgress = editor.plugins.get( ImageUploadProgress );
  230. uploadProgress.placeholder = base64Sample;
  231. setModelData( model, '<paragraph>[]foo</paragraph>' );
  232. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  233. expect( getViewData( view ) ).to.equal(
  234. '[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
  235. `<img src="${ base64Sample }"></img>` +
  236. '<div class="ck-upload-placeholder-loader"></div>' +
  237. '</figure>]<p>foo</p>'
  238. );
  239. } );
  240. it( 'should not process attribute change if it is already consumed', () => {
  241. editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:image', ( evt, data, conversionApi ) => {
  242. conversionApi.consumable.consume( data.item, evt.name );
  243. }, { priority: 'highest' } );
  244. setModelData( model, '<paragraph>[]foo</paragraph>' );
  245. editor.execute( 'imageUpload', { file: createNativeFileMock() } );
  246. expect( getViewData( view ) ).to.equal(
  247. '[<figure class="ck-widget image" contenteditable="false"><img></img></figure>]<p>foo</p>'
  248. );
  249. } );
  250. it( 'should not show progress bar and complete icon if there is no loader with given uploadId', () => {
  251. setModelData( model, '<image uploadId="123" uploadStatus="reading"></image>' );
  252. const image = doc.getRoot().getChild( 0 );
  253. model.change( writer => {
  254. writer.setAttribute( 'uploadStatus', 'uploading', image );
  255. } );
  256. expect( getViewData( view ) ).to.equal(
  257. '[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
  258. `<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
  259. '<div class="ck-upload-placeholder-loader"></div>' +
  260. '</figure>]'
  261. );
  262. model.change( writer => {
  263. writer.setAttribute( 'uploadStatus', 'complete', image );
  264. } );
  265. expect( getViewData( view ) ).to.equal(
  266. '[<figure class="ck-widget image" contenteditable="false">' +
  267. `<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
  268. '</figure>]'
  269. );
  270. } );
  271. } );