diff --git a/frontend/src/components/account/ModelWhitelistSelector.vue b/frontend/src/components/account/ModelWhitelistSelector.vue index 584345b3b4..d8cec05cb9 100644 --- a/frontend/src/components/account/ModelWhitelistSelector.vue +++ b/frontend/src/components/account/ModelWhitelistSelector.vue @@ -47,28 +47,44 @@ />
- + + + + + + + {{ model.value }} + + +
{{ t('admin.accounts.noMatchingModels') }}
@@ -134,6 +150,7 @@ import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { accountsAPI } from '@/api/admin/accounts' import type { SyncUpstreamPreviewParams } from '@/api/admin/accounts' +import { useClipboard } from '@/composables/useClipboard' import ModelIcon from '@/components/common/ModelIcon.vue' import Icon from '@/components/icons/Icon.vue' import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist' @@ -158,6 +175,7 @@ const emit = defineEmits<{ }>() const appStore = useAppStore() +const { copyToClipboard } = useClipboard() const showDropdown = ref(false) const searchQuery = ref('') @@ -233,6 +251,10 @@ const toggleModel = (model: string) => { } } +const copyModelId = async (model: string) => { + await copyToClipboard(model) +} + const addCustom = () => { const model = customModel.value.trim() if (!model) return diff --git a/frontend/src/components/account/__tests__/ModelWhitelistSelector.spec.ts b/frontend/src/components/account/__tests__/ModelWhitelistSelector.spec.ts new file mode 100644 index 0000000000..51f18611cc --- /dev/null +++ b/frontend/src/components/account/__tests__/ModelWhitelistSelector.spec.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' + +const copyToClipboard = vi.fn().mockResolvedValue(true) + +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') + return { + ...actual, + useI18n: () => ({ + t: (key: string) => (key === 'common.copy' ? '复制' : key) + }) + } +}) + +vi.mock('@/stores/app', () => ({ + useAppStore: () => ({ + showError: vi.fn(), + showSuccess: vi.fn(), + showInfo: vi.fn() + }) +})) + +vi.mock('@/composables/useClipboard', () => ({ + useClipboard: () => ({ + copyToClipboard + }) +})) + +import ModelWhitelistSelector from '../ModelWhitelistSelector.vue' + +function mountSelector() { + return mount(ModelWhitelistSelector, { + props: { + modelValue: [], + platform: 'openai' + }, + global: { + stubs: { + ModelIcon: true + } + } + }) +} + +function findModelRow(wrapper: ReturnType, modelId: string) { + const row = wrapper + .findAll('[data-testid="model-option"]') + .find(candidate => candidate.text().includes(modelId)) + + if (!row) { + throw new Error(`Model row not found: ${modelId}`) + } + + return row +} + +describe('ModelWhitelistSelector', () => { + beforeEach(() => { + copyToClipboard.mockClear() + }) + + it('copies a model ID without selecting the model', async () => { + const wrapper = mountSelector() + await wrapper.get('div.cursor-pointer').trigger('click') + + const row = findModelRow(wrapper, 'gpt-5.6-sol') + + const copyButton = row.get('[data-testid="copy-model-id"]') + expect(copyButton.attributes('aria-label')).toBe('复制 gpt-5.6-sol') + + await copyButton.trigger('click') + await flushPromises() + + expect(copyToClipboard).toHaveBeenCalledWith('gpt-5.6-sol') + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + }) + + it('keeps the existing model selection behavior', async () => { + const wrapper = mountSelector() + await wrapper.get('div.cursor-pointer').trigger('click') + + const row = findModelRow(wrapper, 'gpt-5.6-sol') + await row.get('[data-testid="select-model"]').trigger('click') + + expect(wrapper.emitted('update:modelValue')).toEqual([[['gpt-5.6-sol']]]) + expect(copyToClipboard).not.toHaveBeenCalled() + }) +})