Parcourir la source

Merge branch 'master' into i/6091

Piotrek Koszuliński il y a 5 ans
Parent
commit
ba30283e85

+ 4 - 3
packages/ckeditor5-image/src/image/converters.js

@@ -8,6 +8,7 @@
  */
 
 import first from '@ckeditor/ckeditor5-utils/src/first';
+import { getViewImgFromWidget } from './utils';
 
 /**
  * Returns a function that converts the image view representation:
@@ -35,7 +36,7 @@ export function viewFigureToModel() {
 		}
 
 		// Find an image element inside the figure element.
-		const viewImage = Array.from( data.viewItem.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
+		const viewImage = getViewImgFromWidget( data.viewItem );
 
 		// Do not convert if image element is absent, is missing src attribute or was already converted.
 		if ( !viewImage || !viewImage.hasAttribute( 'src' ) || !conversionApi.consumable.test( viewImage, { name: true } ) ) {
@@ -81,7 +82,7 @@ export function srcsetAttributeConverter() {
 
 		const writer = conversionApi.writer;
 		const figure = conversionApi.mapper.toViewElement( data.item );
-		const img = figure.getChild( 0 );
+		const img = getViewImgFromWidget( figure );
 
 		if ( data.attributeNewValue === null ) {
 			const srcset = data.attributeOldValue;
@@ -122,7 +123,7 @@ export function modelToViewAttributeConverter( attributeKey ) {
 
 		const viewWriter = conversionApi.writer;
 		const figure = conversionApi.mapper.toViewElement( data.item );
-		const img = figure.getChild( 0 );
+		const img = getViewImgFromWidget( figure );
 
 		if ( data.attributeNewValue !== null ) {
 			viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, img );

+ 14 - 1
packages/ckeditor5-image/src/image/utils.js

@@ -25,7 +25,7 @@ export function toImageWidget( viewElement, writer, label ) {
 	return toWidget( viewElement, writer, { label: labelCreator } );
 
 	function labelCreator() {
-		const imgElement = viewElement.getChild( 0 );
+		const imgElement = getViewImgFromWidget( viewElement );
 		const altText = imgElement.getAttribute( 'alt' );
 
 		return altText ? `${ altText } ${ label }` : label;
@@ -107,6 +107,19 @@ export function isImageAllowed( model ) {
 		isInOtherImage( selection );
 }
 
+/**
+ * Get view `<img>` element from the view widget (`<figure>`).
+ *
+ * Assuming that image is always a first child of a widget (ie. `figureView.getChild( 0 )`) is unsafe as other features might
+ * inject their own elements to the widget.
+ *
+ * @param {module:engine/view/element~Element} figureView
+ * @returns {module:engine/view/element~Element}
+ */
+export function getViewImgFromWidget( figureView ) {
+	return Array.from( figureView.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
+}
+
 // Checks if image is allowed by schema in optimal insertion parent.
 //
 // @returns {Boolean}

+ 2 - 1
packages/ckeditor5-image/src/imageupload/imageuploadediting.js

@@ -17,6 +17,7 @@ import env from '@ckeditor/ckeditor5-utils/src/env';
 import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import { fetchLocalImage, isLocalImage } from '../../src/imageupload/utils';
 import { createImageTypeRegExp } from './utils';
+import { getViewImgFromWidget } from '../image/utils';
 
 /**
  * The editing part of the image upload feature. It registers the `'imageUpload'` command.
@@ -217,7 +218,7 @@ export default class ImageUploadEditing extends Plugin {
 				/* istanbul ignore next */
 				if ( env.isSafari ) {
 					const viewFigure = editor.editing.mapper.toViewElement( imageElement );
-					const viewImg = viewFigure.getChild( 0 );
+					const viewImg = getViewImgFromWidget( viewFigure );
 
 					editor.editing.view.once( 'render', () => {
 						// Early returns just to be safe. There might be some code ran

+ 3 - 2
packages/ckeditor5-image/src/imageupload/imageuploadprogress.js

@@ -13,6 +13,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import uploadingPlaceholder from '../../theme/icons/image_placeholder.svg';
 import env from '@ckeditor/ckeditor5-utils/src/env';
+import { getViewImgFromWidget } from '../image/utils';
 
 import '../../theme/imageuploadprogress.css';
 import '../../theme/imageuploadicon.css';
@@ -143,7 +144,7 @@ function _showPlaceholder( placeholder, viewFigure, writer ) {
 		writer.addClass( 'ck-image-upload-placeholder', viewFigure );
 	}
 
-	const viewImg = viewFigure.getChild( 0 );
+	const viewImg = getViewImgFromWidget( viewFigure );
 
 	if ( viewImg.getAttribute( 'src' ) !== placeholder ) {
 		writer.setAttribute( 'src', placeholder, viewImg );
@@ -270,7 +271,7 @@ function _removeUIElement( viewFigure, writer, uniqueProperty ) {
 // @param {module:upload/filerepository~FileLoader} loader
 function _displayLocalImage( viewFigure, writer, loader ) {
 	if ( loader.data ) {
-		const viewImg = viewFigure.getChild( 0 );
+		const viewImg = getViewImgFromWidget( viewFigure );
 
 		writer.setAttribute( 'src', loader.data, viewImg );
 	}

+ 18 - 0
packages/ckeditor5-image/tests/image/converters.js

@@ -259,5 +259,23 @@ describe( 'Image converters', () => {
 				'<figure class="ck-widget image" contenteditable="false"><img src=""></img></figure>'
 			);
 		} );
+
+		it( 'should set attribute on <img> even if other element is present inside figure', () => {
+			editor.model.schema.register( 'foo', {
+				allowIn: 'image'
+			} );
+			editor.conversion.for( 'downcast' ).elementToElement( { model: 'foo', view: 'foo' } );
+
+			setModelData( model, '<image src=""><foo></foo></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'alt', 'foo bar', image );
+			} );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><foo></foo><img alt="foo bar" src=""></img></figure>'
+			);
+		} );
 	} );
 } );

