Phase 006: migrate the chat page from naive-ui to Arco Design Vue

Swaps the rendering layer only. Chat store logic, keyboard handling, and the
/chat-run Socket.IO contract are untouched, so a mixed UI cannot hide a chat
regression behind a library swap.

Arco's dropdowns take their items from a #content slot of a-doption children
rather than a droplist prop, and Arco Triggers cannot be nested, so the two
toolbar menus lost their a-tooltip in favour of a native title. Both rules are
recorded in docs/arco-mapping.md, and tests/e2e/chat-input-dropdown.spec.ts plus
tests/e2e/chat-rendering-smoke.spec.ts keep the menus, the streaming markdown
rendering, and the no-Arco-inside-the-bubble red line from regressing silently.
This commit is contained in:
2026-10-17 22:17:32 +08:00
parent 4baccd5d88
commit effc29085c
20 changed files with 1356 additions and 819 deletions
+28 -27
View File
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { nextTick, defineComponent, h } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { config, mount } from '@vue/test-utils'
const apiMocks = vi.hoisted(() => ({
fetchSessionsMock: vi.fn(),
@@ -40,31 +40,32 @@ vi.mock('vue-i18n', () => ({
}),
}))
vi.mock('naive-ui', async () => {
const actual = await vi.importActual<any>('naive-ui')
return {
...actual,
useMessage: () => ({
error: vi.fn(),
}),
NModal: {
props: ['show'],
emits: ['update:show'],
template: '<div v-if="show" class="n-modal-stub"><slot /></div>',
},
NInput: {
props: ['value', 'size'],
emits: ['update:value', 'keydown'],
template: '<input class="n-input-stub" :value="value" @input="$emit(\'update:value\', $event.target.value)" @keydown="$emit(\'keydown\', $event)" />',
},
NSpin: {
template: '<div class="n-spin-stub"><slot /></div>',
},
NButton: {
template: '<button class="n-button-stub"><slot /></button>',
},
}
})
// The search modal teleports to body in jsdom, so stub the overlays to render
// inline and keep the DOM shape the assertions below rely on.
config.global.stubs = {
AModal: {
name: 'AModal',
props: ['visible'],
emits: ['update:visible'],
template: '<div v-if="visible" class="a-modal-stub"><slot /></div>',
},
AInput: {
name: 'AInput',
// `size` is an Arco prop, not an HTML input attribute; declaring it keeps
// it off the native element.
props: ['modelValue', 'size'],
emits: ['update:modelValue'],
template: '<input class="a-input-stub" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
},
ASpin: {
name: 'ASpin',
template: '<div class="a-spin-stub"><slot /></div>',
},
AButton: {
name: 'AButton',
template: '<button class="a-button-stub"><slot /></button>',
},
}
import SessionSearchModal from '@/components/hermes/chat/SessionSearchModal.vue'
import { useSessionSearch } from '@/composables/useSessionSearch'
@@ -167,7 +168,7 @@ describe('session search modal', () => {
await flushPromises()
await nextTick()
const input = wrapper.find('input.n-input-stub')
const input = wrapper.find('input.a-input-stub')
await input.setValue('docker')
await vi.advanceTimersByTimeAsync(200)
await flushPromises()