import { it, afterEach, beforeEach, describe, expect, vi } from "vitest"; import { mockZCanvas, createMockCanvasElement, createMockZoomableCanvas } from "../../mocks"; import { type Layer } from "@/model/types/layer"; import { LayerTypes } from "@/definitions/layer-types"; import DocumentFactory from "@/model/factories/document-factory"; import LayerFactory from "@/model/factories/layer-factory"; import DocumentModule, { createDocumentState, type DocumentState } from "@/store/modules/document-module"; import LayerRenderer from "@/rendering/actors/layer-renderer"; const { getters, mutations, actions } = DocumentModule; mockZCanvas(); let mockUpdateFn: ( fnName: string, ...args: any[]) => void; vi.mock( "@/model/factories/renderer-factory", () => ({ flushLayerRenderers: vi.fn(( ...args: any[]) => mockUpdateFn?.( "flushLayerRenderers", ...args )), runRendererFn: vi.fn(( ...args: any[]) => mockUpdateFn?.( "runRendererFn", ...args )), getRendererForLayer: vi.fn(( ...args: any[]) => mockUpdateFn?.( "getRendererForLayer", ...args )), createRendererForLayer: vi.fn(( ...args: any[]) => mockUpdateFn?.( "createRendererForLayer", ...args )), })); const mockFlushBlendedLayerCache = vi.fn(); vi.mock( "@/rendering/cache/blended-layer-cache", async ( importOriginal ) => { return { ...await importOriginal(), flushBlendedLayerCache: vi.fn(( ...args: any[] ) => mockFlushBlendedLayerCache( ...args )), } }); const mockCreateLayerThumbnail = vi.fn(); const mockFlushThumbnailCache = vi.fn(); const mockFlushThumbnailForLayer = vi.fn(); vi.mock( "@/rendering/cache/thumbnail-cache", () => ({ createLayerThumbnail: vi.fn(( ...args: any[] ) => mockCreateLayerThumbnail( ...args )), flushThumbnailCache: vi.fn(( ...args: any[] ) => mockFlushThumbnailCache( ...args )), flushThumbnailForLayer: vi.fn(( ...args: any[] ) => mockFlushThumbnailForLayer( ...args )), })); const mockFlushTileCacheFn = vi.fn(); vi.mock( "@/rendering/cache/tile-cache", () => ({ flushTileCache: vi.fn(( ...args: any[] ) => mockFlushTileCacheFn( ...args )), })); const mockCanvasInstance = createMockZoomableCanvas(); vi.mock( "@/services/canvas-service", () => ({ getCanvasInstance: vi.fn(( ...args: any[]) => { mockUpdateFn?.( "getCanvasInstance", ...args ); return mockCanvasInstance; }), })); vi.mock( "@/utils/layer-util", async ( importOriginal ) => ({ ...await importOriginal(), resizeLayerContent: vi.fn((...args: any[]) => mockUpdateFn?.( "resizeLayerContent", ...args )), cropLayerContent: vi.fn((...args: any[]) => mockUpdateFn?.( "cropLayerContent", ...args )), })); describe( "Vuex document module", () => { afterEach(() => { vi.resetAllMocks(); }); describe( "getters", () => { it( "should be able to retrieve all open Documents", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo" }), DocumentFactory.create({ name: "bar" }) ] }); expect( getters.documents( state, getters, {}, {} )).toEqual( state.documents ); }); it( "should be able to retrieve the active Document", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo" }), DocumentFactory.create({ name: "bar" }) ], activeIndex: 0 }); expect( getters.activeDocument( state, getters, {}, {} )).toEqual( state.documents[ 0 ]); state.activeIndex = 1; expect( getters.activeDocument( state, getters, {}, {} )).toEqual( state.documents[ 1 ]); }); it( "should be able to retrieve the Layers for the active Document", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "foo" }), LayerFactory.create({ name: "bar" }) ] }) ], activeIndex: 0 }); expect( getters.layers( state, getters, {}, {} )).toEqual( state.documents[ 0 ].layers ); }); it( "should be able to retrieve the active Layer index", () => { const state = createDocumentState({ activeLayerIndex: 2 }); expect( getters.activeLayerIndex( state, getters, {}, {} )).toEqual( 2 ); }); it( "should be able to retrieve the active Layer for the active Document", () => { const state = createDocumentState({ activeLayerIndex: 1 }); const mockedGetters = { layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }), LayerFactory.create({ name: "layer3" }) ] }; expect( getters.activeLayer( state, mockedGetters, {}, {} )).toEqual( mockedGetters.layers[ 1 ]); }); it( "should be able to retrieve the active Layer mask, when set", () => { const state = createDocumentState({ maskActive: false }); const mockedGetters = { activeLayer: LayerFactory.create({ name: "layer1" }), }; // null because mask is not active expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeUndefined(); state.maskActive = true; // null because layer has no mask drawable expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeUndefined(); mockedGetters.activeLayer.mask = createMockCanvasElement(); expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toEqual( mockedGetters.activeLayer.mask ); }); it( "should be able to retrieve the active Layer transform", () => { const mockedGetters = { activeLayer: { name: "layer1", transform: [{ rotation: 1 }] } }; expect( getters.activeLayerTransform( createDocumentState(), mockedGetters, {}, {} )).toEqual( mockedGetters.activeLayer.transform ); }); it( "should know whether the current Document has an active selection", () => { const mockedGetters = { activeDocument: DocumentFactory.create({ activeSelection: [] }) }; expect( getters.hasSelection( createDocumentState(), mockedGetters, {}, {} )).toBe( false ); mockedGetters.activeDocument.activeSelection = [ [] ]; expect( getters.hasSelection( createDocumentState(), mockedGetters, {}, {} )).toBe( false ); mockedGetters.activeDocument.activeSelection = [[ { x: 0, y: 0 }] ]; expect( getters.hasSelection( createDocumentState(), mockedGetters, {}, {} )).toBe( true ); }); }); describe( "mutations", () => { describe( "when setting the active Document", () => { it( "should be able to set the active Document index", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create(), LayerFactory.create() ] }), DocumentFactory.create({ name: "bar", layers: [ LayerFactory.create(), LayerFactory.create(), LayerFactory.create()] }) ], activeIndex: 0, activeLayerIndex: 1, }); mutations.setActiveDocument( state, 1 ); expect( state.activeIndex ).toEqual( 1 ); expect( state.activeLayerIndex ).toEqual( 1 ); }); it( "when switching to a Document with less layers than the currently active one, it should select the top layer", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create(), LayerFactory.create() ] }), DocumentFactory.create({ name: "bar", layers: [ LayerFactory.create(), LayerFactory.create(), LayerFactory.create() ] }) ], activeIndex: 1, activeLayerIndex: 2, }); mutations.setActiveDocument( state, 0 ); expect( state.activeIndex ).toEqual( 0 ); expect( state.activeLayerIndex ).toEqual( 1 ); }); it( "should request the invalidate() method on each renderer for the given Document", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo" }), DocumentFactory.create({ name: "bar" }) ], activeIndex: 0 }); mockUpdateFn = vi.fn(); mutations.setActiveDocument( state, 1 ); expect( mockUpdateFn ).toHaveBeenCalledWith( "runRendererFn", expect.any( Function ), state.documents[ 1 ]); }); }); it( "should be able to update the active Document name", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", width: 5, height: 5 }), DocumentFactory.create({ name: "bar", width: 10, height: 10 }) ], activeIndex: 1, }); const [ document1, document2 ] = state.documents; mutations.setActiveDocumentName( state, "baz" ); expect( state.documents ).toEqual([ document1, { ...document2, name: "baz", } ]); }); describe( "when setting the active Document size", () => { it( "should be able to update the active Document size", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", width: 30, height: 30 }), DocumentFactory.create({ name: "bar", width: 50, height: 50 }), ], activeIndex : 1, }); const [ document1, document2 ] = state.documents; const size = { width: 75, height: 40 }; mutations.setActiveDocumentSize( state, size ); expect( state.documents ).toEqual([ document1, { ...document2, width: size.width, height: size.height }, ]); }); it( "should update the existing zCanvas dimensions and trigger its associated rescale handler", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", width: 30, height: 30, layers: [ LayerFactory.create({ name: "layer1", width: 30, height: 30 }) ] }), DocumentFactory.create({ name: "bar", width: 50, height: 50, layers: [ LayerFactory.create({ name: "layer2", width: 20, height: 10 }), LayerFactory.create({ name: "layer3", width: 15, height: 15 }) ] }), ], activeIndex : 1, }); const size = { width: 75, height: 40 }; mutations.setActiveDocumentSize( state, size ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "getCanvasInstance" ); expect( mockCanvasInstance.setDimensions ).toHaveBeenCalledWith( size.width, size.height, true, true ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "getCanvasInstance" ); expect( mockCanvasInstance.rescaleFn ).toHaveBeenCalled(); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 3, "getCanvasInstance" ); expect( mockCanvasInstance.refreshFn ).toHaveBeenCalled(); }); it( "should flush the thumbnail and tile caches", () => { const state = createDocumentState({ documents: [ DocumentFactory.create() ], activeIndex : 0, }); mutations.setActiveDocumentSize( state, { width: 500, height: 500 }); expect( mockFlushThumbnailCache ).toHaveBeenCalled(); expect( mockFlushTileCacheFn ).toHaveBeenCalled(); }); }); it( "should be able to set the active selection for the currently active document", () => { const selection = [{ x: 0, y: 0 }, { x: 10, y: 10 }]; const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", activeSelection: [] }), DocumentFactory.create({ name: "bar", activeSelection: [] }) ], activeIndex: 1 }); mutations.setActiveSelection( state, selection ); expect( state.documents[ 0 ].activeSelection ).toHaveLength( 0 ); expect( state.documents[ 1 ].activeSelection ).toEqual( selection ); }); it( "should be able to add a new Document to the list", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo" }) ], activeIndex: 0, activeLayerIndex: 2, }); mutations.addNewDocument( state, "bar" ); expect( state.documents ).toHaveLength( 2 ); expect( state.documents[ 1 ].name ).toEqual( "bar" ); expect( state.activeIndex ).toEqual( 1 ); expect( state.activeLayerIndex ).toEqual( 0 ); }); it( "should be able to close the active Document", () => { const layer1 = LayerFactory.create({ name: "layer1" }); const layer2 = LayerFactory.create({ name: "layer2" }); const layer3 = LayerFactory.create({ name: "layer3" }); const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ layer1 ] }), DocumentFactory.create({ name: "bar", layers: [ layer2, layer3 ] }), ], activeIndex: 1 }); const [ document1 ] = state.documents; mockUpdateFn = vi.fn(); mutations.closeActiveDocument( state ); expect( state.documents ).toEqual([ document1 ]); expect( state.activeIndex ).toEqual( 0 ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushLayerRenderers", layer2 ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "flushLayerRenderers", layer3 ); }); describe( "when adding layers", () => { const layerCreateSpy = vi.spyOn( LayerFactory, "create" ); afterEach(() => { layerCreateSpy.mockClear(); }); it( "should be able to add a Layer to the active Document", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", width: 1000, height: 1000 }) ], activeIndex: 0 }); const layerOpts = { name: "layer1", width: 50, height: 100 }; mutations.addLayer( state, layerOpts ); // assert LayerFactory is invoked with provided opts when calling addLayer() expect( layerCreateSpy ).toHaveBeenCalledWith( layerOpts ); const addedLayer = state.documents[ 0 ].layers[ 1 ]; expect( addedLayer.name ).toEqual( "layer1" ); expect( addedLayer.width ).toEqual( 50 ); expect( addedLayer.height ).toEqual( 100 ); }); it( "when adding a Layer without specified dimensions, these should default to the Document dimensions", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", width: 1000, height: 1000 }) ], activeIndex: 0 }); mockUpdateFn = vi.fn((_fn, data) => data ); const layerOpts = { name: "layer1" }; mutations.addLayer( state, layerOpts ); const mutatedLayer = state.documents[ 0 ].layers[ 1 ]; expect( mutatedLayer.name ).toEqual( layerOpts.name ); expect( mutatedLayer.width ).toEqual( state.documents[ 0 ].width ); expect( mutatedLayer.height ).toEqual( state.documents[ 0 ].height ); }); it( "should update the active layer index to the last added layers index", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }) ] }) ], activeIndex: 0, activeLayerIndex: 1 }); mutations.addLayer( state ); expect( state.activeLayerIndex ).toEqual( 2 ); }); it( "should add new layers at the end of the list (to have them appear on top)", () => { const layer1 = LayerFactory.create({ name: "layer1" }); const layer2 = LayerFactory.create({ name: "layer2" }); const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ layer1 ] }) ], activeIndex: 0 }); mutations.addLayer( state, layer2 ); expect( state.documents[ 0 ].layers ).toEqual([ layer1, layer2 ]); }); it( "should be able to add layers at specific indices in the layer list", () => { const layer1 = LayerFactory.create({ name: "layer1" }); const layer2 = LayerFactory.create({ name: "layer2" }); const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ layer1, layer2 ], }) ], activeIndex: 0, activeLayerIndex: 0, }); const layerToInsert = LayerFactory.create({ name: "layer3" }); mutations.insertLayerAtIndex( state, { index: 1, layer: layerToInsert }); expect( state.documents[ 0 ].layers ).toEqual([ layer1, layerToInsert, layer2 ]); expect( state.activeLayerIndex ).toEqual( 1 ); }); }); describe( "when removing layers", () => { let state: DocumentState; let layer1: Layer; let layer2: Layer; let layer3: Layer; beforeEach(() => { state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }), LayerFactory.create({ name: "layer3" }) ] })], activeIndex: 0, activeLayerIndex: 1, }); ([ layer1, layer2, layer3 ] = state.documents[ 0 ].layers ); }); it( "should be able to remove a layer by reference", () => { mutations.removeLayer( state, 1 ); expect( state.documents[ 0 ].layers ).toEqual([ layer1, layer3 ]); }); it( "should set the active layer index to the first Layer", () => { mutations.removeLayer( state, 1 ); expect( state.activeLayerIndex ).toEqual( 0 ); }); it( "should flush the Layer renderers", () => { mockUpdateFn = vi.fn(); mutations.removeLayer( state, 1 ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushLayerRenderers", layer2 ); }); it( "should flush the blended layer cache fully", () => { mutations.removeLayer( state, 1 ); expect( mockFlushBlendedLayerCache ).toHaveBeenCalledWith( true ); }); it( "should flush the thumbnail cache for the layer", () => { mutations.removeLayer( state, 1 ); expect( mockFlushThumbnailForLayer ).toHaveBeenLastCalledWith( layer2 ); }); }); it( "should be able to replace all layers in a single operation", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }), LayerFactory.create({ name: "layer3" }) ] })], activeIndex: 0, activeLayerIndex: 1, }); const newLayers = [ LayerFactory.create({ name: "layer4" }), LayerFactory.create({ name: "layer5" }), ]; mutations.replaceLayers( state, newLayers ); expect( state.documents[ 0 ].layers ).toEqual( newLayers ); }); it( "should be able to swap the layers in the currently active Document", () => { const fooLayer1 = LayerFactory.create({ name: "fooLayer1" }); const fooLayer2 = LayerFactory.create({ name: "fooLayer2" }); const barLayer1 = LayerFactory.create({ name: "barLayer1" }); const barLayer2 = LayerFactory.create({ name: "barLayer2" }); const barLayer3 = LayerFactory.create({ name: "barLayer3" }); const barLayer4 = LayerFactory.create({ name: "barLayer4" }); const document1 = DocumentFactory.create({ name: "foo", layers: [ fooLayer1, fooLayer2 ] }); const document2 = DocumentFactory.create({ name: "bar", layers: [ barLayer1, barLayer2, barLayer3, barLayer4 ] }); const state = createDocumentState({ documents: [ document1, document2 ], activeIndex: 1 }); mutations.swapLayers( state, { index1: 1, index2: 3 }); expect( state.documents ).toEqual([ { ...document1, layers: [ fooLayer1, fooLayer2 ] }, { ...document2, layers: [ barLayer1, barLayer4, barLayer3, barLayer2 ]}, ]); }); it( "should be able to reorder all layers in the currently active Document", () => { const layers = [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }), LayerFactory.create({ name: "layer3" }), LayerFactory.create({ name: "layer4" }), ]; const orgLayers = [ ...layers ]; const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers })], activeIndex: 0 }); mutations.reorderLayers( state, { activeDocument: state.documents[ 0 ], layerIds: [ layers[ 1 ].id, layers[ 2 ].id, layers[ 0 ].id, layers[ 3 ].id ] }); // note we check by reference to ensure all bindings remain expect( state.documents[ 0 ].layers ).toEqual([ orgLayers[ 1 ], orgLayers[ 2 ], orgLayers[ 0 ], orgLayers[ 3 ] ]); expect( mockFlushBlendedLayerCache ).toHaveBeenCalledWith( true ); }); describe( "when setting the active layer content", () => { it( "should be able to set the active layer by index", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }) ] }) ], activeIndex: 0, activeLayerIndex: 0 }); mutations.setActiveLayerIndex( state, 1 ); expect( state.activeLayerIndex ).toEqual( 1 ); }); it( "should be able to set the active layer by reference", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }) ] })], activeIndex: 0, activeLayerIndex: 0 }); mutations.setActiveLayer( state, state.documents[ 0 ].layers[ 1 ] ); expect( state.activeLayerIndex ).toEqual( 1 ); }); it( "should unset the active layer mask when setting the active layer index", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2" }) ] }) ], activeLayerIndex: 0, maskActive: true, }); mutations.setActiveLayerIndex( state, 1 ); expect( state.maskActive ).toBe( false ); }); it( "should be able to set the active layer mask", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ LayerFactory.create({ name: "layer1" }), LayerFactory.create({ name: "layer2", mask: createMockCanvasElement()}) ] })], activeIndex: 0, activeLayerIndex: 0, maskActive: false, }); mutations.setActiveLayerMask( state, 1 ); expect( state.activeLayerIndex ).toEqual( 1 ); expect( state.maskActive ).toBe( true ); }); }); describe( "when updating Layer properties", () => { let layer1: Layer; let layer2: Layer; let state: DocumentState; beforeEach(() => { layer1 = LayerFactory.create({ name: "layer1", transform: { rotation: 0 } }); layer2 = LayerFactory.create({ name: "layer2", transform: { rotation: 0 } }); state = createDocumentState({ documents: [ DocumentFactory.create({ name: "foo", layers: [ layer1, layer2 ] })], activeIndex: 0 }); }); it( "should be able to update the options of a specific Layer within the active Document", () => { const index = 1; const opts = { name: "layer2 updated", x: 100, y: 200, width: 100, height: 150, type: LayerTypes.LAYER_IMAGE }; const layerRenderer = new LayerRenderer( layer2 ); const cacheEffectsSpy = vi.spyOn( layerRenderer, "cacheEffects" ); mockUpdateFn = vi.fn( fn => { if ( fn === "getRendererForLayer" ) return layerRenderer; return true; }); mutations.updateLayer( state, { index, opts }); expect( state.documents[ 0 ].layers[ index ] ).toEqual({ ...layer2, ...opts }); expect( mockUpdateFn ).toHaveBeenCalledWith( "getRendererForLayer", state.documents[ 0 ].layers[ index ] ); expect( cacheEffectsSpy ).toHaveBeenCalled(); }); it( "should be able to update the source image of a specific layer within the active Document, invoking a filter recache on the renderer", () => { const index = 1; const opts = { name: "layer2 updated", source: new Image(), type: LayerTypes.LAYER_IMAGE }; const layerRenderer = new LayerRenderer( layer2 ); const resetAndRecacheSpy = vi.spyOn( layerRenderer, "resetFilterAndRecache" ); mockUpdateFn = vi.fn( fn => { if ( fn === "getRendererForLayer" ) return layerRenderer; return true; }); mutations.updateLayer( state, { index, opts }); expect( resetAndRecacheSpy ).toHaveBeenCalled(); }); it( "should not flush the blended layer cache when no filter properties were updated", () => { const index = 0; const opts = { name: "layer1 updated" }; const layerRenderer = new LayerRenderer( layer1 ); mockUpdateFn = vi.fn( fn => { if ( fn === "getRendererForLayer" ) return layerRenderer; return true; }); mutations.updateLayer( state, { index, opts }); expect( mockFlushBlendedLayerCache ).not.toHaveBeenCalled(); }); it( "should flush the blended layer cache fully when filter properties are updated to ensure correct rendering on history state changes", () => { const index = 0; const opts = { filters: { gamma: 1 } }; const layerRenderer = new LayerRenderer( layer1 ); mockUpdateFn = vi.fn( fn => { if ( fn === "getRendererForLayer" ) return layerRenderer; return true; }); mutations.updateLayer( state, { index, opts }); expect( mockFlushBlendedLayerCache ).toHaveBeenCalledWith( true ); }); describe( "when requesting to also recreate the renderer for the specific Layer", () => { it( "should not do anything related to renderer lifecycle when no recreation request was provided", () => { const index = 0; const opts = { filters: { gamma: 1 } }; mutations.updateLayer( state, { index, opts, recreateRenderer: false }); const layer = state.documents[ 0 ].layers[ index ]; // the layer after mutation expect( mockCanvasInstance.setLock ).not.toHaveBeenCalled(); expect( mockUpdateFn ).not.toHaveBeenCalledWith( "flushLayerRenderers", layer ); expect( mockUpdateFn ).not.toHaveBeenCalledWith( "createRendererForLayer", mockCanvasInstance, layer, true ); }); it( "should lock the canvas, flush the Layers renderer and create a new renderer instance when a recreation request was provided", () => { const index = 0; const opts = { filters: { gamma: 1 } }; mutations.updateLayer( state, { index, opts, recreateRenderer: true }); const layer = state.documents[ 0 ].layers[ index ]; // the layer after mutation expect( mockCanvasInstance.setLock ).toHaveBeenCalledWith( true ); expect( mockUpdateFn ).toHaveBeenCalledWith( "flushLayerRenderers", layer ); expect( mockUpdateFn ).toHaveBeenCalledWith( "createRendererForLayer", mockCanvasInstance, layer, true ); }); }); it( "should be able to update the transform of a specific Layer within the active Document", () => { const index = 0; const transform = { rotation: 1.6 }; const layerRenderer = new LayerRenderer( layer1 ); const invalidateBlendCacheSpy = vi.spyOn( layerRenderer, "invalidateBlendCache" ); mockUpdateFn = vi.fn( fn => { if ( fn === "getRendererForLayer" ) return layerRenderer; return true; }); mutations.updateLayerTransform( state, { index, transform }); expect( state.documents[ 0 ].layers[ index ] ).toEqual({ ...layer1, transform: { ...layer1.transform, ...transform, } }); expect( mockUpdateFn ).toHaveBeenCalledWith( "getRendererForLayer", state.documents[ 0 ].layers[ index ] ); expect( invalidateBlendCacheSpy ).toHaveBeenCalled(); expect( mockCreateLayerThumbnail ).toHaveBeenCalledWith( layer1, state.documents[ 0 ], true ); }); }); it( "should be able to resize active Document content by calling the render util upon each Layer", async () => { const layer1 = LayerFactory.create({ name: "layer1" }); const layer2 = LayerFactory.create({ name: "layer2" }); const state = createDocumentState({ documents: [ DocumentFactory.create({ layers: [ layer1, layer2 ]}), ], activeIndex: 0, }); mockUpdateFn = vi.fn(); const scaleX = 1.1; const scaleY = 1.2; await mutations.resizeActiveDocumentContent( state, { scaleX, scaleY }); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "resizeLayerContent", layer1, scaleX, scaleY ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "resizeLayerContent", layer2, scaleX, scaleY ); }); it( "should be able to crop active Document content by calling the render util upon each Layer", async () => { const layer1 = LayerFactory.create({ name: "layer1" }); const layer2 = LayerFactory.create({ name: "layer2" }); const state = createDocumentState({ documents: [ DocumentFactory.create({ layers: [ layer1, layer2 ]}), ], activeIndex: 0, }); mockUpdateFn = vi.fn(); const left = 10; const top = 15; await mutations.cropActiveDocumentContent( state, { left, top }); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "cropLayerContent", layer1, { left, top }); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "getRendererForLayer", layer1 ); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 3, "cropLayerContent", layer2, { left, top }); expect( mockUpdateFn ).toHaveBeenNthCalledWith( 4, "getRendererForLayer", layer2 ); }); it( "should be able to update the Document Groups", () => { const state = createDocumentState({ documents: [ DocumentFactory.create() ], activeIndex: 0, }); mutations.updateGroups( state, [ 0, 1, 2 ]); expect( state.documents[ 0 ].groups ).toEqual([ 0, 1, 2 ]); }); it( "should be able to update the Documents metadata using partial updates", () => { const state = createDocumentState({ documents: [ DocumentFactory.create({ meta: { dpi: 150, unit: "px", } }), ], activeIndex: 0, }); mutations.updateMeta( state, { fps: 10, }); expect( state.documents[ 0 ].meta ).toEqual({ dpi: 150, fps: 10, unit: "px", swatches: [], }); }); }); describe( "actions", () => { it( "should be able to flush all resources allocated to a Document when closing", () => { const state = createDocumentState({ documents: [ DocumentFactory.create(), DocumentFactory.create() ], activeIndex: 0, }); const commit = vi.fn(); const getters = { activeDocument: state.documents[ 0 ], t: vi.fn(), }; // @ts-expect-error Not all constituents of type 'Action' are callable. actions.requestDocumentClose({ state, commit, getters }); expect( commit ).toHaveBeenCalledTimes( 1 ); expect( commit ).toHaveBeenCalledWith( "openDialog", expect.any( Object )); // grab the Dialog window request actions and confirm Document close const { confirm } = commit.mock.calls[ 0 ][ 1 ]; confirm(); expect( commit ).toHaveBeenCalledTimes( 5 ); expect( commit ).toHaveBeenCalledWith( "closeActiveDocument" ); expect( commit ).toHaveBeenCalledWith( "removeImagesForDocument", getters.activeDocument ); expect( commit ).toHaveBeenCalledWith( "setActiveDocument", 0 ); expect( commit ).toHaveBeenCalledWith( "clearHistory", getters.activeDocument.id ); }); }); });