import { beforeEach, describe, expect, it, vi } from 'vitest' import { ColorComparisonMethod } from '@/extensions/core/maskeditor/types' import { useCanvasTools } from '@/composables/maskeditor/useCanvasTools' const mockCanvasHistory = { saveState: vi.fn() } const mockStore = { maskCtx: null as any, imgCtx: null as any, maskCanvas: null as any, imgCanvas: null as any, rgbCtx: null as any, rgbCanvas: null as any, maskColor: { r: 255, g: 255, b: 255 }, paintBucketTolerance: 10, fillOpacity: 100, colorSelectTolerance: 30, colorComparisonMethod: ColorComparisonMethod.Simple, selectionOpacity: 100, applyWholeImage: false, maskBoundary: false, maskTolerance: 10, canvasHistory: mockCanvasHistory } vi.mock('@/stores/maskEditorStore', () => ({ useMaskEditorStore: vi.fn(() => mockStore) })) describe('useCanvasTools', () => { let mockMaskImageData: ImageData let mockImgImageData: ImageData beforeEach(() => { vi.clearAllMocks() mockMaskImageData = { data: new Uint8ClampedArray(100 * 100 * 4), width: 100, height: 100 } as ImageData mockImgImageData = { data: new Uint8ClampedArray(100 * 100 * 4), width: 100, height: 100 } as ImageData for (let i = 0; i < mockImgImageData.data.length; i += 4) { mockImgImageData.data[i] = 255 mockImgImageData.data[i + 1] = 0 mockImgImageData.data[i + 2] = 0 mockImgImageData.data[i + 3] = 255 } mockStore.maskCtx = { getImageData: vi.fn(() => mockMaskImageData), putImageData: vi.fn(), clearRect: vi.fn() } mockStore.imgCtx = { getImageData: vi.fn(() => mockImgImageData) } mockStore.rgbCtx = { clearRect: vi.fn() } mockStore.maskCanvas = { width: 100, height: 100 } mockStore.imgCanvas = { width: 100, height: 100 } mockStore.rgbCanvas = { width: 100, height: 100 } mockStore.maskColor = { r: 255, g: 255, b: 255 } mockStore.paintBucketTolerance = 10 mockStore.fillOpacity = 100 mockStore.colorSelectTolerance = 30 mockStore.colorComparisonMethod = ColorComparisonMethod.Simple mockStore.selectionOpacity = 100 mockStore.applyWholeImage = false mockStore.maskBoundary = false mockStore.maskTolerance = 10 }) describe('paintBucketFill', () => { it('should fill empty area with mask color', () => { const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.getImageData).toHaveBeenCalledWith( 0, 0, 100, 100 ) expect(mockStore.maskCtx.putImageData).toHaveBeenCalledWith( mockMaskImageData, 0, 0 ) expect(mockCanvasHistory.saveState).toHaveBeenCalled() const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(255) }) it('should erase filled area', () => { for (let i = 0; i < mockMaskImageData.data.length; i += 4) { mockMaskImageData.data[i + 3] = 255 } const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(0) }) it('should respect tolerance', () => { mockStore.paintBucketTolerance = 0 for (let i = 0; i < mockMaskImageData.data.length; i += 4) { mockMaskImageData.data[i + 3] = 0 } const centerIndex = (50 * 100 + 50) * 4 mockMaskImageData.data[centerIndex + 3] = 10 const tools = useCanvasTools() tools.paintBucketFill({ x: 51, y: 50 }) expect(mockMaskImageData.data[centerIndex + 3]).toBe(10) }) it('should return early when point out of bounds', () => { const tools = useCanvasTools() tools.paintBucketFill({ x: -1, y: 50 }) expect(mockStore.maskCtx.putImageData).not.toHaveBeenCalled() }) it('should return early when canvas missing', () => { mockStore.maskCanvas = null const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled() }) it('should apply fill opacity', () => { mockStore.fillOpacity = 50 const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(127) }) it('should apply mask color', () => { mockStore.maskColor = { r: 128, g: 64, b: 32 } const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index]).toBe(128) expect(mockMaskImageData.data[index + 1]).toBe(64) expect(mockMaskImageData.data[index + 2]).toBe(32) }) }) describe('colorSelectFill', () => { it('should select pixels by color with flood fill', async () => { const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.getImageData).toHaveBeenCalledWith( 0, 0, 100, 100 ) expect(mockStore.imgCtx.getImageData).toHaveBeenCalledWith(0, 0, 100, 100) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() expect(mockCanvasHistory.saveState).toHaveBeenCalled() }) it('should select pixels in whole image when applyWholeImage is true', async () => { mockStore.applyWholeImage = true const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should respect color tolerance', async () => { mockStore.colorSelectTolerance = 0 for (let i = 0; i < mockImgImageData.data.length; i += 4) { mockImgImageData.data[i] = 200 } const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(255) }) it('should return early when point out of bounds', async () => { const tools = useCanvasTools() await tools.colorSelectFill({ x: -1, y: 50 }) expect(mockStore.maskCtx.putImageData).not.toHaveBeenCalled() }) it('should return early when canvas missing', async () => { mockStore.imgCanvas = null const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled() }) it('should apply selection opacity', async () => { mockStore.selectionOpacity = 50 const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(127) }) it('should use HSL color comparison method', async () => { mockStore.colorComparisonMethod = ColorComparisonMethod.HSL const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should use LAB color comparison method', async () => { mockStore.colorComparisonMethod = ColorComparisonMethod.LAB const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should respect mask boundary', async () => { mockStore.maskBoundary = true mockStore.maskTolerance = 0 for (let i = 0; i < mockMaskImageData.data.length; i += 4) { mockMaskImageData.data[i + 3] = 255 } const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should update last color select point', async () => { const tools = useCanvasTools() await tools.colorSelectFill({ x: 30, y: 40 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) }) describe('invertMask', () => { it('should invert mask alpha values', () => { for (let i = 0; i < mockMaskImageData.data.length; i += 4) { mockMaskImageData.data[i] = 255 mockMaskImageData.data[i + 1] = 255 mockMaskImageData.data[i + 2] = 255 mockMaskImageData.data[i + 3] = 128 } const tools = useCanvasTools() tools.invertMask() expect(mockStore.maskCtx.getImageData).toHaveBeenCalledWith( 0, 0, 100, 100 ) expect(mockStore.maskCtx.putImageData).toHaveBeenCalledWith( mockMaskImageData, 0, 0 ) expect(mockCanvasHistory.saveState).toHaveBeenCalled() for (let i = 0; i < mockMaskImageData.data.length; i += 4) { expect(mockMaskImageData.data[i + 3]).toBe(127) } }) it('should preserve mask color for empty pixels', () => { for (let i = 0; i < mockMaskImageData.data.length; i += 4) { mockMaskImageData.data[i + 3] = 0 } const firstPixelIndex = 100 mockMaskImageData.data[firstPixelIndex * 4] = 128 mockMaskImageData.data[firstPixelIndex * 4 + 1] = 64 mockMaskImageData.data[firstPixelIndex * 4 + 2] = 32 mockMaskImageData.data[firstPixelIndex * 4 + 3] = 255 const tools = useCanvasTools() tools.invertMask() for (let i = 0; i < mockMaskImageData.data.length; i += 4) { if (i !== firstPixelIndex * 4) { expect(mockMaskImageData.data[i]).toBe(128) expect(mockMaskImageData.data[i + 1]).toBe(64) expect(mockMaskImageData.data[i + 2]).toBe(32) } } }) it('should return early when canvas missing', () => { mockStore.maskCanvas = null const tools = useCanvasTools() tools.invertMask() expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled() }) it('should return early when context missing', () => { mockStore.maskCtx = null const tools = useCanvasTools() tools.invertMask() expect(mockCanvasHistory.saveState).not.toHaveBeenCalled() }) }) describe('clearMask', () => { it('should clear mask canvas', () => { const tools = useCanvasTools() tools.clearMask() expect(mockStore.maskCtx.clearRect).toHaveBeenCalledWith(0, 0, 100, 100) expect(mockStore.rgbCtx.clearRect).toHaveBeenCalledWith(0, 0, 100, 100) expect(mockCanvasHistory.saveState).toHaveBeenCalled() }) it('should handle missing mask canvas', () => { mockStore.maskCanvas = null const tools = useCanvasTools() tools.clearMask() expect(mockStore.maskCtx.clearRect).not.toHaveBeenCalled() expect(mockCanvasHistory.saveState).toHaveBeenCalled() }) it('should handle missing rgb canvas', () => { mockStore.rgbCanvas = null const tools = useCanvasTools() tools.clearMask() expect(mockStore.maskCtx.clearRect).toHaveBeenCalledWith(0, 0, 100, 100) expect(mockStore.rgbCtx.clearRect).not.toHaveBeenCalled() expect(mockCanvasHistory.saveState).toHaveBeenCalled() }) }) describe('clearLastColorSelectPoint', () => { it('should clear last color select point', async () => { const tools = useCanvasTools() await tools.colorSelectFill({ x: 50, y: 50 }) tools.clearLastColorSelectPoint() expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) }) describe('edge cases', () => { it('should handle small canvas', () => { mockStore.maskCanvas.width = 1 mockStore.maskCanvas.height = 1 mockMaskImageData = { data: new Uint8ClampedArray(1 * 1 * 4), width: 1, height: 1 } as ImageData mockStore.maskCtx.getImageData = vi.fn(() => mockMaskImageData) const tools = useCanvasTools() tools.paintBucketFill({ x: 0, y: 0 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should handle fractional coordinates', () => { const tools = useCanvasTools() tools.paintBucketFill({ x: 50.7, y: 50.3 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should handle maximum tolerance', () => { mockStore.paintBucketTolerance = 255 const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) expect(mockStore.maskCtx.putImageData).toHaveBeenCalled() }) it('should handle zero opacity', () => { mockStore.fillOpacity = 0 const tools = useCanvasTools() tools.paintBucketFill({ x: 50, y: 50 }) const index = (50 * 100 + 50) * 4 expect(mockMaskImageData.data[index + 3]).toBe(0) }) }) })