fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
1,649 lines (1,428 loc) • 64.2 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Canvas } from './Canvas';
import { Rect } from '../shapes/Rect';
import { Circle } from '../shapes/Circle';
import { Group } from '../shapes/Group';
import { ActiveSelection } from '../shapes/ActiveSelection';
import { Point } from '../Point';
import { PencilBrush } from '../brushes/PencilBrush';
import type { FabricObject } from '../shapes/Object/FabricObject';
import type {
CanvasEvents,
ObjectEvents,
TPointerEventInfo,
TPointerEvent,
} from '../EventTypeDefs.ts';
import { getFabricDocument, IText, version } from '../../fabric';
import { createPointerEvent } from '../../test/utils';
describe('Canvas events mixin', () => {
const SUB_TARGETS_JSON = `{"version":"${version}","objects":[{"type":"ActiveSelection","left":-152,"top":656.25,"width":356.5,"height":356.5,"scaleX":0.45,"scaleY":0.45,"objects":[]},{"type":"Group","left":11,"top":6,"width":511.5,"height":511.5,"objects":[{"type":"Rect","left":-255.75,"top":-255.75,"width":50,"height":50,"fill":"#6ce798","scaleX":10.03,"scaleY":10.03,"opacity":0.8},{"type":"Group","left":-179.75,"top":22,"width":356.5,"height":356.5,"scaleX":0.54,"scaleY":0.54,"objects":[{"type":"Rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"Group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"Group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]},{"type":"Group","left":-202.75,"top":-228.5,"width":356.5,"height":356.5,"scaleX":0.61,"scaleY":0.61,"objects":[{"type":"Rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"Group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"Group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]},{"type":"Group","left":138.3,"top":-90.22,"width":356.5,"height":356.5,"scaleX":0.42,"scaleY":0.42,"angle":62.73,"objects":[{"type":"Rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"Group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"Group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"Rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"Rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"Triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"Rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]}]}]}`;
let canvas: Canvas;
let upperCanvasEl: HTMLCanvasElement;
beforeEach(() => {
canvas = new Canvas(undefined, {
enableRetinaScaling: false,
width: 600,
height: 600,
});
upperCanvasEl = canvas.upperCanvasEl;
canvas.cancelRequestedRender();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
upperCanvasEl.style.display = '';
canvas.controlsAboveOverlay = Canvas.getDefaults().controlsAboveOverlay;
canvas.preserveObjectStacking = Canvas.getDefaults().preserveObjectStacking;
});
afterEach(() => {
canvas.clear();
canvas.backgroundColor = Canvas.getDefaults().backgroundColor;
canvas.overlayColor = Canvas.getDefaults().overlayColor;
// @ts-expect-error -- private method
canvas.handleSelection = Canvas.prototype.handleSelection;
canvas.off();
canvas.setDimensions({ width: 600, height: 600 });
canvas.calcOffset();
upperCanvasEl.style.display = 'none';
canvas.cancelRequestedRender();
});
it('handles mouse:down with different buttons', () => {
let clickCount = 0;
function mouseHandler() {
clickCount++;
}
canvas.on('mouse:down', mouseHandler);
canvas.fireMiddleClick = false;
canvas.fireRightClick = false;
Object.assign(canvas, { _currentTransform: false });
canvas.isDrawingMode = false;
canvas._onMouseDown(createPointerEvent({ button: 0 }));
expect(clickCount, 'mouse down fired').toBe(1);
clickCount = 0;
canvas._onMouseDown(createPointerEvent({ button: 2 }));
expect(clickCount, 'rightclick did not fire a mouse:down event').toBe(0);
canvas.fireRightClick = true;
canvas._onMouseDown(createPointerEvent({ button: 2 }));
expect(clickCount, 'rightclick did fire a mouse:down event').toBe(1);
clickCount = 0;
canvas._onMouseDown(createPointerEvent({ button: 1 }));
expect(clickCount, 'middleClick did not fire a mouse:down event').toBe(0);
canvas.fireMiddleClick = true;
canvas._onMouseDown(createPointerEvent({ button: 1 }));
expect(clickCount, 'middleClick did fire a mouse:down event').toBe(1);
});
it('handles mouse:down:before with different buttons', () => {
let clickCount = 0;
function mouseHandler() {
clickCount++;
}
canvas.on('mouse:down:before', mouseHandler);
canvas.fireMiddleClick = false;
canvas.fireRightClick = false;
Object.assign(canvas, { _currentTransform: false });
canvas.isDrawingMode = false;
canvas._onMouseDown(createPointerEvent({ which: 1 }));
expect(clickCount, 'mouse:down:before fired').toBe(1);
clickCount = 0;
canvas._onMouseDown(createPointerEvent({ which: 3 }));
expect(clickCount, 'rightclick fired a mouse:down:before event').toBe(1);
canvas.fireRightClick = true;
canvas._onMouseDown(createPointerEvent({ which: 3 }));
expect(clickCount, 'rightclick did fire a mouse:down:before event').toBe(2);
clickCount = 0;
canvas._onMouseDown(createPointerEvent({ which: 2 }));
expect(
clickCount,
'middleClick did not fire a mouse:down:before event',
).toBe(1);
canvas.fireMiddleClick = true;
canvas._onMouseDown(createPointerEvent({ which: 2 }));
expect(clickCount, 'middleClick did fire a mouse:down:before event').toBe(
2,
);
});
it('handles mouse:down and group selector', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 40,
});
const rect = new Rect({ top: 75, left: 75, width: 150, height: 150 });
const expectedGroupSelector = { x: 80, y: 120, deltaX: 0, deltaY: 0 };
canvas.absolutePan(new Point(50, 80));
canvas._onMouseDown(e);
expect(canvas, 'a new groupSelector is created').toHaveProperty(
'_groupSelector',
expectedGroupSelector,
);
canvas.add(rect);
canvas.__onMouseUp(e);
canvas.__onMouseDown(e);
expect(
canvas,
'with object on target no groupSelector is started',
).toHaveProperty('_groupSelector', null);
rect.selectable = false;
canvas._onMouseUp(e);
canvas._onMouseDown(e);
expect(
canvas,
'with object non selectable but already selected groupSelector is not started',
).toHaveProperty('_groupSelector', null);
canvas._onMouseUp(e);
canvas.discardActiveObject();
Object.assign(rect, { isEditing: true });
canvas._onMouseDown(e);
expect(
canvas,
'with object editing, groupSelector is not started',
).toHaveProperty('_groupSelector', null);
canvas._onMouseUp(e);
canvas.discardActiveObject();
Object.assign(rect, { isEditing: false });
canvas._onMouseDown(e);
expect(canvas, 'a new groupSelector is created').toHaveProperty(
'_groupSelector',
expectedGroupSelector,
);
canvas._onMouseUp(e);
});
it('handles activeOn object selection', () => {
const rect = new Rect({ width: 200, height: 200, activeOn: 'down' });
canvas.add(rect);
const e = createPointerEvent({
clientX: 30,
clientY: 15,
});
canvas._onMouseDown(e);
expect(
canvas._activeObject,
'with activeOn of down object is selected on mouse down',
).toBe(rect);
canvas._onMouseUp(e);
canvas.discardActiveObject();
rect.activeOn = 'up';
canvas._onMouseDown(e);
expect(
canvas._activeObject,
'with activeOn of up object is not selected on mouse down',
).toBeUndefined();
canvas._onMouseUp(e);
expect(
canvas._activeObject,
'with activeOn of up object is selected on mouse up',
).toBe(rect);
});
it('handles specific bug #5317 for multiselection', () => {
const greenRect = new Rect({
width: 300,
height: 300,
left: 150,
top: 150,
fill: 'green',
selectable: false,
});
canvas.add(greenRect);
// add red, half-transparent circle
const redCircle = new Circle({
radius: 40,
left: 240,
top: 140,
fill: 'red',
opacity: 0.5,
});
canvas.add(redCircle);
// add blue, half-transparent circle
const blueCircle = new Circle({
radius: 40,
left: 40,
top: 40,
fill: 'blue',
opacity: 0.5,
});
canvas.add(blueCircle);
const e = createPointerEvent({
clientX: 40,
clientY: 40,
});
canvas._onMouseDown(e);
expect(
canvas._activeObject,
'blue circle is selected with first click',
).toBe(blueCircle);
canvas._onMouseUp(e);
const e2 = createPointerEvent({
clientX: 240,
clientY: 140,
shiftKey: true,
});
canvas._onMouseDown(e2);
const selection = canvas.getActiveObjects();
expect(selection[1], 'blue circle is still selected').toBe(blueCircle);
expect(selection[0], 'red circle is selected with shift click').toBe(
redCircle,
);
canvas._onMouseUp(e2);
const e3 = createPointerEvent({
clientX: 140,
clientY: 90,
shiftKey: true,
});
canvas.on('mouse:down', function (options) {
// TODO: fix this, for some reason first target on mouse down is active selection and then second target is the green rectangle
if (options.target instanceof ActiveSelection) {
return;
}
expect(options.target, 'green rectangle was the target').toBe(greenRect);
});
canvas._onMouseDown(e3);
const nextSelection = canvas.getActiveObjects();
expect(nextSelection[1], 'blue circle is still selected 2').toBe(
blueCircle,
);
expect(nextSelection[0], 'red circle is still selected 2').toBe(redCircle);
expect(nextSelection.length, 'no other object have been selected').toBe(2);
canvas._onMouseUp(e3);
const e4 = createPointerEvent({
clientX: 290,
clientY: 290,
});
canvas.on('mouse:down', function (options) {
expect(options.target, 'green rectangle was the target 2').toBe(
greenRect,
);
});
canvas._onMouseDown(e4);
const finalSelection = canvas.getActiveObjects();
expect(
finalSelection.length,
'no other object have been selected because green rect is unselectable',
).toBe(0);
canvas._onMouseUp(e4);
});
it('handles specific bug #6314 for partial intersection with drag', () => {
const testCanvas = new Canvas(undefined, {
enableRetinaScaling: false,
width: 600,
height: 600,
});
let renderRequested = false;
const greenRect = new Rect({
width: 300,
height: 300,
left: 50,
top: 0,
fill: 'green',
});
testCanvas.add(greenRect);
testCanvas._onMouseDown(
createPointerEvent({
clientX: 25,
clientY: 25,
target: testCanvas.upperCanvasEl,
}),
);
testCanvas._onMouseMove(
createPointerEvent({
clientX: 30,
clientY: 30,
target: testCanvas.upperCanvasEl,
}),
);
testCanvas._onMouseMove(
createPointerEvent({
clientX: 100,
clientY: 50,
target: testCanvas.upperCanvasEl,
}),
);
testCanvas.requestRenderAll = function () {
renderRequested = true;
};
testCanvas._onMouseUp(
createPointerEvent({
clientX: 100,
clientY: 50,
target: testCanvas.upperCanvasEl,
}),
);
expect(renderRequested, 'a render has been requested').toBe(true);
});
it('reports isClick = true for mouse:up without movement', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 30,
});
let isClick = false;
canvas.on('mouse:up', function (opt) {
isClick = opt.isClick;
});
canvas._onMouseDown(e);
canvas._onMouseUp(e);
expect(isClick, 'without moving the pointer, the click is true').toBe(true);
});
it('reports isClick = false for mouse:up after movement', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 30,
});
const e2 = createPointerEvent({
clientX: 31,
clientY: 31,
});
let isClick = true;
canvas.on('mouse:up', function (opt) {
isClick = opt.isClick;
});
canvas._onMouseDown(e);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(isClick, 'moving the pointer, the click is false').toBe(false);
});
it('reports isClick = false for mouse:up after dragging', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 30,
});
const e2 = createPointerEvent({
clientX: 31,
clientY: 31,
});
let isClick = true;
canvas.on('mouse:up', function (opt) {
isClick = opt.isClick;
});
canvas._onMouseDown(e);
// @ts-expect-error private method
canvas._onDragStart({
preventDefault() {},
stopPropagation() {},
});
canvas._onMouseUp(e2);
expect(isClick, 'moving the pointer, the click is false').toBe(false);
});
it('handles setDimensions and active brush', () => {
let prepareFor = false;
let rendered = false;
const testCanvas = new Canvas(undefined, { width: 500, height: 500 });
const brush = new PencilBrush(testCanvas);
testCanvas.isDrawingMode = true;
testCanvas.freeDrawingBrush = brush;
Object.assign(testCanvas, { _isCurrentlyDrawing: true });
brush._render = function () {
rendered = true;
};
brush._setBrushStyles = function () {
prepareFor = true;
};
testCanvas.setDimensions({ width: 200, height: 200 });
testCanvas.renderAll();
expect(rendered, 'the brush called the _render method').toBe(true);
expect(prepareFor, 'the brush called the _setBrushStyles method').toBe(
true,
);
});
it('returns target in mouse:up event', () => {
const e1 = createPointerEvent({
clientX: 30,
clientY: 30,
});
const e2 = createPointerEvent({
clientX: 100,
clientY: 100,
});
const rect1 = new Rect({
left: 25,
top: 25,
width: 50,
height: 50,
lockMovementX: true,
lockMovementY: true,
});
const rect2 = new Rect({ left: 100, top: 100, width: 50, height: 50 });
canvas.add(rect1);
canvas.add(rect2);
let opt;
canvas.on('mouse:up', function (_opt) {
opt = _opt;
});
canvas._onMouseDown(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(opt!.target, 'options match model - target').toBe(rect1);
});
it('fires object:modified event', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 30,
});
const e2 = createPointerEvent({
clientX: 31,
clientY: 31,
});
const rect = new Rect({ left: 25, top: 25, width: 50, height: 50 });
canvas.add(rect);
let count = 0;
let opt;
canvas.on('object:modified', function (_opt) {
count++;
opt = _opt;
});
canvas._onMouseDown(e);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(count, 'object:modified fired').toBe(1);
expect(opt!.e, 'options match model - event').toBe(e2);
expect(opt!.target, 'options match model - target').toBe(rect);
expect(opt!.transform.action, 'options match model - target').toBe('drag');
});
it('drags small object when mousemove + drag, not active', () => {
const e = createPointerEvent({
clientX: 2,
clientY: 2,
});
const e1 = createPointerEvent({
clientX: 4,
clientY: 4,
});
const e2 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const rect = new Rect({
left: 1.5,
top: 1.5,
width: 3,
height: 3,
strokeWidth: 0,
});
canvas.add(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(rect.top, 'rect moved by 4 pixels top').toBe(5.5);
expect(rect.left, 'rect moved by 4 pixels left').toBe(5.5);
expect(rect.scaleX, 'rect did not scale Y').toBe(1);
expect(rect.scaleY, 'rect did not scale X').toBe(1);
});
it('scales small object when mousemove + drag, active', () => {
const e = createPointerEvent({
clientX: 3,
clientY: 3,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
const rect = new Rect({
left: 1.5,
top: 1.5,
width: 3,
height: 3,
strokeWidth: 0,
});
expect(rect.scaleX, 'rect not scaled X').toBe(1);
expect(rect.scaleY, 'rect not scaled Y').toBe(1);
canvas.add(rect);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(rect.scaleX, 'rect scaled X').toBe(3);
expect(rect.scaleY, 'rect scaled Y').toBe(3);
});
it('scales a nested target', () => {
const e = createPointerEvent({
clientX: 3,
clientY: 3,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
let mouseUpCalled = false;
let mouseDownCalled = false;
const rect = new Rect({
left: 0,
top: 0,
width: 3,
height: 3,
strokeWidth: 0,
scaleX: 0.5,
});
rect.setPositionByOrigin(new Point(0, 0), 'left', 'top');
const otherRect = new Rect({ left: 100, top: 100, width: 3, height: 3 });
otherRect.setPositionByOrigin(new Point(100, 100), 'left', 'top');
rect.controls = {
br: rect.controls.br,
};
rect.controls.br.mouseUpHandler = function () {
mouseUpCalled = true;
};
rect.controls.br.mouseDownHandler = function () {
mouseDownCalled = true;
};
const group = new Group([rect, otherRect], {
interactive: true,
subTargetCheck: true,
scaleX: 2,
});
group.setPositionByOrigin(new Point(0, 0), 'left', 'top');
canvas.add(group);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(mouseUpCalled, 'mouse up handler for control has been called').toBe(
true,
);
expect(
mouseDownCalled,
'mouse down handler for control has been called',
).toBe(true);
expect(rect.calcTransformMatrix()).toEqual([3, 0, 0, 3, 4.5, 4.5]);
expect(rect.getXY()).toEqual(new Point(4.5, 4.5));
});
it('drags a nested target', () => {
const e = createPointerEvent({
clientX: 1,
clientY: 1,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
const rect = new Rect({
left: 0,
top: 0,
width: 3,
height: 3,
strokeWidth: 0,
scaleX: 0.5,
});
rect.controls = {};
rect.setPositionByOrigin(new Point(0, 0), 'left', 'top');
const otherRect = new Rect({ left: 100, top: 100, width: 3, height: 3 });
otherRect.setPositionByOrigin(new Point(100, 100), 'left', 'top');
const group = new Group([rect, otherRect], {
interactive: true,
subTargetCheck: true,
scaleX: 2,
});
group.setPositionByOrigin(new Point(0, 0), 'left', 'top');
canvas.add(group);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(rect.calcTransformMatrix()).toEqual([1, 0, 0, 1, 9.5, 9.5]);
expect(rect.getXY()).toEqual(new Point(9.5, 9.5));
});
it('calls mouseup and mousedown on the control during transform', () => {
const e = createPointerEvent({
clientX: 3,
clientY: 3,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
const rect = new Rect({
left: 0,
top: 0,
width: 3,
height: 3,
strokeWidth: 0,
});
let mouseUpCalled = false;
let mouseDownCalled = false;
rect.controls = {
br: rect.controls.br,
};
rect.controls.br.mouseUpHandler = function () {
mouseUpCalled = true;
};
rect.controls.br.mouseDownHandler = function () {
mouseDownCalled = true;
};
canvas.add(rect);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e2);
expect(mouseUpCalled, 'mouse up handler for control has been called').toBe(
true,
);
expect(
mouseDownCalled,
'mouse down handler for control has been called',
).toBe(true);
});
it('calls mouseup handler even when transform ends outside the object', () => {
const e = createPointerEvent({
clientX: 3,
clientY: 3,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
const e3 = createPointerEvent({
clientX: 100,
clientY: 100,
});
const rect = new Rect({
left: 0,
top: 0,
width: 3,
height: 3,
strokeWidth: 0,
});
let mouseUpCalled = false;
rect.controls = {
br: rect.controls.br,
};
rect.controls.br.mouseUpHandler = function () {
mouseUpCalled = true;
};
canvas.add(rect);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e3);
expect(
mouseUpCalled,
'mouse up handler for control has been called anyway',
).toBe(true);
});
it('calls both mouseup handlers when transform ends on a new control', () => {
const e = createPointerEvent({
clientX: 3,
clientY: 3,
});
const e1 = createPointerEvent({
clientX: 6,
clientY: 6,
});
const e2 = createPointerEvent({
clientX: 9,
clientY: 9,
});
const e3 = createPointerEvent({
clientX: 9,
clientY: 3,
});
const rect = new Rect({
left: 0,
top: 0,
width: 3,
height: 3,
strokeWidth: 0,
});
let mouseUpCalled1 = false;
let mouseUpCalled2 = false;
rect.controls = {
br: rect.controls.br,
tr: rect.controls.tr,
};
rect.controls.br.mouseUpHandler = function () {
mouseUpCalled1 = true;
};
rect.controls.tr.mouseUpHandler = function () {
mouseUpCalled2 = true;
};
canvas.add(rect);
canvas.setActiveObject(rect);
canvas._onMouseDown(e);
canvas._onMouseMove(e1);
canvas._onMouseMove(e2);
canvas._onMouseUp(e3);
expect(
mouseUpCalled1,
'mouse up handler for rect has been called anyway',
).toBe(true);
expect(mouseUpCalled2, 'mouse up handler for rect2 has been called').toBe(
true,
);
});
it('fires drop:before and drop events', () => {
const eventNames: (keyof CanvasEvents)[] = ['drop:before', 'drop'];
const c = new Canvas();
const fired: string[] = [];
eventNames.forEach(function (eventName) {
c.on(eventName, function () {
fired.push(eventName);
});
});
const event = getFabricDocument().createEvent('HTMLEvents');
event.initEvent('drop', true, true);
c.upperCanvasEl.dispatchEvent(event);
expect(fired, 'bad drop event fired').toEqual(eventNames);
});
it('handles drag event cycle', async () => {
async function testDragCycle(
cycle: readonly (keyof ObjectEvents)[],
canDrop?: boolean,
) {
const c = new Canvas();
const rect = new Rect({ width: 10, height: 10 });
rect.canDrop = function () {
return !!canDrop;
};
c.add(rect);
const registry: string[] = [];
const canvasRegistry: string[] = [];
for (const eventName of cycle) {
rect.once(eventName, function () {
registry.push(eventName);
});
c.once(eventName as any, function (opt) {
expect(
opt.target,
`${eventName} on canvas has rect as a target`,
).toBe(rect);
canvasRegistry.push(eventName);
});
// create a mouseDownEvent
const event = getFabricDocument().createEvent('HTMLEvents');
event.initEvent(eventName, true, true);
Object.assign(event, { clientX: 5 });
Object.assign(event, { clientY: 5 });
c._cacheTransformEventData(event as TPointerEvent);
c.upperCanvasEl.dispatchEvent(event);
}
await c.dispose();
expect(canvasRegistry.length, 'should fire cycle on canvas').toBe(
cycle.length,
);
expect(canvasRegistry, 'should fire all events on canvas').toEqual(cycle);
return registry;
}
const cycle = [
'dragenter',
'dragover',
'dragover',
'dragover',
'drop',
] as const;
const res = await testDragCycle(cycle, true);
expect(res, 'should fire all events on rect').toEqual(cycle);
const cycle1 = [
'dragenter',
'dragover',
'dragover',
'dragover',
'dragleave',
] as const;
const res1 = await testDragCycle(cycle1, true);
expect(res1, 'should fire all events on rect').toEqual(cycle1);
const cycle2 = [
'dragenter',
'dragover',
'dragover',
'dragover',
'drop',
] as const;
const res2 = await testDragCycle(cycle2);
expect(res2, 'should fire all events on rect').toEqual(cycle2);
const cycle3 = [
'dragenter',
'dragover',
'dragover',
'dragover',
'dragleave',
] as const;
const res3 = await testDragCycle(cycle3);
expect(res3, 'should fire all events on rect').toEqual(cycle3);
});
// Test common mouse events
['mousedown', 'mousemove', 'wheel', 'dblclick'].forEach(function (eventType) {
it(`fires fabric event - ${eventType}`, () => {
let eventname: keyof CanvasEvents = (eventType.slice(0, 5) +
':' +
eventType.slice(5)) as keyof CanvasEvents;
if (eventType === 'wheel' || eventType === 'dblclick') {
eventname = ('mouse:' + eventType) as keyof CanvasEvents;
}
if (eventType === 'mouseenter') {
eventname = 'mouse:over' as keyof CanvasEvents;
}
let counter = 0;
let target;
const c = new Canvas();
const rect = new Rect({ top: 2, left: 2, width: 12, height: 12 });
c.add(rect);
c.on(eventname as any, function (opt) {
counter++;
target = opt.target;
});
const event = getFabricDocument().createEvent('HTMLEvents');
event.initEvent(eventType, true, true);
Object.assign(event, { clientX: 5 });
Object.assign(event, { clientY: 5 });
if (eventType === 'dblclick') {
Object.assign(event, { detail: 2 });
}
c.upperCanvasEl.dispatchEvent(event);
expect(counter, `${eventname} fabric event fired`).toBe(1);
expect(target, `${eventname} on canvas has rect as a target`).toBe(rect);
});
});
it('fires mouse:over event for mouseenter', () => {
const eventname = 'mouse:over';
let counter = 0;
const c = new Canvas();
c.on(eventname, function () {
counter++;
});
const event = getFabricDocument().createEvent('HTMLEvents');
event.initEvent('mouseenter', true, true);
c.upperCanvasEl.dispatchEvent(event);
expect(counter, `${eventname} fabric event fired`).toBe(1);
});
it('handles mouseout events', () => {
const eventName = 'mouseout';
const canvasEventName = 'mouse:out';
const c = new Canvas();
const o1 = new Rect();
const o2 = new Rect();
const o3 = new Rect();
const control: TPointerEventInfo[] = [];
const targetControl: FabricObject[] = [];
[o1, o2, o3].forEach((target) => {
target.on(canvasEventName.replace(':', '') as keyof ObjectEvents, () => {
targetControl.push(target);
});
});
canvas.add(o1, o2, o3);
c.on(canvasEventName, function (ev) {
control.push(ev);
});
const event = getFabricDocument().createEvent('HTMLEvents');
event.initEvent(eventName, true, true);
// with targets
c._hoveredTarget = o3;
c._hoveredTargets = [o2, o1];
c.upperCanvasEl.dispatchEvent(event);
expect(
c._hoveredTarget,
'should clear `_hoveredTarget` ref',
).toBeUndefined();
expect(c._hoveredTargets, 'should clear `_hoveredTargets` ref').toEqual([]);
const expected = [o3, o2, o1];
expect(
control.map((ev) => ev.target),
'should equal control',
).toEqual(expected);
expect(targetControl, 'should equal target control').toEqual(expected);
// without targets
control.length = 0;
targetControl.length = 0;
c.upperCanvasEl.dispatchEvent(event);
expect(control.length, 'should have fired once').toBe(1);
expect(control[0].target, 'no target should be referenced').toBeUndefined();
expect(targetControl, 'no target should be referenced').toEqual([]);
});
it('fires mouseover and mouseout events for subTargets when subTargetCheck is enabled', async () => {
let counterOver = 0,
counterOut = 0;
const testCanvas = new Canvas();
function setSubTargetCheckRecursive(obj: any) {
if (obj._objects) {
obj._objects.forEach(setSubTargetCheckRecursive);
}
obj.subTargetCheck = true;
obj.on('mouseover', function () {
counterOver++;
});
obj.on('mouseout', function () {
counterOut++;
});
}
await testCanvas.loadFromJSON(SUB_TARGETS_JSON);
const activeSelection = new ActiveSelection();
activeSelection.add(...testCanvas.getObjects());
testCanvas.setActiveObject(activeSelection);
setSubTargetCheckRecursive(activeSelection);
// perform MouseOver event on a deeply nested subTarget
const moveEvent = createPointerEvent();
const target = testCanvas.item(1) as any;
// @ts-expect-error protected
testCanvas._targetInfo = {
subTargets: [
target.item(1),
target.item(1).item(1),
target.item(1).item(1).item(1),
],
};
testCanvas._fireOverOutEvents(moveEvent, target);
expect(
counterOver,
'mouseover fabric event fired 4 times for primary hoveredTarget & subTargets',
).toBe(4);
expect(testCanvas._hoveredTarget, 'activeSelection is _hoveredTarget').toBe(
target,
);
expect(
testCanvas._hoveredTargets.length,
'3 additional subTargets are captured as _hoveredTargets',
).toBe(3);
// perform MouseOut even on all hoveredTargets
// @ts-expect-error protected
testCanvas._targetInfo.subTargets = [];
// @ts-expect-error private method
testCanvas._fireOverOutEvents(moveEvent, null);
expect(
counterOut,
'mouseout fabric event fired 4 times for primary hoveredTarget & subTargets',
).toBe(4);
expect(
testCanvas._hoveredTarget,
'_hoveredTarget has been set to null',
).toBeNull();
expect(
testCanvas._hoveredTargets.length,
'_hoveredTargets array is empty',
).toBe(0);
});
it('fires mouseover and mouseout events for subTargets when subTargetCheck is enabled but not twice', async () => {
let counterOver = 0,
counterOut = 0;
const testCanvas = new Canvas();
function setSubTargetCheckRecursive(obj: any) {
if (obj._objects) {
obj._objects.forEach(setSubTargetCheckRecursive);
}
obj.subTargetCheck = true;
obj.on('mouseover', function () {
counterOver++;
});
obj.on('mouseout', function () {
counterOut++;
});
}
await testCanvas.loadFromJSON(SUB_TARGETS_JSON);
const activeSelection = new ActiveSelection();
activeSelection.add(...testCanvas.getObjects());
testCanvas.setActiveObject(activeSelection);
setSubTargetCheckRecursive(activeSelection);
// perform MouseOver event on a deeply nested subTarget
const moveEvent = createPointerEvent();
const target = testCanvas.item(1) as any;
// @ts-expect-error protected
testCanvas._targetInfo = {
subTargets: [
target,
target.item(1),
target.item(1).item(1),
target.item(1).item(1).item(1),
],
};
testCanvas._fireOverOutEvents(moveEvent, target);
expect(
counterOver,
'mouseover fabric event fired 4 times for primary hoveredTarget & subTargets',
).toBe(4);
expect(testCanvas._hoveredTarget, 'activeSelection is _hoveredTarget').toBe(
target,
);
// perform MouseOut even on all hoveredTargets
// @ts-expect-error protected
testCanvas._targetInfo.subTargets = [];
// @ts-expect-error private method
testCanvas._fireOverOutEvents(moveEvent, null);
expect(
counterOut,
'mouseout fabric event fired 4 times for primary hoveredTarget & subTargets',
).toBe(4);
});
it('tracks _hoveredActualTarget and emits transitions when actual target changes', () => {
const testCanvas = new Canvas();
const moveEvent = createPointerEvent();
const target = new Rect();
const actualTargetA = new Rect();
const actualTargetB = new Rect();
const canvasOutEvents: CanvasEvents['mouse:out'][] = [];
const canvasOverEvents: CanvasEvents['mouse:over'][] = [];
const currentAOutEvents: ObjectEvents['mouseout'][] = [];
const currentBOverEvents: ObjectEvents['mouseover'][] = [];
testCanvas.on('mouse:out', (opt) => canvasOutEvents.push(opt));
testCanvas.on('mouse:over', (opt) => canvasOverEvents.push(opt));
actualTargetA.on('mouseout', (opt) => currentAOutEvents.push(opt));
actualTargetB.on('mouseover', (opt) => currentBOverEvents.push(opt));
// @ts-expect-error protected
testCanvas._targetInfo = {
subTargets: [],
currentSubTargets: [],
currentTarget: actualTargetA,
};
testCanvas._fireOverOutEvents(moveEvent, target);
expect(
testCanvas._hoveredActualTarget,
'first call stores actual target',
).toBe(actualTargetA);
expect(canvasOutEvents.length, 'no out event on first hover').toBe(0);
expect(canvasOverEvents.length, 'first hover emits over').toBe(1);
// @ts-expect-error protected
testCanvas._targetInfo = {
subTargets: [],
currentSubTargets: [],
currentTarget: actualTargetB,
};
testCanvas._fireOverOutEvents(moveEvent, target);
expect(
testCanvas._hoveredActualTarget,
'second call updates actual target',
).toBe(actualTargetB);
expect(canvasOutEvents.length, 'changing actual target emits out').toBe(1);
expect(canvasOverEvents.length, 'changing actual target emits over').toBe(
2,
);
expect(
canvasOutEvents[0].actualTarget,
'canvas out payload exposes previous actual target',
).toBe(actualTargetA);
expect(
canvasOutEvents[0].nextActualTarget,
'canvas out payload exposes next actual target',
).toBe(actualTargetB);
expect(
canvasOverEvents[1].actualTarget,
'canvas over payload exposes new actual target',
).toBe(actualTargetB);
expect(
canvasOverEvents[1].previousActualTarget,
'canvas over payload exposes previous actual target',
).toBe(actualTargetA);
expect(
currentAOutEvents.length,
'previous actual target receives mouseout',
).toBe(1);
expect(
currentBOverEvents.length,
'next actual target receives mouseover',
).toBe(1);
});
it('fireSyntheticInOutEvents reacts to actual target changes even without primary target changes', () => {
const testCanvas = new Canvas();
const moveEvent = createPointerEvent();
const actualTargetA = new Rect();
const actualTargetB = new Rect();
const canvasOutEvents: CanvasEvents['mouse:out'][] = [];
const canvasOverEvents: CanvasEvents['mouse:over'][] = [];
testCanvas.on('mouse:out', (opt) => canvasOutEvents.push(opt));
testCanvas.on('mouse:over', (opt) => canvasOverEvents.push(opt));
testCanvas.fireSyntheticInOutEvents('mouse', {
e: moveEvent,
target: undefined,
oldTarget: undefined,
actualTarget: actualTargetB,
oldActualTarget: actualTargetA,
fireCanvas: true,
});
expect(
canvasOutEvents.length,
'out event fired for actual target change',
).toBe(1);
expect(
canvasOutEvents[0].actualTarget,
'out payload has old actual target',
).toBe(actualTargetA);
expect(
canvasOutEvents[0].nextActualTarget,
'out payload has next actual target',
).toBe(actualTargetB);
expect(
canvasOverEvents.length,
'over event fired for actual target change',
).toBe(1);
expect(
canvasOverEvents[0].actualTarget,
'over payload has actual target',
).toBe(actualTargetB);
expect(
canvasOverEvents[0].previousActualTarget,
'over payload has previous actual target',
).toBe(actualTargetA);
});
it('updates groupSelector during mouse move', () => {
const e = createPointerEvent({
clientX: 30,
clientY: 40,
});
const expectedGroupSelector = { x: 15, y: 30, deltaX: 65, deltaY: 90 };
canvas.absolutePan(new Point(50, 80));
Object.assign(canvas, {
_groupSelector: { x: 15, y: 30, deltaX: 0, deltaY: 0 },
});
canvas.__onMouseMove(e);
expect(canvas, 'groupSelector is updated').toHaveProperty(
'_groupSelector',
expectedGroupSelector,
);
});
it('removes _hoveredTarget on mouseEnter', () => {
const event = getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
const c = new Canvas();
c._hoveredTarget = new Rect();
c.upperCanvasEl.dispatchEvent(event);
expect(c._hoveredTarget, '_hoveredTarget has been removed').toBeUndefined();
});
it('does not remove _hoveredTarget on mouseEnter if a transform is happening', () => {
const event = getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
const c = new Canvas();
const obj = new Rect();
c._hoveredTarget = obj;
Object.assign(c, { _currentTransform: {} });
c.upperCanvasEl.dispatchEvent(event);
expect(c._hoveredTarget, '_hoveredTarget has been not removed').toBe(obj);
});
it('removes __corner on mouseEnter', () => {
const event = getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
const c = new Canvas();
const obj = new Rect({ top: 100, left: 100 });
c.add(obj);
c.setActiveObject(obj);
obj.__corner = 'test';
c.upperCanvasEl.dispatchEvent(event);
expect(
obj.__corner,
'__corner has been resetted from activeObject',
).toBeUndefined();
});
it('does not remove __corner on mouseEnter if there is a transform', () => {
const event = getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
const c = new Canvas();
const obj = new Rect();
Object.assign(c, { _currentTransform: {} });
c.setActiveObject(obj);
obj.__corner = 'test';
c.upperCanvasEl.dispatchEvent(event);
expect(obj.__corner, '__corner has not been reset').toBe('test');
});
it('sets cursor correctly for different controls', () => {
const key = canvas.altActionKey!.toString();
const key2 = canvas.uniScaleKey!.toString();
const target = new Rect({ width: 100, height: 100 });
canvas.add(target);
canvas.setActiveObject(target);
target.setCoords();
const expected: Record<string, string> = {
mt: 'n-resize',
mb: 's-resize',
ml: 'w-resize',
mr: 'e-resize',
tl: 'nw-resize',
tr: 'ne-resize',
bl: 'sw-resize',
br: 'se-resize',
mtr: 'crosshair',
};
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${expected[corner]} action is not disabled`,
).toBe(expected[corner]);
}
const expectedLockScalinX: Record<string, string> = {
mt: 'n-resize',
mb: 's-resize',
ml: 'not-allowed',
mr: 'not-allowed',
tl: 'not-allowed',
tr: 'not-allowed',
bl: 'not-allowed',
br: 'not-allowed',
mtr: 'crosshair',
};
target.lockScalingX = true;
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedLockScalinX[corner]} for lockScalingX`,
).toBe(expectedLockScalinX[corner]);
}
// Test with uniScaleKey pressed
const expectedUniScale: Record<string, string> = {
mt: 'ew-resize', // skewing
mb: 'ew-resize', // skewing
ml: 'ns-resize', // skewing
mr: 'ns-resize', // skewing
tl: 'nw-resize',
tr: 'ne-resize',
bl: 'sw-resize',
br: 'se-resize',
mtr: 'crosshair',
};
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
[key2]: true,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedUniScale[corner]} for uniScaleKey pressed`,
).toBe(expectedUniScale[corner]);
}
const expectedLockScalinY: Record<string, string> = {
mt: 'not-allowed',
mb: 'not-allowed',
ml: 'w-resize',
mr: 'e-resize',
tl: 'not-allowed',
tr: 'not-allowed',
bl: 'not-allowed',
br: 'not-allowed',
mtr: 'crosshair',
};
target.lockScalingX = false;
target.lockScalingY = true;
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedLockScalinY[corner]} for lockScalingY`,
).toBe(expectedLockScalinY[corner]);
}
const expectedLockScalinYUniscaleKey: Record<string, string> = {
mt: 'ew-resize', // skewing
mb: 'ew-resize', // skewing
ml: 'ns-resize', // skewing
mr: 'ns-resize', // skewing
tl: 'nw-resize',
tr: 'ne-resize',
bl: 'sw-resize',
br: 'se-resize',
mtr: 'crosshair',
};
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
[key2]: true,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedLockScalinYUniscaleKey[corner]} for lockScalingY + uniscaleKey`,
).toBe(expectedLockScalinYUniscaleKey[corner]);
}
const expectedAllLock: Record<string, string> = {
mt: 'not-allowed',
mb: 'not-allowed',
ml: 'not-allowed',
mr: 'not-allowed',
tl: 'not-allowed',
tr: 'not-allowed',
bl: 'not-allowed',
br: 'not-allowed',
mtr: 'crosshair',
};
target.lockScalingY = true;
target.lockScalingX = true;
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedAllLock[corner]} for all locked`,
).toBe(expectedAllLock[corner]);
}
// Test with uniscale key
const expectedAllLockUniscale: Record<string, string> = {
mt: 'ew-resize', // skewing
mb: 'ew-resize', // skewing
ml: 'ns-resize', // skewing
mr: 'ns-resize', // skewing
tl: 'not-allowed',
tr: 'not-allowed',
bl: 'not-allowed',
br: 'not-allowed',
mtr: 'crosshair',
};
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
[key2]: true,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} is ${expectedAllLockUniscale[corner]} for all locked + uniscale`,
).toBe(expectedAllLockUniscale[corner]);
}
// Test rotation lock
target.lockRotation = true;
target.lockScalingY = false;
target.lockScalingX = false;
const e = createPointerEvent({
clientX: target.oCoords.mtr.x,
clientY: target.oCoords.mtr.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
'mtr is not allowed for locked rotation',
).toBe('not-allowed');
// Test skewing lock
target.lockSkewingX = true;
target.lockSkewingY = true;
target.lockRotation = false;
// With lock-skewing we are back at normal
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: false,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${key} is ${expected[corner]} for both lockskewing`,
).toBe(expected[corner]);
}
// Test skewing Y lock
target.lockSkewingY = true;
target.lockSkewingX = false;
const expectedLockSkewingY: Record<string, string> = {
mt: 'ew-resize', // skewing
mb: 'ew-resize', // skewing
ml: 'not-allowed', // skewing
mr: 'not-allowed', // skewing
tl: 'nw-resize',
tr: 'ne-resize',
bl: 'sw-resize',
br: 'se-resize',
mtr: 'crosshair',
};
for (const [corner, coords] of Object.entries(target.oCoords)) {
const e = createPointerEvent({
clientX: coords.x,
clientY: coords.y,
[key]: true,
});
canvas._setCursorFromEvent(e, target);
expect(
canvas.upperCanvasEl.style.cursor,
`${corner} ${expectedLockSkewingY[corner]} for lockSkewingY`,
).toBe(expectedLockSkewingY[corner]);
}
// Test skewing X lock
target.lockSkewingY = false;
target.lockSkewingX = true;
const expect