mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
## Summary Migrates all unit tests from `tests-ui/` to colocate with their source files in `src/`, improving discoverability and maintainability. ## Changes - **What**: Relocated all unit tests to be adjacent to the code they test, following the `<source>.test.ts` naming convention - **Config**: Updated `vitest.config.ts` to remove `tests-ui` include pattern and `@tests-ui` alias - **Docs**: Moved testing documentation to `docs/testing/` with updated paths and patterns ## Review Focus - Migration patterns documented in `temp/plans/migrate-tests-ui-to-src.md` - Tests use `@/` path aliases instead of relative imports - Shared fixtures placed in `__fixtures__/` directories ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7811-chore-migrate-tests-from-tests-ui-to-colocate-with-source-files-2da6d73d36508147a4cce85365dee614) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
481 lines
13 KiB
TypeScript
481 lines
13 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|