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:
@@ -1,6 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
|
||||
const mockSettingsStore = vi.hoisted(() => ({
|
||||
sessionReset: { mode: 'both', idle_minutes: 60, at_hour: 0 },
|
||||
@@ -10,7 +11,7 @@ const mockSettingsStore = vi.hoisted(() => ({
|
||||
updateLocal: vi.fn((section: string, values: Record<string, any>) => {
|
||||
Object.assign((mockSettingsStore as any)[section], values)
|
||||
}),
|
||||
saveSection: vi.fn(),
|
||||
saveSection: vi.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
const mockPrefsStore = vi.hoisted(() => ({
|
||||
@@ -34,19 +35,40 @@ vi.mock('vue-i18n', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('naive-ui', async () => {
|
||||
const actual = await vi.importActual<any>('naive-ui')
|
||||
return {
|
||||
...actual,
|
||||
useMessage: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
vi.mock('@/composables/useMessage', () => ({
|
||||
useMessage: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
import SessionSettings from '@/components/hermes/settings/SessionSettings.vue'
|
||||
|
||||
const SettingRowStub = {
|
||||
props: ['label', 'hint'],
|
||||
template: '<div class="setting-row"><div class="setting-row-label">{{ label }}</div><slot /></div>',
|
||||
}
|
||||
|
||||
// `app.use(ArcoVue)` registers the components globally, so test-utils stubs only
|
||||
// match Arco's internal component names, not the kebab-case template tags.
|
||||
const arcoGlobal = {
|
||||
plugins: [ArcoVue],
|
||||
stubs: {
|
||||
SettingRow: SettingRowStub,
|
||||
Select: true,
|
||||
InputNumber: true,
|
||||
},
|
||||
}
|
||||
|
||||
/** The Arco switch rendered inside the SettingRow carrying `label`. */
|
||||
function switchByLabel(wrapper: any, label: string) {
|
||||
const row = wrapper
|
||||
.findAll('.setting-row')
|
||||
.find((node: any) => node.get('.setting-row-label').text() === label)
|
||||
if (!row) throw new Error(`no SettingRow labelled "${label}"`)
|
||||
return row.get('.arco-switch')
|
||||
}
|
||||
|
||||
describe('SessionSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -56,41 +78,15 @@ describe('SessionSettings', () => {
|
||||
})
|
||||
|
||||
it('surfaces the human-only preference in the Session tab', async () => {
|
||||
let emittedValue: boolean | undefined
|
||||
const wrapper = mount(SessionSettings, {
|
||||
global: {
|
||||
stubs: {
|
||||
SettingRow: {
|
||||
props: ['label', 'hint'],
|
||||
template: '<div class="setting-row"><div class="setting-row-label">{{ label }}</div><slot /></div>',
|
||||
},
|
||||
NSelect: true,
|
||||
NInputNumber: true,
|
||||
NButton: { template: '<button><slot /></button>' },
|
||||
NTag: { template: '<span><slot /></span>' },
|
||||
NSwitch: {
|
||||
props: ['value'],
|
||||
emits: ['update:value'],
|
||||
template: '<div class="n-switch" @click="$emit(\'update:value\', !value)"></div>',
|
||||
setup(props: any, { emit }: any) {
|
||||
return {
|
||||
onClick: () => {
|
||||
emittedValue = !props.value
|
||||
emit('update:value', emittedValue)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('settings.session.liveMonitorHumanOnly')
|
||||
// requireAuth + memory + skills write approval + human-only
|
||||
expect(wrapper.findAll('.arco-switch').length).toBe(4)
|
||||
|
||||
const toggles = wrapper.findAll('.n-switch')
|
||||
expect(toggles.length).toBe(4)
|
||||
const humanOnlyToggle = toggles[3]
|
||||
const humanOnlyToggle = switchByLabel(wrapper, 'settings.session.liveMonitorHumanOnly')
|
||||
expect(humanOnlyToggle.attributes('aria-checked')).toBe('true')
|
||||
|
||||
await humanOnlyToggle.trigger('click')
|
||||
await Promise.resolve()
|
||||
@@ -99,34 +95,17 @@ describe('SessionSettings', () => {
|
||||
})
|
||||
|
||||
it('saves write approval toggles to memory and skills config sections', async () => {
|
||||
const wrapper = mount(SessionSettings, {
|
||||
global: {
|
||||
stubs: {
|
||||
SettingRow: {
|
||||
props: ['label', 'hint'],
|
||||
template: '<div class="setting-row"><div class="setting-row-label">{{ label }}</div><slot /></div>',
|
||||
},
|
||||
NSelect: true,
|
||||
NInputNumber: true,
|
||||
NButton: { template: '<button><slot /></button>' },
|
||||
NTag: { template: '<span><slot /></span>' },
|
||||
NSwitch: {
|
||||
props: ['value'],
|
||||
emits: ['update:value'],
|
||||
template: '<button class="n-switch" @click="$emit(\'update:value\', !value)"></button>',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('settings.session.memoryWriteApproval')
|
||||
expect(wrapper.text()).toContain('settings.session.skillsWriteApproval')
|
||||
const memoryToggle = switchByLabel(wrapper, 'settings.session.memoryWriteApproval')
|
||||
const skillsToggle = switchByLabel(wrapper, 'settings.session.skillsWriteApproval')
|
||||
expect(memoryToggle.attributes('aria-checked')).toBe('false')
|
||||
expect(skillsToggle.attributes('aria-checked')).toBe('true')
|
||||
|
||||
const toggles = wrapper.findAll('.n-switch')
|
||||
await toggles[1].trigger('click')
|
||||
await memoryToggle.trigger('click')
|
||||
await Promise.resolve()
|
||||
await toggles[2].trigger('click')
|
||||
await skillsToggle.trigger('click')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(mockSettingsStore.updateLocal).toHaveBeenCalledWith('memory', { write_approval: true })
|
||||
@@ -136,23 +115,7 @@ describe('SessionSettings', () => {
|
||||
})
|
||||
|
||||
it('shows write approval toggles without probing Hermes Agent support', async () => {
|
||||
const wrapper = mount(SessionSettings, {
|
||||
global: {
|
||||
stubs: {
|
||||
SettingRow: {
|
||||
props: ['label', 'hint'],
|
||||
template: '<div class="setting-row"><div class="setting-row-label">{{ label }}</div><slot /></div>',
|
||||
},
|
||||
NSelect: true,
|
||||
NInputNumber: true,
|
||||
NSwitch: {
|
||||
props: ['value'],
|
||||
emits: ['update:value'],
|
||||
template: '<button class="n-switch" @click="$emit(\'update:value\', !value)"></button>',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
||||
|
||||
await flushPromises()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user