UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

899 lines (805 loc) 24.9 kB
/* eslint-disable no-restricted-globals */ import '../../../jest.extend'; import { Point } from '../../Point'; import { ActiveSelection } from '../../shapes/ActiveSelection'; import { Circle } from '../../shapes/Circle'; import { Group } from '../../shapes/Group'; import { IText } from '../../shapes/IText/IText'; import { FabricObject } from '../../shapes/Object/FabricObject'; import { Rect } from '../../shapes/Rect'; import { Triangle } from '../../shapes/Triangle'; import type { TMat2D } from '../../typedefs'; import { Canvas } from '../Canvas'; const genericVpt = [2.3, 0, 0, 2.3, 120, 80] as TMat2D; const registerTestObjects = (objects: Record<string, FabricObject>) => { Object.entries(objects).forEach(([key, object]) => { jest.spyOn(object, 'toJSON').mockReturnValue(key); }); }; describe('Canvas event data', () => { let canvas: Canvas; let spy: jest.SpyInstance; const snapshotOptions = { cloneDeepWith: (value: any) => { if (value instanceof Point) { return new Point(Math.round(value.x), Math.round(value.y)); } }, }; beforeEach(() => { canvas = new Canvas(); spy = jest.spyOn(canvas, 'fire'); }); afterEach(() => { return canvas.dispose(); }); test.each([ 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'dblclick', 'wheel', 'contextmenu', ] as (keyof WindowEventMap)[])( 'HTML event "%s" should fire a corresponding canvas event', (type) => { canvas.setViewportTransform(genericVpt); canvas.getSelectionElement().dispatchEvent( new MouseEvent(type, { clientX: 50, clientY: 50, detail: type === 'dblclick' ? 2 : undefined, }), ); expect(spy.mock.calls).toMatchSnapshot(snapshotOptions); }, ); // must call mousedown for mouseup to be listened to test('HTML event "mouseup" should fire a corresponding canvas event', () => { canvas.setViewportTransform(genericVpt); canvas .getSelectionElement() .dispatchEvent(new MouseEvent('mousedown', { clientX: 50, clientY: 50 })); spy.mockReset(); document.dispatchEvent( new MouseEvent('mouseup', { clientX: 50, clientY: 50 }), ); expect(spy.mock.calls).toMatchSnapshot(snapshotOptions); }); test.each([ 'drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop', ] as (keyof WindowEventMap)[])( 'HTML event "%s" should fire a corresponding canvas event', (type) => { canvas.setViewportTransform(genericVpt); // select target and mock some essentials for events to fire const dragTarget = new IText('Drag Target', { originX: 'center', originY: 'center', }); jest.spyOn(dragTarget, 'onDragStart').mockReturnValue(true); jest.spyOn(dragTarget, 'renderDragSourceEffect').mockImplementation(); jest.spyOn(dragTarget, 'toJSON').mockReturnValue('Drag Target'); canvas.add(dragTarget); canvas.setActiveObject(dragTarget); spy.mockReset(); canvas.getSelectionElement().dispatchEvent( new MouseEvent('dragstart', { clientX: 50, clientY: 50, }), ); canvas.getSelectionElement().dispatchEvent( new MouseEvent(type, { clientX: 50, clientY: 50, }), ); expect(spy.mock.calls).toMatchSnapshot(snapshotOptions); }, ); test('getScenePoint', () => { const canvas = new Canvas(undefined, { enableRetinaScaling: true, width: 200, height: 200, }); jest.spyOn(canvas, 'getRetinaScaling').mockReturnValue(200); const spy = jest.spyOn(canvas, 'getPointer'); jest.spyOn(canvas.upperCanvasEl, 'getBoundingClientRect').mockReturnValue({ width: 500, height: 500, }); jest.spyOn(canvas.upperCanvasEl, 'width', 'get').mockReturnValue(200); jest.spyOn(canvas.upperCanvasEl, 'height', 'get').mockReturnValue(200); const ev = new MouseEvent('mousemove', { clientX: 50, clientY: 50, }); const point = canvas.getScenePoint(ev); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenNthCalledWith(1, ev); canvas._cacheTransformEventData(ev); expect(point).toEqual(canvas['_absolutePointer']); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenNthCalledWith(2, ev, true); }); }); describe('Event targets', () => { it('A selected subtarget should not fire an event twice', () => { const target = new FabricObject(); const group = new Group([target], { subTargetCheck: true, interactive: true, }); const canvas = new Canvas(); canvas.add(group); const targetSpy = jest.fn(); target.on('mousedown', targetSpy); jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); canvas.getSelectionElement().dispatchEvent( new MouseEvent('mousedown', { clientX: 50, clientY: 50, }), ); expect(targetSpy).toHaveBeenCalledTimes(1); }); test('mouseover and mouseout with subTargetCheck', () => { const rect1 = new FabricObject({ width: 5, height: 5, left: 5, top: 0, strokeWidth: 0, }); const rect2 = new FabricObject({ width: 5, height: 5, left: 5, top: 5, strokeWidth: 0, }); const rect3 = new FabricObject({ width: 5, height: 5, left: 0, top: 5, strokeWidth: 0, }); const rect4 = new FabricObject({ width: 5, height: 5, left: 0, top: 0, strokeWidth: 0, }); const rect5 = new FabricObject({ width: 5, height: 5, left: 2.5, top: 2.5, strokeWidth: 0, }); const group1 = new Group([rect1, rect2], { subTargetCheck: true, }); const group2 = new Group([rect3, rect4], { subTargetCheck: true, }); // a group with 2 groups, with 2 rects each, one group left one group right // each with 2 rects vertically aligned const group = new Group([group1, group2], { subTargetCheck: true, }); const enter = jest.fn(); const exit = jest.fn(); const getTargetsFromEventStream = (mock: jest.Mock) => mock.mock.calls.map((args) => args[0].target); registerTestObjects({ rect1, rect2, rect3, rect4, rect5, group1, group2, group, }); Object.values({ rect1, rect2, rect3, rect4, rect5, group1, group2, group, }).forEach((object) => { object.on('mouseover', enter); object.on('mouseout', exit); }); const canvas = new Canvas(); canvas.add(group, rect5); const fire = (x: number, y: number) => { enter.mockClear(); exit.mockClear(); canvas .getSelectionElement() .dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y })); }; fire(1, 1); expect(getTargetsFromEventStream(enter)).toEqual([group, rect4, group2]); expect(getTargetsFromEventStream(exit)).toEqual([]); fire(5, 5); expect(getTargetsFromEventStream(enter)).toEqual([rect5]); expect(getTargetsFromEventStream(exit)).toEqual([group, rect4, group2]); fire(9, 9); expect(getTargetsFromEventStream(enter)).toEqual([group, rect2, group1]); expect(getTargetsFromEventStream(exit)).toEqual([rect5]); fire(9, 1); expect(getTargetsFromEventStream(enter)).toEqual([rect1]); expect(getTargetsFromEventStream(exit)).toEqual([rect2]); }); describe('findTarget', () => { const mockEvent = ({ canvas, ...init }: MouseEventInit & { canvas: Canvas }) => { const e = new MouseEvent('mousedown', { ...init, }); jest .spyOn(e, 'target', 'get') .mockReturnValue(canvas.getSelectionElement()); return e; }; const findTarget = (canvas: Canvas, ev?: MouseEventInit) => { const target = canvas.findTarget( mockEvent({ canvas, clientX: 0, clientY: 0, ...ev }), ); const targets = canvas.targets; canvas.targets = []; return { target, targets }; }; test.skip.each([true, false])( 'findTargetsTraversal: search all is %s', (searchAll) => { const subTarget1 = new FabricObject(); const target1 = new Group([subTarget1], { subTargetCheck: true, interactive: true, }); const subTarget2 = new FabricObject(); const target2 = new Group([subTarget2], { subTargetCheck: true, }); const parent = new Group([target1, target2], { subTargetCheck: true, interactive: true, }); registerTestObjects({ subTarget1, target1, subTarget2, target2, parent, }); const canvas = new Canvas(); canvas.add(parent); jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); const found = canvas['findTargetsTraversal']([parent], new Point(), { searchStrategy: searchAll ? 'search-all' : 'first-hit', }); expect(found).toEqual( searchAll ? [subTarget2, target2, subTarget1, target1, parent] : [subTarget2, target2, parent], ); }, ); test.failing('searchPossibleTargets', () => { const subTarget = new FabricObject(); const target = new Group([subTarget], { subTargetCheck: true, }); const parent = new Group([target], { subTargetCheck: true, interactive: true, }); registerTestObjects({ subTarget, target, parent }); const canvas = new Canvas(); canvas.add(parent); jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); const found = canvas.searchPossibleTargets([parent], new Point()); expect(found).toBe(target); expect(canvas.targets).toEqual([subTarget, target, parent]); }); test('searchPossibleTargets with selection', () => { const subTarget = new FabricObject(); const target = new Group([subTarget], { subTargetCheck: true, }); const other = new FabricObject(); const activeSelection = new ActiveSelection(); registerTestObjects({ subTarget, target, other, activeSelection }); const canvas = new Canvas(undefined, { activeSelection }); canvas.add(other, target); activeSelection.add(target, other); canvas.setActiveObject(activeSelection); jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); const found = canvas.searchPossibleTargets( [activeSelection], new Point(), ); expect(found).toBe(activeSelection); expect(canvas.targets).toEqual([]); }); test('findTarget clears prev targets', () => { const canvas = new Canvas(); canvas.targets = [new FabricObject()]; expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ target: undefined, targets: [], }); }); test('findTarget preserveObjectStacking false', () => { const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10, controls: {}, }); const rectOver = new FabricObject({ left: 0, top: 0, width: 10, height: 10, controls: {}, }); registerTestObjects({ rect, rectOver }); const canvas = new Canvas(undefined, { preserveObjectStacking: false }); canvas.add(rect, rectOver); canvas.setActiveObject(rect); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: rect, targets: [], }); }); test('findTarget preserveObjectStacking true', () => { const rect = new FabricObject({ left: 0, top: 0, width: 30, height: 30 }); const rectOver = new FabricObject({ left: 0, top: 0, width: 30, height: 30, }); registerTestObjects({ rect, rectOver }); const canvas = new Canvas(undefined, { preserveObjectStacking: true }); canvas.add(rect, rectOver); const e = { clientX: 15, clientY: 15, shiftKey: true, }; const e2 = { clientX: 4, clientY: 4 }; expect(findTarget(canvas, e)).toEqual( { target: rectOver, targets: [] }, // 'Should return the rectOver, rect is not considered' ); canvas.setActiveObject(rect); expect(findTarget(canvas, e)).toEqual( { target: rectOver, targets: [] }, // 'Should still return rectOver because is above active object' ); expect(findTarget(canvas, e2)).toEqual( { target: rect, targets: [] }, // 'Should rect because a corner of the activeObject has been hit' ); canvas.altSelectionKey = 'shiftKey'; expect(findTarget(canvas, e)).toEqual( { target: rect, targets: [] }, // 'Should rect because active and altSelectionKey is pressed' ); }); test('findTarget with subTargetCheck', () => { const canvas = new Canvas(); const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10 }); const rect2 = new FabricObject({ left: 30, top: 30, width: 10, height: 10, }); const group = new Group([rect, rect2]); registerTestObjects({ rect, rect2, group }); canvas.add(group); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: group, targets: [], }); expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ target: group, targets: [], }); group.subTargetCheck = true; group.setCoords(); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: group, targets: [rect], }); expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ target: group, targets: [], }); expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ target: group, targets: [rect2], }); }); test('findTarget with subTargetCheck and canvas zoom', () => { const nested1 = new FabricObject({ width: 100, height: 100, fill: 'yellow', }); const nested2 = new FabricObject({ width: 100, height: 100, left: 100, top: 100, fill: 'purple', }); const nestedGroup = new Group([nested1, nested2], { scaleX: 0.5, scaleY: 0.5, top: 100, left: 0, subTargetCheck: true, }); const rect1 = new FabricObject({ width: 100, height: 100, fill: 'red', }); const rect2 = new FabricObject({ width: 100, height: 100, left: 100, top: 100, fill: 'blue', }); const group = new Group([rect1, rect2, nestedGroup], { top: -150, left: -50, subTargetCheck: true, }); registerTestObjects({ rect1, rect2, nested1, nested2, nestedGroup, group, }); const canvas = new Canvas(undefined, { viewportTransform: [0.1, 0, 0, 0.1, 100, 200], }); canvas.add(group); expect(findTarget(canvas, { clientX: 96, clientY: 186 })).toEqual({ target: group, targets: [rect1], }); expect(findTarget(canvas, { clientX: 98, clientY: 188 })).toEqual({ target: group, targets: [rect1], }); expect(findTarget(canvas, { clientX: 100, clientY: 190 })).toEqual({ target: group, targets: [rect1], }); expect(findTarget(canvas, { clientX: 102, clientY: 192 })).toEqual({ target: group, targets: [rect1], }); expect(findTarget(canvas, { clientX: 104, clientY: 194 })).toEqual({ target: group, targets: [rect1], }); expect(findTarget(canvas, { clientX: 106, clientY: 196 })).toEqual({ target: group, targets: [rect2], }); }); test.each([true, false])( 'findTarget on activeObject with subTargetCheck and preserveObjectStacking %s', (preserveObjectStacking) => { const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10, }); const rect2 = new FabricObject({ left: 30, top: 30, width: 10, height: 10, }); const group = new Group([rect, rect2], { subTargetCheck: true }); registerTestObjects({ rect, rect2, group }); const canvas = new Canvas(undefined, { preserveObjectStacking }); canvas.add(group); canvas.setActiveObject(group); expect(findTarget(canvas, { clientX: 9, clientY: 9 })).toEqual({ target: group, targets: [rect], }); }, ); test('findTarget with perPixelTargetFind', () => { const triangle = new Triangle({ width: 30, height: 30 }); registerTestObjects({ triangle }); const canvas = new Canvas(); canvas.add(triangle); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: triangle, targets: [], }); canvas.perPixelTargetFind = true; expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: undefined, targets: [], }); expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ target: triangle, targets: [], }); }); describe('findTarget with perPixelTargetFind in nested group', () => { const prepareTest = () => { const deepTriangle = new Triangle({ left: 0, top: 0, width: 30, height: 30, fill: 'yellow', }); const triangle2 = new Triangle({ left: 100, top: 120, width: 30, height: 30, angle: 100, fill: 'pink', }); const deepCircle = new Circle({ radius: 30, top: 0, left: 30, fill: 'blue', }); const circle2 = new Circle({ scaleX: 2, scaleY: 2, radius: 10, top: 120, left: -20, fill: 'purple', }); const deepRect = new Rect({ width: 50, height: 30, top: 10, left: 110, fill: 'red', skewX: 40, skewY: 20, }); const rect2 = new Rect({ width: 100, height: 80, top: 50, left: 60, fill: 'green', }); const deepGroup = new Group([deepTriangle, deepCircle, deepRect], { subTargetCheck: true, }); const group2 = new Group([deepGroup, circle2, rect2, triangle2], { subTargetCheck: true, }); const group3 = new Group([group2], { subTargetCheck: true }); registerTestObjects({ deepTriangle, triangle2, deepCircle, circle2, rect2, deepRect, deepGroup, group2, group3, }); const canvas = new Canvas(undefined, { perPixelTargetFind: true }); canvas.add(group3); return { canvas, deepTriangle, triangle2, deepCircle, circle2, rect2, deepRect, deepGroup, group2, group3, }; }; test.each([ { x: 5, y: 5 }, { x: 21, y: 9 }, { x: 37, y: 7 }, { x: 89, y: 47 }, { x: 16, y: 122 }, { x: 127, y: 37 }, { x: 87, y: 139 }, ])('transparent hit on %s', ({ x: clientX, y: clientY }) => { const { canvas } = prepareTest(); expect(findTarget(canvas, { clientX, clientY })).toEqual({ target: undefined, targets: [], }); }); test('findTarget with perPixelTargetFind in nested group', () => { const { canvas, deepTriangle, triangle2, deepCircle, circle2, rect2, deepRect, deepGroup, group2, group3, } = prepareTest(); expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ target: group3, targets: [deepTriangle, deepGroup, group2], }); expect(findTarget(canvas, { clientX: 50, clientY: 20 })).toEqual({ target: group3, targets: [deepCircle, deepGroup, group2], }); expect(findTarget(canvas, { clientX: 117, clientY: 16 })).toEqual({ target: group3, targets: [deepRect, deepGroup, group2], }); expect(findTarget(canvas, { clientX: 100, clientY: 90 })).toEqual({ target: group3, targets: [rect2, group2], }); expect(findTarget(canvas, { clientX: 9, clientY: 145 })).toEqual({ target: group3, targets: [circle2, group2], }); expect(findTarget(canvas, { clientX: 66, clientY: 143 })).toEqual({ target: group3, targets: [triangle2, group2], }); }); }); test('findTarget on active selection', () => { const rect1 = new FabricObject({ left: 0, top: 0, width: 10, height: 10, }); const rect2 = new FabricObject({ left: 20, top: 20, width: 10, height: 10, }); const rect3 = new FabricObject({ left: 20, top: 0, width: 10, height: 10, }); const activeSelection = new ActiveSelection([rect1, rect2], { subTargetCheck: true, cornerSize: 2, }); registerTestObjects({ rect1, rect2, rect3, activeSelection }); const canvas = new Canvas(undefined, { activeSelection }); canvas.add(rect1, rect2, rect3); canvas.setActiveObject(activeSelection); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: activeSelection, targets: [rect1], }); expect(findTarget(canvas, { clientX: 40, clientY: 15 })).toEqual({ target: undefined, targets: [], }); expect(activeSelection.__corner).toBeUndefined(); expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ target: activeSelection, targets: [], }); expect(activeSelection.__corner).toBe('tl'); expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( { target: activeSelection, targets: [], }, // 'Should not return the rect behind active selection' ); canvas.discardActiveObject(); expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( { target: rect3, targets: [], }, // 'Should return the rect after clearing selection' ); }); test('findTarget on active selection with perPixelTargetFind', () => { const rect1 = new Rect({ left: 0, top: 0, width: 10, height: 10, }); const rect2 = new Rect({ left: 20, top: 20, width: 10, height: 10, }); const activeSelection = new ActiveSelection([rect1, rect2]); registerTestObjects({ rect1, rect2, activeSelection }); const canvas = new Canvas(undefined, { activeSelection, perPixelTargetFind: true, preserveObjectStacking: true, }); canvas.add(rect1, rect2); canvas.setActiveObject(activeSelection); expect(findTarget(canvas, { clientX: 8, clientY: 8 })).toEqual({ target: activeSelection, targets: [], }); expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ target: undefined, targets: [], }); }); }); it('should fire mouse over/out events on target', () => { const target = new FabricObject({ width: 10, height: 10 }); const canvas = new Canvas(); canvas.add(target); jest.spyOn(target, 'toJSON').mockReturnValue('target'); const targetSpy = jest.spyOn(target, 'fire'); const canvasSpy = jest.spyOn(canvas, 'fire'); const enter = new MouseEvent('mousemove', { clientX: 5, clientY: 5 }); const exit = new MouseEvent('mousemove', { clientX: 20, clientY: 20 }); canvas._onMouseMove(enter); canvas._onMouseMove(exit); expect(targetSpy.mock.calls).toMatchSnapshot(); expect(canvasSpy.mock.calls).toMatchSnapshot(); }); });