workspaceShell.test.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import { mount } from '@vue/test-utils'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { createMemoryHistory, createRouter } from 'vue-router'
  4. import { defineComponent } from 'vue'
  5. import type { Profile } from '@shared/types'
  6. import WorkspaceShell from '@/components/app/WorkspaceShell.vue'
  7. const profiles: Profile[] = [
  8. { name: 'default', agentName: '丫头', isDefault: true },
  9. { name: 'ops:blue/team', agentName: '运维 Agent', isDefault: false },
  10. ]
  11. const SettingsCenterDialogStub = defineComponent({
  12. name: 'SettingsCenterDialog',
  13. emits: ['close'],
  14. props: {
  15. open: Boolean,
  16. initialPage: String,
  17. },
  18. template: '<div v-if="open" data-testid="settings-center" :data-page="initialPage"><button data-testid="close-settings" type="button" @click="$emit(\'close\')">close</button></div>',
  19. })
  20. async function mountShell(start = '/chat') {
  21. const router = createRouter({
  22. history: createMemoryHistory(),
  23. routes: [
  24. { path: '/chat', component: { template: '<div></div>' } },
  25. { path: '/history', component: { template: '<div></div>' } },
  26. { path: '/conversations', component: { template: '<div></div>' } },
  27. { path: '/groups', component: { template: '<div></div>' } },
  28. { path: '/kanban', component: { template: '<div></div>' } },
  29. { path: '/files', component: { template: '<div></div>' } },
  30. ],
  31. })
  32. await router.push(start)
  33. await router.isReady()
  34. return mount(WorkspaceShell, {
  35. attachTo: document.body,
  36. props: {
  37. activeProfile: profiles[0],
  38. profiles,
  39. userName: 'owner',
  40. pairingUserName: 'owner',
  41. isAdmin: true,
  42. sidebarTitle: '历史记录',
  43. },
  44. global: {
  45. plugins: [router],
  46. stubs: {
  47. SettingsCenterDialog: SettingsCenterDialogStub,
  48. AgentAvatar: true,
  49. AppIcon: true,
  50. BrandMark: true,
  51. YaoYaoSidebarIcon: true,
  52. },
  53. },
  54. })
  55. }
  56. beforeEach(() => {
  57. localStorage.clear()
  58. })
  59. afterEach(() => {
  60. document.body.innerHTML = ''
  61. })
  62. describe('Workspace shell account controls', () => {
  63. it('includes the Kanban workspace in desktop and mobile navigation', async () => {
  64. const wrapper = await mountShell()
  65. expect(wrapper.get('.desktop-sidebar').text()).toContain('看板')
  66. expect(wrapper.get('.desktop-sidebar').text()).toContain('历史记录')
  67. expect(wrapper.get('.mobile-drawer').text()).toContain('看板')
  68. await wrapper.get('.desktop-sidebar .sidebar-feature-nav button[title="看板"]').trigger('click')
  69. await vi.waitFor(() => expect(wrapper.get('.sidebar-context__heading').text()).toContain('看板列表'))
  70. expect(wrapper.get('.desktop-sidebar').find('button[aria-label="搜索"]').exists()).toBe(false)
  71. expect(wrapper.get('.mobile-drawer').find('button[aria-label="搜索"]').exists()).toBe(false)
  72. wrapper.unmount()
  73. })
  74. it('keeps Agent switching focused and opens settings from its independent button', async () => {
  75. const wrapper = await mountShell()
  76. const desktop = wrapper.get('.desktop-sidebar')
  77. const agentTrigger = desktop.get('.sidebar-account-switcher__main')
  78. expect(agentTrigger.attributes('aria-haspopup')).toBe('listbox')
  79. expect(agentTrigger.attributes('aria-expanded')).toBe('false')
  80. await agentTrigger.trigger('click')
  81. expect(agentTrigger.attributes('aria-expanded')).toBe('true')
  82. const menu = desktop.get('.profile-menu')
  83. expect(menu.attributes('role')).toBe('listbox')
  84. expect(menu.text()).toContain('切换 Agent')
  85. expect(menu.text()).toContain('丫头')
  86. expect(menu.text()).toContain('运维 Agent')
  87. expect(menu.text()).not.toContain('Agent 设置')
  88. expect(menu.findAll('button')).toHaveLength(profiles.length)
  89. expect(menu.findAll('[role="option"]').map(option => option.attributes('aria-selected'))).toEqual(['true', 'false'])
  90. expect(menu.text()).not.toContain('账号安全')
  91. expect(menu.text()).not.toContain('系统管理')
  92. expect(menu.text()).not.toContain('系统更新')
  93. expect(menu.text()).not.toContain('退出登录')
  94. expect(document.activeElement).toBe(menu.find('[role="option"][aria-selected="true"]').element)
  95. await menu.trigger('keydown', { key: 'ArrowDown' })
  96. const targetProfile = menu.findAll<HTMLButtonElement>('button')
  97. .find(button => button.text().includes('运维 Agent'))!
  98. expect(document.activeElement).toBe(targetProfile.element)
  99. await targetProfile.trigger('click')
  100. expect(wrapper.emitted('selectProfile')).toEqual([['ops:blue/team']])
  101. expect(desktop.find('.profile-menu').exists()).toBe(false)
  102. const settingsTrigger = desktop.get<HTMLButtonElement>('.sidebar-settings-trigger')
  103. expect(settingsTrigger.text()).toBe('')
  104. expect(settingsTrigger.attributes('aria-label')).toBe('设置与模式')
  105. await settingsTrigger.trigger('click')
  106. const actions = document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')
  107. expect([...actions].map(button => button.textContent?.trim())).toEqual(['进入设置', '进入 Bot 模式'])
  108. actions[0]!.click()
  109. await wrapper.vm.$nextTick()
  110. const settings = wrapper.get('[data-testid="settings-center"]')
  111. expect(settings.attributes('data-page')).toBe('agent-identity')
  112. expect(desktop.find('.profile-menu').exists()).toBe(false)
  113. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  114. await wrapper.vm.$nextTick()
  115. expect(document.activeElement).toBe(settingsTrigger.element)
  116. const mobileNavigation = wrapper.get<HTMLButtonElement>('.mobile-header button[aria-label="打开导航"]')
  117. await mobileNavigation.trigger('click')
  118. await wrapper.vm.$nextTick()
  119. expect(wrapper.get('.mobile-header').attributes()).toHaveProperty('inert')
  120. expect(document.activeElement).toBe(wrapper.get('.mobile-drawer button[aria-label="关闭导航"]').element)
  121. await wrapper.get('.mobile-drawer .sidebar-settings-trigger').trigger('click')
  122. document.querySelector<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')!.click()
  123. await wrapper.vm.$nextTick()
  124. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  125. await wrapper.vm.$nextTick()
  126. expect(document.activeElement).toBe(mobileNavigation.element)
  127. expect(desktop.findAll('.sidebar-feature-nav button').map(button => button.text())).not.toContain('聊天')
  128. await settingsTrigger.trigger('click')
  129. document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')[1]!.click()
  130. await vi.waitFor(() => expect(wrapper.classes()).toContain('workspace-shell--conversations'))
  131. expect(document.querySelector('.workspace-settings-menu')).toBeNull()
  132. wrapper.unmount()
  133. })
  134. })
  135. it('uses the reference Bot list header and keeps mode changes in settings', async () => {
  136. const wrapper = await mountShell('/conversations')
  137. const rail = wrapper.get('.desktop-sidebar')
  138. expect(rail.find('.sidebar-feature-nav').exists()).toBe(false)
  139. expect(rail.find('.sidebar-primary-action').exists()).toBe(false)
  140. expect(rail.find('.sidebar-footer').exists()).toBe(true)
  141. expect(rail.get('.sidebar-account-switcher__main').text()).toContain('owner')
  142. expect(rail.get('.account-initial-avatar').text()).toBe('O')
  143. expect(rail.find('.sidebar-account-switcher__main .agent-avatar').exists()).toBe(false)
  144. expect(rail.find('.bot-logo-trigger').exists()).toBe(true)
  145. expect(rail.find('.bot-account-trigger').exists()).toBe(false)
  146. expect(rail.find('.sidebar-collapse').exists()).toBe(false)
  147. expect(rail.find('.sidebar-context__heading').exists()).toBe(false)
  148. await rail.get('.sidebar-account-switcher__main').trigger('click')
  149. expect(wrapper.get('[data-testid="settings-center"]').attributes('data-page')).toBe('account-security')
  150. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  151. await rail.get('.sidebar-create-trigger').trigger('click')
  152. const items = document.querySelectorAll<HTMLButtonElement>('.workspace-create-menu [role="menuitem"]')
  153. expect([...items].map(item => item.textContent?.trim())).toEqual(['新建 Bot', '新建群聊'])
  154. items[0]?.click()
  155. await wrapper.vm.$nextTick()
  156. expect(wrapper.emitted('createAgent')).toHaveLength(1)
  157. await rail.get('.sidebar-search-trigger').trigger('click')
  158. document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search-close'))
  159. await wrapper.vm.$nextTick()
  160. wrapper.unmount()
  161. })