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 { mount, flushPromises } from '@vue/test-utils'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
|
||||
const fetchSkillUsageStatsMock = vi.hoisted(() => vi.fn())
|
||||
const mockProfilesStore = vi.hoisted(() => ({
|
||||
@@ -27,20 +28,10 @@ vi.mock('vue-i18n', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('naive-ui', async () => {
|
||||
const actual = await vi.importActual<any>('naive-ui')
|
||||
return {
|
||||
...actual,
|
||||
NButton: {
|
||||
props: ['loading', 'type', 'size', 'quaternary', 'secondary'],
|
||||
inheritAttrs: false,
|
||||
template: '<button :data-type="type" :aria-pressed="$attrs[\'aria-pressed\']" @click="$emit(\'click\')"><slot /></button>',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
import SkillsUsageView from '@/views/hermes/SkillsUsageView.vue'
|
||||
|
||||
const arcoGlobal = { plugins: [ArcoVue] }
|
||||
|
||||
const sevenDayStats = {
|
||||
period_days: 7,
|
||||
summary: {
|
||||
@@ -75,6 +66,30 @@ const sevenDayStats = {
|
||||
],
|
||||
}
|
||||
|
||||
const thirtyDayStats = {
|
||||
period_days: 30,
|
||||
summary: {
|
||||
total_skill_loads: 5,
|
||||
total_skill_edits: 2,
|
||||
total_skill_actions: 7,
|
||||
distinct_skills_used: 1,
|
||||
},
|
||||
by_day: [
|
||||
{
|
||||
date: '2026-05-12',
|
||||
view_count: 5,
|
||||
manage_count: 2,
|
||||
total_count: 7,
|
||||
skills: [
|
||||
{ skill: 'thirty-day-skill', view_count: 5, manage_count: 2, total_count: 7 },
|
||||
],
|
||||
},
|
||||
],
|
||||
top_skills: [
|
||||
{ skill: 'thirty-day-skill', view_count: 5, manage_count: 2, total_count: 7, percentage: 100, last_used_at: null },
|
||||
],
|
||||
}
|
||||
|
||||
describe('SkillsUsageView', () => {
|
||||
beforeEach(() => {
|
||||
fetchSkillUsageStatsMock.mockReset()
|
||||
@@ -85,7 +100,7 @@ describe('SkillsUsageView', () => {
|
||||
})
|
||||
|
||||
it('loads rolling 7 day skill usage and renders statistics beside a skill-colored visual trend', async () => {
|
||||
const wrapper = mount(SkillsUsageView)
|
||||
const wrapper = mount(SkillsUsageView, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
expect(fetchSkillUsageStatsMock).toHaveBeenCalledWith(7)
|
||||
@@ -102,7 +117,7 @@ describe('SkillsUsageView', () => {
|
||||
})
|
||||
|
||||
it('reloads the selected period when the period button changes', async () => {
|
||||
const wrapper = mount(SkillsUsageView)
|
||||
const wrapper = mount(SkillsUsageView, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
fetchSkillUsageStatsMock.mockClear()
|
||||
|
||||
@@ -117,7 +132,7 @@ describe('SkillsUsageView', () => {
|
||||
})
|
||||
|
||||
it('flips the chart tooltip away from the hovered side of the bars', async () => {
|
||||
const wrapper = mount(SkillsUsageView)
|
||||
const wrapper = mount(SkillsUsageView, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
const bars = wrapper.findAll('.skill-bar-col')
|
||||
@@ -133,7 +148,7 @@ describe('SkillsUsageView', () => {
|
||||
})
|
||||
|
||||
it('keeps stale data visible while refreshing an already loaded period', async () => {
|
||||
const wrapper = mount(SkillsUsageView)
|
||||
const wrapper = mount(SkillsUsageView, { global: arcoGlobal })
|
||||
await flushPromises()
|
||||
|
||||
let resolveRefresh!: (value: unknown) => void
|
||||
@@ -172,20 +187,31 @@ describe('SkillsUsageView', () => {
|
||||
})
|
||||
|
||||
it('does not let an older refresh overwrite newer stats for the same period', async () => {
|
||||
const wrapper = mount(SkillsUsageView)
|
||||
await flushPromises()
|
||||
|
||||
// Arco buttons swallow clicks while `loading`, so the overlapping same-period
|
||||
// requests are staged the way a user can actually produce them: a slow 7 day
|
||||
// request, a detour through the 30 day period, then back to 7 days before the
|
||||
// first one settles.
|
||||
let resolveOlder!: (value: unknown) => void
|
||||
let resolveNewer!: (value: unknown) => void
|
||||
fetchSkillUsageStatsMock
|
||||
.mockReturnValueOnce(new Promise(resolve => { resolveOlder = resolve }))
|
||||
.mockResolvedValueOnce(thirtyDayStats)
|
||||
.mockReturnValueOnce(new Promise(resolve => { resolveNewer = resolve }))
|
||||
|
||||
const refreshButton = wrapper.findAll('button').find(button => button.text() === 'skillsUsage.refresh')
|
||||
expect(refreshButton).toBeTruthy()
|
||||
const wrapper = mount(SkillsUsageView, { global: arcoGlobal })
|
||||
|
||||
await refreshButton!.trigger('click')
|
||||
await refreshButton!.trigger('click')
|
||||
const thirtyDayButton = wrapper.findAll('button').find(button => button.text() === '30d')
|
||||
expect(thirtyDayButton).toBeTruthy()
|
||||
await thirtyDayButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const sevenDayButton = wrapper.findAll('button').find(button => button.text() === '7d')
|
||||
expect(sevenDayButton).toBeTruthy()
|
||||
await sevenDayButton!.trigger('click')
|
||||
|
||||
expect(fetchSkillUsageStatsMock).toHaveBeenNthCalledWith(1, 7)
|
||||
expect(fetchSkillUsageStatsMock).toHaveBeenNthCalledWith(2, 30)
|
||||
expect(fetchSkillUsageStatsMock).toHaveBeenNthCalledWith(3, 7)
|
||||
|
||||
resolveNewer({
|
||||
period_days: 7,
|
||||
|
||||
Reference in New Issue
Block a user