import { Canvas } from './Canvas'; import { Rect } from '../shapes/Rect'; import { IText } from '../shapes/IText/IText'; import '../shapes/ActiveSelection'; import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { config } from '../config'; import type { FabricObject, MultiSelectionStacking, TPointerEvent, } from '../../fabric'; import { createPointerEvent, makeRect } from '../../test/utils'; import { ActiveSelection, Circle, classRegistry, FabricText, getFabricDocument, Group, Path, version, } from '../../fabric'; import TEST_IMAGE from '../../test/fixtures/test_image.gif'; import { isJSDOM } from '../../vitest.extend'; import { EMPTY_JSON, PATH_DATALESS_JSON, PATH_OBJ_JSON, PATH_WITHOUT_DEFAULTS_JSON, PATH_JSON, RECT_JSON, ERROR_IMAGE_JSON, } from './Canvas.fixtures.ts'; describe('Canvas', () => { let canvas: Canvas; let upperCanvasEl: HTMLCanvasElement; let lowerCanvasEl: HTMLCanvasElement; beforeEach(() => { canvas = new Canvas(undefined, { enableRetinaScaling: false, width: 600, height: 600, }); upperCanvasEl = canvas.upperCanvasEl; lowerCanvasEl = canvas.lowerCanvasEl; }); afterEach(() => { config.restoreDefaults(); classRegistry.setClass(ActiveSelection); return canvas.dispose(); }); describe('touchStart', () => { test('will prevent default to not allow dom scrolling on canvas touch drag', () => { const canvas = new Canvas(undefined, { allowTouchScrolling: false, }); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evt = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); evt.preventDefault = vi.fn(); canvas._onTouchStart(evt); expect(evt.preventDefault).toHaveBeenCalled(); }); test('will not prevent default when allowTouchScrolling is true and there is no action', () => { const canvas = new Canvas(undefined, { allowTouchScrolling: true, }); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evt = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); evt.preventDefault = vi.fn(); canvas._onTouchStart(evt); expect(evt.preventDefault).not.toHaveBeenCalled(); }); test('will prevent default when allowTouchScrolling is true but we are drawing', () => { const canvas = new Canvas(undefined, { allowTouchScrolling: true, isDrawingMode: true, }); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evt = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); evt.preventDefault = vi.fn(); canvas._onTouchStart(evt); expect(evt.preventDefault).toHaveBeenCalled(); }); test('will prevent default when allowTouchScrolling is true and we are dragging an object', () => { const canvas = new Canvas(undefined, { allowTouchScrolling: true, }); const rect = new Rect({ width: 2000, height: 2000, left: -500, top: -500, }); canvas.add(rect); canvas.setActiveObject(rect); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evt = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); evt.preventDefault = vi.fn(); canvas._onTouchStart(evt); expect(evt.preventDefault).toHaveBeenCalled(); }); test('will NOT prevent default when allowTouchScrolling is true and we just lost selection', () => { const canvas = new Canvas(undefined, { allowTouchScrolling: true, }); const rect = new Rect({ width: 200, height: 200, left: 1000, top: 1000, }); canvas.add(rect); canvas.setActiveObject(rect); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evt = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); evt.preventDefault = vi.fn(); canvas._onTouchStart(evt); expect(evt.preventDefault).not.toHaveBeenCalled(); }); test('dispose after _onTouchStart', () => { const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); const canvas = new Canvas(undefined, { allowTouchScrolling: true, isDrawingMode: true, }); const touch = new Touch({ clientX: 10, clientY: 0, identifier: 1, target: canvas.upperCanvasEl, }); const evtStart = new TouchEvent('touchstart', { touches: [touch], changedTouches: [touch], }); canvas._onTouchStart(evtStart); const evtEnd = new TouchEvent('touchend', { touches: [], changedTouches: [touch], }); canvas._onTouchEnd(evtEnd); // @ts-expect-error -- private method expect(+canvas._willAddMouseDown).toBeGreaterThan(0); canvas.dispose(); // @ts-expect-error -- private method expect(clearTimeoutSpy).toHaveBeenCalledWith(canvas._willAddMouseDown); }); }); describe('handleMultiSelection', () => { const canvas = new Canvas(); const rect = new Rect({ left: 100, width: 100, height: 100 }); const iText = new IText('itext'); canvas.add(rect, iText); test('Selecting shapes containing text does not trigger the exit event', () => { const exitMock = vi.fn(); iText.on('editing:exited', exitMock); const firstClick = new MouseEvent('click', { clientX: 0, clientY: 0, }); canvas._onMouseDown(firstClick); canvas._onMouseUp(firstClick); const secondClick = new MouseEvent('click', { shiftKey: true, clientX: 100, clientY: 0, }); canvas._onMouseDown(secondClick); canvas._onMouseUp(secondClick); expect(exitMock).toHaveBeenCalledTimes(0); }); }); it('prevents multiple canvas initialization', () => { const newCanvas = new Canvas(); expect(newCanvas.lowerCanvasEl).toBeTruthy(); expect(() => new Canvas(newCanvas.lowerCanvasEl)).toThrow(); }); it('initializes with element existing in the dom', () => { const doc = getFabricDocument(); const wrapper = doc.createElement('div'); const canvasEl = doc.createElement('canvas'); wrapper.appendChild(canvasEl); doc.body.appendChild(wrapper); const newCanvas = new Canvas(canvasEl); expect(wrapper.firstChild, 'replaced canvas el in dom').toBe( newCanvas.elements.container, ); expect( newCanvas.elements.container.firstChild, 'appended canvas el to container', ).toBe(newCanvas.elements.lower.el); expect( newCanvas.elements.container.lastChild, 'appended upper canvas el to container', ).toBe(newCanvas.elements.upper.el); }); it('has expected initial properties', () => { expect('backgroundColor' in canvas).toBeTruthy(); expect(canvas.includeDefaultValues).toBe(true); }); it('implements getObjects method', () => { expect( canvas.getObjects, 'should respond to `getObjects` method', ).toBeTypeOf('function'); expect( canvas.getObjects(), 'should return empty array for `getObjects` when empty', ).toEqual([]); expect( canvas.getObjects().length, 'should have a 0 length when empty', ).toBe(0); }); it('implements getElement method', () => { expect( canvas.getElement, 'should respond to `getElement` method', ).toBeTypeOf('function'); expect(canvas.getElement(), 'should return a proper element').toBe( lowerCanvasEl, ); }); it('implements item method', () => { const rect = makeRect(); expect(canvas.item, 'should respond to item').toBeTypeOf('function'); canvas.add(rect); expect(canvas.item(0), 'should return proper item').toBe(rect); }); it('preserveObjectStacking property', () => { expect(canvas.preserveObjectStacking).toBeTypeOf('boolean'); expect(canvas.preserveObjectStacking, 'default is true').toBeTruthy(); }); it('uniformScaling property', () => { expect(canvas.uniformScaling).toBeTypeOf('boolean'); expect(canvas.uniformScaling, 'default is true').toBeTruthy(); }); it('uniScaleKey property', () => { expect(canvas.uniScaleKey).toBeTypeOf('string'); expect(canvas.uniScaleKey, 'default is shift').toBe('shiftKey'); }); it('centeredScaling property', () => { expect(canvas.centeredScaling).toBeTypeOf('boolean'); expect(canvas.centeredScaling, 'default is false').toBeFalsy(); }); it('centeredRotation property', () => { expect(canvas.centeredRotation).toBeTypeOf('boolean'); expect(canvas.centeredRotation, 'default is false').toBeFalsy(); }); it('centeredKey property', () => { expect(canvas.centeredKey).toBeTypeOf('string'); expect(canvas.centeredKey, 'default is alt').toBe('altKey'); }); it('altActionKey property', () => { expect(canvas.altActionKey).toBeTypeOf('string'); expect(canvas.altActionKey, 'default is shift').toBe('shiftKey'); }); it('selection property', () => { expect(canvas.selection).toBeTypeOf('boolean'); expect(canvas.selection, 'default is true').toBeTruthy(); }); it('initializes DOM elements correctly', () => { expect( canvas.lowerCanvasEl.getAttribute('data-fabric'), 'el should be marked by canvas init', ).toBe('main'); expect( canvas.upperCanvasEl.getAttribute('data-fabric'), 'el should be marked by canvas init', ).toBe('top'); expect( canvas.wrapperEl.getAttribute('data-fabric'), 'el should be marked by canvas init', ).toBe('wrapper'); }); it('implements renderTop method', () => { expect(canvas.renderTop).toBeTypeOf('function'); }); it('implements _chooseObjectsToRender method', () => { expect(canvas._chooseObjectsToRender).toBeTypeOf('function'); canvas.preserveObjectStacking = false; const rect = makeRect(), rect2 = makeRect(), rect3 = makeRect(); canvas.add(rect); canvas.add(rect2); canvas.add(rect3); let objs = canvas._chooseObjectsToRender(); expect(objs[0]).toBe(rect); expect(objs[1]).toBe(rect2); expect(objs[2]).toBe(rect3); canvas.setActiveObject(rect); objs = canvas._chooseObjectsToRender(); expect(objs[0]).toBe(rect2); expect(objs[1]).toBe(rect3); expect(objs[2]).toBe(rect); canvas.setActiveObject(rect2); canvas.preserveObjectStacking = true; objs = canvas._chooseObjectsToRender(); expect(objs[0]).toBe(rect); expect(objs[1]).toBe(rect2); expect(objs[2]).toBe(rect3); }); it('implements calcOffset method', () => { expect(canvas.calcOffset, 'should respond to `calcOffset`').toBeTypeOf( 'function', ); expect(canvas.calcOffset(), 'should return offset').toEqual({ left: 0, top: 0, }); }); it('implements add method', () => { const rect1 = makeRect(), rect2 = makeRect(), rect3 = makeRect(), rect4 = makeRect(); expect(canvas.add).toBeTypeOf('function'); expect( canvas.add(rect1), 'should return the new length of objects array', ).toBe(1); expect(canvas.item(0)).toBe(rect1); canvas.add(rect2, rect3, rect4); expect( canvas.getObjects().length, 'should support multiple arguments', ).toBe(4); expect(canvas.item(1)).toBe(rect2); expect(canvas.item(2)).toBe(rect3); expect(canvas.item(3)).toBe(rect4); }); it('implements insertAt method', () => { const rect1 = makeRect(), rect2 = makeRect(); canvas.add(rect1, rect2); expect(canvas.insertAt, 'should respond to `insertAt` method').toBeTypeOf( 'function', ); const rect = makeRect(); canvas.insertAt(1, rect); expect(canvas.item(1)).toBe(rect); canvas.insertAt(2, rect); expect(canvas.item(2)).toBe(rect); }); it('implements remove method', () => { const rect1 = makeRect(), rect2 = makeRect(), rect3 = makeRect(), rect4 = makeRect(); canvas.add(rect1, rect2, rect3, rect4); expect(canvas.remove).toBeTypeOf('function'); expect(canvas.remove(rect1)[0], 'should return the object removed').toBe( rect1, ); expect(canvas.item(0), 'should be second object').toBe(rect2); canvas.remove(rect2, rect3); expect(canvas.item(0)).toBe(rect4); canvas.remove(rect4); expect(canvas.isEmpty(), 'canvas should be empty').toBe(true); }); it('clears hovered target when removed', () => { const rect1 = makeRect(); canvas.add(rect1); canvas._hoveredTarget = rect1; canvas.remove(rect1); expect( canvas._hoveredTarget, 'reference to hovered target should be removed', ).toBeUndefined(); }); it('fires before:selection:cleared only when removing active objects', () => { let isFired = false; canvas.on('before:selection:cleared', () => { isFired = true; }); canvas.add(new Rect()); canvas.remove(canvas.item(0)); expect( isFired, 'removing inactive object shouldnt fire "before:selection:cleared"', ).toBe(false); canvas.add(new Rect()); canvas.setActiveObject(canvas.item(0)); canvas.remove(canvas.item(0)); expect( isFired, 'removing active object should fire "before:selection:cleared"', ).toBe(true); }); it('provides deselected objects in before:selection:cleared event', () => { let deselected: FabricObject[] = []; canvas.on('before:selection:cleared', (options) => { deselected = options.deselected; }); const rect = new Rect(); canvas.add(rect); canvas.setActiveObject(rect); canvas.discardActiveObject(); expect(deselected.length, 'options.deselected was the removed object').toBe( 1, ); expect(deselected[0], 'options.deselected was the removed object').toBe( rect, ); const rect1 = new Rect(); const rect2 = new Rect(); canvas.add(rect1, rect2); const activeSelection = new ActiveSelection(); activeSelection.add(rect1, rect2); canvas.setActiveObject(activeSelection); canvas.discardActiveObject(); expect(deselected.length, 'options.deselected was the removed object').toBe( 1, ); expect( deselected[0], 'removing an activeSelection pass that as a target', ).toBe(activeSelection); }); it('fires selection:cleared only when removing active objects', () => { let isFired = false; canvas.on('selection:cleared', () => { isFired = true; }); canvas.add(new Rect()); canvas.remove(canvas.item(0)); expect( isFired, 'removing inactive object shouldnt fire "selection:cleared"', ).toBe(false); canvas.add(new Rect()); canvas.setActiveObject(canvas.item(0)); canvas.remove(canvas.item(0)); expect( isFired, 'removing active object should fire "selection:cleared"', ).toBe(true); canvas.off('selection:cleared'); }); it('creating active selection fires selection:created event', () => { let isFired = false; const rect1 = new Rect(); const rect2 = new Rect(); canvas.add(rect1, rect2); canvas.on('selection:created', () => { isFired = true; }); initActiveSelection(canvas, rect1, rect2, 'selection-order'); expect(canvas._hoveredTarget, 'the created selection is also hovered').toBe( canvas.getActiveObject(), ); expect(isFired, 'selection:created fired').toBe(true); canvas.off('selection:created'); canvas.clear(); }); it('creating active selection fires selected event on new objects', () => { let isFired = false; const rect1 = new Rect(); const rect2 = new Rect(); canvas.add(rect1, rect2); rect2.on('selected', () => { isFired = true; }); initActiveSelection(canvas, rect1, rect2, 'selection-order'); const activeSelection = canvas.getActiveObjects(); expect(isFired, 'selected fired on rect2').toBe(true); expect(activeSelection[0], 'first rec1').toBe(rect1); expect(activeSelection[1], 'then rect2').toBe(rect2); canvas.clear(); }); it('starts multiselection with correct order (default)', () => { const rect1 = new Rect(); const rect2 = new Rect(); canvas.add(rect1, rect2); initActiveSelection(canvas, rect2, rect1, 'selection-order'); const activeSelection = canvas.getActiveObjects(); expect(activeSelection[0], 'first rect2').toBe(rect2); expect(activeSelection[1], 'then rect1').toBe(rect1); }); it('starts multiselection with canvas stacking order', () => { const rect1 = new Rect(); const rect2 = new Rect(); canvas.add(rect1, rect2); initActiveSelection(canvas, rect2, rect1, 'canvas-stacking'); const activeSelection = canvas.getActiveObjects(); expect(activeSelection[0], 'first rect1').toBe(rect1); expect(activeSelection[1], 'then rect2').toBe(rect2); }); it('fires selection:updated when updating active selection', () => { let isFired = false; const rect1 = new Rect(); const rect2 = new Rect(); const rect3 = new Rect(); canvas.add(rect1, rect2, rect3); canvas.on('selection:updated', () => { isFired = true; }); updateActiveSelection(canvas, [rect1, rect2], rect3, 'selection-order'); expect(isFired, 'selection:updated fired').toBe(true); expect(canvas._hoveredTarget, 'hovered target is updated').toBe( canvas.getActiveObject(), ); }); it('fires deselected event when removing object from active selection', () => { let isFired = false; const rect1 = new Rect({ width: 10, height: 10 }); const rect2 = new Rect({ width: 10, height: 10 }); canvas.add(rect1, rect2); rect2.on('deselected', () => { isFired = true; }); updateActiveSelection(canvas, [rect1, rect2], rect2, 'selection-order'); expect(isFired, 'deselected on rect2 fired').toBe(true); }); it('fires selected event when adding object to active selection', () => { let isFired = false; const rect1 = new Rect(); const rect2 = new Rect(); const rect3 = new Rect(); canvas.add(rect1, rect2, rect3); rect3.on('selected', () => { isFired = true; }); updateActiveSelection(canvas, [rect1, rect2], rect3, 'selection-order'); expect(isFired, 'selected on rect3 fired').toBe(true); }); it('respects order of objects in continuing multiselection', () => { const rect1 = new Rect(); const rect2 = new Rect(); const rect3 = new Rect(); canvas.add(rect1, rect2, rect3); function assertObjectsInOrder(init: FabricObject[], added: FabricObject) { updateActiveSelection(canvas, init, added, 'canvas-stacking'); expect( canvas.getActiveObjects(), 'updated selection while preserving canvas stacking order', ).toEqual([rect1, rect2, rect3]); canvas.discardActiveObject(); updateActiveSelection(canvas, init, added, 'selection-order'); expect( canvas.getActiveObjects(), 'updated selection while preserving click order', ).toEqual([...init, added]); canvas.discardActiveObject(); } function assertObjectsInOrderOnCanvas( init: FabricObject[], added: FabricObject, ) { expect(canvas.getObjects()).toEqual([rect1, rect2, rect3]); assertObjectsInOrder(init, added); expect(canvas.getObjects()).toEqual([rect1, rect2, rect3]); } assertObjectsInOrderOnCanvas([rect1, rect2], rect3); assertObjectsInOrderOnCanvas([rect1, rect3], rect2); assertObjectsInOrderOnCanvas([rect2, rect3], rect1); canvas.remove(rect2, rect3); const group = new Group([rect2, rect3], { subTargetCheck: true, interactive: true, }); canvas.add(group); function assertNestedObjectsInOrder( init: FabricObject[], added: FabricObject, ) { expect(canvas.getObjects()).toEqual([rect1, group]); expect(group.getObjects()).toEqual([rect2, rect3]); assertObjectsInOrder(init, added); expect(canvas.getObjects()).toEqual([rect1, group]); expect(group.getObjects()).toEqual([rect2, rect3]); } assertNestedObjectsInOrder([rect1, rect2], rect3); assertNestedObjectsInOrder([rect1, rect3], rect2); assertNestedObjectsInOrder([rect2, rect3], rect1); canvas.remove(rect1); group.insertAt(0, rect1); group.remove(rect3); canvas.add(rect3); function assertNestedObjectsInOrder2( init: FabricObject[], added: FabricObject, ) { expect(canvas.getObjects()).toEqual([group, rect3]); expect(group.getObjects()).toEqual([rect1, rect2]); assertObjectsInOrder(init, added); expect(canvas.getObjects()).toEqual([group, rect3]); expect(group.getObjects()).toEqual([rect1, rect2]); } assertNestedObjectsInOrder2([rect1, rect2], rect3); assertNestedObjectsInOrder2([rect1, rect3], rect2); assertNestedObjectsInOrder2([rect2, rect3], rect1); }); it('toggles selected objects in multiselection', () => { const rect1 = new Rect(); const rect2 = new Rect(); const rect3 = new Rect(); let isFired = false; rect2.on('deselected', () => { isFired = true; }); canvas.add(rect1, rect2, rect3); updateActiveSelection( canvas, [rect1, rect2, rect3], rect2, 'selection-order', ); expect(canvas.getActiveObjects(), 'rect2 was deselected').toEqual([ rect1, rect3, ]); expect(isFired, 'fired deselected').toBeTruthy(); }); it('toggles nested target when clicking inside active selection', () => { const rect1 = new Rect({ width: 10, height: 10 }); const rect2 = new Rect({ width: 10, height: 10 }); const rect3 = new Rect({ width: 10, height: 10 }); let isFired = false; rect3.on('deselected', () => { isFired = true; }); canvas.add(rect1, rect2, rect3); updateActiveSelection( canvas, [rect1, rect2, rect3], null, 'selection-order', ); expect(canvas.getActiveObjects(), 'rect3 was deselected').toEqual([ rect1, rect2, ]); expect(isFired, 'fired deselected').toBeTruthy(); }); it('does nothing when clicking active selection area', () => { const rect1 = new Rect({ left: 10, width: 10, height: 10 }); const rect2 = new Rect({ left: -10, width: 5, height: 5 }); const rect3 = new Rect({ top: 10, width: 10, height: 10 }); canvas.add(rect1, rect2, rect3); updateActiveSelection( canvas, [rect1, rect2, rect3], null, 'selection-order', ); expect(canvas.getActiveObjects(), 'nothing happened').toEqual([ rect1, rect2, rect3, ]); expect( canvas.getActiveObject() === canvas.getActiveObject(), 'still selected', ).toBeTruthy(); }); it('selects target behind active selection when using selection key', () => { const rect1 = new Rect({ left: 15, top: 5, width: 10, height: 10 }); const rect2 = new Rect({ width: 10, height: 10, left: 5, top: 5 }); const rect3 = new Rect({ top: 15, left: 5, width: 10, height: 10 }); canvas.add(rect1, rect2, rect3); initActiveSelection(canvas, rect1, rect3); expect( canvas.getActiveObject() === canvas.getActiveObject(), 'selected', ).toBeTruthy(); expect(canvas.getActiveObjects(), 'created').toEqual([rect1, rect3]); canvas._onMouseDown({ clientX: 7, clientY: 7, [canvas.selectionKey as string]: true, } as unknown as TPointerEvent); expect( canvas.getActiveObjects(), 'added from behind active selection', ).toEqual([rect1, rect2, rect3]); expect( canvas.getActiveObject() === canvas.getActiveObject(), 'still selected', ).toBeTruthy(); }); it('fires deselected event when changing active object', () => { let isFired = false; const rect1 = new Rect(); const rect2 = new Rect(); rect1.on('deselected', () => { isFired = true; }); canvas.setActiveObject(rect1); canvas.setActiveObject(rect2); expect(isFired, 'switching active group fires deselected').toBe(true); }); it('fires selected event for each object when group selecting', () => { let fired = 0; const rect1 = new Rect({ width: 10, height: 10 }); const rect2 = new Rect({ width: 10, height: 10 }); const rect3 = new Rect({ width: 10, height: 10 }); rect1.on('selected', () => { fired++; }); rect2.on('selected', () => { fired++; }); rect3.on('selected', () => { fired++; }); canvas.add(rect1, rect2, rect3); setGroupSelector(canvas, { x: 1, y: 1, deltaX: 5, deltaY: 5, }); canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl })); expect(fired, 'event fired for each of 3 rects').toBe(3); }); it('fires selection:created when multiple objects are selected', () => { let isFired = false; const rect1 = new Rect({ width: 10, height: 10 }); const rect2 = new Rect({ width: 10, height: 10 }); const rect3 = new Rect({ width: 10, height: 10 }); canvas.on('selection:created', () => { isFired = true; }); canvas.add(rect1, rect2, rect3); setGroupSelector(canvas, { x: 1, y: 1, deltaX: 5, deltaY: 5, }); canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl })); expect(isFired, 'selection created fired').toBe(true); expect( // @ts-expect-error -- constructor function has type canvas.getActiveObject()!.constructor.type, 'an active selection is created', ).toBe('ActiveSelection'); expect(canvas.getActiveObjects()[0], 'rect1 is first object').toBe(rect1); expect(canvas.getActiveObjects()[1], 'rect2 is second object').toBe(rect2); expect(canvas.getActiveObjects()[2], 'rect3 is third object').toBe(rect3); expect(canvas.getActiveObjects().length, 'contains exactly 3 objects').toBe( 3, ); }); it('fires selection:created when a single object is selected', () => { let isFired = false; const rect1 = new Rect({ width: 10, height: 10 }); canvas.on('selection:created', () => { isFired = true; }); canvas.add(rect1); setGroupSelector(canvas, { x: 1, y: 1, deltaX: 5, deltaY: 5, }); canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl })); expect(isFired, 'selection:created fired').toBe(true); expect(canvas.getActiveObject(), 'rect1 is set as activeObject').toBe( rect1, ); }); it('collects topmost object when no dragging occurs', () => { const rect1 = new Rect({ width: 10, height: 10, top: 0, left: 0 }); const rect2 = new Rect({ width: 10, height: 10, top: 0, left: 0 }); const rect3 = new Rect({ width: 10, height: 10, top: 0, left: 0 }); canvas.add(rect1, rect2, rect3); setGroupSelector(canvas, { x: 1, y: 1, deltaX: 0, deltaY: 0 }); // @ts-expect-error -- protected method expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy(); expect( canvas.getActiveObjects().length, 'a rect that contains all objects collects them all', ).toBe(1); expect(canvas.getActiveObjects()[0], 'rect3 is collected').toBe(rect3); }); it('does not collect objects with onSelect returning true', () => { const rect1 = new Rect({ width: 10, height: 10, top: 2, left: 2 }); rect1.onSelect = () => { return true; }; const rect2 = new Rect({ width: 10, height: 10, top: 2, left: 2 }); canvas.add(rect1, rect2); setGroupSelector(canvas, { x: 1, y: 1, deltaX: 20, deltaY: 20 }); // @ts-expect-error -- protected method expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy(); expect( canvas.getActiveObjects().length, 'objects are in the same position buy only one gets selected', ).toBe(1); expect(canvas.getActiveObjects()[0], 'contains rect2 but not rect 1').toBe( rect2, ); }); it('does not call onSelect on objects that are not intersected', () => { const rect1 = new Rect({ width: 10, height: 10, top: 5, left: 5 }); const rect2 = new Rect({ width: 10, height: 10, top: 5, left: 15 }); let onSelectRect1CallCount = 0; let onSelectRect2CallCount = 0; rect1.onSelect = () => { onSelectRect1CallCount++; return false; }; rect2.onSelect = () => { onSelectRect2CallCount++; return false; }; canvas.add(rect1, rect2); // Intersects none setGroupSelector(canvas, { x: 25, y: 25, deltaX: 1, deltaY: 1 }); // @ts-expect-error -- protected method expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy(); const onSelectCalls = onSelectRect1CallCount + onSelectRect2CallCount; expect(onSelectCalls, 'none of the onSelect methods was called').toBe(0); // Intersects one setGroupSelector(canvas, { x: 0, y: 0, deltaX: 5, deltaY: 5 }); // @ts-expect-error -- protected method expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy(); expect(canvas.getActiveObject(), 'rect1 was selected').toBe(rect1); expect( onSelectRect1CallCount, 'rect1 onSelect was called while setting active object', ).toBe(1); expect(onSelectRect2CallCount, 'rect2 onSelect was not called').toBe(0); // Intersects both setGroupSelector(canvas, { x: 0, y: 0, deltaX: 15, deltaY: 5 }); // @ts-expect-error -- protected method expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy(); expect(canvas.getActiveObjects(), 'rect1 selected').toEqual([rect1, rect2]); expect( onSelectRect1CallCount, 'rect1 onSelect was called once when collectiong it and once when selecting it', ).toBe(2); expect(onSelectRect2CallCount, 'rect2 onSelect was called').toBe(1); }); it('returns false from handleMultiSelection when onSelect returns true', () => { const rect = new Rect(); const rect2 = new Rect(); rect.onSelect = () => { return true; }; canvas._activeObject = rect2; const selectionKey = canvas.selectionKey; const event = {}; // @ts-expect-error -- typed as readonly but in test case we want to override event[selectionKey] = true; // @ts-expect-error -- protected method const returned = canvas.handleMultiSelection(event, rect); expect(returned, 'if onSelect returns true, shouldGroup return false').toBe( false, ); }); it('returns true from handleMultiSelection when onSelect returns false and selectionKey is true', () => { const rect = new Rect(); const rect2 = new Rect(); rect.onSelect = () => { return false; }; canvas._activeObject = rect2; const selectionKey = canvas.selectionKey; const event = {}; // @ts-expect-error -- typed as readonly but in test case we want to override event[selectionKey] = true; // @ts-expect-error -- protected method const returned = canvas.handleMultiSelection(event, rect); expect(returned, 'if onSelect returns false, shouldGroup return true').toBe( true, ); }); it('returns false from handleMultiSelection when selectionKey is false', () => { const rect = new Rect(); const rect2 = new Rect(); rect.onSelect = () => { return false; }; canvas._activeObject = rect2; const selectionKey = canvas.selectionKey; const event = {}; // @ts-expect-error -- typed as readonly but in test case we want to override event[selectionKey] = false; // @ts-expect-error -- protected method const returned = canvas.handleMultiSelection(event, rect); expect(returned, 'shouldGroup return false').toBe(false); }); it('fires multiple events from _fireSelectionEvents', () => { let rect1Deselected = false; let rect3Selected = false; const rect1 = new Rect(); const rect2 = new Rect(); const rect3 = new Rect(); const activeSelection = new ActiveSelection(); activeSelection.add(rect1, rect2); canvas.setActiveObject(activeSelection); rect1.on('deselected', () => { rect1Deselected = true; }); rect3.on('selected', () => { rect3Selected = true; }); const currentObjects = canvas.getActiveObjects(); activeSelection.remove(rect1); activeSelection.add(rect3); canvas._fireSelectionEvents(currentObjects, {} as TPointerEvent); expect(rect3Selected, 'rect 3 selected').toBeTruthy(); expect(rect1Deselected, 'rect 1 deselected').toBeTruthy(); }); it('implements getContext method', () => { expect(canvas.getContext).toBeTypeOf('function'); }); it('implements clearContext method', () => { expect(canvas.clearContext).toBeTypeOf('function'); canvas.clearContext(canvas.getContext()); }); it('implements clear method and empties the canvas', () => { expect(canvas.clear).toBeTypeOf('function'); canvas.clear(); expect(canvas.getObjects().length).toBe(0); }); it('implements renderAll method', () => { expect(canvas.renderAll).toBeTypeOf('function'); }); it('implements _drawSelection method', () => { expect(canvas._drawSelection).toBeTypeOf('function'); }); it('finds target objects correctly with findTarget', () => { expect(canvas.findTarget).toBeTypeOf('function'); const rect = makeRect({ left: 0, top: 0 }); canvas.add(rect); const { target } = canvas.findTarget( createPointerEvent({ clientX: 5, clientY: 5, target: canvas.upperCanvasEl, }), ); expect(target, 'Should return the rect').toBe(rect); const { target: target2 } = canvas.findTarget( createPointerEvent({ clientX: 30, clientY: 30, target: canvas.upperCanvasEl, }), ); expect(target2, 'Should not find target').toBeUndefined(); canvas.remove(rect); }); it('implements toCanvasElement method that clears the contextTop', () => { const canvas = new Canvas(); const mockSetCtx = vi.fn(); class UpperMock { declare el: any; set ctx(value: any) { mockSetCtx(value); } get ctx() { return undefined; } constructor() { this.el = { getContext: vi.fn(), }; } } canvas.elements.upper = new UpperMock(); canvas.toCanvasElement(); expect(mockSetCtx).toHaveBeenCalledWith(undefined); expect(mockSetCtx).toHaveBeenCalledTimes(2); }); it('implements toDataURL method that returns valid data URL', () => { expect(canvas.toDataURL).toBeTypeOf('function'); const dataURL = canvas.toDataURL(); // don't compare actual data url, as it is often browser-dependent expect(typeof dataURL, 'is a string').toBe('string'); expect(dataURL.substring(0, 21), 'starts with correct prefix').toBe( 'data:image/png;base64', ); }); it('implements getCenterPoint method that returns canvas center as Point', () => { expect(canvas.getCenterPoint).toBeTypeOf('function'); const center = canvas.getCenterPoint(); expect(center.x, 'center x is half width').toBe(upperCanvasEl.width / 2); expect(center.y, 'center y is half height').toBe(upperCanvasEl.height / 2); }); it('centers objects horizontally with centerObjectH', () => { expect(canvas.centerObjectH).toBeTypeOf('function'); const rect = makeRect({ left: 102, top: 202 }); canvas.add(rect); canvas.centerObjectH(rect); expect( rect.getCenterPoint().x, 'object\'s "left" property should correspond to canvas element\'s center', ).toBe(upperCanvasEl.width / 2); }); it('centers objects vertically with centerObjectV', () => { expect(canvas.centerObjectV).toBeTypeOf('function'); const rect = makeRect({ left: 102, top: 202 }); canvas.add(rect); canvas.centerObjectV(rect); expect( rect.getCenterPoint().y, 'object\'s "top" property should correspond to canvas element\'s center', ).toBe(upperCanvasEl.height / 2); }); it('centers objects with centerObject', () => { expect(canvas.centerObject).toBeTypeOf('function'); const rect = makeRect({ left: 102, top: 202 }); canvas.add(rect); canvas.centerObject(rect); expect( rect.getCenterPoint().y, 'object\'s "top" property should correspond to canvas element\'s center', ).toBe(upperCanvasEl.height / 2); expect( rect.getCenterPoint().x, 'object\'s "left" property should correspond to canvas element\'s center', ).toBe(upperCanvasEl.width / 2); }); it('serializes to JSON with toJSON', () => { expect(canvas.toJSON).toBeTypeOf('function'); expect(JSON.stringify(canvas.toJSON())).toBe(JSON.stringify(EMPTY_JSON)); canvas.backgroundColor = '#ff5555'; canvas.overlayColor = 'rgba(0,0,0,0.2)'; expect( canvas.toJSON(), '`background` and `overlayColor` value should be reflected in json', ).toEqual({ version: version, objects: [], background: '#ff5555', overlay: 'rgba(0,0,0,0.2)', }); canvas.add(makeRect()); expect(canvas.toJSON()).toEqual(RECT_JSON); }); it('serializes to JSON with active selection', () => { const rect = new Rect({ width: 50, height: 50, left: 100, top: 100 }); const circle = new Circle({ radius: 50, left: 50, top: 50 }); canvas.add(rect, circle); const json = JSON.stringify(canvas); const activeSelection = new ActiveSelection(); activeSelection.add(rect, circle); canvas.setActiveObject(activeSelection); const jsonWithActiveGroup = JSON.stringify(canvas); expect(json).toBe(jsonWithActiveGroup); }); it('serializes to dataless JSON with toDatalessJSON', () => { const path = new Path('M 100 100 L 300 100 L 200 300 z', { sourcePath: 'http://example.com/', }); canvas.add(path); expect(canvas.toDatalessJSON()).toEqual(PATH_DATALESS_JSON); }); it('converts to object with toObject', () => { expect(canvas.toObject).toBeTypeOf('function'); const expectedObject = { version: version, objects: canvas.getObjects(), }; expect(canvas.toObject()).toEqual(expectedObject); const rect = makeRect(); canvas.add(rect); // @ts-expect-error -- constructor function has type expect(canvas.toObject().objects[0].type).toBe(rect.constructor.type); }); it('includes clipPath in toObject when present', () => { const clipPath = makeRect(); const canvasWithClipPath = new Canvas(undefined, { clipPath: clipPath }); const expectedObject = { version: version, objects: canvasWithClipPath.getObjects(), clipPath: { type: 'Rect', version: version, originX: 'center', originY: 'center', left: 0, top: 0, width: 10, height: 10, fill: 'rgb(0,0,0)', stroke: null, strokeWidth: 1, strokeDashArray: null, strokeLineCap: 'butt', strokeDashOffset: 0, strokeLineJoin: 'miter', strokeMiterLimit: 4, scaleX: 1, scaleY: 1, angle: 0, flipX: false, flipY: false, opacity: 1, shadow: null, visible: true, backgroundColor: '', fillRule: 'nonzero', paintFirst: 'fill', globalCompositeOperation: 'source-over', skewX: 0, skewY: 0, rx: 0, ry: 0, strokeUniform: false, }, }; expect(canvasWithClipPath.toObject).toBeTypeOf('function'); expect(canvasWithClipPath.toObject()).toEqual(expectedObject); const rect = makeRect(); canvasWithClipPath.add(rect); expect(canvasWithClipPath.toObject().objects[0].type).toBe( // @ts-expect-error -- constructor function has type rect.constructor.type, ); }); it('converts to dataless object with toDatalessObject', () => { expect(canvas.toDatalessObject).toBeTypeOf('function'); const expectedObject = { version: version, objects: canvas.getObjects(), }; expect(canvas.toDatalessObject()).toEqual(expectedObject); const rect = makeRect(); canvas.add(rect); // @ts-expect-error -- constructor function has type expect(canvas.toObject().objects[0].type).toBe(rect.constructor.type); // TODO (kangax): need to test this method with fabric.Path to ensure that path is not populated }); it('checks if canvas is empty with isEmpty', () => { expect(canvas.isEmpty).toBeTypeOf('function'); expect(canvas.isEmpty()).toBeTruthy(); canvas.add(makeRect()); expect(canvas.isEmpty()).toBeFalsy(); }); it('loads from JSON string with loadFromJSON', async () => { expect(canvas.loadFromJSON).toBeTypeOf('function'); await canvas.loadFromJSON(PATH_JSON); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a path object').toBe('Path'); expect( canvas.backgroundColor, 'backgroundColor is populated properly', ).toBe('#ff5555'); expect(canvas.overlayColor, 'overlayColor is populated properly').toBe( 'rgba(0,0,0,0.2)', ); expect(obj.get('left')).toBe(268); expect(obj.get('top')).toBe(266); expect(obj.get('width')).toBe(49.803999999999995); expect(obj.get('height')).toBe(48.027); expect(obj.get('fill')).toBe('rgb(0,0,0)'); expect(obj.get('stroke')).toBe(null); expect(obj.get('strokeWidth')).toBe(1); expect(obj.get('scaleX')).toBe(1); expect(obj.get('scaleY')).toBe(1); expect(obj.get('angle')).toBe(0); expect(obj.get('flipX')).toBe(false); expect(obj.get('flipY')).toBe(false); expect(obj.get('opacity')).toBe(1); expect(obj.get('path').length > 0).toBeTruthy(); }); it('loads from JSON object with loadFromJSON', async () => { await canvas.loadFromJSON(PATH_JSON); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a path object').toBe('Path'); expect( canvas.backgroundColor, 'backgroundColor is populated properly', ).toBe('#ff5555'); expect(canvas.overlayColor, 'overlayColor is populated properly').toBe( 'rgba(0,0,0,0.2)', ); expect(obj.get('left')).toBe(268); expect(obj.get('top')).toBe(266); expect(obj.get('width')).toBe(49.803999999999995); expect(obj.get('height')).toBe(48.027); expect(obj.get('fill')).toBe('rgb(0,0,0)'); expect(obj.get('stroke')).toBe(null); expect(obj.get('strokeWidth')).toBe(1); expect(obj.get('scaleX')).toBe(1); expect(obj.get('scaleY')).toBe(1); expect(obj.get('angle')).toBe(0); expect(obj.get('flipX')).toBe(false); expect(obj.get('flipY')).toBe(false); expect(obj.get('opacity')).toBe(1); expect(obj.get('path').length > 0).toBeTruthy(); }); it('loads from JSON string with loadFromJSON with images not existing', async () => { expect(canvas.loadFromJSON).toBeTypeOf('function'); await canvas.loadFromJSON(ERROR_IMAGE_JSON); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); expect(canvas.getObjects().length).toBe(1); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a Image object').toBe('Text'); }); it('loads from JSON string with loadFromJSON with images not existing passing reviver', async () => { expect(canvas.loadFromJSON).toBeTypeOf('function'); await canvas.loadFromJSON( ERROR_IMAGE_JSON, async (serializedObject, instance, error) => { if (error) { return new FabricText('text-placeholder'); } }, ); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); expect(canvas.getObjects().length).toBe(2); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a Image object').toBe('Text'); expect(obj).toBeInstanceOf(FabricText); expect((obj as FabricText).text).toBe('text-placeholder'); }); it('loads from JSON object without default values', async () => { await canvas.loadFromJSON(PATH_WITHOUT_DEFAULTS_JSON); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a path object').toBe('Path'); expect( canvas.backgroundColor, 'backgroundColor is populated properly', ).toBe('#ff5555'); expect(canvas.overlayColor, 'overlayColor is populated properly').toBe( 'rgba(0,0,0,0.2)', ); expect(obj.get('originX')).toBe('center'); expect(obj.get('originY')).toBe('center'); expect(obj.get('left')).toBe(268); expect(obj.get('top')).toBe(266); expect(obj.get('width')).toBe(49.803999999999995); expect(obj.get('height')).toBe(48.027); expect(obj.get('fill')).toBe('rgb(0,0,0)'); expect(obj.get('stroke')).toBe(null); expect(obj.get('strokeWidth')).toBe(1); expect(obj.get('scaleX')).toBe(1); expect(obj.get('scaleY')).toBe(1); expect(obj.get('angle')).toBe(0); expect(obj.get('flipX')).toBe(false); expect(obj.get('flipY')).toBe(false); expect(obj.get('opacity')).toBe(1); expect(obj.get('path').length > 0).toBeTruthy(); }); it('loads from JSON with reviver function', async () => { await canvas.loadFromJSON(PATH_JSON, function (obj, instance) { expect(obj).toEqual(PATH_OBJ_JSON); // @ts-expect-error -- constructor function has type if (instance.constructor.type === 'Path') { // @ts-expect-error -- custom prop instance.customID = 'fabric_1'; } }); const obj = canvas.item(0); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); // @ts-expect-error -- constructor function has type expect(obj.constructor.type, 'first object is a path object').toBe('Path'); expect( canvas.backgroundColor, 'backgroundColor is populated properly', ).toBe('#ff5555'); expect(canvas.overlayColor, 'overlayColor is populated properly').toBe( 'rgba(0,0,0,0.2)', ); expect(obj.get('left')).toBe(268); expect(obj.get('top')).toBe(266); expect(obj.get('width')).toBe(49.803999999999995); expect(obj.get('height')).toBe(48.027); expect(obj.get('fill')).toBe('rgb(0,0,0)'); expect(obj.get('stroke')).toBe(null); expect(obj.get('strokeWidth')).toBe(1); expect(obj.get('scaleX')).toBe(1); expect(obj.get('scaleY')).toBe(1); expect(obj.get('angle')).toBe(0); expect(obj.get('flipX')).toBe(false); expect(obj.get('flipY')).toBe(false); expect(obj.get('opacity')).toBe(1); expect(obj.get('customID')).toBe('fabric_1'); expect(obj.get('path').length > 0).toBeTruthy(); }); it('loads from JSON with no objects', async () => { const canvas1 = getFabricDocument().createElement('canvas'); const canvas2 = getFabricDocument().createElement('canvas'); const c1 = new Canvas(canvas1, { backgroundColor: 'green', overlayColor: 'yellow', }); const c2 = new Canvas(canvas2, { backgroundColor: 'red', overlayColor: 'orange', }); const json = c1.toJSON(); let fired = false; await c2.loadFromJSON(json).then(() => { fired = true; }); expect(fired, 'Callback should be fired even if no objects').toBeTruthy(); expect(c2.backgroundColor, 'Color should be set properly').toBe('green'); expect(c2.overlayColor, 'Color should be set properly').toBe('yellow'); }); it('loads from JSON without "objects" property', async () => { const canvas1 = getFabricDocument().createElement('canvas'); const canvas2 = getFabricDocument().createElement('canvas'); const c1 = new Canvas(canvas1, { backgroundColor: 'green', overlayColor: 'yellow', }); const c2 = new Canvas(canvas2, { backgroundColor: 'red', overlayColor: 'orange', }); const json = c1.toJSON(); let fired = false; delete json.objects; await c2.loadFromJSON(json).then(() => { fired = true; }); expect( fired, 'Callback should be fired even if no "objects" property exists', ).toBeTruthy(); expect(c2.backgroundColor, 'Color should be set properly').toBe('green'); expect(c2.overlayColor, 'Color should be set properly').toBe('yellow'); }); it('loads from JSON with empty Group', async () => { const canvas1 = getFabricDocument().createElement('canvas'); const canvas2 = getFabricDocument().createElement('canvas'); const c1 = new Canvas(canvas1); const c2 = new Canvas(canvas2); const group = new Group(); c1.add(group); expect(c1.isEmpty(), 'canvas is not empty').toBeFalsy(); const json = c1.toJSON(); let fired = false; await c2.loadFromJSON(json).then(() => { fired = true; }); expect( fired, 'Callback should be fired even if empty fabric.Group exists', ).toBeTruthy(); }); it('loads from JSON with async content', async () => { const group = new Group([ new Rect({ width: 10, height: 20 }), new Circle({ radius: 10 }), ]); const rect = new Rect({ width: 20, height: 10 }); const circle = new Circle({ radius: 25 }); canvas.add(group, rect, circle); const json = JSON.stringify(canvas); canvas.clear(); expect(canvas.getObjects().length).toBe(0); await canvas.loadFromJSON(json); expect(canvas.getObjects().length).toBe(3); }); it('loads custom properties on Canvas with no async objects', async () => { const serialized = JSON.parse(JSON.stringify(PATH_JSON)); serialized.controlsAboveOverlay = true; serialized.preserveObjectStacking = true; expect(canvas.controlsAboveOverlay).toBe( Canvas.getDefaults().controlsAboveOverlay, ); expect(canvas.preserveObjectStacking).toBe( Canvas.getDefaults().preserveObjectStacking, ); await canvas.loadFromJSON(serialized); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); expect(canvas.controlsAboveOverlay).toBe(true); expect(canvas.preserveObjectStacking).toBe(true); }); it('loads custom properties on Canvas with image', async () => { const serialized = { objects: [ { type: 'image', originX: 'left', originY: 'top', left: 13.6, top: -1.4, width: 3000, height: 3351, fill: 'rgb(0,0,0)', stroke: null, strokeWidth: 0, strokeDashArray: null, strokeLineCap: 'butt', strokeDashOffset: 0, strokeLineJoin: 'miter', strokeMiterLimit: 4, scaleX: 0.05, scaleY: 0.05, angle: 0, flipX: false, flipY: false, opacity: 1, shadow: null, visible: true, backgroundColor: '', fillRule: 'nonzero', globalCompositeOperation: 'source-over', skewX: 0, skewY: 0, src: isJSDOM() ? 'test_image.gif' : TEST_IMAGE, filters: [], crossOrigin: '', }, ], background: 'green', }; // @ts-expect-error -- custom prop serialized.controlsAboveOverlay = true; // @ts-expect-error -- custom prop serialized.preserveObjectStacking = false; expect(canvas.controlsAboveOverlay).toBe( Canvas.getDefaults().controlsAboveOverlay, ); expect(canvas.preserveObjectStacking).toBe( Canvas.getDefaults().preserveObjectStacking, ); // before callback the properties are still false. expect(canvas.controlsAboveOverlay).toBe(false); expect(canvas.preserveObjectStacking).toBe(true); await canvas.loadFromJSON(serialized); expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy(); expect(canvas.controlsAboveOverlay).toBe(true); expect(canvas.preserveObjectStacking).toBe(false); }); // TODO: does this test makes sense in vitest? // QUnit.test('loadFromJSON with backgroundImage', function(assert) { // var done = assert.async(); // canvas.setBackgroundImage('../../assets/pug.jpg'); // var anotherCanvas = new fabric.Canvas(); // setTimeout(function() { // var json = JSON.stringify(canvas); // anotherCanvas.loadFromJSON(json); // setTimeout(function() { // assert.equal(JSON.stringify(anotherCanvas), json, 'backgrondImage and properties are initialized correctly'); // done(); // }, 1000); // }, 1000); // }); it('sends objects to the back of the stack', () => { expect(canvas.sendObjectToBack).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); const rect3 = makeRect(); canvas.add(rect1, rect2, rect3); canvas.sendObjectToBack(rect3); expect(canvas.item(0), 'third should now be the first one').toBe(rect3); canvas.sendObjectToBack(rect2); expect(canvas.item(0), 'second should now be the first one').toBe(rect2); canvas.sendObjectToBack(rect2); expect(canvas.item(0), 'second should *still* be the first one').toBe( rect2, ); }); it('brings objects to the front of the stack', () => { expect(canvas.bringObjectToFront).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); const rect3 = makeRect(); canvas.add(rect1, rect2, rect3); canvas.bringObjectToFront(rect1); expect(canvas.item(2), 'first should now be the last one').toBe(rect1); canvas.bringObjectToFront(rect2); expect(canvas.item(2), 'second should now be the last one').toBe(rect2); canvas.bringObjectToFront(rect2); expect(canvas.item(2), 'second should *still* be the last one').toBe(rect2); }); it('sends objects backwards in the stack', () => { expect(canvas.sendObjectBackwards).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); const rect3 = makeRect(); canvas.add(rect1, rect2, rect3); // [ 1, 2, 3 ] expect(canvas.item(0)).toBe(rect1); expect(canvas.item(1)).toBe(rect2); expect(canvas.item(2)).toBe(rect3); canvas.sendObjectBackwards(rect3); // moved 3 one level back — [1, 3, 2] expect(canvas.item(0)).toBe(rect1); expect(canvas.item(2)).toBe(rect2); expect(canvas.item(1)).toBe(rect3); canvas.sendObjectBackwards(rect3); // moved 3 one level back — [3, 1, 2] expect(canvas.item(1)).toBe(rect1); expect(canvas.item(2)).toBe(rect2); expect(canvas.item(0)).toBe(rect3); canvas.sendObjectBackwards(rect3); // 3 stays at the same position — [3, 1, 2] expect(canvas.item(1)).toBe(rect1); expect(canvas.item(2)).toBe(rect2); expect(canvas.item(0)).toBe(rect3); canvas.sendObjectBackwards(rect2); expect(canvas.item(2)).toBe(rect1); expect(canvas.item(1)).toBe(rect2); expect(canvas.item(0)).toBe(rect3); canvas.sendObjectBackwards(rect2); expect(canvas.item(2)).toBe(rect1); expect(canvas.item(0)).toBe(rect2); expect(canvas.item(1)).toBe(rect3); }); it('brings objects forward in the stack', () => { expect(canvas.bringObjectForward).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); const rect3 = makeRect(); canvas.add(rect1, rect2, rect3); // initial position — [ 1, 2, 3 ] expect(canvas.item(0)).toBe(rect1); expect(canvas.item(1)).toBe(rect2); expect(canvas.item(2)).toBe(rect3); canvas.bringObjectForward(rect1); // 1 moves one way up — [ 2, 1, 3 ] expect(canvas.item(1)).toBe(rect1); expect(canvas.item(0)).toBe(rect2); expect(canvas.item(2)).toBe(rect3); canvas.bringObjectForward(rect1); // 1 moves one way up again — [ 2, 3, 1 ] expect(canvas.item(2)).toBe(rect1); expect(canvas.item(0)).toBe(rect2); expect(canvas.item(1)).toBe(rect3); canvas.bringObjectForward(rect1); // 1 is already all the way on top and so doesn't change position — [ 2, 3, 1 ] expect(canvas.item(2)).toBe(rect1); expect(canvas.item(0)).toBe(rect2); expect(canvas.item(1)).toBe(rect3); canvas.bringObjectForward(rect3); // 3 moves one way up — [ 2, 1, 3 ] expect(canvas.item(1)).toBe(rect1); expect(canvas.item(0)).toBe(rect2); expect(canvas.item(2)).toBe(rect3); }); it('sets active object and tracks it', () => { expect(canvas.setActiveObject).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); canvas.add(rect1, rect2); expect(canvas.setActiveObject(rect1), 'selected').toBeTruthy(); expect(rect1 === canvas._activeObject).toBeTruthy(); expect(canvas.setActiveObject(rect2), 'selected').toBeTruthy(); expect(canvas.setActiveObject(rect2), 'no effect').toBeFalsy(); expect(rect2 === canvas._activeObject).toBeTruthy(); }); it('gets active object', () => { expect(canvas.getActiveObject).toBeTypeOf('function'); expect( canvas.getActiveObject(), 'should initially be undefined', ).toBeUndefined(); const rect1 = makeRect(); const rect2 = makeRect(); canvas.add(rect1, rect2); canvas.setActiveObject(rect1); expect(canvas.getActiveObject()).toBe(rect1); canvas.setActiveObject(rect2); expect(canvas.getActiveObject()).toBe(rect2); }); it('sets and gets active object with groups', () => { expect( canvas.getActiveObject(), 'should initially be undefined', ).toBeUndefined(); const group = new Group([ makeRect({ left: 10, top: 10 }), makeRect({ left: 20, top: 20 }), ]); canvas.setActiveObject(group); expect(canvas.getActiveObject()).toBe(group); }); it('retrieves objects by index with item method', () => { expect(canvas.item).toBeTypeOf('function'); const rect1 = makeRect(); const rect2 = makeRect(); canvas.add(rect1, rect2); expect(canvas.item(0)).toBe(rect1); expect(canvas.item(1)).toBe(rect2); canvas.remove(canvas.item(0)); expect(canvas.item(0)).toBe(rect2); }); it('discards active object on ActiveSelection', () => { const group = new ActiveSelection([makeRect(), makeRect()], { canvas }); canvas.setActiveObject(group); canvas.discardActiveObject(); expect( canvas.getActiveObject(), 'removing active group sets it to undefined', ).toBeUndefined(); }); it('discards active object with internal method', () => { canvas.add(makeRect()); canvas.setActiveObject(canvas.item(0)); expect(canvas._discardActiveObject(), 'discarded').toBeTruthy(); expect(canvas._discardActiveObject(), 'no effect').toBeFalsy(); expect(canvas.getActiveObject()).toBeUndefined(); }); it('cleans up transform when discarding active object', () => { const e = createPointerEvent({ clientX: 5, clientY: 5, target: canvas.upperCanvasEl, }); const target = makeRect(); canvas.add(target); canvas.setActiveObject(target); canvas._setupCurrentTransform(e, target, true); expect(canvas._currentTransform, 'transform should be set').toBeTruthy(); target.isMoving = true; canvas._discardActiveObject(); expect(canvas._currentTransform, 'transform should be cleared').toBeFalsy(); expect(target.isMoving, 'moving flag should have been negated').toBeFalsy(); expect(canvas.getActiveObject()).toBeUndefined(); }); it('discards active object and fires events', () => { expect(canvas.discardActiveObject).toBeTypeOf('function'); canvas.add(makeRect()); canvas.setActiveObject(canvas.item(0)); const group = new Group([ makeRect({ left: 10, top: 10 }), makeRect({ left: 20, top: 20 }), ]); canvas.setActiveObject(group); const eventsFired: Record = { selectionCleared: false, }; canvas.on('selection:cleared', () => { eventsFired.selectionCleared = true; }); expect(canvas.discardActiveObject(), 'deselected').toBeTruthy(); expect(canvas.getActiveObject(), 'no active object').toBeUndefined(); expect(canvas.discardActiveObject(), 'no effect').toBeFalsy(); expect(canvas.getActiveObject()).toBeUndefined(); for (const prop in eventsFired) { expect(eventsFired[prop]).toBeTruthy(); } }); it('refuses to discard active object when onDeselect returns true', () => { const rect = makeRect(); rect.onDeselect = () => true; canvas.setActiveObject(rect); expect(canvas.discardActiveObject(), 'no effect').toBeFalsy(); expect(canvas.getActiveObject() === rect, 'active object').toBeTruthy(); canvas.clear(); expect(canvas.getActiveObject(), 'cleared the stubborn ref').toBeFalsy(); }); it('calculates complexity based on number of objects', () => { expect(canvas.complexity).toBeTypeOf('function'); expect(canvas.complexity()).toBe(0); canvas.add(makeRect()); expect(canvas.complexity()).toBe(1); canvas.add(makeRect(), makeRect()); expect(canvas.complexity()).toBe(3); }); it('converts to string representation', () => { expect(canvas.toString).toBeTypeOf('function'); expect(canvas.toString()).toBe('#'); canvas.add(makeRect()); expect(canvas.toString()).toBe('#'); }); it('produces same SVG with or without active selection', () => { const rect = new Rect({ width: 50, height: 50, left: 100, top: 100 }); const circle = new Circle({ radius: 50, left: 50, top: 50 }); canvas.add(rect, circle); const svg = canvas.toSVG(); const activeSelection = new ActiveSelection(); activeSelection.add(rect, circle); canvas.setActiveObject(activeSelection); const svgWithActiveGroup = canvas.toSVG(); expect(svg).toBe(svgWithActiveGroup); }); describe.each([true, false])( 'set dimensions with enableRetinaScaling=%s', (enableRetinaScaling) => { it('sets and restores dimensions correctly', async () => { const el = getFabricDocument().createElement('canvas'); const parentEl = getFabricDocument().createElement('div'); el.width = 200; el.height = 200; parentEl.className = 'rootNode'; parentEl.appendChild(el); const dpr = 1.25; config.configure({ devicePixelRatio: dpr }); expect( parentEl.firstChild, 'canvas should be appended at partentEl', ).toBe(el); expect(parentEl.childNodes.length, 'parentEl has 1 child only').toBe(1); el.style.position = 'relative'; const elStyle = el.style.cssText; expect(elStyle, 'el style should not be empty').toBe( 'position: relative;', ); const canvasObj = new Canvas(el, { enableRetinaScaling, renderOnAddRemove: false, }); canvasObj.setDimensions({ width: 500, height: 500 }); expect( // @ts-expect-error -- private prop canvasObj.elements._originalCanvasStyle, 'saved original canvas style for disposal', ).toBe(elStyle); expect(el.style.cssText, 'canvas el style has been changed').not.toBe( // @ts-expect-error -- private prop canvasObj.elements._originalCanvasStyle, ); expect(el.width, 'expected width').toBe( 500 * (enableRetinaScaling ? dpr : 1), ); expect(el.height, 'expected height').toBe( 500 * (enableRetinaScaling ? dpr : 1), ); expect(canvasObj.upperCanvasEl.width, 'expected width').toBe( 500 * (enableRetinaScaling ? dpr : 1), ); expect(canvasObj.upperCanvasEl.height, 'expected height').toBe( 500 * (enableRetinaScaling ? dpr : 1), ); await canvasObj.dispose(); expect( // @ts-expect-error -- private prop canvasObj.elements._originalCanvasStyle, 'removed original canvas style', ).toBe(undefined); expect(el.style.cssText, 'restored original canvas style').toBe( elStyle, ); expect(el.width, 'restored width').toBe(500); expect(el.height, 'restored height').toBe(500); }); }, ); it('clones the canvas and its contents', async () => { expect(canvas.clone).toBeTypeOf('function'); canvas.add( new Rect({ width: 100, height: 110, top: 120, left: 130, fill: 'rgba(0,1,2,0.3)', }), ); const canvasData = JSON.stringify(canvas); const clone = await canvas.clone([]); expect(clone).toBeInstanceOf(Canvas); expect( JSON.stringify(clone), 'data on cloned canvas should be identical', ).toBe(canvasData); expect(clone.getWidth()).toBe(canvas.getWidth()); expect(clone.getHeight()).toBe(canvas.getHeight()); clone.renderAll(); }); it('clones the canvas without data', () => { expect(canvas.cloneWithoutData).toBeTypeOf('function'); canvas.add( new Rect({ width: 100, height: 110, top: 120, left: 130, fill: 'rgba(0,1,2,0.3)', }), ); const clone = canvas.cloneWithoutData(); expect(clone).toBeInstanceOf(Canvas); expect(clone.toJSON(), 'data on cloned canvas should be empty').toEqual( EMPTY_JSON, ); expect(clone.getWidth()).toBe(canvas.getWidth()); expect(clone.getHeight()).toBe(canvas.getHeight()); clone.renderAll(); }); it('gets and sets width', () => { expect(canvas.getWidth).toBeTypeOf('function'); expect(canvas.getWidth()).toBe(600); canvas.setDimensions({ width: 444 }); expect(canvas.getWidth()).toBe(444); expect(canvas.lowerCanvasEl.style.width).toBe('444px'); }); it('gets and sets height', () => { expect(canvas.getHeight).toBeTypeOf('function'); expect(canvas.getHeight()).toBe(600); canvas.setDimensions({ height: 765 }); expect(canvas.getHeight()).toBe(765); expect(canvas.lowerCanvasEl.style.height).toBe('765px'); }); it('sets width with cssOnly option', () => { canvas.setDimensions({ width: 123 }); canvas.setDimensions({ width: '100%' }, { cssOnly: true }); expect( canvas.lowerCanvasEl.style.width, 'Should be as the css only value', ).toBe('100%'); expect( canvas.upperCanvasEl.style.width, 'Should be as the css only value', ).toBe('100%'); expect( canvas.wrapperEl.style.width, 'Should be as the css only value', ).toBe('100%'); expect(canvas.getWidth(), 'Should be as the none css only value').toBe(123); }); it('sets height with cssOnly option', () => { canvas.setDimensions({ height: 123 }); canvas.setDimensions({ height: '100%' }, { cssOnly: true }); expect( canvas.lowerCanvasEl.style.height, 'Should be as the css only value', ).toBe('100%'); expect( canvas.upperCanvasEl.style.height, 'Should be as the css only value', ).toBe('100%'); expect( canvas.wrapperEl.style.height, 'Should be as the css only value', ).toBe('100%'); expect(canvas.getHeight(), 'Should be as the none css only value').toBe( 123, ); }); it('sets width with backstoreOnly option', () => { canvas.setDimensions({ width: 123 }); canvas.setDimensions({ width: 500 }, { backstoreOnly: true }); expect( canvas.lowerCanvasEl.style.width, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect( canvas.upperCanvasEl.style.width, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect( canvas.wrapperEl.style.width, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect(canvas.getWidth(), 'Should be as the backstore only value').toBe( 500, ); }); it('sets height with backstoreOnly option', () => { canvas.setDimensions({ height: 123 }); canvas.setDimensions({ height: 500 }, { backstoreOnly: true }); expect( canvas.lowerCanvasEl.style.height, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect( canvas.upperCanvasEl.style.height, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect( canvas.wrapperEl.style.height, 'Should be as none backstore only value + "px"', ).toBe('123px'); expect(canvas.getHeight(), 'Should be as the backstore only value').toBe( 500, ); }); it('sets up current transform based on interaction point', () => { expect(canvas._setupCurrentTransform).toBeTypeOf('function'); const rect = new Rect({ left: 100, top: 100, width: 50, height: 50 }); canvas.add(rect); const canvasOffset = canvas.calcOffset(); let eventStub = createPointerEvent({ clientX: canvasOffset.left + 100, clientY: canvasOffset.top + 100, target: canvas.upperCanvasEl, }); canvas.setActiveObject(rect); const targetCorner = rect.findControl(canvas.getViewportPoint(eventStub)); rect.__corner = targetCorner ? targetCorner.key : undefined; canvas._setupCurrentTransform(eventStub, rect, false); let t = canvas._currentTransform!; expect(t.target, 'should have rect as a target').toBe(rect); expect(t.action, 'should target inside rect and setup drag').toBe('drag'); expect(t.corner, 'no corner selected').toBe(''); expect(t.originX, 'no origin change for drag').toBe(rect.originX); expect(t.originY, 'no origin change for drag').toBe(rect.originY); eventStub = createPointerEvent({ clientX: canvasOffset.left + rect.oCoords.tl.corner.tl.x + 1, clientY: canvasOffset.top + rect.oCoords.tl.corner.tl.y + 1, target: canvas.upperCanvasEl, }); rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key; canvas._setupCurrentTransform(eventStub, rect, false); t = canvas._currentTransform!; expect(t.target, 'should have rect as a target').toBe(rect); expect( t.action, 'should setup drag since the object was not selected', ).toBe('drag'); expect(t.corner, 'tl selected').toBe('tl'); expect(t.shiftKey, 'shift was not pressed').toBe(undefined); const alreadySelected = true; rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key; canvas._setupCurrentTransform(eventStub, rect, alreadySelected); t = canvas._currentTransform!; expect(t.target, 'should have rect as a target').toBe(rect); expect(t.action, 'should target a corner and setup scale').toBe('scale'); expect(t.corner, 'tl selected').toBe('tl'); expect(t.originX, 'origin in opposite direction').toBe('right'); expect(t.originY, 'origin in opposite direction').toBe('bottom'); expect(t.shiftKey, 'shift was not pressed').toBe(undefined); eventStub = createPointerEvent({ clientX: canvasOffset.left + rect.left - 2 - rect.width / 2, clientY: canvasOffset.top + rect.top, target: canvas.upperCanvasEl, shiftKey: true, }); rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key; canvas._setupCurrentTransform(eventStub, rect, alreadySelected); t = canvas._currentTransform!; expect(t.target, 'should have rect as a target').toBe(rect); expect(t.action, 'should target a corner and setup skew').toBe('skewY'); expect(t.shiftKey, 'shift was pressed').toBe(true); expect(t.corner, 'ml selected').toBe('ml'); expect(t.originX, 'origin in opposite direction').toBe('right'); // to be replaced with new api test // eventStub = { // clientX: canvasOffset.left + rect.oCoords.mtr.x, // clientY: canvasOffset.top + rect.oCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect, alreadySelected); // t = canvas._currentTransform; // expect(t.target, 'should have rect as a target').toBe(rect); // expect(t.action, 'should target a corner and setup rotate').toBe('mtr'); // expect(t.corner, 'mtr selected').toBe('mtr'); // expect(t.originX, 'origin in center').toBe('center'); // expect(t.originY, 'origin in center').toBe('center'); // canvas._currentTransform = false; }); // TODO: do these tests make sense in vitest? // QUnit.test('_rotateObject', function(assert) { // assert.ok(typeof canvas._rotateObject === 'function'); // var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 }); // canvas.add(rect); // var canvasEl = canvas.getElement(), // canvasOffset = fabric.util.getElementOffset(canvasEl); // var eventStub = { // clientX: canvasOffset.left + rect.oCoords.mtr.x, // clientY: canvasOffset.top + rect.oCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect); // var rotated = canvas._rotateObject(30, 30, 'equally'); // assert.equal(rotated, true, 'return true if a rotation happened'); // rotated = canvas._rotateObject(30, 30); // assert.equal(rotated, false, 'return true if no rotation happened'); // }); // // QUnit.test('_rotateObject do not change origins', function(assert) { // assert.ok(typeof canvas._rotateObject === 'function'); // var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50, originX: 'right', originY: 'bottom' }); // canvas.add(rect); // var canvasEl = canvas.getElement(), // canvasOffset = fabric.util.getElementOffset(canvasEl); // var eventStub = { // clientX: canvasOffset.left + rect.oCoords.mtr.x, // clientY: canvasOffset.top + rect.oCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect); // assert.equal(rect.originX, 'right'); // assert.equal(rect.originY, 'bottom'); // }); // // QUnit.test('backgroundImage', function(assert) { // var done = assert.async(); // assert.deepEqual('', canvas.backgroundImage); // canvas.setBackgroundImage('../../assets/pug.jpg'); // setTimeout(function() { // assert.ok(typeof canvas.backgroundImage == 'object'); // assert.ok(/pug\.jpg$/.test(canvas.backgroundImage.src)); // assert.deepEqual(canvas.toJSON(), { // "objects": [], // "background": "rgba(0, 0, 0, 0)", // "backgroundImage": (fabric.getFabricDocument().location.protocol + // '//' + // fabric.getFabricDocument().location.hostname + // ((fabric.getFabricDocument().location.port === '' || parseInt(fabric.getFabricDocument().location.port, 10) === 80) // ? '' // : (':' + fabric.getFabricDocument().location.port)) + // '/assets/pug.jpg'), // "backgroundImageOpacity": 1, // "backgroundImageStretch": true // }); // done(); // }, 1000); // }); // // QUnit.skip('fxRemove', function(assert) { // var done = assert.async(); // assert.ok(typeof canvas.fxRemove === 'function'); // // var rect = new fabric.Rect(); // canvas.add(rect); // // var callbackFired = false; // function onComplete() { // callbackFired = true; // } // // assert.equal(canvas.item(0), rect); // assert.ok(typeof canvas.fxRemove(rect, { onComplete: onComplete }).abort === 'function', 'should return animation abort function'); // // setTimeout(function() { // assert.equal(canvas.item(0), undefined); // assert.ok(callbackFired); // done(); // }, 1000); // }); describe.each([true, false])( 'isTargetTransparent with objectCaching = %s', (objectCaching) => { function testPixelDetection( canvas: Canvas, target: FabricObject, expectedHits: { start: number; end: number; message: string; transparent: boolean; }[], ) { function execute(context = '') { expectedHits.forEach(({ start, end, message, transparent }) => { // make less sensitive by skipping edges for firefox 110 const round = 0; for (let index = start + round; index < end - round; index++) { expect( canvas.isTargetTransparent(target, index, index), `checking transparency of (${index}, ${index}), expected to be ${transparent}, ${message}, ${context}`, ).toBe(transparent); } }); } execute(); canvas.setActiveObject(target); execute('target is selected'); } it('detects transparent regions correctly', () => { const rect = new Rect({ width: 10, height: 10, strokeWidth: 4, stroke: 'red', fill: '', top: 7, left: 7, objectCaching, }); canvas.add(rect); testPixelDetection(canvas, rect, [ { start: -5, end: 0, message: 'outside', transparent: true }, { start: 0, end: 4, message: 'stroke', transparent: false }, { start: 4, end: 10, message: 'fill', transparent: true }, { start: 10, end: 14, message: 'stroke', transparent: false }, { start: 14, end: 20, message: 'outside', transparent: true }, ]); }); it('detects transparent regions correctly with viewport transform', () => { const rect = new Rect({ width: 10, height: 10, strokeWidth: 4, stroke: 'red', fill: '', top: 7, left: 7, objectCaching, }); canvas.add(rect); canvas.setViewportTransform([2, 0, 0, 2, 0, 0]); testPixelDetection(canvas, rect, [ { start: -5, end: 0, message: 'outside', transparent: true }, { start: 0, end: 8, message: 'stroke', transparent: false }, { start: 8, end: 20, message: 'fill', transparent: true }, { start: 20, end: 28, message: 'stroke', transparent: false }, { start: 28, end: 40, message: 'outside', transparent: true }, ]); }); it('detects transparent regions correctly with viewport transform and tolerance', () => { const rect = new Rect({ width: 10, height: 10, strokeWidth: 4, stroke: 'red', fill: '', top: 7, left: 7, objectCaching, }); canvas.add(rect); canvas.setTargetFindTolerance(5); canvas.setViewportTransform([2, 0, 0, 2, 0, 0]); testPixelDetection(canvas, rect, [ { start: -10, end: -5, message: 'outside', transparent: true }, { start: -5, end: 0, message: 'stroke tolerance not affected by vpt', transparent: false, }, { start: 0, end: 8, message: 'stroke', transparent: false }, { start: 8, end: 13, message: 'stroke tolerance not affected by vpt', transparent: false, }, { start: 13, end: 15, message: 'fill', transparent: true }, { start: 15, end: 20, message: 'stroke tolerance not affected by vpt', transparent: false, }, { start: 20, end: 28, message: 'stroke', transparent: false }, { start: 28, end: 33, message: 'stroke tolerance not affected by vpt', transparent: false, }, { start: 33, end: 40, message: 'outside', transparent: true }, ]); }); }, ); it('provides access to top context with getTopContext', () => { expect(canvas.getTopContext).toBeTypeOf('function'); expect(canvas.getTopContext(), 'it just returns contextTop').toBe( canvas.contextTop, ); }); it('determines when transformations should be centered', () => { expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, 'someAction', false), 'a non standard action does not center scale', ).toBe(false); expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, 'someAction', true), 'a non standard action will center scale if altKey is true', ).toBe(true); canvas.centeredScaling = true; ['scale', 'scaleX', 'scaleY', 'resizing'].forEach((action) => { expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, action, false), action + ' standard action will center scale if canvas.centeredScaling is true and no centeredKey pressed', ).toBe(true); }); ['scale', 'scaleX', 'scaleY', 'resizing'].forEach((action) => { expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, action, true), action + ' standard action will NOT center scale if canvas.centeredScaling is true and centeredKey is pressed', ).toBe(false); }); expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, 'rotate', false), 'rotate standard action will NOT center scale if canvas.centeredScaling is true', ).toBe(false); canvas.centeredRotation = true; expect( // @ts-expect-error -- private method canvas._shouldCenterTransform({}, 'rotate', false), 'rotate standard action will center scale if canvas.centeredRotation is true', ).toBe(true); }); }); function initActiveSelection( canvas: Canvas, activeObject: FabricObject, target: FabricObject, multiSelectionStacking?: MultiSelectionStacking, ) { classRegistry.setClass( class TextActiveSelection extends ActiveSelection { static ownDefaults = { multiSelectionStacking, }; constructor( objects: FabricObject[], options: Record, ) { super(objects, { ...TextActiveSelection.ownDefaults, ...options }); } }, ); canvas.setActiveObject(activeObject); // @ts-expect-error -- protected method canvas.handleMultiSelection( { clientX: 0, clientY: 0, [canvas.selectionKey as string]: true, } as unknown as TPointerEvent, target, ); } function updateActiveSelection( canvas: Canvas, existing: FabricObject[], target: FabricObject | null, multiSelectionStacking?: MultiSelectionStacking, ) { const activeSelection = new ActiveSelection([], { canvas }); if (multiSelectionStacking) { activeSelection.multiSelectionStacking = multiSelectionStacking; } activeSelection.add(...existing); canvas.setActiveObject(activeSelection); // @ts-expect-error -- protected method canvas.handleMultiSelection( { clientX: 1, clientY: 1, [canvas.selectionKey as string]: true, target: canvas.upperCanvasEl, } as unknown as TPointerEvent, target || activeSelection, ); } function setGroupSelector( canvas: Canvas, { x = 0, y = 0, deltaX = 0, deltaY = 0 } = {}, ) { // @ts-expect-error -- protected member canvas._groupSelector = { x, y, deltaX, deltaY, }; }