2026-07-27 15:53:28 +08:00
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
|
|
|
|
|
|
|
|
|
|
import BatchAssignModelsDialog from '../BatchAssignModelsDialog.vue'
|
|
|
|
|
|
|
|
|
|
const globalModelMocks = vi.hoisted(() => ({
|
|
|
|
|
getGlobalModels: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const endpointMocks = vi.hoisted(() => ({
|
|
|
|
|
getProviderModels: vi.fn(),
|
|
|
|
|
getProviderKeys: vi.fn(),
|
|
|
|
|
batchAssignModelsToProvider: vi.fn(),
|
|
|
|
|
deleteModel: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
vi.mock('@/api/endpoints/global-models', () => globalModelMocks)
|
|
|
|
|
vi.mock('@/api/endpoints', () => endpointMocks)
|
|
|
|
|
vi.mock('@/composables/useToast', () => ({
|
|
|
|
|
useToast: () => ({
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
success: vi.fn(),
|
|
|
|
|
warning: vi.fn(),
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
vi.mock('@/composables/useConfirm', () => ({
|
|
|
|
|
useConfirm: () => ({
|
|
|
|
|
confirmWarning: vi.fn().mockResolvedValue(true),
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
vi.mock('@/features/providers/composables/useUpstreamModelsCache', () => ({
|
|
|
|
|
useUpstreamModelsCache: () => ({
|
|
|
|
|
fetchModels: vi.fn(),
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
vi.mock('@/components/ui/dialog/Dialog.vue', async () => {
|
|
|
|
|
const { defineComponent, h } = await import('vue')
|
|
|
|
|
return {
|
|
|
|
|
default: defineComponent({
|
|
|
|
|
name: 'DialogStub',
|
|
|
|
|
setup: (_props, { slots }) => () => h('section', [slots.default?.(), slots.footer?.()]),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
vi.mock('@/components/ui', async () => {
|
|
|
|
|
const { defineComponent } = await import('vue')
|
|
|
|
|
const passthrough = (name: string) => defineComponent({
|
|
|
|
|
name,
|
|
|
|
|
inheritAttrs: false,
|
|
|
|
|
setup: (_props, { slots }) => () => slots.default?.(),
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
DropdownMenu: passthrough('DropdownMenuStub'),
|
|
|
|
|
DropdownMenuTrigger: passthrough('DropdownMenuTriggerStub'),
|
|
|
|
|
DropdownMenuContent: passthrough('DropdownMenuContentStub'),
|
|
|
|
|
DropdownMenuItem: passthrough('DropdownMenuItemStub'),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
|
|
|
|
|
|
|
|
|
async function settle() {
|
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
await nextTick()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
globalModelMocks.getGlobalModels.mockReset()
|
|
|
|
|
globalModelMocks.getGlobalModels.mockResolvedValue({ models: [], total: 0 })
|
|
|
|
|
endpointMocks.getProviderModels.mockReset()
|
|
|
|
|
endpointMocks.getProviderModels.mockResolvedValue([])
|
|
|
|
|
endpointMocks.getProviderKeys.mockReset()
|
|
|
|
|
endpointMocks.getProviderKeys.mockResolvedValue([])
|
|
|
|
|
endpointMocks.batchAssignModelsToProvider.mockReset()
|
|
|
|
|
endpointMocks.deleteModel.mockReset()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
for (const { app, root } of mountedApps.splice(0)) {
|
|
|
|
|
app.unmount()
|
|
|
|
|
root.remove()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-14 07:56:19 +00:00
|
|
|
function createGlobalModel(id: string, name: string, displayName = name) {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
name,
|
|
|
|
|
display_name: displayName,
|
|
|
|
|
is_active: true,
|
|
|
|
|
default_tiered_pricing: { tiers: [] },
|
|
|
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createProviderModel(id: string, globalModelId: string) {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
provider_id: 'provider-1',
|
|
|
|
|
global_model_id: globalModelId,
|
|
|
|
|
provider_model_name: globalModelId,
|
|
|
|
|
is_active: true,
|
|
|
|
|
is_available: true,
|
|
|
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
|
|
|
updated_at: '2026-01-01T00:00:00Z',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function visibleModelIds(root: HTMLElement): string[] {
|
|
|
|
|
return Array.from(root.querySelectorAll('[data-testid^="batch-assign-model-"]'))
|
|
|
|
|
.map(node => node.getAttribute('data-testid')?.replace('batch-assign-model-', '') ?? '')
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 15:53:28 +08:00
|
|
|
describe('BatchAssignModelsDialog loading', () => {
|
|
|
|
|
it('loads model choices when lazily mounted in the open state', async () => {
|
|
|
|
|
const root = document.createElement('div')
|
|
|
|
|
document.body.appendChild(root)
|
|
|
|
|
const app = createApp(defineComponent({
|
|
|
|
|
setup() {
|
|
|
|
|
return () => h(BatchAssignModelsDialog, {
|
|
|
|
|
open: true,
|
|
|
|
|
providerId: 'provider-1',
|
|
|
|
|
providerName: 'Provider One',
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
app.mount(root)
|
|
|
|
|
mountedApps.push({ app, root })
|
|
|
|
|
|
|
|
|
|
await settle()
|
|
|
|
|
|
|
|
|
|
expect(globalModelMocks.getGlobalModels).toHaveBeenCalledOnce()
|
|
|
|
|
expect(globalModelMocks.getGlobalModels).toHaveBeenCalledWith({ limit: 1000 })
|
|
|
|
|
expect(endpointMocks.getProviderModels).toHaveBeenCalledWith('provider-1')
|
|
|
|
|
expect(endpointMocks.getProviderKeys).toHaveBeenCalledWith('provider-1')
|
|
|
|
|
})
|
2026-09-14 07:56:19 +00:00
|
|
|
|
|
|
|
|
it('pins already associated models to the top of the list', async () => {
|
|
|
|
|
globalModelMocks.getGlobalModels.mockResolvedValue({
|
|
|
|
|
models: [
|
|
|
|
|
createGlobalModel('gm-zeta', 'zeta-model', 'Zeta'),
|
|
|
|
|
createGlobalModel('gm-alpha', 'alpha-model', 'Alpha'),
|
|
|
|
|
createGlobalModel('gm-mu', 'mu-model', 'Mu'),
|
|
|
|
|
],
|
|
|
|
|
total: 3,
|
|
|
|
|
})
|
|
|
|
|
endpointMocks.getProviderModels.mockResolvedValue([
|
|
|
|
|
createProviderModel('pm-mu', 'gm-mu'),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const root = document.createElement('div')
|
|
|
|
|
document.body.appendChild(root)
|
|
|
|
|
const app = createApp(defineComponent({
|
|
|
|
|
setup() {
|
|
|
|
|
return () => h(BatchAssignModelsDialog, {
|
|
|
|
|
open: true,
|
|
|
|
|
providerId: 'provider-1',
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
app.mount(root)
|
|
|
|
|
mountedApps.push({ app, root })
|
|
|
|
|
await settle()
|
|
|
|
|
|
|
|
|
|
expect(visibleModelIds(root)).toEqual(['gm-mu', 'gm-alpha', 'gm-zeta'])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps selected matches pinned above other search results', async () => {
|
|
|
|
|
globalModelMocks.getGlobalModels.mockResolvedValue({
|
|
|
|
|
models: [
|
|
|
|
|
createGlobalModel('gm-beta', 'beta-flash', 'Beta Flash'),
|
|
|
|
|
createGlobalModel('gm-alpha', 'alpha-flash', 'Alpha Flash'),
|
|
|
|
|
createGlobalModel('gm-other', 'other-model', 'Other'),
|
|
|
|
|
],
|
|
|
|
|
total: 3,
|
|
|
|
|
})
|
|
|
|
|
endpointMocks.getProviderModels.mockResolvedValue([
|
|
|
|
|
createProviderModel('pm-beta', 'gm-beta'),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const root = document.createElement('div')
|
|
|
|
|
document.body.appendChild(root)
|
|
|
|
|
const app = createApp(defineComponent({
|
|
|
|
|
setup() {
|
|
|
|
|
return () => h(BatchAssignModelsDialog, {
|
|
|
|
|
open: true,
|
|
|
|
|
providerId: 'provider-1',
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
app.mount(root)
|
|
|
|
|
mountedApps.push({ app, root })
|
|
|
|
|
await settle()
|
|
|
|
|
|
|
|
|
|
const search = root.querySelector('input') as HTMLInputElement
|
|
|
|
|
search.value = 'flash'
|
|
|
|
|
search.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
|
|
await settle()
|
|
|
|
|
|
|
|
|
|
expect(visibleModelIds(root)).toEqual(['gm-beta', 'gm-alpha'])
|
|
|
|
|
})
|
2026-07-27 15:53:28 +08:00
|
|
|
})
|