Compare commits

...

1 Commits

Author SHA1 Message Date
huang47
1f8b62cd89 test: cover system and workspace stores 2026-06-30 22:37:32 -07:00
9 changed files with 820 additions and 19 deletions

View 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([])
})
})

View 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'
)
})
})

View File

@@ -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

View File

@@ -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

View 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' }])
})
})

View File

@@ -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')
})
})
})
})

View File

@@ -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()
})
})
})

View 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)
})
})

View 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)
})
})