mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-04 20:50:06 +00:00
## Summary Improved type safety in test files by eliminating unsafe type assertions and adopting official testing patterns. Reduced unsafe `as unknown as` type assertions and eliminated all `null!` assertions. ## Changes - **Adopted @pinia/testing patterns** - Replaced manual Pinia store mocking with `createTestingPinia()` in `useSelectionState.test.ts` - Eliminated ~120 lines of mock boilerplate - Created `createMockSettingStore()` helper to replace duplicated store mocks in `useCoreCommands.test.ts` - **Eliminated unsafe null assertions** - Created explicit `MockMaskEditorStore` interface with proper nullable types in `useCanvasTools.test.ts` - Replaced `null!` initializations with `null` and used `!` at point of use or `?.` for optional chaining - **Made partial mock intent explicit** - Updated test utilities in `litegraphTestUtils.ts` to use explicit `Partial<T>` typing - Changed cast pattern from `as T` to `as Partial<T> as T` to show incomplete mock intent - Applied to `createMockLGraphNode()`, `createMockPositionable()`, and `createMockLGraphGroup()` - **Created centralized mock utilities** in `src/utils/__tests__/litegraphTestUtils.ts` - `createMockLGraphNode()`, `createMockPositionable()`, `createMockLGraphGroup()`, `createMockSubgraphNode()` - Updated 8+ test files to use centralized utilities - Used union types `Partial<T> | Record<string, unknown>` for flexible mock creation ## Results - ✅ 0 typecheck errors - ✅ 0 lint errors - ✅ All tests passing in modified files - ✅ Eliminated all `null!` assertions - ✅ Reduced unsafe double-cast patterns significantly ## Files Modified (18) - `src/components/graph/SelectionToolbox.test.ts` - `src/components/graph/selectionToolbox/{BypassButton,ColorPickerButton,ExecuteButton}.test.ts` - `src/components/sidebar/tabs/queue/ResultGallery.test.ts` - `src/composables/canvas/useSelectedLiteGraphItems.test.ts` - `src/composables/graph/{useGraphHierarchy,useSelectionState}.test.ts` - `src/composables/maskeditor/{useCanvasHistory,useCanvasManager,useCanvasTools,useCanvasTransform}.test.ts` - `src/composables/node/{useNodePricing,useWatchWidget}.test.ts` - `src/composables/{useBrowserTabTitle,useCoreCommands}.test.ts` - `src/utils/__tests__/litegraphTestUtils.ts` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8258-refactor-eliminate-unsafe-type-assertions-from-Group-2-test-files-2f16d73d365081549c65fd546cc7c765) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
640 lines
17 KiB
TypeScript
640 lines
17 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { nextTick } from 'vue'
|
|
import { useCanvasHistory } from '@/composables/maskeditor/useCanvasHistory'
|
|
|
|
// Define the store shape to avoid 'any' and cast to the expected type
|
|
interface MaskEditorStoreState {
|
|
maskCanvas: HTMLCanvasElement | null
|
|
rgbCanvas: HTMLCanvasElement | null
|
|
imgCanvas: HTMLCanvasElement | null
|
|
maskCtx: CanvasRenderingContext2D | null
|
|
rgbCtx: CanvasRenderingContext2D | null
|
|
imgCtx: CanvasRenderingContext2D | null
|
|
}
|
|
|
|
// Use vi.hoisted to create isolated mock state container
|
|
const mockRefs = vi.hoisted(() => ({
|
|
maskCanvas: null as HTMLCanvasElement | null,
|
|
rgbCanvas: null as HTMLCanvasElement | null,
|
|
imgCanvas: null as HTMLCanvasElement | null,
|
|
maskCtx: null as CanvasRenderingContext2D | null,
|
|
rgbCtx: null as CanvasRenderingContext2D | null,
|
|
imgCtx: null as CanvasRenderingContext2D | null
|
|
}))
|
|
|
|
const mockStore: MaskEditorStoreState = {
|
|
get maskCanvas() {
|
|
return mockRefs.maskCanvas
|
|
},
|
|
set maskCanvas(val) {
|
|
mockRefs.maskCanvas = val
|
|
},
|
|
get rgbCanvas() {
|
|
return mockRefs.rgbCanvas
|
|
},
|
|
set rgbCanvas(val) {
|
|
mockRefs.rgbCanvas = val
|
|
},
|
|
get imgCanvas() {
|
|
return mockRefs.imgCanvas
|
|
},
|
|
set imgCanvas(val) {
|
|
mockRefs.imgCanvas = val
|
|
},
|
|
get maskCtx() {
|
|
return mockRefs.maskCtx
|
|
},
|
|
set maskCtx(val) {
|
|
mockRefs.maskCtx = val
|
|
},
|
|
get rgbCtx() {
|
|
return mockRefs.rgbCtx
|
|
},
|
|
set rgbCtx(val) {
|
|
mockRefs.rgbCtx = val
|
|
},
|
|
get imgCtx() {
|
|
return mockRefs.imgCtx
|
|
},
|
|
set imgCtx(val) {
|
|
mockRefs.imgCtx = val
|
|
}
|
|
}
|
|
|
|
vi.mock('@/stores/maskEditorStore', () => ({
|
|
useMaskEditorStore: vi.fn(() => mockStore)
|
|
}))
|
|
|
|
// Mock ImageBitmap using safe global augmentation pattern
|
|
if (typeof globalThis.ImageBitmap === 'undefined') {
|
|
globalThis.ImageBitmap = class ImageBitmap {
|
|
width: number
|
|
height: number
|
|
constructor(width = 100, height = 100) {
|
|
this.width = width
|
|
this.height = height
|
|
}
|
|
close() {}
|
|
} as typeof ImageBitmap
|
|
}
|
|
|
|
describe('useCanvasHistory', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
let rafCallCount = 0
|
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
|
|
(cb: FrameRequestCallback) => {
|
|
if (rafCallCount++ < 100) {
|
|
setTimeout(() => cb(0), 0)
|
|
}
|
|
return rafCallCount
|
|
}
|
|
)
|
|
|
|
const createMockImageData = (): ImageData => {
|
|
return {
|
|
data: new Uint8ClampedArray(100 * 100 * 4),
|
|
width: 100,
|
|
height: 100
|
|
} as ImageData
|
|
}
|
|
|
|
// Mock contexts using explicit partial-cast pattern
|
|
mockRefs.maskCtx = {
|
|
getImageData: vi.fn(() => createMockImageData()),
|
|
putImageData: vi.fn(),
|
|
clearRect: vi.fn(),
|
|
drawImage: vi.fn()
|
|
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
|
|
|
|
mockRefs.rgbCtx = {
|
|
getImageData: vi.fn(() => createMockImageData()),
|
|
putImageData: vi.fn(),
|
|
clearRect: vi.fn(),
|
|
drawImage: vi.fn()
|
|
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
|
|
|
|
mockRefs.imgCtx = {
|
|
getImageData: vi.fn(() => createMockImageData()),
|
|
putImageData: vi.fn(),
|
|
clearRect: vi.fn(),
|
|
drawImage: vi.fn()
|
|
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
|
|
|
|
// Mock canvases using explicit partial-cast pattern
|
|
mockRefs.maskCanvas = {
|
|
width: 100,
|
|
height: 100
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
|
|
mockRefs.rgbCanvas = {
|
|
width: 100,
|
|
height: 100
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
|
|
mockRefs.imgCanvas = {
|
|
width: 100,
|
|
height: 100
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
})
|
|
|
|
describe('initialization', () => {
|
|
it('should initialize with default values', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should save initial state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
expect(mockRefs.maskCtx!.getImageData).toHaveBeenCalledWith(
|
|
0,
|
|
0,
|
|
100,
|
|
100
|
|
)
|
|
expect(mockRefs.rgbCtx!.getImageData).toHaveBeenCalledWith(0, 0, 100, 100)
|
|
expect(mockRefs.imgCtx!.getImageData).toHaveBeenCalledWith(0, 0, 100, 100)
|
|
expect(history.canUndo.value).toBe(false)
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should wait for canvas to be ready', () => {
|
|
const rafSpy = vi.spyOn(window, 'requestAnimationFrame')
|
|
|
|
mockRefs.maskCanvas = {
|
|
// oxlint-disable-next-line no-misused-spread
|
|
...mockRefs.maskCanvas,
|
|
width: 0,
|
|
height: 0
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
|
|
const history = useCanvasHistory()
|
|
history.saveInitialState()
|
|
|
|
expect(rafSpy).toHaveBeenCalled()
|
|
|
|
mockRefs.maskCanvas = {
|
|
width: 100,
|
|
height: 100
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
})
|
|
|
|
it('should wait for context to be ready', () => {
|
|
const rafSpy = vi.spyOn(window, 'requestAnimationFrame')
|
|
|
|
mockRefs.maskCtx = null
|
|
|
|
const history = useCanvasHistory()
|
|
history.saveInitialState()
|
|
|
|
expect(rafSpy).toHaveBeenCalled()
|
|
|
|
const createMockImageData = (): ImageData => {
|
|
return {
|
|
data: new Uint8ClampedArray(100 * 100 * 4),
|
|
width: 100,
|
|
height: 100
|
|
} as ImageData
|
|
}
|
|
|
|
mockRefs.maskCtx = {
|
|
getImageData: vi.fn(() => createMockImageData()),
|
|
putImageData: vi.fn(),
|
|
clearRect: vi.fn(),
|
|
drawImage: vi.fn()
|
|
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
|
|
})
|
|
})
|
|
|
|
describe('saveState', () => {
|
|
it('should save a new state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
vi.mocked(mockRefs.maskCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.getImageData).mockClear()
|
|
|
|
history.saveState()
|
|
|
|
expect(mockRefs.maskCtx!.getImageData).toHaveBeenCalledWith(
|
|
0,
|
|
0,
|
|
100,
|
|
100
|
|
)
|
|
expect(mockRefs.rgbCtx!.getImageData).toHaveBeenCalledWith(0, 0, 100, 100)
|
|
expect(mockRefs.imgCtx!.getImageData).toHaveBeenCalledWith(0, 0, 100, 100)
|
|
expect(history.canUndo.value).toBe(true)
|
|
})
|
|
|
|
it('should clear redo states when saving new state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
expect(history.canRedo.value).toBe(true)
|
|
|
|
history.saveState()
|
|
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should respect maxStates limit', () => {
|
|
const history = useCanvasHistory(3)
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
history.saveState()
|
|
history.saveState()
|
|
|
|
expect(history.canUndo.value).toBe(true)
|
|
|
|
let undoCount = 0
|
|
while (history.canUndo.value && undoCount < 10) {
|
|
history.undo()
|
|
undoCount++
|
|
}
|
|
|
|
expect(undoCount).toBe(2)
|
|
})
|
|
|
|
it('should call saveInitialState if not initialized', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveState()
|
|
|
|
expect(mockRefs.maskCtx!.getImageData).toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.getImageData).toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.getImageData).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not save state if context is missing', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
const savedMaskCtx = mockRefs.maskCtx
|
|
mockRefs.maskCtx = null
|
|
vi.mocked(savedMaskCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.getImageData).mockClear()
|
|
|
|
history.saveState()
|
|
|
|
expect(savedMaskCtx!.getImageData).not.toHaveBeenCalled()
|
|
|
|
mockRefs.maskCtx = savedMaskCtx
|
|
})
|
|
})
|
|
|
|
describe('undo', () => {
|
|
it('should undo to previous state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
history.undo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).toHaveBeenCalled()
|
|
expect(history.canUndo.value).toBe(false)
|
|
expect(history.canRedo.value).toBe(true)
|
|
})
|
|
|
|
it('should not undo when no undo states available', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.undo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should undo multiple times', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
history.saveState()
|
|
|
|
history.undo()
|
|
expect(history.canUndo.value).toBe(true)
|
|
|
|
history.undo()
|
|
expect(history.canUndo.value).toBe(true)
|
|
|
|
history.undo()
|
|
expect(history.canUndo.value).toBe(false)
|
|
})
|
|
|
|
it('should not undo beyond first state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
history.undo()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.undo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('redo', () => {
|
|
it('should redo to next state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.redo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).toHaveBeenCalled()
|
|
expect(history.canRedo.value).toBe(false)
|
|
expect(history.canUndo.value).toBe(true)
|
|
})
|
|
|
|
it('should not redo when no redo states available', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.redo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should redo multiple times', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
history.saveState()
|
|
|
|
history.undo()
|
|
history.undo()
|
|
history.undo()
|
|
|
|
history.redo()
|
|
expect(history.canRedo.value).toBe(true)
|
|
|
|
history.redo()
|
|
expect(history.canRedo.value).toBe(true)
|
|
|
|
history.redo()
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should not redo beyond last state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
history.redo()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.redo()
|
|
|
|
expect(mockRefs.maskCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.putImageData).not.toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.putImageData).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('clearStates', () => {
|
|
it('should clear all states', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
|
|
history.clearStates()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should allow saving initial state after clear', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.clearStates()
|
|
|
|
vi.mocked(mockRefs.maskCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.getImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.getImageData).mockClear()
|
|
|
|
history.saveInitialState()
|
|
|
|
expect(mockRefs.maskCtx!.getImageData).toHaveBeenCalled()
|
|
expect(mockRefs.rgbCtx!.getImageData).toHaveBeenCalled()
|
|
expect(mockRefs.imgCtx!.getImageData).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('canUndo computed', () => {
|
|
it('should be false with no states', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
})
|
|
|
|
it('should be false with only initial state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
})
|
|
|
|
it('should be true after saving a state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
expect(history.canUndo.value).toBe(true)
|
|
})
|
|
|
|
it('should be false after undoing to first state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('canRedo computed', () => {
|
|
it('should be false with no undo', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should be true after undo', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
expect(history.canRedo.value).toBe(true)
|
|
})
|
|
|
|
it('should be false after redo to last state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
history.redo()
|
|
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
|
|
it('should be false after saving new state', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.undo()
|
|
|
|
expect(history.canRedo.value).toBe(true)
|
|
|
|
history.saveState()
|
|
|
|
expect(history.canRedo.value).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('restoreState', () => {
|
|
it('should not restore if context is missing', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
const savedMaskCtx = mockRefs.maskCtx
|
|
mockRefs.maskCtx = null
|
|
vi.mocked(savedMaskCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.rgbCtx!.putImageData).mockClear()
|
|
vi.mocked(mockRefs.imgCtx!.putImageData).mockClear()
|
|
|
|
history.undo()
|
|
|
|
expect(savedMaskCtx!.putImageData).not.toHaveBeenCalled()
|
|
|
|
mockRefs.maskCtx = savedMaskCtx
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle rapid state saves', async () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
history.saveState()
|
|
await nextTick()
|
|
}
|
|
|
|
expect(history.canUndo.value).toBe(true)
|
|
})
|
|
|
|
it('should handle maxStates of 1', () => {
|
|
const history = useCanvasHistory(1)
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
|
|
expect(history.canUndo.value).toBe(false)
|
|
})
|
|
|
|
it('should handle undo/redo cycling', () => {
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
history.saveState()
|
|
history.saveState()
|
|
|
|
history.undo()
|
|
history.redo()
|
|
history.undo()
|
|
history.redo()
|
|
history.undo()
|
|
|
|
expect(history.canRedo.value).toBe(true)
|
|
expect(history.canUndo.value).toBe(true)
|
|
})
|
|
|
|
it('should handle zero-sized canvas', () => {
|
|
if (mockRefs.maskCanvas) {
|
|
mockRefs.maskCanvas = {
|
|
width: 0,
|
|
height: 0
|
|
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
|
}
|
|
|
|
const history = useCanvasHistory()
|
|
|
|
history.saveInitialState()
|
|
|
|
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|