mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-02 21:28:08 +00:00
Compare commits
1 Commits
drjkl/prev
...
shihchi/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f8b62cd89 |
26
src/stores/electronDownloadStore.nonDesktop.test.ts
Normal file
26
src/stores/electronDownloadStore.nonDesktop.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
|
||||
const electronAPI = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({ isDesktop: false }))
|
||||
vi.mock('@/utils/envUtil', () => ({ electronAPI }))
|
||||
|
||||
describe('electronDownloadStore outside desktop', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
electronAPI.mockClear()
|
||||
})
|
||||
|
||||
it('skips the Electron bridge when not running on desktop', async () => {
|
||||
const store = useElectronDownloadStore()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(electronAPI).not.toHaveBeenCalled()
|
||||
expect(store.downloads).toEqual([])
|
||||
})
|
||||
})
|
||||
103
src/stores/electronDownloadStore.test.ts
Normal file
103
src/stores/electronDownloadStore.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { DownloadStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
|
||||
const downloadManagerMock = vi.hoisted(() => ({
|
||||
cancelDownload: vi.fn(),
|
||||
getAllDownloads: vi.fn(),
|
||||
onDownloadProgress: vi.fn(),
|
||||
pauseDownload: vi.fn(),
|
||||
resumeDownload: vi.fn(),
|
||||
startDownload: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isDesktop: true
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/envUtil', () => ({
|
||||
electronAPI: () => ({
|
||||
DownloadManager: downloadManagerMock
|
||||
})
|
||||
}))
|
||||
|
||||
describe('electronDownloadStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
Object.values(downloadManagerMock).forEach((mock) => mock.mockReset())
|
||||
downloadManagerMock.getAllDownloads.mockResolvedValue([
|
||||
{
|
||||
filename: 'done.bin',
|
||||
status: DownloadStatus.COMPLETED,
|
||||
url: 'https://example.com/done.bin'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('loads existing downloads and applies progress updates by URL', async () => {
|
||||
let progressCallback:
|
||||
| Parameters<typeof downloadManagerMock.onDownloadProgress>[0]
|
||||
| undefined
|
||||
downloadManagerMock.onDownloadProgress.mockImplementation((callback) => {
|
||||
progressCallback = callback
|
||||
})
|
||||
const store = useElectronDownloadStore()
|
||||
|
||||
await store.initialize()
|
||||
progressCallback?.({
|
||||
filename: 'model.bin',
|
||||
progress: 25,
|
||||
savePath: '/tmp/model.bin',
|
||||
status: DownloadStatus.IN_PROGRESS,
|
||||
url: 'https://example.com/model.bin'
|
||||
})
|
||||
progressCallback?.({
|
||||
filename: 'model.bin',
|
||||
progress: 50,
|
||||
savePath: '/tmp/model.bin',
|
||||
status: DownloadStatus.IN_PROGRESS,
|
||||
url: 'https://example.com/model.bin'
|
||||
})
|
||||
|
||||
expect(store.findByUrl('https://example.com/done.bin')?.status).toBe(
|
||||
DownloadStatus.COMPLETED
|
||||
)
|
||||
expect(store.findByUrl('https://example.com/model.bin')).toMatchObject({
|
||||
filename: 'model.bin',
|
||||
progress: 50,
|
||||
status: DownloadStatus.IN_PROGRESS
|
||||
})
|
||||
expect(store.inProgressDownloads).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('delegates download controls to the Electron bridge', async () => {
|
||||
const store = useElectronDownloadStore()
|
||||
|
||||
await store.start({
|
||||
filename: 'model.bin',
|
||||
savePath: '/tmp/model.bin',
|
||||
url: 'https://example.com/model.bin'
|
||||
})
|
||||
await store.pause('https://example.com/model.bin')
|
||||
await store.resume('https://example.com/model.bin')
|
||||
await store.cancel('https://example.com/model.bin')
|
||||
|
||||
expect(downloadManagerMock.startDownload).toHaveBeenCalledWith(
|
||||
'https://example.com/model.bin',
|
||||
'/tmp/model.bin',
|
||||
'model.bin'
|
||||
)
|
||||
expect(downloadManagerMock.pauseDownload).toHaveBeenCalledWith(
|
||||
'https://example.com/model.bin'
|
||||
)
|
||||
expect(downloadManagerMock.resumeDownload).toHaveBeenCalledWith(
|
||||
'https://example.com/model.bin'
|
||||
)
|
||||
expect(downloadManagerMock.cancelDownload).toHaveBeenCalledWith(
|
||||
'https://example.com/model.bin'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
const mockData = vi.hoisted(() => ({ isDesktop: false }))
|
||||
const mockData = vi.hoisted(() => ({ isCloud: false, isDesktop: false }))
|
||||
|
||||
// Mock the API
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -19,7 +19,9 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
get isDesktop() {
|
||||
return mockData.isDesktop
|
||||
},
|
||||
isCloud: false
|
||||
get isCloud() {
|
||||
return mockData.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useSystemStatsStore', () => {
|
||||
@@ -138,6 +140,7 @@ describe('useSystemStatsStore', () => {
|
||||
describe('getFormFactor', () => {
|
||||
beforeEach(() => {
|
||||
// Reset systemStats for each test
|
||||
mockData.isCloud = false
|
||||
store.systemStats = null
|
||||
})
|
||||
|
||||
@@ -162,6 +165,12 @@ describe('useSystemStatsStore', () => {
|
||||
expect(store.getFormFactor()).toBe('other')
|
||||
})
|
||||
|
||||
it('should return "cloud" in cloud mode', () => {
|
||||
mockData.isCloud = true
|
||||
|
||||
expect(store.getFormFactor()).toBe('cloud')
|
||||
})
|
||||
|
||||
describe('desktop environment', () => {
|
||||
beforeEach(() => {
|
||||
mockData.isDesktop = true
|
||||
|
||||
@@ -90,6 +90,12 @@ describe('templateRankingStore', () => {
|
||||
})
|
||||
|
||||
describe('computePopularScore', () => {
|
||||
it('normalizes usage against itself before a largest score is loaded', () => {
|
||||
const store = useTemplateRankingStore()
|
||||
|
||||
expect(store.computePopularScore('2024-01-01', 10)).toBeGreaterThan(0.8)
|
||||
})
|
||||
|
||||
it('does not use searchRank', () => {
|
||||
const store = useTemplateRankingStore()
|
||||
store.largestUsageScore = 100
|
||||
|
||||
25
src/stores/topbarBadgeStore.test.ts
Normal file
25
src/stores/topbarBadgeStore.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
|
||||
|
||||
describe('topbarBadgeStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('collects topbar badges from registered extensions', () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
extensionStore.registerExtension({
|
||||
name: 'badges',
|
||||
topbarBadges: [{ text: 'Beta', label: 'BETA' }]
|
||||
})
|
||||
extensionStore.registerExtension({ name: 'plain' })
|
||||
|
||||
const store = useTopbarBadgeStore()
|
||||
|
||||
expect(store.badges).toEqual([{ text: 'Beta', label: 'BETA' }])
|
||||
})
|
||||
})
|
||||
@@ -116,6 +116,33 @@ describe('useUserFileStore', () => {
|
||||
"Failed to load file 'file1.txt': 404 Not Found"
|
||||
)
|
||||
})
|
||||
|
||||
it('should skip loading temporary and already loaded files', async () => {
|
||||
const temporaryFile = UserFile.createTemporary('draft.txt')
|
||||
const loadedFile = new UserFile('file1.txt', 123, 100)
|
||||
loadedFile.content = 'content'
|
||||
loadedFile.originalContent = 'content'
|
||||
|
||||
await temporaryFile.load()
|
||||
await loadedFile.load()
|
||||
|
||||
expect(api.getUserData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should force reload loaded files', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'old'
|
||||
file.originalContent = 'old'
|
||||
vi.mocked(api.getUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve('new')
|
||||
} as Response)
|
||||
|
||||
await file.load({ force: true })
|
||||
|
||||
expect(api.getUserData).toHaveBeenCalledWith('file1.txt')
|
||||
expect(file.content).toBe('new')
|
||||
})
|
||||
})
|
||||
|
||||
describe('save', () => {
|
||||
@@ -148,6 +175,60 @@ describe('useUserFileStore', () => {
|
||||
|
||||
expect(api.storeUserData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save unmodified files when forced', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'content'
|
||||
file.originalContent = 'content'
|
||||
vi.mocked(api.storeUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve('file1.txt')
|
||||
} as Response)
|
||||
|
||||
await file.save({ force: true })
|
||||
|
||||
expect(api.storeUserData).toHaveBeenCalledWith('file1.txt', 'content', {
|
||||
throwOnError: true,
|
||||
full_info: true,
|
||||
overwrite: true
|
||||
})
|
||||
expect(file.lastModified).toBe(123)
|
||||
expect(file.size).toBe(100)
|
||||
})
|
||||
|
||||
it('should normalize string modified times', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'modified content'
|
||||
file.originalContent = 'original content'
|
||||
vi.mocked(api.storeUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({ modified: '2024-01-02T03:04:05Z', size: 200 })
|
||||
} as Response)
|
||||
|
||||
await file.save()
|
||||
|
||||
expect(file.lastModified).toBe(
|
||||
new Date('2024-01-02T03:04:05Z').getTime()
|
||||
)
|
||||
expect(file.size).toBe(200)
|
||||
})
|
||||
|
||||
it('should fall back when modified time is invalid', async () => {
|
||||
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(999)
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'modified content'
|
||||
file.originalContent = 'original content'
|
||||
vi.mocked(api.storeUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ modified: 'bad date', size: 200 })
|
||||
} as Response)
|
||||
|
||||
await file.save()
|
||||
|
||||
expect(file.lastModified).toBe(999)
|
||||
dateNow.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
@@ -161,6 +242,26 @@ describe('useUserFileStore', () => {
|
||||
|
||||
expect(api.deleteUserData).toHaveBeenCalledWith('file1.txt')
|
||||
})
|
||||
|
||||
it('should skip deleting temporary files', async () => {
|
||||
const file = UserFile.createTemporary('draft.txt')
|
||||
|
||||
await file.delete()
|
||||
|
||||
expect(api.deleteUserData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw when delete fails', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
vi.mocked(api.deleteUserData).mockResolvedValue({
|
||||
status: 500,
|
||||
statusText: 'Server Error'
|
||||
} as Response)
|
||||
|
||||
await expect(file.delete()).rejects.toThrow(
|
||||
"Failed to delete file 'file1.txt': 500 Server Error"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rename', () => {
|
||||
@@ -181,6 +282,41 @@ describe('useUserFileStore', () => {
|
||||
expect(file.lastModified).toBe(456)
|
||||
expect(file.size).toBe(200)
|
||||
})
|
||||
|
||||
it('should rename temporary files locally', async () => {
|
||||
const file = UserFile.createTemporary('draft.txt')
|
||||
|
||||
await file.rename('renamed.txt')
|
||||
|
||||
expect(api.moveUserData).not.toHaveBeenCalled()
|
||||
expect(file.path).toBe('renamed.txt')
|
||||
})
|
||||
|
||||
it('should throw when rename fails', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
vi.mocked(api.moveUserData).mockResolvedValue({
|
||||
status: 409,
|
||||
statusText: 'Conflict'
|
||||
} as Response)
|
||||
|
||||
await expect(file.rename('newfile.txt')).rejects.toThrow(
|
||||
"Failed to rename file 'file1.txt': 409 Conflict"
|
||||
)
|
||||
})
|
||||
|
||||
it('should leave metadata unchanged when rename returns a string', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
vi.mocked(api.moveUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve('newfile.txt')
|
||||
} as Response)
|
||||
|
||||
await file.rename('newfile.txt')
|
||||
|
||||
expect(file.path).toBe('newfile.txt')
|
||||
expect(file.lastModified).toBe(123)
|
||||
expect(file.size).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('saveAs', () => {
|
||||
@@ -207,6 +343,25 @@ describe('useUserFileStore', () => {
|
||||
expect(newFile.size).toBe(200)
|
||||
expect(newFile.content).toBe('file content')
|
||||
})
|
||||
|
||||
it('should save temporary files in place', async () => {
|
||||
const file = UserFile.createTemporary('draft.txt')
|
||||
file.content = 'file content'
|
||||
vi.mocked(api.storeUserData).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ modified: 456, size: 200 })
|
||||
} as Response)
|
||||
|
||||
const newFile = await file.saveAs('newfile.txt')
|
||||
|
||||
expect(api.storeUserData).toHaveBeenCalledWith(
|
||||
'draft.txt',
|
||||
'file content',
|
||||
{ throwOnError: true, full_info: true, overwrite: false }
|
||||
)
|
||||
expect(newFile).toBe(file)
|
||||
expect(newFile.path).toBe('draft.txt')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,61 +1,72 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useUserStore } from './userStore'
|
||||
|
||||
const getUserConfig = vi.fn()
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
createUser: vi.fn(),
|
||||
getUserConfig: vi.fn(),
|
||||
user: undefined as string | undefined
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getUserConfig: (...args: unknown[]) => getUserConfig(...args)
|
||||
}
|
||||
api: apiMock
|
||||
}))
|
||||
|
||||
describe('userStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
getUserConfig.mockReset()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
apiMock.createUser.mockReset()
|
||||
apiMock.getUserConfig.mockReset()
|
||||
apiMock.user = undefined
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('initialize', () => {
|
||||
it('returns an empty user list before initialization', () => {
|
||||
const store = useUserStore()
|
||||
|
||||
expect(store.users).toEqual([])
|
||||
})
|
||||
|
||||
it('fetches user config on first call', async () => {
|
||||
getUserConfig.mockResolvedValue({})
|
||||
apiMock.getUserConfig.mockResolvedValue({})
|
||||
const store = useUserStore()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(getUserConfig).toHaveBeenCalledTimes(1)
|
||||
expect(apiMock.getUserConfig).toHaveBeenCalledTimes(1)
|
||||
expect(store.initialized).toBe(true)
|
||||
})
|
||||
|
||||
it('is a no-op once already initialized', async () => {
|
||||
getUserConfig.mockResolvedValue({})
|
||||
apiMock.getUserConfig.mockResolvedValue({})
|
||||
const store = useUserStore()
|
||||
await store.initialize()
|
||||
getUserConfig.mockClear()
|
||||
apiMock.getUserConfig.mockClear()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(getUserConfig).not.toHaveBeenCalled()
|
||||
expect(apiMock.getUserConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retries on a subsequent call when the first fetch failed', async () => {
|
||||
getUserConfig.mockRejectedValueOnce(new Error('network down'))
|
||||
getUserConfig.mockResolvedValueOnce({})
|
||||
apiMock.getUserConfig.mockRejectedValueOnce(new Error('network down'))
|
||||
apiMock.getUserConfig.mockResolvedValueOnce({})
|
||||
const store = useUserStore()
|
||||
|
||||
await expect(store.initialize()).rejects.toThrow('network down')
|
||||
expect(store.initialized).toBe(false)
|
||||
await expect(store.initialize()).resolves.toBeUndefined()
|
||||
|
||||
expect(getUserConfig).toHaveBeenCalledTimes(2)
|
||||
expect(apiMock.getUserConfig).toHaveBeenCalledTimes(2)
|
||||
expect(store.initialized).toBe(true)
|
||||
})
|
||||
|
||||
it('deduplicates concurrent calls before the first fetch resolves', async () => {
|
||||
let resolveConfig: (value: unknown) => void = () => {}
|
||||
getUserConfig.mockImplementation(
|
||||
apiMock.getUserConfig.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveConfig = resolve
|
||||
@@ -68,7 +79,100 @@ describe('userStore', () => {
|
||||
resolveConfig({})
|
||||
await Promise.all([a, b])
|
||||
|
||||
expect(getUserConfig).toHaveBeenCalledTimes(1)
|
||||
expect(apiMock.getUserConfig).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('derives multi-user state and restores the current user from storage', async () => {
|
||||
localStorage['Comfy.userId'] = 'user-2'
|
||||
apiMock.getUserConfig.mockResolvedValue({
|
||||
users: { 'user-1': 'Ada', 'user-2': 'Grace' }
|
||||
})
|
||||
const store = useUserStore()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(store.isMultiUserServer).toBe(true)
|
||||
expect(store.needsLogin).toBe(false)
|
||||
expect(store.users).toEqual([
|
||||
{ userId: 'user-1', username: 'Ada' },
|
||||
{ userId: 'user-2', username: 'Grace' }
|
||||
])
|
||||
expect(store.currentUser).toEqual({ userId: 'user-2', username: 'Grace' })
|
||||
await vi.waitFor(() => expect(apiMock.user).toBe('user-2'))
|
||||
})
|
||||
|
||||
it('requires login on multi-user servers without a stored user', async () => {
|
||||
apiMock.getUserConfig.mockResolvedValue({
|
||||
users: { 'user-1': 'Ada' }
|
||||
})
|
||||
const store = useUserStore()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(store.needsLogin).toBe(true)
|
||||
expect(store.currentUser).toBeNull()
|
||||
expect(apiMock.user).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createUser', () => {
|
||||
it('returns the created user id with the requested username', async () => {
|
||||
apiMock.createUser.mockResolvedValue({
|
||||
json: () => Promise.resolve('user-1'),
|
||||
status: 201
|
||||
})
|
||||
const store = useUserStore()
|
||||
|
||||
await expect(store.createUser('Ada')).resolves.toEqual({
|
||||
userId: 'user-1',
|
||||
username: 'Ada'
|
||||
})
|
||||
})
|
||||
|
||||
it('throws API errors returned by user creation', async () => {
|
||||
apiMock.createUser.mockResolvedValue({
|
||||
json: () => Promise.resolve({ error: 'name taken' }),
|
||||
status: 409,
|
||||
statusText: 'Conflict'
|
||||
})
|
||||
const store = useUserStore()
|
||||
|
||||
await expect(store.createUser('Ada')).rejects.toThrow('name taken')
|
||||
})
|
||||
|
||||
it('throws a fallback error when user creation has no error body', async () => {
|
||||
apiMock.createUser.mockResolvedValue({
|
||||
json: () => Promise.resolve({}),
|
||||
status: 500,
|
||||
statusText: 'Server Error'
|
||||
})
|
||||
const store = useUserStore()
|
||||
|
||||
await expect(store.createUser('Ada')).rejects.toThrow(
|
||||
'Error creating user: 500 Server Error'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('login/logout', () => {
|
||||
it('persists login identity and clears it on logout', async () => {
|
||||
const store = useUserStore()
|
||||
|
||||
await store.login({ userId: 'user-1', username: 'Ada' })
|
||||
expect(localStorage['Comfy.userId']).toBe('user-1')
|
||||
expect(localStorage['Comfy.userName']).toBe('Ada')
|
||||
|
||||
await store.logout()
|
||||
expect(localStorage['Comfy.userId']).toBeUndefined()
|
||||
expect(localStorage['Comfy.userName']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not set api.user when login happens before user config loads', async () => {
|
||||
const store = useUserStore()
|
||||
|
||||
await store.login({ userId: 'user-1', username: 'Ada' })
|
||||
|
||||
expect(apiMock.user).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
258
src/stores/workspace/favoritedWidgetsStore.test.ts
Normal file
258
src/stores/workspace/favoritedWidgetsStore.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { toNodeId } from '@/types/nodeId'
|
||||
|
||||
const { mockState } = vi.hoisted(() => ({
|
||||
mockState: {
|
||||
graph: null as { extra: Record<string, unknown> } | null,
|
||||
nodes: {} as Record<string, unknown>,
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
get rootGraph() {
|
||||
return mockState.graph
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: undefined,
|
||||
nodeToNodeLocatorId: (node: { id: unknown }) => String(node.id),
|
||||
nodeIdToNodeLocatorId: (id: unknown) => String(id)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({ canvas: { setDirty: mockState.setDirty } })
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getNodeByLocatorId: (_graph: unknown, id: string) =>
|
||||
mockState.nodes[id] ?? null
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeTitleUtil', () => ({
|
||||
resolveNodeDisplayName: (node: { title?: string }) => node.title ?? 'Node'
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: (_key: string, fallback: string) => fallback
|
||||
}))
|
||||
|
||||
interface FakeWidget {
|
||||
name: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
function makeWidget({ name, label }: FakeWidget): IBaseWidget {
|
||||
return {
|
||||
name,
|
||||
label,
|
||||
options: {},
|
||||
type: 'number',
|
||||
y: 0
|
||||
} as IBaseWidget
|
||||
}
|
||||
|
||||
function makeNode(id: number, widgets: FakeWidget[] = [], title = 'My Node') {
|
||||
const node = new LGraphNode(title)
|
||||
node.id = toNodeId(id)
|
||||
node.title = title
|
||||
node.widgets = widgets.map(makeWidget)
|
||||
return node
|
||||
}
|
||||
|
||||
function registerNode(node: { id: unknown }) {
|
||||
mockState.nodes[String(node.id)] = node
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockState.graph = { extra: {} }
|
||||
mockState.nodes = {}
|
||||
mockState.setDirty = vi.fn()
|
||||
})
|
||||
|
||||
describe('favoritedWidgetsStore', () => {
|
||||
it('adds a favorite, marks workflow dirty, and persists to graph.extra', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
expect(store.isFavorited(node, 'seed')).toBe(true)
|
||||
expect(mockState.setDirty).toHaveBeenCalledWith(true, true)
|
||||
expect(mockState.graph?.extra.favoritedWidgets).toEqual({
|
||||
favorites: [{ nodeLocatorId: '1', widgetName: 'seed' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('does not add the same favorite twice', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
|
||||
store.addFavorite(node, 'seed')
|
||||
const persisted = structuredClone(mockState.graph?.extra.favoritedWidgets)
|
||||
const dirtyCalls = mockState.setDirty.mock.calls.length
|
||||
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
expect(store.favoritedWidgets).toHaveLength(1)
|
||||
expect(mockState.graph?.extra.favoritedWidgets).toEqual(persisted)
|
||||
expect(mockState.setDirty).toHaveBeenCalledTimes(dirtyCalls)
|
||||
})
|
||||
|
||||
it('removes a favorite and treats removing an absent one as a no-op', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
store.addFavorite(node, 'seed')
|
||||
const persisted = structuredClone(mockState.graph?.extra.favoritedWidgets)
|
||||
const dirtyCalls = mockState.setDirty.mock.calls.length
|
||||
|
||||
store.removeFavorite(node, 'missing')
|
||||
expect(store.isFavorited(node, 'seed')).toBe(true)
|
||||
expect(mockState.graph?.extra.favoritedWidgets).toEqual(persisted)
|
||||
expect(mockState.setDirty).toHaveBeenCalledTimes(dirtyCalls)
|
||||
|
||||
store.removeFavorite(node, 'seed')
|
||||
expect(store.isFavorited(node, 'seed')).toBe(false)
|
||||
})
|
||||
|
||||
it('toggles favorite state in both directions', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
|
||||
store.toggleFavorite(node, 'seed')
|
||||
expect(store.isFavorited(node, 'seed')).toBe(true)
|
||||
|
||||
store.toggleFavorite(node, 'seed')
|
||||
expect(store.isFavorited(node, 'seed')).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves a valid favorite to a node/widget with a composed label', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(7, [{ name: 'cfg', label: 'CFG Scale' }], 'KSampler')
|
||||
registerNode(node)
|
||||
|
||||
store.addFavorite(node, 'cfg')
|
||||
|
||||
const [resolved] = store.favoritedWidgets
|
||||
expect(resolved.label).toBe('KSampler / CFG Scale')
|
||||
expect(store.validFavoritedWidgets).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('labels favorites whose node was deleted and excludes them from valid', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(2, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
delete mockState.nodes['2']
|
||||
|
||||
expect(store.favoritedWidgets[0].label).toContain('(node deleted)')
|
||||
expect(store.validFavoritedWidgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('labels favorites whose widget no longer exists', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(3, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
mockState.nodes['3'] = makeNode(3, [], 'My Node')
|
||||
|
||||
expect(store.favoritedWidgets[0].label).toContain('(widget not found)')
|
||||
})
|
||||
|
||||
it('prunes invalid favorites while keeping valid ones', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const valid = makeNode(1, [{ name: 'seed' }])
|
||||
const stale = makeNode(2, [{ name: 'steps' }])
|
||||
registerNode(valid)
|
||||
registerNode(stale)
|
||||
store.addFavorite(valid, 'seed')
|
||||
store.addFavorite(stale, 'steps')
|
||||
|
||||
delete mockState.nodes['2']
|
||||
store.pruneInvalidFavorites()
|
||||
|
||||
expect(store.favoritedWidgets).toHaveLength(1)
|
||||
expect(store.isFavorited(valid, 'seed')).toBe(true)
|
||||
})
|
||||
|
||||
it('reorders favorites to match the provided order', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const a = makeNode(1, [{ name: 'seed' }])
|
||||
const b = makeNode(2, [{ name: 'steps' }])
|
||||
registerNode(a)
|
||||
registerNode(b)
|
||||
store.addFavorite(a, 'seed')
|
||||
store.addFavorite(b, 'steps')
|
||||
|
||||
store.reorderFavorites([...store.validFavoritedWidgets].reverse())
|
||||
|
||||
expect(store.favoritedWidgets.map((fw) => fw.nodeLocatorId)).toEqual([
|
||||
'2',
|
||||
'1'
|
||||
])
|
||||
})
|
||||
|
||||
it('clears all favorites', () => {
|
||||
const store = useFavoritedWidgetsStore()
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
store.clearFavorites()
|
||||
|
||||
expect(store.favoritedWidgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('loads favorites from graph.extra on init, normalizing legacy nodeId entries', () => {
|
||||
mockState.graph = {
|
||||
extra: {
|
||||
favoritedWidgets: {
|
||||
favorites: [
|
||||
{ nodeLocatorId: '1', widgetName: 'seed' },
|
||||
{ nodeId: 2, widgetName: 'steps' },
|
||||
{ widgetName: 'no-node' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
registerNode(makeNode(1, [{ name: 'seed' }]))
|
||||
registerNode(makeNode(2, [{ name: 'steps' }]))
|
||||
|
||||
const store = useFavoritedWidgetsStore()
|
||||
|
||||
expect(store.favoritedWidgets.map((fw) => fw.nodeLocatorId)).toEqual([
|
||||
'1',
|
||||
'2'
|
||||
])
|
||||
})
|
||||
|
||||
it('labels existing favorites when the graph is not loaded', () => {
|
||||
const node = makeNode(1, [{ name: 'seed' }])
|
||||
registerNode(node)
|
||||
const store = useFavoritedWidgetsStore()
|
||||
store.addFavorite(node, 'seed')
|
||||
|
||||
mockState.graph = null
|
||||
|
||||
expect(store.favoritedWidgets[0].label).toContain('(graph not loaded)')
|
||||
store.clearFavorites()
|
||||
expect(store.favoritedWidgets).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
115
src/stores/workspaceStore.test.ts
Normal file
115
src/stores/workspaceStore.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const storeMocks = vi.hoisted(() => ({
|
||||
apiKeyAuthStore: {
|
||||
isAuthenticated: false
|
||||
},
|
||||
authStore: {
|
||||
currentUser: null as null | { uid: string }
|
||||
},
|
||||
commandStore: {
|
||||
commands: [],
|
||||
execute: vi.fn()
|
||||
},
|
||||
executionErrorStore: {
|
||||
lastExecutionError: null,
|
||||
lastNodeErrors: null
|
||||
},
|
||||
queueSettingsStore: {},
|
||||
settingStore: {
|
||||
settingsById: {},
|
||||
get: vi.fn(),
|
||||
set: vi.fn()
|
||||
},
|
||||
sidebarTabStore: {
|
||||
registerSidebarTab: vi.fn(),
|
||||
unregisterSidebarTab: vi.fn(),
|
||||
sidebarTabs: []
|
||||
},
|
||||
toastStore: {},
|
||||
workflowStore: {}
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useMagicKeys: () => ({ shift: false })
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => storeMocks.settingStore
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
useToastStore: () => storeMocks.toastStore
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => storeMocks.workflowStore
|
||||
}))
|
||||
|
||||
vi.mock('@/services/colorPaletteService', () => ({
|
||||
useColorPaletteService: () => ({})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: () => storeMocks.apiKeyAuthStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => storeMocks.authStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => storeMocks.commandStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/executionErrorStore', () => ({
|
||||
useExecutionErrorStore: () => storeMocks.executionErrorStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/queueStore', () => ({
|
||||
useQueueSettingsStore: () => storeMocks.queueSettingsStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/bottomPanelStore', () => ({
|
||||
useBottomPanelStore: () => ({})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
useSidebarTabStore: () => storeMocks.sidebarTabStore
|
||||
}))
|
||||
|
||||
describe('useWorkspaceStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
storeMocks.apiKeyAuthStore.isAuthenticated = false
|
||||
storeMocks.authStore.currentUser = null
|
||||
})
|
||||
|
||||
it('reports logged out when neither auth source is active', () => {
|
||||
const store = useWorkspaceStore()
|
||||
|
||||
expect(store.user.isLoggedIn).toBe(false)
|
||||
})
|
||||
|
||||
it('reports logged in for API-key auth', () => {
|
||||
storeMocks.apiKeyAuthStore.isAuthenticated = true
|
||||
const store = useWorkspaceStore()
|
||||
|
||||
expect(store.user.isLoggedIn).toBe(true)
|
||||
})
|
||||
|
||||
it('reports logged in for Firebase auth', () => {
|
||||
storeMocks.authStore.currentUser = { uid: 'user-1' }
|
||||
const store = useWorkspaceStore()
|
||||
|
||||
expect(store.user.isLoggedIn).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user