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); }); }); });