mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Add a cancel (x) button overlay to active job cards in grid view, matching the existing list view behavior. ## Changes - **What**: Added hover-triggered cancel button to `ActiveJobCard.vue` using the existing `useJobActions` composable - Button appears on hover for jobs in cancellable states (pending, initialization, running) <img width="1710" height="1107" alt="스크린샷 2026-01-23 오후 5 11 04" src="https://github.com/user-attachments/assets/31a6b6d1-46e7-49c4-a253-92260cb28514" /> <img width="1710" height="1107" alt="스크린샷 2026-01-23 오후 5 10 59" src="https://github.com/user-attachments/assets/b559a81e-8e62-4858-b8e7-92de9caa2919" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8264-feat-Add-cancel-button-to-active-job-card-in-grid-view-2f16d73d3650817d83f0dd3b955759cb) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
225 lines
5.9 KiB
TypeScript
225 lines
5.9 KiB
TypeScript
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
|
|
|
// Use vi.hoisted to create a mutable reference for isCloud
|
|
const mockIsCloud = vi.hoisted(() => ({ value: false }))
|
|
|
|
// Track the filename passed to createAnnotatedPath
|
|
const capturedFilenames = vi.hoisted(() => ({ values: [] as string[] }))
|
|
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
get isCloud() {
|
|
return mockIsCloud.value
|
|
}
|
|
}))
|
|
|
|
vi.mock('primevue/usetoast', () => ({
|
|
useToast: () => ({
|
|
add: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: (key: string) => key
|
|
}),
|
|
createI18n: () => ({
|
|
global: {
|
|
t: (key: string) => key
|
|
}
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/stores/dialogStore', () => ({
|
|
useDialogStore: () => ({
|
|
showDialog: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/composables/useCopyToClipboard', () => ({
|
|
useCopyToClipboard: () => ({
|
|
copyToClipboard: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/platform/workflow/core/services/workflowActionsService', () => ({
|
|
useWorkflowActionsService: () => ({
|
|
openWorkflowAction: vi.fn(),
|
|
exportWorkflowAction: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/services/litegraphService', () => ({
|
|
useLitegraphService: () => ({
|
|
addNodeOnGraph: vi.fn().mockReturnValue({
|
|
widgets: [{ name: 'image', value: '', callback: vi.fn() }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
} as unknown as LGraphNode),
|
|
getCanvasCenter: vi.fn().mockReturnValue([100, 100])
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/stores/nodeDefStore', () => ({
|
|
useNodeDefStore: () => ({
|
|
nodeDefsByName: {
|
|
LoadImage: {
|
|
name: 'LoadImage',
|
|
display_name: 'Load Image'
|
|
}
|
|
}
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/utils/createAnnotatedPath', () => ({
|
|
createAnnotatedPath: vi.fn((item: { filename: string }) => {
|
|
capturedFilenames.values.push(item.filename)
|
|
return item.filename
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/utils/loaderNodeUtil', () => ({
|
|
detectNodeTypeFromFilename: vi.fn().mockReturnValue({
|
|
nodeType: 'LoadImage',
|
|
widgetName: 'image'
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/utils/typeGuardUtil', () => ({
|
|
isResultItemType: vi.fn().mockReturnValue(true)
|
|
}))
|
|
|
|
vi.mock('@/platform/assets/utils/assetTypeUtil', () => ({
|
|
getAssetType: vi.fn().mockReturnValue('input')
|
|
}))
|
|
|
|
vi.mock('../schemas/assetMetadataSchema', () => ({
|
|
getOutputAssetMetadata: vi.fn().mockReturnValue(null)
|
|
}))
|
|
|
|
function createMockAsset(overrides: Partial<AssetItem> = {}): AssetItem {
|
|
return {
|
|
id: 'test-asset-id',
|
|
name: 'original-name.jpeg',
|
|
size: 1024,
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
tags: ['input'],
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('useMediaAssetActions', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
capturedFilenames.values = []
|
|
mockIsCloud.value = false
|
|
})
|
|
|
|
describe('addWorkflow', () => {
|
|
describe('OSS mode (isCloud = false)', () => {
|
|
beforeEach(() => {
|
|
mockIsCloud.value = false
|
|
})
|
|
|
|
it('should use asset.name as filename', async () => {
|
|
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
|
const actions = useMediaAssetActions()
|
|
|
|
const asset = createMockAsset({
|
|
name: 'my-image.jpeg',
|
|
asset_hash: 'hash123.jpeg'
|
|
})
|
|
|
|
await actions.addWorkflow(asset)
|
|
|
|
expect(capturedFilenames.values).toContain('my-image.jpeg')
|
|
})
|
|
})
|
|
|
|
describe('Cloud mode (isCloud = true)', () => {
|
|
beforeEach(() => {
|
|
mockIsCloud.value = true
|
|
})
|
|
|
|
it('should use asset_hash as filename when available', async () => {
|
|
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
|
const actions = useMediaAssetActions()
|
|
|
|
const asset = createMockAsset({
|
|
name: 'original.jpeg',
|
|
asset_hash: 'abc123hash.jpeg'
|
|
})
|
|
|
|
await actions.addWorkflow(asset)
|
|
|
|
expect(capturedFilenames.values).toContain('abc123hash.jpeg')
|
|
})
|
|
|
|
it('should fall back to asset.name when asset_hash is not available', async () => {
|
|
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
|
const actions = useMediaAssetActions()
|
|
|
|
const asset = createMockAsset({
|
|
name: 'fallback-name.jpeg',
|
|
asset_hash: undefined
|
|
})
|
|
|
|
await actions.addWorkflow(asset)
|
|
|
|
expect(capturedFilenames.values).toContain('fallback-name.jpeg')
|
|
})
|
|
|
|
it('should fall back to asset.name when asset_hash is null', async () => {
|
|
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
|
const actions = useMediaAssetActions()
|
|
|
|
const asset = createMockAsset({
|
|
name: 'fallback-null.jpeg',
|
|
asset_hash: null
|
|
})
|
|
|
|
await actions.addWorkflow(asset)
|
|
|
|
expect(capturedFilenames.values).toContain('fallback-null.jpeg')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('addMultipleToWorkflow', () => {
|
|
describe('Cloud mode (isCloud = true)', () => {
|
|
beforeEach(() => {
|
|
mockIsCloud.value = true
|
|
})
|
|
|
|
it('should use asset_hash for each asset', async () => {
|
|
const { useMediaAssetActions } = await import('./useMediaAssetActions')
|
|
const actions = useMediaAssetActions()
|
|
|
|
const assets = [
|
|
createMockAsset({
|
|
id: '1',
|
|
name: 'file1.jpeg',
|
|
asset_hash: 'hash1.jpeg'
|
|
}),
|
|
createMockAsset({
|
|
id: '2',
|
|
name: 'file2.jpeg',
|
|
asset_hash: 'hash2.jpeg'
|
|
})
|
|
]
|
|
|
|
await actions.addMultipleToWorkflow(assets)
|
|
|
|
expect(capturedFilenames.values).toContain('hash1.jpeg')
|
|
expect(capturedFilenames.values).toContain('hash2.jpeg')
|
|
expect(capturedFilenames.values).not.toContain('file1.jpeg')
|
|
expect(capturedFilenames.values).not.toContain('file2.jpeg')
|
|
})
|
|
})
|
|
})
|
|
})
|