Phase 007: migrate every remaining view from naive-ui to Arco Design Vue
The project now has exactly one UI library. Every naive-ui component in
packages/client/src is replaced by its Arco equivalent across 24 views and
roughly 80 components, and naive-ui is dropped from package.json along with
its transitive vueuc / @css-render / vdirs / seemly dependencies.
The migration is rendering-layer only. No store logic, no API contract, and
no /chat-run payload was touched.
Three classes of leftover were invisible to `grep naive-ui` and are now
covered by tests/client/no-naive-ui.test.ts, which asserts all three:
- dead .n-* class names in style blocks
- dead --n-* custom properties
- naive-ui in package.json
Notable behavioural traps handled along the way, recorded in
docs/arco-migration-rules.md:
- a-input-password defaults to masked; passing default-visibility="false"
would have exposed API keys in clear text
- a-upload fires a real XHR unless auto-upload is false
- nested Trigger components fight over the first element child, so the
group-chat settings dropdown was flattened
- Arco renders tooltip default-slot text inline, which was silently
collapsing the chat and group-chat toolbars
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
import ProviderCard from '@/components/hermes/models/ProviderCard.vue'
|
||||
import type { AvailableModelGroup } from '@/api/hermes/system'
|
||||
|
||||
@@ -91,60 +92,6 @@ vi.mock('vue-i18n', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('naive-ui', () => ({
|
||||
NButton: defineComponent({
|
||||
name: 'NButton',
|
||||
emits: ['click'],
|
||||
setup(_, { slots, emit }) {
|
||||
return () => h('button', { onClick: () => emit('click') }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NCheckbox: defineComponent({
|
||||
name: 'NCheckbox',
|
||||
setup(_, { slots }) {
|
||||
return () => h('label', slots.default?.())
|
||||
},
|
||||
}),
|
||||
NCheckboxGroup: defineComponent({
|
||||
name: 'NCheckboxGroup',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.())
|
||||
},
|
||||
}),
|
||||
NInput: defineComponent({
|
||||
name: 'NInput',
|
||||
setup() {
|
||||
return () => h('input')
|
||||
},
|
||||
}),
|
||||
NSelect: defineComponent({
|
||||
name: 'NSelect',
|
||||
props: {
|
||||
value: { type: [String, Number], default: null },
|
||||
options: { type: Array, default: () => [] },
|
||||
placeholder: { type: String, default: '' },
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('select', {
|
||||
value: props.value || '',
|
||||
onChange: (event: Event) => emit('update:value', (event.target as HTMLSelectElement).value),
|
||||
}, [
|
||||
h('option', { value: '' }, props.placeholder),
|
||||
...(props.options as Array<{ label: string; value: string }>).map(option => h('option', { value: option.value }, option.label)),
|
||||
])
|
||||
},
|
||||
}),
|
||||
NModal: defineComponent({
|
||||
name: 'NModal',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.())
|
||||
},
|
||||
}),
|
||||
useMessage: () => messageMock,
|
||||
useDialog: () => dialogMock,
|
||||
}))
|
||||
|
||||
// ProviderCard reads message/dialog through the Arco adapter layer, not
|
||||
// naive-ui's composables, so the mock must target the adapter modules.
|
||||
vi.mock('@/composables/useMessage', () => ({ useMessage: () => messageMock }))
|
||||
@@ -155,6 +102,51 @@ vi.mock('@/stores/hermes/app', () => ({ useAppStore: () => appStoreMock }))
|
||||
vi.mock('@/stores/hermes/chat', () => ({ useChatStore: () => chatStoreMock }))
|
||||
vi.mock('@/api/hermes/copilot-auth', () => copilotAuthMock)
|
||||
|
||||
/**
|
||||
* Arco's a-select renders a custom dropdown (no native <select>), and a-modal
|
||||
* teleports to body in jsdom. Stub both so the card's model picker stays
|
||||
* drivable through a plain <select> and modals render inline.
|
||||
* Arco is registered globally, so stubs key off Arco's internal names.
|
||||
*/
|
||||
const ASelectStub = defineComponent({
|
||||
name: 'Select',
|
||||
props: {
|
||||
modelValue: { type: [String, Number], default: null },
|
||||
options: { type: Array, default: () => [] },
|
||||
placeholder: { type: String, default: '' },
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { emit }) {
|
||||
return () =>
|
||||
h(
|
||||
'select',
|
||||
{
|
||||
class: 'arco-select-stub',
|
||||
disabled: props.disabled,
|
||||
value: props.modelValue ?? '',
|
||||
onChange: (event: Event) => {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
},
|
||||
},
|
||||
[
|
||||
h('option', { value: '' }, props.placeholder),
|
||||
...(props.options as Array<{ label: string; value: string }>).map(option =>
|
||||
h('option', { key: option.value, value: option.value }, option.label),
|
||||
),
|
||||
],
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const AModalStub = defineComponent({
|
||||
name: 'Modal',
|
||||
props: ['visible'],
|
||||
template: '<div v-if="visible" class="a-modal-stub"><slot /><slot name="footer" /></div>',
|
||||
})
|
||||
|
||||
function provider(overrides: Partial<AvailableModelGroup>): AvailableModelGroup {
|
||||
return {
|
||||
provider: 'deepseek',
|
||||
@@ -169,7 +161,13 @@ function provider(overrides: Partial<AvailableModelGroup>): AvailableModelGroup
|
||||
|
||||
function mountCard(group: AvailableModelGroup) {
|
||||
modelsStoreMock.allProviders = [group]
|
||||
return mount(ProviderCard, { props: { provider: group } })
|
||||
return mount(ProviderCard, {
|
||||
props: { provider: group },
|
||||
global: {
|
||||
plugins: [ArcoVue],
|
||||
stubs: { Select: ASelectStub, Modal: AModalStub },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ProviderCard built-in destructive action labels', () => {
|
||||
|
||||
Reference in New Issue
Block a user