UNPKG

fabric

Version:

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

1,792 lines (1,470 loc) 52.3 kB
import { Shadow } from '../../Shadow'; import { Rect } from '../Rect'; import { FabricObject } from './Object'; import { Group } from '../Group'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { ObjectEvents } from '../../../fabric'; import { ActiveSelection, Canvas, config, FabricImage, Point, runningAnimations, StaticCanvas, version, Object, } from '../../../fabric'; import { toFixed } from '../../util'; describe('Object', () => { const canvas = new StaticCanvas(undefined, { enableRetinaScaling: false }); afterEach(() => { config.configure({ perfLimitSizeTotal: 2097152, maxCacheSideLimit: 4096, minCacheSideLimit: 256, devicePixelRatio: 1, }); canvas.enableRetinaScaling = false; canvas.setZoom(1); canvas.clear(); canvas.backgroundColor = Canvas.prototype.backgroundColor; canvas.calcOffset(); }); it('tests constructor & properties', () => { expect(typeof FabricObject).toBe('function'); const cObj = new FabricObject(); expect(cObj).toBeDefined(); expect(cObj instanceof FabricObject).toBe(true); expect(cObj.constructor).toBe(FabricObject); expect((cObj.constructor as typeof FabricObject).type).toBe('FabricObject'); expect(cObj.includeDefaultValues).toBe(true); //TODO: Add message 'object caching default value' expect(cObj.objectCaching).toBe(true); }); it('rotate with centered rotation', () => { const fObj = new FabricObject({ originX: 'left', centeredRotation: true, width: 10, height: 10, strokeWidth: 0, }); // test starting defaul values before change expect(fObj.angle).toBe(0); expect(fObj.top).toBe(0); expect(fObj.left).toBe(0); fObj.rotate(180); // test that angle has been changed expect(fObj.angle).toBe(180); // test that top changed because of centered rotation expect(fObj.top).toBe(0); // test that left changed because of centered rotation expect(fObj.left).toBe(10); }); it('rotate with origin rotation', () => { const fObj = new FabricObject({ centeredRotation: false, width: 10, height: 10, strokeWidth: 0, }); // test starting defaul values before change expect(fObj.angle).toBe(0); expect(fObj.top).toBe(0); expect(fObj.left).toBe(0); fObj.rotate(180); // test that angle has been changed expect(fObj.angle).toBe(180); // top and left are still 0, 0 expect(fObj.top).toBe(0); expect(fObj.left).toBe(0); }); it('rotate with centered rotation but origin set to center', () => { const fObj = new FabricObject({ centeredRotation: true, originX: 'center', originY: 'center', width: 10, height: 10, strokeWidth: 0, }); // test starting defaul values before change expect(fObj.angle).toBe(0); expect(fObj.top).toBe(0); expect(fObj.left).toBe(0); fObj.rotate(180); // test that angle has been changed expect(fObj.angle).toBe(180); // test that left is unchanged because of origin being center expect(fObj.top).toBe(0); expect(fObj.left).toBe(0); }); describe('needsItsOwnCache', () => { it('returns false for default values', () => { const rect = new Rect({ width: 100, height: 100 }); expect(rect.needsItsOwnCache()).toBe(false); }); it('returns true when a clipPath is present', () => { const rect = new Rect({ width: 100, height: 100 }); rect.clipPath = new Rect({ width: 50, height: 50 }); expect(rect.needsItsOwnCache()).toBe(true); }); it('returns true when paintFirst is stroke and there is a shadow', () => { const rect = new Rect({ width: 100, height: 100 }); rect.paintFirst = 'stroke'; rect.stroke = 'black'; rect.shadow = new Shadow({ color: 'green' }); expect(rect.needsItsOwnCache()).toBe(true); }); it('returns false when paintFirst is stroke and there is no shadow', () => { const rect = new Rect({ width: 100, height: 100 }); rect.paintFirst = 'stroke'; rect.stroke = 'black'; rect.shadow = null; expect(rect.needsItsOwnCache()).toBe(false); }); it('returns false when paintFirst is stroke but no stroke', () => { const rect = new Rect({ width: 100, height: 100 }); rect.paintFirst = 'stroke'; rect.stroke = ''; rect.shadow = new Shadow({ color: 'green' }); expect(rect.needsItsOwnCache()).toBe(false); }); it('returns false when paintFirst is stroke but no fill', () => { const rect = new Rect({ width: 100, height: 100 }); rect.paintFirst = 'stroke'; rect.stroke = 'black'; rect.fill = ''; rect.shadow = new Shadow({ color: 'green' }); expect(rect.needsItsOwnCache()).toBe(false); }); }); describe('set method and dirty flag bubbling', () => { it('when dirty is true it bubbles', () => { const rect = new Rect({ width: 100, height: 100 }); const group = new Group([rect]); group.dirty = false; expect(group.dirty).toBe(false); rect.set('dirty', true); expect(group.dirty).toBe(true); }); it('when dirty is false it does not bubble', () => { const rect = new Rect({ width: 100, height: 100 }); const group = new Group([rect]); group.dirty = true; expect(group.dirty).toBe(true); rect.set('dirty', false); expect(group.dirty).toBe(true); }); it('when dirty is true it bubbles to the parent', () => { const rect = new Rect({ width: 100, height: 100 }); rect.group = new Group(); rect.parent = new Group(); rect.group.dirty = false; rect.parent.dirty = false; rect.dirty = false; rect.set('dirty', true); expect(rect.group.dirty).toBe(false); expect(rect.parent.dirty).toBe(true); }); }); it('test strokeDashArray with an odd number of elements.', () => { const dashArrayBase = [1]; const ctx = { setLineDash: vi.fn(), } as unknown as CanvasRenderingContext2D; const obj = new FabricObject({ strokeDashArray: [1], }); obj._setLineDash(ctx, dashArrayBase); expect(ctx.setLineDash).toHaveBeenCalledWith(dashArrayBase); expect(obj.strokeDashArray).toEqual([1]); }); it('get', () => { const cObj = new FabricObject({ left: 11, top: 22, width: 50, height: 60, opacity: 0.7, }); expect(cObj.get('left'), 'should get left property').toBe(11); expect(cObj.get('top'), 'should get top property').toBe(22); expect(cObj.get('width'), 'should get width property').toBe(50); expect(cObj.get('height'), 'should get height property').toBe(60); expect(cObj.get('opacity'), 'should get opacity property').toBe(0.7); }); it('set', () => { const cObj = new FabricObject({ left: 11, top: 22, width: 50, height: 60, opacity: 0.7, }); cObj.set('left', 12); cObj.set('top', 23); cObj.set('width', 51); cObj.set('height', 61); cObj.set('opacity', 0.5); expect(cObj.get('left'), 'left property should be updated').toBe(12); expect(cObj.get('top'), 'top property should be updated').toBe(23); expect(cObj.get('width'), 'width property should be updated').toBe(51); expect(cObj.get('height'), 'height property should be updated').toBe(61); expect(cObj.get('opacity'), 'opacity property should be updated').toBe(0.5); expect(cObj.set('opacity', 0.5), 'set should be chainable').toBe(cObj); }); it('set with object of prop/values', () => { const cObj = new FabricObject({}); expect( cObj.set({ width: 99, height: 88, fill: 'red' }), 'set should be chainable', ).toBe(cObj); expect(cObj.get('fill'), 'fill should be set').toBe('red'); expect(cObj.get('width'), 'width should be set').toBe(99); expect(cObj.get('height'), 'height should be set').toBe(88); }); // it('Dynamically generated accessors', () => { // const cObj = new FabricObject({}); // // expect(typeof cObj.getWidth, 'getWidth should be a function').toBe('function'); // expect(typeof cObj.setWidth, 'setWidth should be a function').toBe('function'); // // expect(typeof cObj.getFill, 'getFill should be a function').toBe('function'); // expect(typeof cObj.setFill, 'setFill should be a function').toBe('function'); // // expect(cObj.setFill('red'), 'setFill should be chainable').toBe(cObj); // expect(cObj.getFill(), 'getFill should return set value').toBe('red'); // // cObj.setScaleX(2.3); // expect(cObj.getScaleX(), 'getScaleX should return set value').toBe(2.3); // // cObj.setOpacity(0.123); // expect(cObj.getOpacity(), 'getOpacity should return set value').toBe(0.123); // }); it('stateProperties', () => { const cObj = new FabricObject(); expect(cObj.constructor, 'stateProperties should exist').toHaveProperty( 'stateProperties', ); expect( 'stateProperties' in cObj.constructor && (cObj.constructor.stateProperties as string[]).length > 0, 'stateProperties should not be empty', ).toBeTruthy(); }); it('transform', () => { const cObj = new FabricObject(); expect(cObj.transform, 'transform should be a function').toBeTypeOf( 'function', ); }); it('toJSON', () => { const emptyObjectJSON = '{"type":"FabricObject","version":"' + version + '","originX":"center","originY":"center","left":0,"top":0,"width":0,"height":0,"fill":"rgb(0,0,0)",' + '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"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}'; const augmentedJSON = '{"type":"FabricObject","version":"' + version + '","originX":"center","originY":"center","left":0,"top":0,"width":122,"height":0,"fill":"rgb(0,0,0)",' + '"stroke":null,"strokeWidth":1,"strokeDashArray":[5,2],"strokeLineCap":"round","strokeDashOffset":0,"strokeLineJoin":"bevel","strokeUniform":false,"strokeMiterLimit":5,' + '"scaleX":1.3,"scaleY":1,"angle":0,"flipX":false,"flipY":true,"opacity":0.88,' + '"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over",' + '"skewX":0,"skewY":0}'; const cObj = new FabricObject(); expect(cObj.toJSON, 'toJSON should be a function').toBeTypeOf('function'); expect( JSON.stringify(cObj.toJSON()), 'default object JSON representation', ).toBe(emptyObjectJSON); expect( JSON.stringify(cObj), 'stringified object should equal JSON representation', ).toBe(emptyObjectJSON); cObj .set('opacity', 0.88) .set('scaleX', 1.3) .set('width', 122) .set('flipY', true) .set('strokeDashArray', [5, 2]) .set('strokeLineCap', 'round') .set('strokeLineJoin', 'bevel') .set('strokeMiterLimit', 5); expect( JSON.stringify(cObj.toJSON()), 'augmented object JSON representation', ).toBe(augmentedJSON); }); it('toObject', () => { const emptyObjectRepr = { version: version, type: 'FabricObject', originX: 'center', originY: 'center', left: 0, top: 0, width: 0, height: 0, 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, strokeUniform: false, }; const augmentedObjectRepr = { version: version, type: 'FabricObject', originX: 'center', originY: 'center', left: 10, top: 20, width: 30, height: 40, fill: 'rgb(0,0,0)', stroke: null, strokeWidth: 1, strokeDashArray: [5, 2], strokeLineCap: 'round', strokeDashOffset: 0, strokeLineJoin: 'bevel', strokeMiterLimit: 5, scaleX: 1, scaleY: 1, angle: 0, flipX: true, flipY: false, opacity: 0.13, shadow: null, visible: true, backgroundColor: '', fillRule: 'nonzero', paintFirst: 'fill', globalCompositeOperation: 'source-over', skewX: 0, skewY: 0, strokeUniform: false, }; const cObj = new FabricObject(); expect(cObj.toObject(), 'should match empty object representation').toEqual( emptyObjectRepr, ); cObj .set('left', 10) .set('top', 20) .set('width', 30) .set('height', 40) .set('flipX', true) .set('opacity', 0.13) .set('strokeDashArray', [5, 2]) .set('strokeLineCap', 'round') .set('strokeLineJoin', 'bevel') .set('strokeMiterLimit', 5); expect( cObj.toObject(), 'should match augmented object representation', ).toEqual(augmentedObjectRepr); const fractionalValue = 166.66666666666666; const testedProperties = 'left top width height'.split(' '); const fractionDigitsDefault = 2; function testFractionDigits( fractionDigits: number, expectedValue: unknown, ) { config.configure({ NUM_FRACTION_DIGITS: fractionDigits }); testedProperties.forEach(function (property) { cObj.set(property, fractionalValue); expect( cObj.toObject()[property], `value of ${property} should have ${fractionDigits} fractional digits`, ).toBe(expectedValue); }); config.configure({ NUM_FRACTION_DIGITS: fractionDigitsDefault }); } testFractionDigits(2, 166.67); testFractionDigits(3, 166.667); testFractionDigits(0, 167); }); it('toObject without default values', () => { const emptyObjectRepr = { version: version, type: 'FabricObject', top: 0, left: 0, }; const augmentedObjectRepr = { version: version, type: 'FabricObject', left: 10, top: 20, width: 30, height: 40, strokeDashArray: [5, 2], strokeLineCap: 'round', strokeLineJoin: 'bevel', strokeMiterLimit: 5, flipX: true, opacity: 0.13, }; const cObj = new FabricObject(); cObj.includeDefaultValues = false; expect(cObj.toObject(), 'top and left are always maintained').toEqual( emptyObjectRepr, ); cObj .set('left', 10) .set('top', 20) .set('width', 30) .set('height', 40) .set('flipX', true) .set('opacity', 0.13) .set('strokeDashArray', [5, 2]) .set('strokeLineCap', 'round') .set('strokeLineJoin', 'bevel') .set('strokeMiterLimit', 5); const toObjectObj = cObj.toObject(); expect(toObjectObj, 'non-default values should be present').toEqual( augmentedObjectRepr, ); expect( toObjectObj.strokeDashArray, 'strokeDashArray should be a new array', ).not.toBe(augmentedObjectRepr.strokeDashArray); expect( toObjectObj.strokeDashArray, 'strokeDashArray should equal the original array', ).toEqual(augmentedObjectRepr.strokeDashArray); }); it('toDatalessObject', () => { const cObj = new FabricObject(); expect( cObj.toDatalessObject, 'toDatalessObject should be a function', ).toBeTypeOf('function'); expect( cObj.toDatalessObject(), 'toDatalessObject should equal toObject', ).toEqual(cObj.toObject()); }); it('toString', () => { class Moo extends FabricObject { static type = 'Moo'; } const cObj = new FabricObject(); expect(cObj.toString(), 'toString should return class name').toBe( '#<FabricObject>', ); expect( new Moo().toString(), 'toString should return custom class name', ).toBe('#<Moo>'); }); it('render', () => { const cObj = new FabricObject(); expect(cObj.render, 'render should be a function').toBeTypeOf('function'); }); it('scale', () => { const cObj = new FabricObject(); expect(cObj.scale, 'scale should be a function').toBeTypeOf('function'); expect(cObj.get('scaleX'), 'default scaleX should be 1').toBe(1); expect(cObj.get('scaleY'), 'default scaleY should be 1').toBe(1); cObj.scale(1.5); expect(cObj.get('scaleX'), 'scaleX should be updated').toBe(1.5); expect(cObj.get('scaleY'), 'scaleY should be updated').toBe(1.5); }); it('setOpacity', () => { const cObj = new FabricObject(); expect(cObj.get('opacity'), 'default opacity should be 1').toBe(1); cObj.set('opacity', 0.68); expect(cObj.get('opacity'), 'opacity should be updated').toBe(0.68); expect(cObj.set('opacity', 1), 'set should be chainable').toBe(cObj); }); it('getAngle', () => { const cObj = new FabricObject(); expect(cObj.get('angle'), 'default angle should be 0').toBe(0); cObj.rotate(45); expect(cObj.get('angle'), 'angle should be 45 after rotation').toBe(45); }); it('rotate what?', () => { const cObj = new FabricObject(); expect(cObj.get('angle'), 'default angle should be 0').toBe(0); expect(cObj.set('angle', 45), 'set should be chainable').toBe(cObj); expect(cObj.get('angle'), 'angle should be 45 after setting').toBe(45); }); it('clone', async () => { const cObj = new FabricObject({ left: 123, top: 456, opacity: 0.66 }); expect(cObj.clone, 'clone should be a function').toBeTypeOf('function'); const clone = await cObj.clone(); expect(clone.get('left'), 'clone should have same left').toBe(123); expect(clone.get('top'), 'clone should have same top').toBe(456); expect(clone.get('opacity'), 'clone should have same opacity').toBe(0.66); // augmenting clone properties should not affect original instance clone.set('left', 12).set('scaleX', 2.5).rotate(33); expect(cObj.get('left'), 'original left should not change').toBe(123); expect(cObj.get('scaleX'), 'original scaleX should not change').toBe(1); expect(cObj.get('angle'), 'original angle should not change').toBe(0); }); it('cloneAsImage', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); expect(cObj.cloneAsImage, 'cloneAsImage should be a function').toBeTypeOf( 'function', ); const image = cObj.cloneAsImage({}); expect(image, 'image should exist').toBeTruthy(); expect(image, 'image should be a FabricImage').toBeInstanceOf(FabricImage); expect(image.width, 'the image has same dimension of object').toBe(100); }); it('cloneAsImage with retina scaling enabled', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); config.configure({ devicePixelRatio: 2 }); const image = cObj.cloneAsImage({ enableRetinaScaling: true }); expect(image, 'image should exist').toBeTruthy(); expect(image, 'image should be a FabricImage').toBeInstanceOf(FabricImage); expect(image.width, 'the image has been scaled by retina').toBe(200); }); it('toCanvasElement', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, canvas: canvas, }); expect( cObj.toCanvasElement, 'toCanvasElement should be a function', ).toBeTypeOf('function'); const canvasEl = cObj.toCanvasElement(); expect(canvasEl.getContext, 'the element returned is a canvas').toBeTypeOf( 'function', ); expect(cObj.canvas, 'canvas ref should remain unchanged').toBe(canvas); }); it('toCanvasElement activeSelection', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); const cObj2 = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); canvas.add(cObj, cObj2); const activeSel = new ActiveSelection([cObj, cObj2], { canvas: canvas }); expect(cObj.canvas, 'canvas is the main one step 1').toBe(canvas); activeSel.toCanvasElement(); expect(cObj.canvas, 'canvas is the main one step 2').toBe(canvas); activeSel.removeAll(); expect(cObj.canvas, 'canvas is the main one step 3').toBe(canvas); }); it('toCanvasElement does not modify oCoords on zoomed canvas', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); canvas.setZoom(2); canvas.add(cObj); const originaloCoords = cObj.oCoords; const originalaCoords = cObj.aCoords; cObj.toCanvasElement(); expect(cObj.oCoords, 'cObj did not get object coords changed').toEqual( originaloCoords, ); expect(cObj.aCoords, 'cObj did not get absolute coords changed').toEqual( originalaCoords, ); }); it('toDataURL', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0, }); expect(cObj.toDataURL, 'toDataURL should be a function').toBeTypeOf( 'function', ); const dataURL = cObj.toDataURL(); expect(dataURL, 'dataURL should be a string').toBeTypeOf('string'); expect(dataURL.substring(0, 21), 'dataURL should start with PNG data').toBe( 'data:image/png;base64', ); try { const jpegDataURL = cObj.toDataURL({ format: 'jpeg' }); expect( jpegDataURL.substring(0, 22), 'JPEG dataURL should start with JPEG data', ).toBe('data:image/jpeg;base64'); } catch { // eslint-disable-next-line no-restricted-syntax -- fine in test console.log('jpeg toDataURL not supported'); } }); it('toDataURL & reference to canvas', () => { const cObj = new Rect({ width: 100, height: 100, fill: 'red', }); canvas.add(cObj); const objCanvas = cObj.canvas; cObj.toDataURL(); expect(objCanvas, 'canvas reference should be maintained').toBe( cObj.canvas, ); }); it('isType', () => { const cObj = new FabricObject(); expect(cObj.isType, 'isType should be a function').toBeTypeOf('function'); expect( cObj.isType('FabricObject'), 'object is a FabricObject', ).toBeTruthy(); expect(cObj.isType('object'), 'object is an object').toBeTruthy(); expect(cObj.isType('Rect'), 'object is not a Rect').toBeFalsy(); const rect = new Rect(); expect(rect.isType('Rect'), 'rect is a Rect').toBeTruthy(); expect( rect.isType('rect'), 'rect is a rect (case insensitive)', ).toBeTruthy(); expect(rect.isType('Object'), 'rect is not an Object').toBeFalsy(); expect( rect.isType('Object', 'Rect'), 'rect is a Rect or Object', ).toBeTruthy(); expect( rect.isType('Object', 'Circle'), 'rect is not a Circle or Object', ).toBeFalsy(); }); it('toggle', () => { const object = new FabricObject({ left: 100, top: 124, width: 210, height: 66, }); expect(object.toggle, 'toggle should be a function').toBeTypeOf('function'); object.set('flipX', false); expect(object.toggle('flipX'), 'toggle should be chainable').toBe(object); expect(object.get('flipX'), 'flipX should be toggled to true').toBe(true); object.toggle('flipX'); expect(object.get('flipX'), 'flipX should be toggled back to false').toBe( false, ); object.set('left', 112.45); object.toggle('left'); expect( object.get('left'), 'non boolean properties should not be affected', ).toBe(112.45); }); it.skip('straighten', () => { const object: FabricObject & { straighten?: () => void } = new FabricObject( { left: 100, top: 124, width: 210, height: 66, }, ); expect(object.straighten, 'straighten should be a function').toBeTypeOf( 'function', ); object.rotate(123.456); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to 90').toBe(90); object.rotate(97.111); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to 90').toBe(90); object.rotate(3.45); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to 0').toBe(0); object.rotate(-157); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to -180').toBe( -180, ); object.rotate(159); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to 180').toBe( 180, ); object.rotate(999); object.straighten!(); expect(object.get('angle'), 'angle should be straightened to 270').toBe( 270, ); }); it.skip('fxStraighten', async () => { const object: FabricObject & { fxStraighten?: (cb?: any) => { abort: unknown }; } = new FabricObject({ left: 20, top: 30, width: 40, height: 50, angle: 43, }); let onCompleteFired = false; const onComplete = function () { onCompleteFired = true; }; let onChangeFired = false; const onChange = function () { onChangeFired = true; }; const callbacks = { onComplete: onComplete, onChange: onChange }; expect(object.fxStraighten, 'fxStraighten should be a function').toBeTypeOf( 'function', ); expect( object.fxStraighten!(callbacks).abort, 'should return animation context', ).toBeTypeOf('function'); expect(toFixed(object.get('angle'), 0), 'initial angle').toBe('43'); // Wait for animation to complete await new Promise((resolve) => setTimeout(resolve, 1000)); expect(onCompleteFired, 'onComplete should fire').toBeTruthy(); expect(onChangeFired, 'onChange should fire').toBeTruthy(); expect( object.get('angle'), 'angle should be set to 0 by the end of animation', ).toBe(0); expect( object.fxStraighten!().abort, 'should work without callbacks', ).toBeTypeOf('function'); }); it('observable', () => { const object = new FabricObject< any, any, { foo: unknown; bar: unknown; baz: unknown } & ObjectEvents >({ left: 20, top: 30, width: 40, height: 50, angle: 43, }); let fooFired = false; let barFired = false; const fooDisposer = object.on('foo', function () { fooFired = true; }); const barDisposer = object.on('bar', function () { barFired = true; }); expect(fooDisposer, 'should return disposer').toBeTypeOf('function'); expect(barDisposer, 'should return disposer').toBeTypeOf('function'); object.fire('foo'); expect(fooFired, 'foo event should have fired').toBeTruthy(); expect(barFired, 'bar event should not have fired').toBeFalsy(); object.fire('bar'); expect(fooFired, 'foo event should still be fired').toBeTruthy(); expect(barFired, 'bar event should have fired').toBeTruthy(); let firedOptions; object.on('baz', function (options) { firedOptions = options; }); object.fire('baz', { param1: 'abrakadabra', param2: 3.1415 }); expect( firedOptions!.param1, 'param1 should be passed to event handler', ).toBe('abrakadabra'); expect( firedOptions!.param2, 'param2 should be passed to event handler', ).toBe(3.1415); }); it('object:added', () => { const object = new Object(); let addedEventFired = false; object.on('added', function (opt) { addedEventFired = true; expect(opt.target, 'target should equal to canvas').toBe(canvas); }); canvas.add(object); expect(addedEventFired, 'added event should have fired').toBeTruthy(); }); it('canvas reference', () => { const object = new Object(); const object2 = new Object(); canvas.add(object); canvas.insertAt(0, object2); expect(object.canvas, 'object.canvas should reference canvas').toBe(canvas); expect(object2.canvas, 'object2.canvas should reference canvas').toBe( canvas, ); }); it('object:removed', () => { const object = new Object(); let removedEventFired = false; canvas.add(object); object.on('removed', function (opt) { removedEventFired = true; expect(opt.target, 'target should equal to canvas').toBe(canvas); expect(object.canvas, 'canvas should not be referenced').toBeUndefined(); }); canvas.remove(object); expect(removedEventFired, 'removed event should have fired').toBeTruthy(); }); it('getTotalObjectScaling with zoom', () => { const object = new Object({ scaleX: 3, scaleY: 2 }); canvas.setZoom(3); canvas.add(object); const objectScale = object.getTotalObjectScaling(); expect(objectScale, 'objectScale should be a Point').toBeInstanceOf(Point); expect(objectScale, 'objectScale should include zoom factor').toEqual( new Point(object.scaleX * 3, object.scaleY * 3), ); }); it('getTotalObjectScaling with retina', () => { const object = new Object({ scaleX: 3, scaleY: 2 }); canvas.enableRetinaScaling = true; config.configure({ devicePixelRatio: 4 }); canvas.add(object); const objectScale = object.getTotalObjectScaling(); expect(objectScale, 'objectScale should be a Point').toBeInstanceOf(Point); expect(objectScale, 'objectScale should include devicePixelRatio').toEqual( new Point(object.scaleX * 4, object.scaleY * 4), ); }); it('getObjectScaling', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2 }); const objectScale = object.getObjectScaling(); expect(objectScale, 'objectScale should be a Point').toBeInstanceOf(Point); expect(objectScale, 'objectScale should match object scale').toEqual( new Point(object.scaleX, object.scaleY), ); }); it('getObjectScaling in group', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2 }); const group = new Group(); group.scaleX = 2; group.scaleY = 2; object.group = group; const objectScale = object.getObjectScaling(); expect(objectScale, 'objectScale should be a Point').toBeInstanceOf(Point); expect(objectScale, 'objectScale should include group scale').toEqual( new Point(object.scaleX * group.scaleX, object.scaleY * group.scaleY), ); }); it('getObjectScaling in group with object rotated', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2, angle: 45 }); const group = new Group(); group.scaleX = 2; group.scaleY = 3; object.group = group; const objectScale = object.getObjectScaling(); expect( new Point( Math.round(objectScale.x * 1000) / 1000, Math.round(objectScale.y * 1000) / 1000, ), 'objectScale should include rotation effects', ).toEqual(new Point(7.649, 4.707)); }); it('dirty flag on set property', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2 }); const originalCacheProps = FabricObject.cacheProperties; FabricObject.cacheProperties = ['propA', 'propB']; object.dirty = false; expect(object.dirty, 'object starts with dirty flag disabled').toBe(false); object.set('propC', '3'); expect( object.dirty, 'after setting a property out of cache, dirty flag is still false', ).toBe(false); object.set('propA', '2'); expect( object.dirty, 'after setting a property from cache, dirty flag is true', ).toBe(true); FabricObject.cacheProperties = originalCacheProps; }); it('_createCacheCanvas sets object as dirty', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2, width: 1, height: 2, }); expect(object.dirty, 'object is dirty after creation').toBe(true); object.dirty = false; expect(object.dirty, 'object is not dirty after specifying it').toBe(false); object._createCacheCanvas(); expect(object.dirty, 'object is dirty again if cache gets created').toBe( true, ); }); it('isCacheDirty', () => { const object = new FabricObject({ scaleX: 3, scaleY: 2, width: 1, height: 2, }); expect(object.dirty, 'object is dirty after creation').toBe(true); const originalCacheProps = FabricObject.cacheProperties; FabricObject.cacheProperties = ['propA', 'propB']; object.dirty = false; expect( object.isCacheDirty(), 'object is not dirty if dirty flag is false', ).toBe(false); object.dirty = true; expect(object.isCacheDirty(), 'object is dirty if dirty flag is true').toBe( true, ); FabricObject.cacheProperties = originalCacheProps; }); it('_getCacheCanvasDimensions returns dimensions and zoom for cache canvas', () => { const object = new FabricObject({ width: 10, height: 10, strokeWidth: 0 }); const dims = object._getCacheCanvasDimensions(); expect(dims, 'if no scaling is applied cache is as big as object').toEqual({ width: 12, height: 12, zoomX: 1, zoomY: 1, x: 10, y: 10, }); object.strokeWidth = 2; const dimsWithStroke = object._getCacheCanvasDimensions(); expect(dimsWithStroke, 'cache contains the stroke').toEqual({ width: 14, height: 14, zoomX: 1, zoomY: 1, x: 12, y: 12, }); object.scaleX = 2; object.scaleY = 3; const dimsWithScale = object._getCacheCanvasDimensions(); expect(dimsWithScale, 'cache is as big as the scaled object').toEqual({ width: 26, height: 38, zoomX: 2, zoomY: 3, x: 24, y: 36, }); }); it('_getCacheCanvasDimensions and strokeUniform', () => { const object = new FabricObject({ width: 10, height: 10, strokeWidth: 2 }); const dims = object._getCacheCanvasDimensions(); expect( dims, 'if no scaling is applied cache is as big as object + strokeWidth', ).toEqual({ width: 14, height: 14, zoomX: 1, zoomY: 1, x: 12, y: 12, }); object.strokeUniform = true; const dimsWithUniform = object._getCacheCanvasDimensions(); expect( dimsWithUniform, 'if no scaling is applied strokeUniform makes no difference', ).toEqual({ width: 14, height: 14, zoomX: 1, zoomY: 1, x: 12, y: 12, }); object.scaleX = 2; object.scaleY = 3; const dimsWithScale = object._getCacheCanvasDimensions(); expect(dimsWithScale, 'cache is as big as the scaled object').toEqual({ width: 24, height: 34, zoomX: 2, zoomY: 3, x: 22, y: 32, }); }); it('_updateCacheCanvas check if cache canvas should be updated', () => { config.configure({ perfLimitSizeTotal: 10000, maxCacheSideLimit: 4096, minCacheSideLimit: 1, }); const object = new FabricObject({ width: 10, height: 10, strokeWidth: 0 }); object._createCacheCanvas(); expect( object._updateCacheCanvas(), 'second execution of cache canvas return false', ).toBe(false); object.scaleX = 2; expect( object._updateCacheCanvas(), 'if scale change, it returns true', ).toBe(true); expect(object.zoomX, 'current scale level is saved').toBe(2); object.width = 2; expect( object._updateCacheCanvas(), 'if dimension change, it returns true', ).toBe(true); object.strokeWidth = 2; expect( object._updateCacheCanvas(), 'if strokeWidth change, it returns true', ).toBe(true); }); it('_limitCacheSize limit min to 256', () => { config.configure({ perfLimitSizeTotal: 50000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 200, height: 200, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width gets minimum to the cacheSideLimit').toBe(256); expect(dims.height, 'height gets minimum to the cacheSideLimit').toBe(256); expect(zoomX, 'zoom factor X does not need a change').toBe(dims.zoomX); expect(zoomY, 'zoom factor Y does not need a change').toBe(dims.zoomY); }); it('_limitCacheSize does not limit if not necessary', () => { config.configure({ perfLimitSizeTotal: 1000000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 400, height: 400, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width is in the middle of limits').toBe(402); expect(dims.height, 'height is in the middle of limits').toBe(402); expect(zoomX, 'zoom factor X does not need a change').toBe(dims.zoomX); expect(zoomY, 'zoom factor Y does not need a change').toBe(dims.zoomY); }); it('_limitCacheSize does cap up minCacheSideLimit', () => { config.configure({ perfLimitSizeTotal: 10000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 400, height: 400, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const width = dims.width; const height = dims.height; const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width is capped to min').toBe(256); expect(dims.height, 'height is capped to min').toBe(256); expect( (zoomX * dims.width) / width, 'zoom factor X gets updated to represent the shrink', ).toBe(dims.zoomX); expect( (zoomY * dims.height) / height, 'zoom factor Y gets updated to represent the shrink', ).toBe(dims.zoomY); }); it('_limitCacheSize does cap up if necessary', () => { config.configure({ perfLimitSizeTotal: 1000000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 2046, height: 2046, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const width = dims.width; const height = dims.height; const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width is capped to max allowed by area').toBe(1000); expect(dims.height, 'height is capped to max allowed by area').toBe(1000); expect( (zoomX * dims.width) / width, 'zoom factor X gets updated to represent the shrink', ).toBe(dims.zoomX); expect( (zoomY * dims.height) / height, 'zoom factor Y gets updated to represent the shrink', ).toBe(dims.zoomY); }); it('_limitCacheSize does cap up if necessary to maxCacheSideLimit', () => { config.configure({ perfLimitSizeTotal: 100000000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 8192, height: 8192, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width is capped to max allowed by fabric').toBe( config.maxCacheSideLimit, ); expect(dims.height, 'height is capped to max allowed by fabric').toBe( config.maxCacheSideLimit, ); expect( dims.zoomX, 'zoom factor X gets updated to represent the shrink', ).toBe((zoomX * 4096) / 8194); expect( dims.zoomY, 'zoom factor Y gets updated to represent the shrink', ).toBe((zoomY * 4096) / 8194); }); it('_limitCacheSize does cap up if necessary to maxCacheSideLimit, different AR', () => { config.configure({ perfLimitSizeTotal: 100000000, maxCacheSideLimit: 4096, minCacheSideLimit: 256, }); const object = new FabricObject({ width: 16384, height: 8192, strokeWidth: 0, }); const dims = object._getCacheCanvasDimensions(); const width = dims.width; const height = dims.height; const zoomX = dims.zoomX; const zoomY = dims.zoomY; const limitedDims = object._limitCacheSize(dims); expect(dims, 'object is mutated').toBe(limitedDims); expect(dims.width, 'width is capped to max allowed by fabric').toBe( config.maxCacheSideLimit, ); expect(dims.height, 'height is capped to max allowed by fabric').toBe( config.maxCacheSideLimit, ); expect( dims.zoomX, 'zoom factor X gets updated to represent the shrink', ).toBe((zoomX * config.maxCacheSideLimit) / width); expect( dims.zoomY, 'zoom factor Y gets updated to represent the shrink', ).toBe((zoomY * config.maxCacheSideLimit) / height); }); it('_setShadow', () => { const canvas = new StaticCanvas(undefined, { enableRetinaScaling: false, width: 600, height: 600, }); const context = canvas.contextContainer; const object = new FabricObject({ scaleX: 1, scaleY: 1 }); const group = new Group(); group.scaleX = 2; group.scaleY = 2; object.shadow = new Shadow({ color: 'red', blur: 10, offsetX: 5, offsetY: 15, }); object._setShadow(context); expect(context.shadowOffsetX, 'shadow offsetX is set').toBe( object.shadow.offsetX, ); expect(context.shadowOffsetY, 'shadow offsetY is set').toBe( object.shadow.offsetY, ); expect(context.shadowBlur, 'shadow blur is set').toBe(object.shadow.blur); config.configure({ browserShadowBlurConstant: 1.5 }); object._setShadow(context); expect( context.shadowOffsetX, 'shadow offsetX is unchanged with browserConstant', ).toBe(object.shadow.offsetX); expect( context.shadowOffsetY, 'shadow offsetY is unchanged with browserConstant', ).toBe(object.shadow.offsetY); expect( context.shadowBlur, 'shadow blur is affected with browserConstant', ).toBe(object.shadow.blur * 1.5); config.configure({ browserShadowBlurConstant: 1 }); object.scaleX = 2; object.scaleY = 3; object._setShadow(context); expect(context.shadowOffsetX, 'shadow offsetX is affected by scaleX').toBe( object.shadow.offsetX * object.scaleX, ); expect(context.shadowOffsetY, 'shadow offsetY is affected by scaleY').toBe( object.shadow.offsetY * object.scaleY, ); expect( context.shadowBlur, 'shadow blur is affected by scaleY and scaleX', ).toBe((object.shadow.blur * (object.scaleX + object.scaleY)) / 2); object.group = group; object._setShadow(context); expect( context.shadowOffsetX, 'shadow offsetX is affected by scaleX and group.scaleX', ).toBe(object.shadow.offsetX * object.scaleX * group.scaleX); expect( context.shadowOffsetY, 'shadow offsetX is affected by scaleX and group.scaleX', ).toBe(object.shadow.offsetY * object.scaleY * group.scaleY); expect(context.shadowBlur, 'shadow blur is affected by scales').toBe( (object.shadow.blur * (object.scaleX * group.scaleX + object.scaleY * group.scaleY)) / 2, ); }); it('willDrawShadow', () => { // @ts-expect-error -- Mock shadow const object = new FabricObject({ shadow: { offsetX: 0, offsetY: 0 } }); expect(object.willDrawShadow(), 'object will not drawShadow').toBe(false); object.shadow!.offsetX = 1; expect(object.willDrawShadow(), 'object will drawShadow').toBe(true); }); it('_set change a property', () => { const object = new FabricObject({ fill: 'blue' }); object._set('fill', 'red'); expect(object.fill, 'property changed').toBe('red'); }); it('_set can rise the dirty flag', () => { const object = new FabricObject({ fill: 'blue' }); object.dirty = false; object._set('fill', 'red'); expect(object.dirty, 'dirty is raised').toBe(true); }); it('_set rise dirty flag only if value changed', () => { const object = new FabricObject({ fill: 'blue' }); object.dirty = false; object._set('fill', 'blue'); expect(object.dirty, 'dirty is not raised').toBe(false); }); it('isNotVisible', () => { const object = new FabricObject({ fill: 'blue', width: 100, height: 100 }); expect(object.isNotVisible(), 'object is default visible').toBe(false); const objectWithStroke = new FabricObject({ fill: 'blue', width: 0, height: 0, strokeWidth: 1, }); expect( objectWithStroke.isNotVisible(), 'object is visible with width and height equal 0, but strokeWidth 1', ).toBe(false); const transparentObject = new FabricObject({ opacity: 0, fill: 'blue' }); expect( transparentObject.isNotVisible(), 'object is not visible with opacity 0', ).toBe(true); const invisibleObject = new FabricObject({ fill: 'blue', visible: false }); expect( invisibleObject.isNotVisible(), 'object is not visible with visible false', ).toBe(true); const zeroSizeObject = new FabricObject({ fill: 'blue', width: 0, height: 0, strokeWidth: 0, }); expect( zeroSizeObject.isNotVisible(), 'object is not visible with also strokeWidth equal 0', ).toBe(true); }); it('shouldCache', () => { const object = new FabricObject(); object.objectCaching = false; expect( object.shouldCache(), 'if objectCaching is false, object should not cache', ).toBe(false); object.objectCaching = true; expect( object.shouldCache(), 'if objectCaching is true, object should cache', ).toBe(true); object.objectCaching = false; object.needsItsOwnCache = function () { return true; }; expect( object.shouldCache(), 'if objectCaching is false, but we have a clipPath, shouldCache returns true', ).toBe(true); object.needsItsOwnCache = function () { return false; }; object.objectCaching = true; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return true; }, }; expect( object.shouldCache(), 'if objectCaching is true, but we are in a group, shouldCache returns false', ).toBe(false); object.objectCaching = true; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return false; }, }; expect( object.shouldCache(), 'if objectCaching is true, but we are in a not cached group, shouldCache returns true', ).toBe(true); object.objectCaching = false; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return false; }, }; expect( object.shouldCache(), 'if objectCaching is false, but we are in a not cached group, shouldCache returns false', ).toBe(false); object.objectCaching = false; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return true; }, }; expect( object.shouldCache(), 'if objectCaching is false, but we are in a cached group, shouldCache returns false', ).toBe(false); object.needsItsOwnCache = function () { return true; }; object.objectCaching = false; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return true; }, }; expect( object.shouldCache(), 'if objectCaching is false, but we have a clipPath, group cached, we cache anyway', ).toBe(true); object.objectCaching = false; // @ts-expect-error -- Mock group object.parent = { isOnACache: function () { return false; }, }; expect( object.shouldCache(), 'if objectCaching is false, but we have a clipPath, group not cached, we cache anyway', ).toBe(true); }); it('hasStroke', () => { const object = new FabricObject({ fill: 'blue', width: 100, height: 100, strokeWidth: 3, stroke: 'black', }); expect( object.hasStroke(), 'if strokeWidth is present and stroke is black hasStroke is true', ).toBe(true); object.stroke = ''; expect( object.hasStroke(), 'if strokeWidth is present and stroke is empty string hasStroke is false', ).toBe(false); object.stroke = 'transparent'; expect( object.hasStroke(), 'if strokeWidth is present and stroke is transparent hasStroke is false', ).toBe(false); object.stroke = 'black';