Fanu2's picture
Deploy full app to HF Space
b456468
import { fabric } from 'fabric';
import Graphics from '@/graphics';
import Invoker from '@/invoker';
import commandFactory from '@/factory/command';
import { stamp, hasStamp } from '@/util';
import { commandNames as commands } from '@/consts';
import addObjectCommand from '@/command/addObject';
import changeSelectionCommand from '@/command/changeSelection';
import loadImageCommand from '@/command/loadImage';
import flipCommand from '@/command/flip';
import addTextCommand from '@/command/addText';
import changeTextStyleCommand from '@/command/changeTextStyle';
import rotateCommand from '@/command/rotate';
import addShapeCommand from '@/command/addShape';
import changeShapeCommand from '@/command/changeShape';
import clearObjectsCommand from '@/command/clearObjects';
import removeObjectCommand from '@/command/removeObject';
import resizeCommand from '@/command/resize';
import img1 from 'fixtures/sampleImage.jpg';
import img2 from 'fixtures/TOAST UI Component.png';
describe('commandFactory', () => {
let invoker, mockImage, canvas, graphics, dimensions;
beforeAll(() => {
commandFactory.register(addObjectCommand);
commandFactory.register(changeSelectionCommand);
commandFactory.register(loadImageCommand);
commandFactory.register(flipCommand);
commandFactory.register(addTextCommand);
commandFactory.register(changeTextStyleCommand);
commandFactory.register(rotateCommand);
commandFactory.register(addShapeCommand);
commandFactory.register(changeShapeCommand);
commandFactory.register(clearObjectsCommand);
commandFactory.register(removeObjectCommand);
commandFactory.register(resizeCommand);
});
beforeEach(() => {
dimensions = { width: 100, height: 100 };
graphics = new Graphics(document.createElement('canvas'));
invoker = new Invoker();
mockImage = new fabric.Image(null, dimensions);
graphics.setCanvasImage('', mockImage);
canvas = graphics.getCanvas();
});
describe('functions', () => {
it('should register custom command', async () => {
const testCommand = {
name: 'testCommand',
execute: jest.fn(() => Promise.resolve('testCommand')),
undo: jest.fn(() => Promise.resolve()),
};
commandFactory.register(testCommand);
const command = commandFactory.create('testCommand');
expect(command).not.toBeNull();
const commandName = await invoker.execute('testCommand', graphics);
expect(commandName).toBe('testCommand');
expect(testCommand.execute).toHaveBeenCalledWith(graphics);
});
it('should pass parameters on execute', async () => {
commandFactory.register({
name: 'testCommand',
execute(compMap, obj1, obj2, obj3) {
expect(obj1).toBe(1);
expect(obj2).toBe(2);
expect(obj3).toBe(3);
return Promise.resolve();
},
});
await invoker.execute('testCommand', graphics, 1, 2, 3);
});
it('should pass parameters on undo', async () => {
commandFactory.register({
name: 'testCommand',
execute() {
return Promise.resolve();
},
undo(compMap, obj1, obj2, obj3) {
expect(obj1).toBe(1);
expect(obj2).toBe(2);
expect(obj3).toBe(3);
return Promise.resolve();
},
});
await invoker.execute('testCommand', graphics, 1, 2, 3);
await invoker.undo();
});
});
describe('addObjectCommand', () => {
let obj;
beforeEach(() => {
obj = new fabric.Rect();
});
it('should stamp object', async () => {
await invoker.execute(commands.ADD_OBJECT, graphics, obj);
expect(hasStamp(obj)).toBe(true);
});
it('should add object to canvas', async () => {
await invoker.execute(commands.ADD_OBJECT, graphics, obj);
expect(canvas.contains(obj)).toBe(true);
});
it('should remove object from canvas', async () => {
await invoker.execute(commands.ADD_OBJECT, graphics, obj);
await invoker.undo();
expect(canvas.contains(obj)).toBe(false);
});
});
describe('changeSelectionCommand', () => {
let obj;
beforeEach(() => {
canvas.getPointer = jest.fn();
obj = new fabric.Rect({
width: 10,
height: 10,
top: 10,
left: 10,
scaleX: 1,
scaleY: 1,
angle: 0,
});
graphics._addFabricObject(obj);
graphics._onMouseDown({ target: obj });
const props = [
{
id: graphics.getObjectId(obj),
width: 30,
height: 30,
top: 30,
left: 30,
scaleX: 0.5,
scaleY: 0.5,
angle: 10,
},
];
const makeCommand = commandFactory.create(commands.CHANGE_SELECTION, graphics, props);
makeCommand.execute(graphics, props);
invoker.pushUndoStack(makeCommand);
});
it('should work undo command correctly', async () => {
await invoker.undo();
expect(obj).toMatchObject({
width: 10,
height: 10,
left: 10,
top: 10,
scaleX: 1,
scaleY: 1,
angle: 0,
});
});
it('should work redo command correctly', async () => {
await invoker.undo();
await invoker.redo();
expect(obj).toMatchObject({
width: 30,
height: 30,
left: 30,
top: 30,
scaleX: 0.5,
scaleY: 0.5,
angle: 10,
});
});
});
describe('loadImageCommand', () => {
const img = new fabric.Image(img1);
beforeEach(() => {
graphics.setCanvasImage('', null);
});
it('should clear canvas', async () => {
jest.spyOn(canvas, 'clear');
await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
expect(canvas.clear).toHaveBeenCalled();
});
it('should load new image', async () => {
const changedSize = await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
expect(graphics.getImageName()).toBe('image');
expect(changedSize).toMatchObject({
oldWidth: expect.any(Number),
oldHeight: expect.any(Number),
newWidth: expect.any(Number),
newHeight: expect.any(Number),
});
});
it('should not include cropzone after running the LOAD_IMAGE command', async () => {
const objCropzone = new fabric.Object({ type: 'cropzone' });
await invoker.execute(commands.ADD_OBJECT, graphics, objCropzone);
await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
const lastUndoIndex = invoker._undoStack.length - 1;
const savedObjects = invoker._undoStack[lastUndoIndex].undoData.objects;
expect(savedObjects).toHaveLength(0);
});
it('should be true after LOAD_IMAGE command.', async () => {
const objCircle = new fabric.Object({ type: 'circle', evented: false });
await invoker.execute(commands.ADD_OBJECT, graphics, objCircle);
await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
const lastUndoIndex = invoker._undoStack.length - 1;
const [savedObject] = invoker._undoStack[lastUndoIndex].undoData.objects;
expect(savedObject.evented).toBe(true);
});
it('should clear image if not exists prev image', async () => {
await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
await invoker.undo();
expect(graphics.getCanvasImage()).toBeNull();
expect(graphics.getImageName()).toBe('');
});
it('should restore to prev image', async () => {
const newImg = new fabric.Image(img2);
await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img);
await invoker.execute(commands.LOAD_IMAGE, graphics, 'newImage', newImg);
expect(graphics.getImageName()).toBe('newImage');
await invoker.undo();
expect(graphics.getImageName()).toBe('image');
});
});
describe('flipImageCommand', () => {
it('should be flipped over to the x-axis.', async () => {
const flipStatus = mockImage.flipX;
await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX');
expect(mockImage.flipX).toBe(!flipStatus);
});
it('should be flipped over to the y-axis.', async () => {
const flipStatus = mockImage.flipY;
await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY');
expect(mockImage.flipY).toBe(!flipStatus);
});
it('should reset flip', async () => {
mockImage.flipX = true;
mockImage.flipY = true;
await invoker.execute(commands.FLIP_IMAGE, graphics, 'reset');
expect(mockImage).toMatchObject({ flipX: false, flipY: false });
});
it('should restore flipX', async () => {
const flipStatus = mockImage.flipX;
await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX');
await invoker.undo();
expect(mockImage.flipX).toBe(flipStatus);
});
it('should restore flipY', async () => {
const flipStatus = mockImage.flipY;
await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY');
await invoker.undo();
expect(mockImage.flipY).toBe(flipStatus);
});
});
describe('textCommand', () => {
let textObjectId;
const fontSize = 50;
const underline = false;
const newFontSize = 30;
const newUnderline = true;
beforeEach(async () => {
const textObject = await invoker.execute(commands.ADD_TEXT, graphics, 'text', {
styles: {
fontSize,
underline,
},
});
textObjectId = textObject.id;
});
it('should set text style', async () => {
await invoker.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, {
fontSize: newFontSize,
underline: newUnderline,
});
const textObject = graphics.getObject(textObjectId);
expect(textObject).toMatchObject({ fontSize: 30, underline: true });
});
it('should restore fontSize', async () => {
await invoker.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, {
fontSize: newFontSize,
underline: newUnderline,
});
await invoker.undo();
const textObject = graphics.getObject(textObjectId);
expect(textObject).toMatchObject({ fontSize, underline });
});
});
describe('rotateCommand', () => {
it('should add angle', async () => {
const originAngle = mockImage.angle;
await invoker.execute(commands.ROTATE_IMAGE, graphics, 'rotate', 10);
expect(mockImage.angle).toBe(originAngle + 10);
});
it('should set angle', async () => {
mockImage.angle = 100;
await invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 30);
expect(mockImage.angle).toBe(30);
});
it('should restore angle', async () => {
const originalAngle = mockImage.angle;
await invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 100);
await invoker.undo();
expect(mockImage.angle).toBe(originalAngle);
});
});
describe('shapeCommand', () => {
let shapeObjectId;
const defaultStrokeWidth = 12;
const strokeWidth = 50;
beforeEach(async () => {
const shapeObject = await invoker.execute(commands.ADD_SHAPE, graphics, 'rect', {
strokeWidth: defaultStrokeWidth,
});
shapeObjectId = shapeObject.id;
});
it('should set strokeWidth', async () => {
await invoker.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, { strokeWidth });
const shapeObject = graphics.getObject(shapeObjectId);
expect(shapeObject.strokeWidth).toBe(strokeWidth);
});
it('should restore strokeWidth', async () => {
await invoker.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, { strokeWidth });
await invoker.undo();
const shapeObject = graphics.getObject(shapeObjectId);
expect(shapeObject.strokeWidth).toBe(defaultStrokeWidth);
});
});
describe('clearCommand', () => {
let canvasContext, objects;
beforeEach(() => {
canvasContext = canvas;
objects = [new fabric.Rect(), new fabric.Rect(), new fabric.Rect()];
});
it('should clear all objects', async () => {
canvas.add.apply(canvasContext, objects);
expect(canvas.contains(objects[0])).toBe(true);
expect(canvas.contains(objects[1])).toBe(true);
expect(canvas.contains(objects[2])).toBe(true);
await invoker.execute(commands.CLEAR_OBJECTS, graphics);
expect(canvas.contains(objects[0])).toBe(false);
expect(canvas.contains(objects[1])).toBe(false);
expect(canvas.contains(objects[2])).toBe(false);
});
it('should restore all objects', async () => {
canvas.add.apply(canvasContext, objects);
await invoker.execute(commands.CLEAR_OBJECTS, graphics);
await invoker.undo();
expect(canvas.contains(objects[0])).toBe(true);
expect(canvas.contains(objects[1])).toBe(true);
expect(canvas.contains(objects[2])).toBe(true);
});
});
describe('removeCommand', () => {
let object, object2, group;
beforeEach(() => {
object = new fabric.Rect({ left: 10, top: 10 });
object2 = new fabric.Rect({ left: 5, top: 20 });
group = new fabric.Group();
graphics.add(object);
graphics.add(object2);
graphics.add(group);
group.add(object, object2);
});
it('should remove an object', async () => {
graphics.setActiveObject(object);
await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(object));
expect(canvas.contains(object)).toBe(false);
});
it('should remove objects in group', async () => {
canvas.setActiveObject(group);
await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(group));
expect(canvas.contains(object)).toBe(false);
expect(canvas.contains(object2)).toBe(false);
});
it('should restore the removed object', async () => {
canvas.setActiveObject(object);
await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(object));
await invoker.undo();
expect(canvas.contains(object)).toBe(true);
});
it('should restore the removed objects in group', async () => {
canvas.setActiveObject(group);
await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(group));
await invoker.undo();
expect(canvas.contains(object)).toBe(true);
expect(canvas.contains(object2)).toBe(true);
});
it('should restore the position of the removed object in group', async () => {
const activeSelection = graphics.getActiveSelectionFromObjects(canvas.getObjects());
graphics.setActiveObject(activeSelection);
await invoker.execute(
commands.REMOVE_OBJECT,
graphics,
graphics.getActiveObjectIdForRemove()
);
await invoker.undo();
expect(object).toMatchObject({ left: 10, top: 10 });
expect(object2).toMatchObject({ left: 5, top: 20 });
});
});
describe('resizeCommand', () => {
const newDimensions = { width: 20, height: 20 };
it('should resize image', async () => {
await invoker.execute(commands.RESIZE_IMAGE, graphics, newDimensions);
const { width, height, scaleX, scaleY } = mockImage;
expect({ width: width * scaleX, height: height * scaleY }).toEqual(newDimensions);
});
it('should restore dimensions of image', async () => {
await invoker.execute(commands.RESIZE_IMAGE, graphics, newDimensions);
await invoker.undo();
const { width, height, scaleX, scaleY } = mockImage;
expect({ width: width * scaleX, height: height * scaleY }).toEqual(dimensions);
});
});
});