+ 16 - 0
packages/ckeditor5-image/tests/image/imageloadobserver.js

@@ -93,6 +93,22 @@ describe( 'ImageLoadObserver', () => {
 		sinon.assert.notCalled( spy );
 	} );
 
+	it( 'should not fire `loadImage` event for non-image elements', () => {
+		const spy = sinon.spy();
+
+		viewDocument.on( 'imageLoaded', spy );
+
+		setData( view, '<p>foo</p>' );
+
+		sinon.assert.notCalled( spy );
+
+		const img = domRoot.querySelector( 'p' );
+
+		img.dispatchEvent( new Event( 'load' ) );
+
+		sinon.assert.notCalled( spy );
+	} );
+
 	it( 'should do nothing with an image when changes are in the other parent', () => {
 		setData(
 			view,

+ 142 - 572
packages/ckeditor5-image/tests/imageresize.js

@@ -19,12 +19,19 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Table from '@ckeditor/ckeditor5-table/src/table';
 
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
-import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import {
+	focusEditor,
+	resizerMouseSimulator,
+	getWidgetDomParts,
+	getHandleCenterPoint
+} from '@ckeditor/ckeditor5-widget/tests/widgetresize/_utils/utils';
+
+import WidgetResize from '@ckeditor/ckeditor5-widget/src/widgetresize';
 
 describe( 'ImageResize', () => {
-	// Id of the left mouse button.
-	const MOUSE_BUTTON_MAIN = 0;
-	// 60x50 black png image
+	// 100x50 black png image
 	const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
 		'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
 
@@ -32,7 +39,7 @@ describe( 'ImageResize', () => {
 
 	before( () => {
 		// This container is required to position editor element in a reliable element.
-		// See fireMouseEvent method for more information regarding imprecise mouse position.
+		// @todo ensure whether it's required after migrating the tests.
 		absoluteContainer = document.createElement( 'div' );
 		absoluteContainer.style.top = '50px';
 		absoluteContainer.style.left = '50px';
@@ -84,6 +91,31 @@ describe( 'ImageResize', () => {
 			expect( editor.getData() )
 				.to.equal( `<figure class="image image_resized" style="width:50%;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
 		} );
+
+		it( 'removes style and extra class when no longer resized', () => {
+			setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="50%"></image>` );
+
+			const imageModel = editor.model.document.getRoot().getChild( 0 );
+
+			editor.model.change( writer => {
+				writer.removeAttribute( 'width', imageModel );
+			} );
+
+			expect( editor.getData() )
+				.to.equal( `<figure class="image"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+		} );
+
+		it( 'doesn\'t downcast consumed tokens', () => {
+			editor.conversion.for( 'downcast' ).add( dispatcher =>
+				dispatcher.on( 'attribute:width:image', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.item, 'attribute:width:image' );
+				}, { priority: 'high' } )
+			);
+			setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="50%"></image>` );
+
+			expect( editor.getData() )
+				.to.equal( `<figure class="image"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+		} );
 	} );
 
 	describe( 'schema', () => {
@@ -106,28 +138,18 @@ describe( 'ImageResize', () => {
 		} );
 
 		it( 'uses the command on commit', async () => {
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-			widget = viewDocument.getRoot().getChild( 1 );
-
 			const spy = sinon.spy( editor.commands.get( 'imageResize' ), 'execute' );
 
-			await generateResizeTest( {
-				expectedWidth: 80,
-				pointerOffset: {
-					x: 10,
-					y: -10
-				},
-				resizerPosition: 'bottom-left'
-			} )();
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			widget = viewDocument.getRoot().getChild( 1 );
+			const domParts = getWidgetDomParts( editor, widget, 'bottom-left' );
 
-			// It's either 80px or 81px depending on the device, so we need to make the test a bit more loose.
-			const realWidth = editor.model.document.getRoot().getChild( 1 ).getAttribute( 'width' );
+			const finalPointerPosition = getHandleCenterPoint( domParts.widget, 'bottom-left' ).moveBy( 10, -10 );
 
-			expect( realWidth ).to.match( /^\d\dpx$/ );
+			resizerMouseSimulator.dragTo( editor, domParts.resizeHandle, finalPointerPosition );
 
 			expect( spy.calledOnce ).to.be.true;
-			expect( spy.args[ 0 ][ 0 ] ).to.deep.equal( { width: realWidth } );
+			expect( spy.args[ 0 ][ 0 ] ).to.deep.equal( { width: '80px' } );
 		} );
 
 		it( 'disables the resizer if the command is disabled', () => {
@@ -171,148 +193,18 @@ describe( 'ImageResize', () => {
 		} );
 	} );
 
-	describe( 'visual resizers', () => {
-		beforeEach( async () => {
-			await createEditor();
-
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-			widget = viewDocument.getRoot().getChild( 1 );
-		} );
-
-		it( 'correct number is added by default', () => {
-			const resizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
-
-			expect( resizers.length ).to.be.equal( 4 );
-		} );
-
-		describe( 'visibility', () => {
-			it( 'is hidden by default', () => {
-				const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
-
-				for ( const resizer of allResizers ) {
-					expect( isVisible( resizer ) ).to.be.false;
-				}
-			} );
-
-			it( 'is shown when image is focused', () => {
-				const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
-				const domEventDataMock = {
-					target: widget,
-					preventDefault: sinon.spy()
-				};
-
-				focusEditor( editor );
-
-				viewDocument.fire( 'mousedown', domEventDataMock );
-
-				for ( const resizer of allResizers ) {
-					expect( isVisible( resizer ) ).to.be.true;
-				}
-			} );
+	it( 'uses percents by default', async () => {
+		const localEditor = await createEditor( {
+			plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize ]
 		} );
-	} );
 
-	describe( 'standard image resizing', () => {
-		beforeEach( async () => {
-			await createEditor();
+		const attachToSpy = sinon.spy( localEditor.plugins.get( WidgetResize ), 'attachTo' );
 
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+		setData( localEditor.model, `[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
-			widget = viewDocument.getRoot().getChild( 1 );
-		} );
+		expect( attachToSpy.args[ 0 ][ 0 ] ).to.have.a.property( 'unit', '%' );
 
-		it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: 10,
-				y: -10
-			},
-			resizerPosition: 'bottom-left'
-		} ) );
-
-		it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: -10,
-				y: -10
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 10,
-				y: 0
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 0,
-				y: 10
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with left-bottom handler, x axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: -10,
-				y: 0
-			},
-			resizerPosition: 'bottom-left'
-		} ) );
-
-		it( 'enlarges correctly with left-bottom handler, y axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 0,
-				y: 10
-			},
-			resizerPosition: 'bottom-left'
-		} ) );
-
-		// --- top handlers ---
-
-		it( 'enlarges correctly with left-top handler', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: -10,
-				y: -10
-			},
-			resizerPosition: 'top-left'
-		} ) );
-
-		it( 'enlarges correctly with left-top handler, y axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 0,
-				y: -10
-			},
-			resizerPosition: 'top-left'
-		} ) );
-
-		it( 'enlarges correctly with right-top handler', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 10,
-				y: -10
-			},
-			resizerPosition: 'top-right'
-		} ) );
-
-		it( 'enlarges correctly with right-top handler, y axis only', generateResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 0,
-				y: -10
-			},
-			resizerPosition: 'top-right'
-		} ) );
+		attachToSpy.restore();
 	} );
 
 	describe( 'side image resizing', () => {
@@ -324,208 +216,39 @@ describe( 'ImageResize', () => {
 			widget = viewDocument.getRoot().getChild( 1 );
 		} );
 
-		it( 'shrinks correctly with left-bottom handler', generateSideResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: 20,
-				y: -10
-			},
-			resizerPosition: 'bottom-left'
-		} ) );
-
-		it( 'shrinks correctly with right-bottom handler', generateSideResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: -20,
-				y: -10
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'shrinks correctly with left-top handler', generateSideResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: 20,
-				y: 10
-			},
-			resizerPosition: 'top-left'
-		} ) );
-
-		it( 'shrinks correctly with right-top handler', generateSideResizeTest( {
-			expectedWidth: 80,
-			pointerOffset: {
-				x: -20,
-				y: 10
-			},
-			resizerPosition: 'top-right'
-		} ) );
-
-		it( 'enlarges correctly with left-bottom handler', generateSideResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: -10,
-				y: 10
-			},
-			resizerPosition: 'bottom-left'
-		} ) );
-
-		it( 'enlarges correctly with right-bottom handler', generateSideResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 10,
-				y: 10
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with right-bottom handler, y axis only', generateSideResizeTest( {
-			expectedWidth: 140,
-			pointerOffset: {
-				x: 0,
-				y: 20
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with right-bottom handler, x axis only', generateSideResizeTest( {
-			expectedWidth: 140,
-			pointerOffset: {
-				x: 40,
-				y: 0
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'enlarges correctly with left-top handler', generateSideResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: -20,
-				y: -10
-			},
-			resizerPosition: 'top-left'
-		} ) );
-
-		it( 'enlarges correctly with right-top handler', generateSideResizeTest( {
-			expectedWidth: 120,
-			pointerOffset: {
-				x: 20,
-				y: 10
-			},
-			resizerPosition: 'top-right'
-		} ) );
-
 		it( 'doesn\'t flicker at the beginning of the resize', async () => {
 			// (#5189)
 			const resizerPosition = 'bottom-left';
-			const domParts = getWidgetDomParts( widget, resizerPosition );
-			const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
+			const domParts = getWidgetDomParts( editor, widget, resizerPosition );
+			const initialPointerPosition = getHandleCenterPoint( domParts.widget, resizerPosition );
 			const resizeWrapperView = widget.getChild( 1 );
 
-			focusEditor( editor );
-			fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
+			resizerMouseSimulator.down( editor, domParts.resizeHandle );
 
 			await wait( 40 );
 
-			fireMouseEvent( domParts.resizeHandle, 'mousemove', initialPointerPosition );
+			resizerMouseSimulator.move( editor, domParts.resizeHandle, null, initialPointerPosition );
 
 			expect( resizeWrapperView.getStyle( 'width' ) ).to.be.equal( '100px' );
 
-			fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
+			resizerMouseSimulator.up( editor );
 		} );
 
 		it( 'makes no change when clicking the handle without drag', () => {
 			const resizerPosition = 'bottom-left';
 			const expectedWidth = 100;
-			const domParts = getWidgetDomParts( widget, resizerPosition );
-			const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
+			const domParts = getWidgetDomParts( editor, widget, resizerPosition );
 
-			focusEditor( editor );
-			fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
+			resizerMouseSimulator.down( editor, domParts.resizeHandle );
 
-			expect( getDomWidth( domParts.figure ), 'DOM width check' ).to.be.closeTo( expectedWidth, 2 );
+			expect( getDomWidth( domParts.widget ), 'DOM width check' ).to.be.closeTo( expectedWidth, 2 );
 
-			fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
+			resizerMouseSimulator.up( editor );
 
 			const modelItem = editor.model.document.getRoot().getChild( 1 );
 
 			expect( modelItem.getAttribute( 'width' ), 'model width attribute' ).to.be.undefined;
 		} );
