fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
1,593 lines (1,328 loc) • 73.7 kB
text/typescript
import { StaticCanvas } from './StaticCanvas';
import { Canvas } from './Canvas';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { TMat2D } from '../typedefs';
import { FabricText, Gradient, Pattern, version } from '../../fabric';
import { config } from '../config';
import { Rect } from '../shapes/Rect';
import { Circle } from '../shapes/Circle';
import type { FabricObject } from '../shapes/Object/Object';
import { getFabricDocument } from '../env';
import { FabricImage } from '../shapes/Image';
import { Point } from '../Point';
import { Group } from '../shapes/Group';
import { Path } from '../shapes/Path';
import { Ellipse } from '../shapes/Ellipse';
import { Line } from '../shapes/Line';
import { Polyline } from '../shapes/Polyline';
import { Triangle } from '../shapes/Triangle';
import { Polygon } from '../shapes/Polygon';
import {
CANVAS_SVG,
CANVAS_SVG_VIEWBOX,
PATH_DATALESS_JSON,
PATH_JSON,
PATH_WITHOUT_DEFAULTS_JSON,
RECT_JSON,
RECT_JSON_WITH_PADDING,
REFERENCE_IMG_OBJECT,
IMG_SRC,
IMG_WIDTH,
IMG_HEIGHT,
} from './StaticCanvas.fixtures';
import { isJSDOM, sanitizeSVG } from '../../vitest.extend';
describe('StaticCanvas', () => {
const canvas = new StaticCanvas(undefined, {
renderOnAddRemove: false,
enableRetinaScaling: false,
width: 200,
height: 200,
});
const canvas2 = new StaticCanvas(undefined, {
renderOnAddRemove: false,
enableRetinaScaling: false,
width: 200,
height: 200,
});
const lowerCanvasEl = canvas.lowerCanvasEl;
beforeEach(() => {
canvas.clear();
canvas.setDimensions({ width: 200, height: 200 });
canvas2.setDimensions({ width: 200, height: 200 });
canvas.backgroundColor = StaticCanvas.getDefaults().backgroundColor;
canvas.backgroundImage = StaticCanvas.getDefaults().backgroundImage;
canvas.overlayColor = StaticCanvas.getDefaults().overlayColor;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.calcOffset();
canvas.requestRenderAll = StaticCanvas.prototype.requestRenderAll;
canvas.cancelRequestedRender();
canvas2.cancelRequestedRender();
canvas.renderOnAddRemove = false;
canvas2.renderOnAddRemove = false;
});
afterEach(() => {
canvas.cancelRequestedRender();
canvas2.cancelRequestedRender();
config.configure({ devicePixelRatio: 1 });
});
it('toBlob', async () => {
const canvas = new StaticCanvas(undefined, { width: 300, height: 300 });
const blob = await canvas.toBlob({
multiplier: 3,
});
expect(blob).toBeInstanceOf(Blob);
expect(blob?.type).toBe('image/png');
});
it('attempts webp format but may fallback to png in node environment', () => {
const canvas = new StaticCanvas(undefined, { width: 300, height: 300 });
const dataURL = canvas.toDataURL({
format: 'webp',
multiplier: 1,
});
/**
* In browser environments this would be 'data:image/webp'
* In Node.js environment (node-canvas) it falls back to PNG.
* @see https://github.com/Automattic/node-canvas/issues/1258 for possible workaround
*/
expect(dataURL).toMatch(/^data:image\/(webp|png)/);
});
it('prevents multiple canvas initialization', () => {
const canvas = new StaticCanvas();
expect(canvas.lowerCanvasEl).toBeTruthy();
expect(() => new StaticCanvas(canvas.lowerCanvasEl)).toThrow();
});
it('has correct initial properties', () => {
const canvas = new StaticCanvas();
expect('backgroundColor' in canvas).toBeTruthy();
expect('overlayColor' in canvas).toBeTruthy();
expect('includeDefaultValues' in canvas).toBeTruthy();
expect('renderOnAddRemove' in canvas).toBeTruthy();
expect('controlsAboveOverlay' in canvas).toBeTruthy();
expect('allowTouchScrolling' in canvas).toBeTruthy();
expect('imageSmoothingEnabled' in canvas).toBeTruthy();
expect('backgroundVpt' in canvas).toBeTruthy();
expect('overlayVpt' in canvas).toBeTruthy();
expect(Array.isArray(canvas._objects)).toBeTruthy();
expect(canvas._objects.length).toBe(0);
expect(canvas.includeDefaultValues).toBe(true);
expect(canvas.renderOnAddRemove).toBe(true);
expect(canvas.controlsAboveOverlay).toBe(false);
expect(canvas.allowTouchScrolling).toBe(false);
expect(canvas.imageSmoothingEnabled).toBe(true);
expect(canvas.backgroundVpt).toBe(true);
expect(canvas.overlayVpt).toBe(true);
expect(canvas.viewportTransform).not.toBe(canvas2.viewportTransform);
});
it('provides getObjects method', () => {
expect(canvas.getObjects).toBeTypeOf('function');
expect(canvas.getObjects()).toEqual([]);
expect(canvas.getObjects().length).toBe(0);
});
it('provides getElement method', () => {
expect(canvas.getElement).toBeTypeOf('function');
expect(canvas.getElement()).toBe(lowerCanvasEl);
});
it('provides item method to access objects by index', () => {
const rect = makeRect();
expect(canvas.item).toBeTypeOf('function');
canvas.add(rect);
expect(canvas.item(0)).toBe(rect);
});
it('calculates offset correctly', () => {
expect(canvas.calcOffset).toBeTypeOf('function');
expect(canvas.calcOffset()).toEqual({ left: 0, top: 0 });
});
it('adds objects to the canvas', () => {
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const rect4 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = true;
canvas.requestRenderAll = countRenderAll;
expect(canvas.add).toBeTypeOf('function');
expect(canvas.add(rect1)).toBe(1);
expect(canvas.item(0)).toBe(rect1);
expect(renderAllCount).toBe(1);
expect(canvas.add(rect2, rect3, rect4)).toBe(4);
expect(canvas.getObjects().length).toBe(4);
expect(renderAllCount).toBe(2);
canvas.add();
expect(renderAllCount).toBe(2);
expect(canvas.item(1)).toBe(rect2);
expect(canvas.item(2)).toBe(rect3);
expect(canvas.item(3)).toBe(rect4);
});
it('handles objects that belong to a different canvas', () => {
const rect1 = makeRect();
const control: {
action: string;
canvas: StaticCanvas;
target: FabricObject;
}[] = [];
canvas.on('object:added', (opt) => {
control.push({
action: 'added',
canvas: canvas,
target: opt.target,
});
});
canvas.on('object:removed', (opt) => {
control.push({
action: 'removed',
canvas: canvas,
target: opt.target,
});
});
canvas2.on('object:added', (opt) => {
control.push({
action: 'added',
canvas: canvas2,
target: opt.target,
});
});
canvas.add(rect1);
expect(canvas.item(0)).toBe(rect1);
canvas2.add(rect1);
expect(canvas.item(0)).toBeUndefined();
expect(canvas.size()).toBe(0);
expect(canvas2.item(0)).toBe(rect1);
const expected = [
{ action: 'added', target: rect1, canvas: canvas },
{ action: 'removed', target: rect1, canvas: canvas },
{ action: 'added', target: rect1, canvas: canvas2 },
];
expect(control).toEqual(expected);
});
it('respects renderOnAddRemove setting', () => {
const rect = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = false;
canvas.requestRenderAll = countRenderAll;
canvas.add(rect);
expect(renderAllCount).toBe(0);
expect(canvas.item(0)).toBe(rect);
canvas.add(makeRect(), makeRect(), makeRect());
expect(canvas.getObjects().length).toBe(4);
expect(renderAllCount).toBe(0);
});
it('fires object:added events', () => {
const objectsAdded: FabricObject[] = [];
canvas.on('object:added', function (e) {
objectsAdded.push(e.target);
});
const rect = new Rect({ width: 10, height: 20 });
canvas.add(rect);
expect(objectsAdded[0]).toBe(rect);
const circle1 = new Circle();
const circle2 = new Circle();
canvas.add(circle1, circle2);
expect(objectsAdded[1]).toBe(circle1);
expect(objectsAdded[2]).toBe(circle2);
const circle3 = new Circle();
canvas.insertAt(2, circle3);
expect(objectsAdded[3]).toBe(circle3);
});
it('inserts objects at specified positions', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
canvas.add(rect1, rect2);
expect(canvas.insertAt).toBeTypeOf('function');
function countRenderAll() {
renderAllCount++;
}
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = true;
expect(renderAllCount).toBe(0);
const rect = makeRect();
canvas.insertAt(1, rect);
expect(renderAllCount).toBe(1);
expect(canvas.item(1)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(2);
expect(canvas.item(2)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(3);
});
it('respects renderOnAddRemove when inserting objects', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = false;
canvas.requestRenderAll = countRenderAll;
canvas.add(rect1, rect2);
expect(renderAllCount).toBe(0);
const rect = makeRect();
canvas.insertAt(1, rect);
expect(renderAllCount).toBe(0);
expect(canvas.item(1)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(0);
});
it('removes objects correctly', () => {
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const rect4 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.add(rect1, rect2, rect3, rect4);
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = true;
expect(canvas.remove).toBeTypeOf('function');
expect(renderAllCount).toBe(0);
expect(canvas.remove(rect1)[0]).toBe(rect1);
expect(canvas.item(0)).toBe(rect2);
canvas.remove(rect2, rect3);
expect(renderAllCount).toBe(2);
expect(canvas.item(0)).toBe(rect4);
canvas.remove(rect4);
expect(renderAllCount).toBe(3);
expect(canvas.isEmpty()).toBe(true);
});
it('respects renderOnAddRemove when removing objects', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = false;
canvas.add(rect1, rect2);
expect(renderAllCount).toBe(0);
expect(canvas.remove(rect1)[0]).toBe(rect1);
expect(renderAllCount).toBe(0);
expect(canvas.item(0)).toBe(rect2);
});
it('fires object:removed events', () => {
const objectsRemoved: FabricObject[] = [];
canvas.on('object:removed', function (e) {
objectsRemoved.push(e.target);
});
const rect = new Rect({ width: 10, height: 20 });
const circle1 = new Circle();
const circle2 = new Circle();
canvas.add(rect, circle1, circle2);
expect(canvas.item(0)).toBe(rect);
expect(canvas.item(1)).toBe(circle1);
expect(canvas.item(2)).toBe(circle2);
canvas.remove(rect);
expect(objectsRemoved[0]).toBe(rect);
expect(rect.canvas).toBeUndefined();
canvas.remove(circle1, circle2);
expect(objectsRemoved[1]).toBe(circle1);
expect(circle1.canvas).toBeUndefined();
expect(objectsRemoved[2]).toBe(circle2);
expect(circle2.canvas).toBeUndefined();
expect(canvas.isEmpty()).toBe(true);
});
it('provides clearContext method', () => {
expect(canvas.clearContext).toBeTypeOf('function');
canvas.clearContext(canvas.contextContainer);
});
it('clears the canvas completely', () => {
expect(canvas.clear).toBeTypeOf('function');
const bg = new Rect({ width: 10, height: 20 });
canvas.backgroundColor = '#FF0000';
canvas.overlayColor = '#FF0000';
canvas.backgroundImage = bg;
canvas.overlayImage = bg;
const objectsRemoved: FabricObject[] = [];
canvas.on('object:removed', function (e) {
objectsRemoved.push(e.target);
});
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
canvas.add(rect1, rect2, rect3);
canvas.clear();
expect(canvas.getObjects().length).toBe(0);
expect(objectsRemoved[0]).toBe(rect1);
expect(objectsRemoved[1]).toBe(rect2);
expect(objectsRemoved[2]).toBe(rect3);
expect(canvas.backgroundColor).toBe('');
expect(canvas.overlayColor).toBe('');
expect(canvas.backgroundImage).toBeUndefined();
expect(canvas.overlayImage).toBeUndefined();
});
it('provides renderAll method', () => {
expect(canvas.renderAll).toBeTypeOf('function');
canvas.renderAll();
});
// TODO: was also commented out prior to vitest migration
// it('sets canvas dimensions correctly', () => {
// expect(canvas.setDimensions).toBeTypeOf('function');
// canvas.setDimensions({ width: 4, height: 5 });
// expect(canvas.getWidth()).toBe(4);
// expect(canvas.getHeight()).toBe(5);
// expect(canvas.lowerCanvasEl.style.width).toBe('5px');
// expect(canvas.lowerCanvasEl.style.height).toBe('4px');
// });
it('exports to canvas element of correct size', () => {
expect(canvas.toCanvasElement).toBeTypeOf('function');
const canvasEl = canvas.toCanvasElement();
expect(canvasEl.width).toBe(canvas.getWidth());
expect(canvasEl.height).toBe(canvas.getHeight());
});
it('exports to canvas element with multiplier', () => {
expect(canvas.toCanvasElement).toBeTypeOf('function');
const multiplier = 2;
const canvasEl = canvas.toCanvasElement(multiplier);
expect(canvasEl.width).toBe(canvas.getWidth() * multiplier);
expect(canvasEl.height).toBe(canvas.getHeight() * multiplier);
});
it('generates data URL correctly', () => {
expect(canvas.toDataURL).toBeTypeOf('function');
const rect = new Rect({
width: 100,
height: 100,
fill: 'red',
top: 0,
left: 0,
});
canvas.add(rect);
const dataURL = canvas.toDataURL();
// don't compare actual data url, as it is often browser-dependent
expect(typeof dataURL).toBe('string');
expect(dataURL.substring(0, 21)).toBe('data:image/png;base64');
//we can just compare that the dataUrl generated differs from the dataURl of an empty canvas
expect(dataURL.substring(200, 210) !== 'AAAAAAAAAA').toBe(true);
});
it('supports retina scaling in data URL generation', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
// @ts-expect-error -- multiplier is missing in options and it is mandatory per typescript
const dataUrl = c.toDataURL({ enableRetinaScaling: true });
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio);
expect(img.height).toBe(c.height * config.devicePixelRatio);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: true with multiplier = 1', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({ enableRetinaScaling: true, multiplier: 1 });
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio);
expect(img.height).toBe(c.height * config.devicePixelRatio);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: true with multiplier = 3', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({ enableRetinaScaling: true, multiplier: 3 });
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio * 3);
expect(img.height).toBe(c.height * config.devicePixelRatio * 3);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with no multiplier', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
// @ts-expect-error -- multiplier is missing in options and it is mandatory per typescript
const dataUrl = c.toDataURL({ enableRetinaScaling: false });
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width);
expect(img.height).toBe(c.height);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with multiplier = 1', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({
enableRetinaScaling: false,
multiplier: 1,
});
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width);
expect(img.height).toBe(c.height);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with multiplier = 3', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({
enableRetinaScaling: false,
multiplier: 3,
});
c.cancelRequestedRender();
return new Promise<void>((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * 3);
expect(img.height).toBe(c.height * 3);
resolve();
};
img.src = dataUrl;
});
});
it('generates JPEG data URL correctly', () => {
try {
// @ts-expect-error -- multiplier is mandatory option per typescript types
const dataURL = canvas.toDataURL({ format: 'jpeg' });
expect(dataURL.substring(0, 22)).toBe('data:image/jpeg;base64');
} catch {
// node-canvas does not support jpeg data urls
expect(true).toBeTruthy();
}
});
it('supports cropping in data URL generation', async () => {
expect(canvas.toDataURL).toBeTypeOf('function');
const croppingWidth = 75;
const croppingHeight = 50;
// @ts-expect-error -- multiplier is mandatory option per typescript types
const dataURL = canvas.toDataURL({
width: croppingWidth,
height: croppingHeight,
});
const img = await FabricImage.fromURL(dataURL);
expect(img.width).toBe(croppingWidth);
expect(img.height).toBe(croppingHeight);
});
it('centers objects horizontally', () => {
expect(canvas.centerObjectH).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
canvas.setZoom(4);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(1);
});
it('centers objects vertically', () => {
expect(canvas.centerObjectV).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObjectV(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
canvas.setZoom(2);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
});
it('centers objects both horizontally and vertically', () => {
expect(canvas.centerObject).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObject(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(4);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(1);
});
it('centers objects horizontally in viewport', () => {
expect(canvas.viewportCenterObjectH).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
const oldY = rect.top;
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
expect(rect.top).toBe(oldY);
canvas.setZoom(2);
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / (2 * canvas.getZoom()));
expect(rect.top).toBe(oldY);
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(
(canvas.width / 2 + pan) / canvas.getZoom(),
);
expect(rect.top).toBe(oldY);
});
it('centers objects vertically in viewport', () => {
expect(canvas.viewportCenterObjectV).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
const oldX = rect.left;
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.left).toBe(oldX);
canvas.setZoom(2);
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(
canvas.height / (2 * canvas.getZoom()),
);
expect(rect.left).toBe(oldX);
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(
(canvas.height / 2 + pan) / canvas.getZoom(),
);
expect(rect.left).toBe(oldX);
});
it('centers objects in viewport both horizontally and vertically', () => {
expect(canvas.viewportCenterObject).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
canvas.setZoom(2);
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(
canvas.height / (2 * canvas.getZoom()),
);
expect(rect.getCenterPoint().x).toBe(canvas.width / (2 * canvas.getZoom()));
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(
(canvas.height / 2 + pan) / canvas.getZoom(),
);
expect(rect.getCenterPoint().x).toBe(
(canvas.width / 2 + pan) / canvas.getZoom(),
);
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
});
it('generates SVG correctly', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
const svg = canvas.toSVG();
expect(svg).toEqualSVG(CANVAS_SVG);
});
it('supports different encodings in SVG (ISO-8859-1)', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
// @ts-expect-error -- TS2322: Type 'ISO-8859-1' is not assignable to type 'UTF-8, seems like types don't allow this encoding
const svg = canvas.toSVG({ encoding: 'ISO-8859-1' });
const svgDefaultEncoding = canvas.toSVG();
expect(svg).not.toBe(svgDefaultEncoding);
expect(svg).toEqualSVG(
CANVAS_SVG.replace('encoding="UTF-8"', 'encoding="ISO-8859-1"'),
);
});
it('can generate SVG without preamble', () => {
expect(canvas.toSVG).toBeTypeOf('function');
const withPreamble = canvas.toSVG();
const withoutPreamble = canvas.toSVG({ suppressPreamble: true });
expect(withPreamble).not.toBe(withoutPreamble);
expect(withoutPreamble.slice(0, 4)).toBe('<svg');
});
it('generates SVG with viewBox', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
const svg = canvas.toSVG({
viewBox: { x: 100, y: 100, width: 300, height: 300 },
});
expect(svg).toEqualSVG(CANVAS_SVG_VIEWBOX);
});
it('handles reviver function for all objects', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
const circle = new Circle();
const rect = new Rect();
const path1 = new Path('M 100 100 L 300 100 L 200 300 z');
const tria = new Triangle();
const polygon = new Polygon([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const polyline = new Polyline([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const line = new Line();
const text = new FabricText('Text');
const group = new Group([text, line]);
const ellipse = new Ellipse();
const image = createImageStub();
const path2 = new Path('M 0 0 L 200 100 L 200 300 z');
const path3 = new Path('M 50 50 L 100 300 L 400 400 z');
const pathGroup = new Group([path2, path3]);
canvas.renderOnAddRemove = false;
canvas.add(
circle,
rect,
path1,
tria,
polygon,
polyline,
group,
ellipse,
image,
pathGroup,
);
let reviverCount = 0;
function reviver(svg: string) {
reviverCount++;
return svg;
}
canvas.toSVG(undefined, reviver);
expect(reviverCount).toBe(14);
canvas.renderOnAddRemove = true;
});
it('handles reviver function with background and overlay images', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
const circle = new Circle();
const rect = new Rect();
const path1 = new Path('M 100 100 L 300 100 L 200 300 z');
const tria = new Triangle();
const polygon = new Polygon([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const polyline = new Polyline([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const line = new Line();
const text = new FabricText('Text');
const group = new Group([text, line]);
const ellipse = new Ellipse();
const image = createImageStub();
const imageBG = createImageStub();
const imageOL = createImageStub();
const path2 = new Path('M 0 0 L 200 100 L 200 300 z');
const path3 = new Path('M 50 50 L 100 300 L 400 400 z');
const pathGroup = new Group([path2, path3]);
canvas.renderOnAddRemove = false;
canvas.add(
circle,
rect,
path1,
tria,
polygon,
polyline,
group,
ellipse,
image,
pathGroup,
);
canvas.backgroundImage = imageBG;
canvas.overlayImage = imageOL;
let reviverCount = 0;
const len = canvas.size() + group.size() + pathGroup.size();
function reviver(svg: string) {
reviverCount++;
return svg;
}
canvas.toSVG(undefined, reviver);
expect(reviverCount).toBe(len + 2);
canvas.backgroundImage = undefined;
canvas.overlayImage = undefined;
canvas.renderOnAddRemove = true;
});
it('excludes objects marked with excludeFromExport from SVG', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
const circle = new Circle({ excludeFromExport: true });
const rect = new Rect({ excludeFromExport: true });
const path1 = new Path('M 100 100 L 300 100 L 200 300 z');
const tria = new Triangle();
const polygon = new Polygon([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const polyline = new Polyline([
{ x: 10, y: 12 },
{ x: 20, y: 22 },
]);
const line = new Line();
const text = new FabricText('Text');
const group = new Group([text, line]);
const ellipse = new Ellipse();
const image = createImageStub();
const path2 = new Path('M 0 0 L 200 100 L 200 300 z');
const path3 = new Path('M 50 50 L 100 300 L 400 400 z');
const pathGroup = new Group([path2, path3]);
canvas.renderOnAddRemove = false;
canvas.add(
circle,
rect,
path1,
tria,
polygon,
polyline,
group,
ellipse,
image,
pathGroup,
);
let reviverCount = 0;
const len = canvas.size() + group.size() + pathGroup.size();
function reviver(svg: string) {
reviverCount++;
return svg;
}
canvas.toSVG(undefined, reviver);
expect(reviverCount).toBe(len - 2);
canvas.renderOnAddRemove = true;
});
it('correctly includes clipPath in SVG output', () => {
const canvasClip = new StaticCanvas(undefined, {
width: 400,
height: 400,
renderOnAddRemove: false,
});
canvasClip.clipPath = new Rect({
left: 100.5,
top: 100.5,
width: 200,
height: 200,
});
canvasClip.add(new Circle({ left: 200.5, top: 200.5, radius: 200 }));
const svg = canvasClip.toSVG();
expect(sanitizeSVG(svg)).toMatchInlineSnapshot(`
"<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="400" height="400" viewBox="0 0 400 400" xml:space="preserve">
<desc>Created with Fabric.js ${version}</desc>
<defs>
<clipPath id="SVGID" >
<rect transform="matrix(1 0 0 1 100.5 100.5)" x="-100" y="-100" rx="0" ry="0" width="200" height="200" />
</clipPath>
</defs>
<g clip-path="url(#SVGID)" >
<g transform="matrix(1 0 0 1 200.5 200.5)" >
<circle style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" cx="0" cy="0" r="200" />
</g>
</g>
</svg>"
`);
});
it('handles excludeFromExport for background and overlay images', () => {
const imageBG = createImageStub();
const imageOL = createImageStub();
canvas.renderOnAddRemove = false;
canvas.backgroundImage = imageBG;
canvas.overlayImage = imageOL;
const expectedSVG =
'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="200" height="200" viewBox="0 0 200 200" xml:space="preserve">\n<desc>Created with Fabric.js ' +
version +
'</desc>\n<defs>\n</defs>\n<g transform="matrix(1 0 0 1 0 0)" >\n\t<image style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" xlink:href="" x="0" y="0" width="0" height="0"></image>\n</g>\n<g transform="matrix(1 0 0 1 0 0)" >\n\t<image style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" xlink:href="" x="0" y="0" width="0" height="0"></image>\n</g>\n</svg>';
const svg1 = canvas.toSVG();
expect(svg1).toEqualSVG(expectedSVG);
imageBG.excludeFromExport = true;
imageOL.excludeFromExport = true;
const expectedSVG2 =
'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="200" height="200" viewBox="0 0 200 200" xml:space="preserve">\n<desc>Created with Fabric.js ' +
version +
'</desc>\n<defs>\n</defs>\n</svg>';
const svg2 = canvas.toSVG();
expect(svg2).toEqualSVG(expectedSVG2);
canvas.backgroundImage = undefined;
canvas.overlayImage = undefined;
canvas.renderOnAddRemove = true;
});
it('converts canvas to JSON correctly', () => {
expect(canvas.toJSON).toBeTypeOf('function');
expect(JSON.stringify(canvas)).toBe(
JSON.stringify({ version: version, objects: [] }),
);
canvas.backgroundColor = '#ff5555';
canvas.overlayColor = 'rgba(0,0,0,0.2)';
expect(canvas.toJSON()).toEqual({
version: version,
objects: [],
background: '#ff5555',
overlay: 'rgba(0,0,0,0.2)',
});
canvas.add(makeRect());
expect(canvas.toJSON()).toEqual(RECT_JSON);
});
it('handles custom properties in toObject correctly', () => {
const rect = new Rect({ width: 10, height: 20 });
rect.padding = 123;
canvas.add(rect);
// @ts-expect-error -- custom prop
rect.foo = 'bar';
// @ts-expect-error -- custom prop
canvas.bar = 456;
const data = canvas.toObject(['padding', 'foo', 'bar', 'baz']);
expect('padding' in data.objects[0]).toBeTruthy();
expect('foo' in data.objects[0]).toBeTruthy();
expect('bar' in data.objects[0]).toBeFalsy();
expect('baz' in data.objects[0]).toBeFalsy();
expect('foo' in data).toBeFalsy();
expect('baz' in data).toBeFalsy();
expect('bar' in data).toBeTruthy();
});
it('serializes backgroundImage to JSON correctly', async () => {
canvas.backgroundImage = await createImageObject();
const json = canvas.toJSON();
fixImageDimension(json.backgroundImage);
expect(json.backgroundImage).toSameImageObject(REFERENCE_IMG_OBJECT);
canvas.backgroundImage = undefined;
});
it('includes custom props for backgroundImage when specified', async () => {
const image = await createImageObject();
canvas.backgroundImage = image;
// @ts-expect-error -- custom prop
image.custom = 'yes';
const json = canvas.toObject(['custom']);
expect(json.backgroundImage.custom).toBe('yes');
canvas.backgroundImage = undefined;
});
it('serializes overlayImage to JSON correctly', async () => {
canvas.overlayImage = await createImageObject();
const json = canvas.toJSON();
fixImageDimension(json.overlayImage);
expect(json.overlayImage).toSameImageObject(REFERENCE_IMG_OBJECT);
canvas.overlayImage = undefined;
});
it('includes custom props for overlayImage when specified', async () => {
const image = await createImageObject();
canvas.overlayImage = image;
// @ts-expect-error -- custom prop
image.custom = 'yes';
const json = canvas.toObject(['custom']);
expect(json.overlayImage.custom).toBe('yes');
canvas.overlayImage = undefined;
});
it('generates dataless JSON correctly', () => {
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('serializes to object correctly', () => {
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('respects includeDefaultValues setting', () => {
canvas.includeDefaultValues = false;
const rect = makeRect();
canvas.add(rect);
const cObject = canvas.toObject();
const expectedRect = {
version: version,
type: 'Rect',
width: 10,
height: 10,
top: 0,
left: 0,
};
expect(cObject.objects[0]).toEqual(expectedRect);
canvas.includeDefaultValues = true;
});
it('respects excludeFromExport setting', () => {
const rect = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const clipPath = makeRect();
canvas.clear();
canvas.add(rect, rect2, rect3);
canvas.clipPath = clipPath;
let canvasObj = canvas.toObject();
expect(canvasObj.objects.length).toBe(3);
expect(canvasObj.clipPath).toEqual(clipPath.toObject());
rect.excludeFromExport = true;
rect2.excludeFromExport = true;
clipPath.excludeFromExport = true;
canvasObj = canvas.toObject();
expect(canvasObj.objects.length).toBe(1);
expect(canvasObj.clipPath).toBeUndefined();
});
it('respects excludeFromExport for background and overlay elements', () => {
const rect = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const bgColor = new Gradient({
type: 'linear',
colorStops: [
{ offset: 0, color: 'black' },
{ offset: 1, color: 'white' },
],
coords: {
x1: 0,
x2: 300,
y1: 0,
y2: 0,
},
});
canvas.clear();
canvas.backgroundImage = rect;
canvas.overlayImage = rect2;
canvas.backgroundColor = bgColor;
canvas.overlayColor = 'red';
canvas.add(rect3);
const rectToObject = rect.toObject();
const rect2ToObject = rect2.toObject();
const bgc2ToObject = bgColor.toObject();
let canvasToObject = canvas.toObject();
expect(canvasToObject.backgroundImage).toEqual(rectToObject);
expect(canvasToObject.overlayImage).toEqual(rect2ToObject);
expect(canvasToObject.background).toEqual(bgc2ToObject);
expect(canvasToObject.overlay).toBe('red');
rect.excludeFromExport = true;
rect2.excludeFromExport = true;
bgColor.excludeFromExport = true;
canvasToObject = canvas.toObject();
expect(canvasToObject.backgroundImage).toBeUndefined();
expect(canvasToObject.overlayImage).toBeUndefined();
expect(canvasToObject.background).toBeUndefined();
});
it('generates dataless object representation', () => {
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 the 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('includes additional properties in object representation when specified', () => {
// @ts-expect-error -- custom prop
canvas.freeDrawingColor = 'red';
// @ts-expect-error -- custom prop
canvas.foobar = 123;
const expectedObject = {
version: version,
objects: canvas.getObjects(),
freeDrawingColor: 'red',
foobar: 123,
};
expect(canvas.toObject(['freeDrawingColor', 'foobar'])).toEqual(
expectedObject,
);
const rect = makeRect();
// @ts-expect-error -- custom prop
rect.foobar = 456;
canvas.add(rect);
expect('foobar' in canvas.toObject(['smthelse']).objects[0]).toBeFalsy();
expect('foobar' in canvas.toObject(['foobar']).objects[0]).toBeTruthy();
});
it('correctly reports if canvas is empty', () => {
expect(canvas.isEmpty).toBeTypeOf('function');
expect(canvas.isEmpty()).toBeTruthy();
canvas.add(makeRect());
expect(canvas.isEmpty()).toBeFalsy();
});
it('loads from JSON string correctly', async () => {
expect(canvas.loadFromJSON).toBeTypeOf('function');
await canvas.loadFromJSON(PATH_JSON);
expect(canvas).not.toHaveProperty('objects');
const obj = canvas.item(0);
expect(canvas.isEmpty()).toBeFalsy();
// @ts-expect-error -- constructor function has type
expect(obj.constructor.type).toBe('Path');
expect(canvas.backgroundColor).toBe('#ff5555');
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')).toBeNull();
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).toBeGreaterThan(0);
});
it('loads from JSON object correctly', async () => {
expect(canvas.loadFromJSON).toBeTypeOf('function');
await canvas.loadFromJSON(PATH_JSON);
const obj = canvas.item(0);
expect(canvas.isEmpty()).toBeFalsy();
// @ts-expect-error -- constructor function has type
expect(obj.constructor.type).toBe('Path');
expect(canvas.backgroundColor).toBe('#ff5555');
expect(canvas.overlayColor).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')).toBeNull();
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).toBeGreaterThan(0);
});
it('loads from JSON object without defaults correctly', async () => {
expect(canvas.loadFromJSON).toBeTypeOf('function');
await canvas.loadFromJSON(PATH_WITHOUT_DEFAULTS_JSON);
const obj = canvas.item(0);
expect(canvas.isEmpty()).toBeFalsy();
// @ts-expect-error -- constructor function has type
expect(obj.constructor.type).toBe('Path');
expect(canvas.backgroundColor).toBe('#ff5555');
expect(canvas.overlayColor).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')).toBeNull();
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).toBeGreaterThan(0);
});
it('loads JSON with image background correctly', async () => {
const serialized = JSON.parse(JSON.stringify(PATH_JSON));
serialized.background = 'green';
serialized.backgroundImage = {
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: IMG_SRC,
filters: [],
crossOrigin: '',
};
await canvas.loadFromJSON(serialized);
expect(canvas.isEmpty()).toBeFalsy();
expect(canvas.backgroundColor).toBe('green');
expect(canvas.backgroundImage).toBeInstanceOf(FabricImage);
});
it('handles AbortController signal when loading JSON', async () => {
const serialized = JSON.parse(JSON.stringify(PATH_JSON));
serialized.background = 'green';
serialized.backgroundImage = {
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: IMG_SRC,
filters: [],
crossOrigin: '',
};
const abortController = new AbortController();
try {
const promise = canvas.loadFromJSON(serialized, undefined, {
signal: abortController.signal,
});
abortController.abort();
await promise;
// Should not reach here
expect(true).toBe(false);
} catch (err) {
expect(err).toHaveProperty('type', 'abort');
}
});
it('preserves custom properties when loading from JSON', async () => {
const rect = new Rect({ width: 10, height: 20 });
rect.padding = 123;
// @ts-expect-error -- custom prop
rect.foo = 'bar';
canvas.add(rect);
const jsonWithoutFoo = canvas.toObject(['padding']);
const jsonWithFoo = canvas.toObject(['padding', 'foo']);
expect(jsonWithFoo).toEqual(
JSON.parse(JSON.stringify(RECT_JSON_WITH_PADDING)),
);
expect(jsonWithoutFoo).not.toEqual(
JSON.parse(JSON.stringify(RECT_JSON_WITH_PADDING)),
);
canvas.clear();
await canvas.loadFromJSON(jsonWithFoo);
const obj = canvas.item(0);
expect(obj.padding).toBe(123);
expect(obj).toHaveProperty('foo', 'bar');
});
it('loads text objects correctly from JSON', async () => {
const json =
'{"objects":[{"type":"Text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"normal","fontFamily":"Delicious_500","fontStyle":"normal","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}],"background":"#ffffff"}';
await canvas.loadFromJSON(json);
canvas.renderAll();
// @ts-expect-error -- constructor function has type property
expect(canvas.item(0).constructor.type).toBe('Text');
expect(canvas.item(0).left).toBe(150);
expect(canvas.item(0).top).toBe(200);
expect(canvas.item(0)).toHaveProperty('text', 'NAME HERE');
});
it('loads clipPath correctly from JSON', async () => {
const canvas3 = new StaticCanvas();
const json =
'{"clipPath": {"type":"Text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"normal","fontFamily":"Delicious_500","fontStyle":"normal","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}}';
await canvas3.loadFromJSON(json);
expect(canvas3.clipPath).toBeInstanceOf(FabricText);
expect(canvas3.clipPath!.constructor).toHaveProperty('type', 'Text');
});
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)).toBe(rect3);
canvas.sendObjectToBack(rect2);
expect(canvas.item(0)).toBe(rect2);
canvas.sendObjectToBack(rect2);
expect(canvas.item(0)).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)).toBe(rect1);
canvas.bringObjectToFront(rect2);
expect(canvas.item(2)).toBe(rect2);
canvas.bringObjectToFront(rect2);
expect(canvas.item(2)).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 deepEqual position — [2, 3, 1]
expect(ca