-
-		function generateSideResizeTest( options ) {
-			return generateResizeTest( Object.assign( {
-				modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="([\d.]+)px"><\/image>/
-			}, options ) );
-		}
-	} );
-
-	describe( 'percent resizing', () => {
-		beforeEach( () => createEditor( {
-			plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize ]
-		} ) );
-
-		describe( 'standard image', () => {
-			beforeEach( () => {
-				setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-				widget = viewDocument.getRoot().getChild( 1 );
-			} );
-
-			it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
-				expectedWidth: 16,
-				modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
-				pointerOffset: {
-					x: 10,
-					y: -10
-				},
-				resizerPosition: 'bottom-left'
-			} ) );
-
-			it( 'enlarges correctly with right-bottom handler', generateResizeTest( {
-				expectedWidth: 22,
-				modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
-				pointerOffset: {
-					x: 0,
-					y: 5
-				},
-				resizerPosition: 'bottom-right'
-			} ) );
-
-			it( 'enlarges correctly an image with unsupported width unit', async () => {
-				setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }" width="50pt"></image>]` );
-
-				widget = viewDocument.getRoot().getChild( 1 );
-
-				await generateResizeTest( {
-					expectedWidth: 15,
-					modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
-					pointerOffset: {
-						x: 0,
-						y: 5
-					},
-					resizerPosition: 'bottom-right'
-				} )();
-			} );
-		} );
-
-		describe( 'side image', () => {
-			beforeEach( () => {
-				setData( editor.model, `<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-				view = editor.editing.view;
-				viewDocument = view.document;
-				widget = viewDocument.getRoot().getChild( 1 );
-			} );
-
-			it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
-				expectedWidth: 18,
-				modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
-				pointerOffset: {
-					x: 10,
-					y: -10
-				},
-				resizerPosition: 'bottom-left'
-			} ) );
-		} );
 	} );
 
 	describe( 'undo integration', () => {
@@ -538,18 +261,25 @@ describe( 'ImageResize', () => {
 		} );
 
 		it( 'has correct border size after undo', async () => {
-			await generateResizeTest( {
-				expectedWidth: 120,
-				pointerOffset: {
-					x: 0,
-					y: 10
-				},
-				resizerPosition: 'bottom-left'
-			} )();
+			const domParts = getWidgetDomParts( editor, widget, 'bottom-left' );
+			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-left' );
+			const finalPointerPosition = initialPosition.clone().moveBy( 0, 10 );
+			const plugin = editor.plugins.get( 'WidgetResize' );
+
+			resizerMouseSimulator.dragTo( editor, domParts.resizeHandle, {
+				from: initialPosition,
+				to: finalPointerPosition
+			} );
+
+			expect( '120px' ).to.be.equal( domParts.widget.style.width );
 
 			editor.commands.get( 'undo' ).execute();
 
-			await wait( 160 ); // ui#update event is throttled.
+			// Toggle _visibleResizer to force synchronous redraw. Otherwise you'd need to wait ~200ms for
+			// throttled redraw to take place, making tests slower.
+			const visibleResizer = plugin._visibleResizer;
+			plugin._visibleResizer = null;
+			plugin._visibleResizer = visibleResizer;
 
 			const resizerWrapper = document.querySelector( '.ck-widget__resizer' );
 			const shadowBoundingRect = resizerWrapper.getBoundingClientRect();
@@ -560,9 +290,9 @@ describe( 'ImageResize', () => {
 	} );
 
 	describe( 'table integration', () => {
-		beforeEach( async () => {
-			await createEditor();
+		beforeEach( () => createEditor() );
 
+		it( 'works when resizing in a table', () => {
 			setData( editor.model,
 				'<table>' +
 					`<tableRow><tableCell>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]</tableCell></tableRow>` +
@@ -570,35 +300,34 @@ describe( 'ImageResize', () => {
 			);
 
 			widget = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
-		} );
+			const model = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
 
-		it( 'works when resizing in a table', generateResizeTest( {
-			getModel: () => editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ),
-			expectedWidth: 60,
-			modelRegExp: /.+/,
-			pointerOffset: {
-				x: -40,
-				y: -20
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
+			const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
+			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-right' );
+			const finalPointerPosition = initialPosition.clone().moveBy( -40, -20 );
+
+			resizerMouseSimulator.dragTo( editor, domParts.resizeHandle, {
+				from: initialPosition,
+				to: finalPointerPosition
+			} );
+
+			expect( model.getAttribute( 'width' ) ).to.be.equal( '60px' );
+		} );
 	} );
 
 	describe( 'srcset integration', () => {
 		// The image is 96x96 pixels.
 		const imageBaseUrl = '/assets/sample.png';
-		const getModel = () => editor.model.document.getRoot().getChild( 0 );
+		let model;
 		let images = [];
 
-		before( () => {
-			return Promise.all( [
+		before( async () => {
+			images = await Promise.all( [
 				preloadImage( imageBaseUrl ),
 				preloadImage( imageBaseUrl + '?a' ),
 				preloadImage( imageBaseUrl + '?b' ),
 				preloadImage( imageBaseUrl + '?c' )
-			] ).then( loadedImages => {
-				images = loadedImages;
-			} );
+			] );
 		} );
 
 		after( () => {
@@ -621,37 +350,38 @@ describe( 'ImageResize', () => {
 			);
 
 			widget = viewDocument.getRoot().getChild( 0 );
+			model = editor.model.document.getRoot().getChild( 0 );
+		} );
+
+		it( 'works with images containing srcset', () => {
+			const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
+			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-right' );
+			const finalPointerPosition = initialPosition.clone().moveBy( -20, -20 );
+
+			resizerMouseSimulator.dragTo( editor, domParts.resizeHandle, {
+				from: initialPosition,
+				to: finalPointerPosition
+			} );
+
+			expect( model.getAttribute( 'width' ) ).to.be.equal( '76px' );
 		} );
 
-		it( 'works with images containing srcset', generateResizeTest( {
-			getModel,
-			expectedWidth: 76,
-			modelRegExp: /.+/,
-			pointerOffset: {
-				x: -20,
-				y: -20
-			},
-			resizerPosition: 'bottom-right'
-		} ) );
-
-		it( 'retains width after removing srcset', async () => {
-			await generateResizeTest( {
-				getModel,
-				expectedWidth: 80,
-				modelRegExp: /.+/,
-				pointerOffset: {
-					x: -16,
-					y: -16
-				},
-				resizerPosition: 'bottom-right'
-			} )();
+		it( 'retains width after removing srcset', () => {
+			const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
+			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-right' );
+			const finalPointerPosition = initialPosition.clone().moveBy( -16, -16 );
+
+			resizerMouseSimulator.dragTo( editor, domParts.resizeHandle, {
+				from: initialPosition,
+				to: finalPointerPosition
+			} );
 
 			editor.model.change( writer => {
-				writer.removeAttribute( 'srcset', getModel() );
+				writer.removeAttribute( 'srcset', model );
 			} );
 
-			expect( editor.getData() )
-				.to.match( /<figure class="image image_resized" style="width:[\d.]{2,}px;"><img src="\/assets\/sample.png"><\/figure>/ );
+			const expectedHtml = '<figure class="image image_resized" style="width:80px;"><img src="/assets/sample.png"></figure>';
+			expect( editor.getData() ).to.be.equal( expectedHtml );
 		} );
 
 		async function preloadImage( imageUrl ) {
@@ -667,46 +397,6 @@ describe( 'ImageResize', () => {
 		}
 	} );
 
-	// TODO move to Resizer tests.
-	describe( 'Resizer', () => {
-		beforeEach( () => createEditor() );
-
-		it( 'uses rounded (int) values', async () => {
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-			widget = viewDocument.getRoot().getChild( 1 );
-
-			await generateResizeTest( {
-				expectedWidth: 97,
-				// Makes it resize the image to 97.2188px, unless there's a rounding.
-				pointerOffset: {
-					x: 7.3,
-					y: -1
-				},
-				resizerPosition: 'bottom-left',
-				checkBeforeMouseUp( domFigure, domResizeWrapper ) {
-					expect( domFigure.style.width ).to.match( /^\d\dpx$/ );
-					expect( domResizeWrapper.style.width ).to.match( /^\d\dpx$/ );
-				}
-			} )();
-
-			expect( editor.model.document.getRoot().getChild( 1 ).getAttribute( 'width' ) ).to.match( /^\d\dpx$/ );
-		} );
-
-		it( 'hides the resize wrapper when its disabled', () => {
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
-
-			const resizer = getSelectedImageResizer( editor );
-			const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
-
-			expect( resizerWrapper.style.display ).to.equal( '' );
-
-			resizer.isEnabled = false;
-
-			expect( resizerWrapper.style.display ).to.equal( 'none' );
-		} );
-	} );
-
 	describe( 'widget toolbar integration', () => {
 		let widgetToolbarRepository;
 
@@ -731,173 +421,49 @@ describe( 'ImageResize', () => {
 		} );
 
 		it( 'visibility during the resize', async () => {
-			await generateResizeTest( {
-				expectedWidth: 100,
-				modelRegExp: /.+/,
-				pointerOffset: {
-					x: 0,
-					y: 0
-				},
-				resizerPosition: 'bottom-right',
-				checkBeforeMouseUp: () => {
-					expect( widgetToolbarRepository.isEnabled ).to.be.false;
-				}
-			} )();
+			const domResizeHandle = getWidgetDomParts( editor, widget, 'bottom-left' ).resizeHandle;
+
+			resizerMouseSimulator.down( editor, domResizeHandle );
+
+			expect( widgetToolbarRepository.isEnabled ).to.be.false;
+
+			resizerMouseSimulator.up( editor, domResizeHandle );
 		} );
 
 		it( 'visibility after the resize', async () => {
-			await generateResizeTest( {
-				expectedWidth: 100,
-				modelRegExp: /.+/,
-				pointerOffset: {
-					x: 0,
-					y: 0
-				},
-				resizerPosition: 'bottom-right'
-			} )();
+			const domResizeHandle = getWidgetDomParts( editor, widget, 'bottom-left' ).resizeHandle;
+
+			resizerMouseSimulator.down( editor, domResizeHandle );
+			resizerMouseSimulator.up( editor, domResizeHandle );
 
 			expect( widgetToolbarRepository.isEnabled ).to.be.true;
 		} );
 
 		it( 'visibility after the resize was canceled', async () => {
 			const resizer = getSelectedImageResizer( editor );
+			const domResizeHandle = getWidgetDomParts( editor, widget, 'bottom-left' ).resizeHandle;
 
-			await generateResizeTest( {
-				expectedWidth: 100,
-				modelRegExp: /.+/,
-				pointerOffset: {
-					x: 0,
-					y: 0
-				},
-				resizerPosition: 'bottom-right',
-				checkBeforeMouseUp: () => {
-					resizer.cancel();
-					expect( widgetToolbarRepository.isEnabled ).to.be.true;
-				}
-			} )();
+			resizerMouseSimulator.down( editor, domResizeHandle );
+
+			resizer.cancel();
+			expect( widgetToolbarRepository.isEnabled ).to.be.true;
 		} );
 
 		it( 'restores toolbar when clicking the handle without drag', () => {
 			// (https://github.com/ckeditor/ckeditor5-widget/pull/112#pullrequestreview-337725256).
-			const resizerPosition = 'bottom-left';
-			const domParts = getWidgetDomParts( widget, resizerPosition );
-			const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
+			const domResizeHandle = getWidgetDomParts( editor, widget, 'bottom-left' ).resizeHandle;
 
-			focusEditor( editor );
-			fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
-			fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
+			resizerMouseSimulator.down( editor, domResizeHandle );
+			resizerMouseSimulator.up( editor, domResizeHandle );
 
 			expect( widgetToolbarRepository.isEnabled ).to.be.true;
 		} );
 	} );
 
-	function isVisible( element ) {
-		// Checks if the DOM element is visible to the end user.
-		return element.offsetParent !== null && !element.classList.contains( 'ck-hidden' );
-	}
-
-	function fireMouseEvent( target, eventType, eventData ) {
-		// Using initMouseEvent instead of MouseEvent constructor, as MouseEvent constructor doesn't support passing pageX
-		// and pageY. See https://stackoverflow.com/questions/45843458/setting-click-events-pagex-and-pagey-always-reverts-to-0
-		// However there's still a problem, that events created with `initMouseEvent` have **floored** pageX, pageY numbers.
-		const event = document.createEvent( 'MouseEvent' );
-		event.initMouseEvent( eventType, true, true, window, null, 0, 0, eventData.pageX, eventData.pageY, false, false, false, false,
-			MOUSE_BUTTON_MAIN, null );
-
-		target.dispatchEvent( event );
-	}
-
 	function wait( delay ) {
 		return new Promise( resolve => window.setTimeout( () => resolve(), delay ) );
 	}
 
-	function generateResizeTest( options ) {
-		// options.resizerPosition - top-left / top-right / bottom-right / bottom-left
-		// options.pointerOffset - object - pointer offset relative to the dragged corner. Negative values are perfectly fine.
-		// e.g. { x: 10, y: -5 }
-		// options.expectedWidth
-		// Returns a test case that puts
-		return async function() {
-			const domParts = getWidgetDomParts( widget, options.resizerPosition );
-			const domResizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
-
-			const modelRegExp = options.modelRegExp ? options.modelRegExp :
-				/<paragraph>foo<\/paragraph><image src=".+?" width="([\d]+)px"><\/image>/;
-
-			focusEditor( editor );
-
-			const initialPointerPosition = getResizerCoordinates( domParts.figure, options.resizerPosition );
-
-			fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
-			fireMouseEvent( domParts.resizeHandle, 'mousemove', initialPointerPosition );
-
-			// We need to wait as mousemove events are throttled.
-			await wait( 40 );
-
-			const finishPointerPosition = Object.assign( {}, initialPointerPosition );
-
-			finishPointerPosition.pageX += options.pointerOffset.x || 0;
-			finishPointerPosition.pageY += options.pointerOffset.y || 0;
-
-			fireMouseEvent( domParts.resizeHandle, 'mousemove', finishPointerPosition );
-
-			expect( parseInt( domParts.figure.style.width ) ).to.be.closeTo( options.expectedWidth, 2, 'DOM width check' );
-
-			if ( options.checkBeforeMouseUp ) {
-				options.checkBeforeMouseUp( domParts.figure, domResizeWrapper );
-			}
-
-			fireMouseEvent( domParts.resizeHandle, 'mouseup', finishPointerPosition );
-
-			expect( getData( editor.model, {
-				withoutSelection: true
-			} ) ).to.match( modelRegExp );
-
-			const modelItem = options.getModel ? options.getModel() : editor.model.document.getRoot().getChild( 1 );
-			const modelWidth = modelItem.getAttribute( 'width' );
-
-			expect( parseFloat( modelWidth, 0 ) )
-				.to.be.closeTo( options.expectedWidth, 2, 'Model width check' );
-		};
-	}
-
-	function focusEditor( editor ) {
-		editor.editing.view.focus();
-		editor.ui.focusTracker.isFocused = true;
-	}
-
-	function getResizerCoordinates( domFigure, resizerPosition ) {
-		const domImage = domFigure.querySelector( 'img' );
-		const imageRect = new Rect( domImage );
-		const initialPointerPosition = {
-			pageX: imageRect.left,
-			pageY: imageRect.top
-		};
-		const resizerPositionParts = resizerPosition.split( '-' );
-
-		if ( resizerPositionParts.includes( 'right' ) ) {
-			initialPointerPosition.pageX = imageRect.right;
-		}
-
-		if ( resizerPositionParts.includes( 'bottom' ) ) {
-			initialPointerPosition.pageY = imageRect.bottom;
-		}
-
-		return initialPointerPosition;
-	}
-
-	function getWidgetDomParts( widget, resizerPosition ) {
-		const resizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
-		const resizeHandle = resizeWrapper.querySelector( `.ck-widget__resizer__handle-${ resizerPosition }` );
-		const figure = view.domConverter.mapViewToDom( widget );
-
-		return {
-			resizeWrapper,
-			resizeHandle,
-			figure
-		};
-	}
-
 	function getDomWidth( domElement ) {
 		return new Rect( domElement ).width;
 	}
@@ -925,6 +491,10 @@ describe( 'ImageResize', () => {
 				view = editor.editing.view;
 				viewDocument = view.document;
 				widget = viewDocument.getRoot().getChild( 1 );
+
+				focusEditor( editor );
+
+				return editor;
 			} );
 	}
 } );