workspaceShell.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { DOMWrapper, mount, flushPromises } 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 { createPinia } from 'pinia'
  6. import { useUnreadStore } from '@/stores/unread'
  7. import type { Profile } from '@shared/types'
  8. import WorkspaceShell from '@/components/app/WorkspaceShell.vue'
  9. import type { DesktopUpdateState } from '@shared/desktopUpdate'
  10. const profiles: Profile[] = [
  11. { name: 'default', agentName: '丫头', isDefault: true },
  12. { name: 'ops:blue/team', agentName: '运维机器人', isDefault: false },
  13. ]
  14. const SettingsCenterDialogStub = defineComponent({
  15. name: 'SettingsCenterDialog',
  16. emits: ['close'],
  17. props: {
  18. open: Boolean,
  19. initialPage: String,
  20. },
  21. 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>',
  22. })
  23. async function mountShell(start = '/chat') {
  24. const router = createRouter({
  25. history: createMemoryHistory(),
  26. routes: [
  27. { path: '/chat', component: { template: '<div></div>' } },
  28. { path: '/history', component: { template: '<div></div>' } },
  29. { path: '/conversations/automations', component: { template: '<div></div>' } },
  30. { path: '/conversations', component: { template: '<div></div>' } },
  31. { path: '/groups', component: { template: '<div></div>' } },
  32. { path: '/kanban', component: { template: '<div></div>' } },
  33. { path: '/files', component: { template: '<div></div>' } },
  34. ],
  35. })
  36. await router.push(start)
  37. await router.isReady()
  38. return mount(WorkspaceShell, {
  39. attachTo: document.body,
  40. props: {
  41. activeProfile: profiles[0],
  42. profiles,
  43. userName: 'owner',
  44. pairingUserName: 'owner',
  45. isAdmin: true,
  46. sidebarTitle: '历史记录',
  47. },
  48. global: {
  49. plugins: [router, createPinia()],
  50. stubs: {
  51. SettingsCenterDialog: SettingsCenterDialogStub,
  52. BotPluginsDialog: defineComponent({emits:['close'],template:'<div data-testid="plugins-dialog"><button @click="$emit(\'close\')">关闭已连接应用</button></div>'}),
  53. BotSettingsDialog: defineComponent({props:['initialPage','isAdmin'],emits:['close'],template:'<div data-testid="bot-settings-dialog" :data-page="initialPage"><button @click="$emit(\'close\')">关闭 Bot 设置</button></div>'}),
  54. UpdateCheckDialog: defineComponent({emits:['close', 'manage'],template:'<div data-testid="update-dialog"><button @click="$emit(\'close\')">关闭检测更新</button><button @click="$emit(\'manage\')">前往更新</button></div>'}),
  55. AboutDialog: defineComponent({emits:['close'],template:'<div data-testid="about-dialog"><button @click="$emit(\'close\')">关闭关于</button></div>'}),
  56. AgentAvatar: true,
  57. AppIcon: true,
  58. BrandMark: true,
  59. YaoYaoSidebarIcon: true,
  60. },
  61. },
  62. })
  63. }
  64. beforeEach(() => {
  65. localStorage.clear()
  66. useUnreadStore().reset()
  67. delete window.yaoyaoDesktop
  68. })
  69. afterEach(() => {
  70. document.body.innerHTML = ''
  71. })
  72. describe('Workspace shell account controls', () => {
  73. it('includes the Kanban workspace in desktop and mobile navigation', async () => {
  74. const wrapper = await mountShell()
  75. expect(wrapper.get('.desktop-sidebar').text()).toContain('看板')
  76. expect(wrapper.get('.desktop-sidebar').text()).toContain('历史记录')
  77. expect(wrapper.get('.mobile-drawer').text()).toContain('看板')
  78. await wrapper.get('.desktop-sidebar .sidebar-feature-nav button[title="看板"]').trigger('click')
  79. await vi.waitFor(() => expect(wrapper.get('.sidebar-context__heading').text()).toContain('看板列表'))
  80. expect(wrapper.get('.desktop-sidebar').find('button[aria-label="搜索"]').exists()).toBe(false)
  81. expect(wrapper.get('.mobile-drawer').find('button[aria-label="搜索"]').exists()).toBe(false)
  82. wrapper.unmount()
  83. })
  84. it('keeps Agent switching focused and opens settings from its independent button', async () => {
  85. const wrapper = await mountShell()
  86. const desktop = wrapper.get('.desktop-sidebar')
  87. const agentTrigger = desktop.get('.sidebar-account-switcher__main')
  88. expect(agentTrigger.attributes('aria-haspopup')).toBe('listbox')
  89. expect(agentTrigger.attributes('aria-expanded')).toBe('false')
  90. await agentTrigger.trigger('click')
  91. expect(agentTrigger.attributes('aria-expanded')).toBe('true')
  92. const menu = desktop.get('.profile-menu')
  93. expect(menu.attributes('role')).toBe('listbox')
  94. expect(menu.text()).toContain('切换机器人')
  95. expect(menu.text()).toContain('丫头')
  96. expect(menu.text()).toContain('运维机器人')
  97. expect(menu.text()).not.toContain('机器人设置')
  98. expect(menu.findAll('button')).toHaveLength(profiles.length)
  99. expect(menu.findAll('[role="option"]').map(option => option.attributes('aria-selected'))).toEqual(['true', 'false'])
  100. expect(menu.text()).not.toContain('账号安全')
  101. expect(menu.text()).not.toContain('系统管理')
  102. expect(menu.text()).not.toContain('系统更新')
  103. expect(menu.text()).not.toContain('退出登录')
  104. expect(document.activeElement).toBe(menu.find('[role="option"][aria-selected="true"]').element)
  105. await menu.trigger('keydown', { key: 'ArrowDown' })
  106. const targetProfile = menu.findAll<HTMLButtonElement>('button')
  107. .find(button => button.text().includes('运维机器人'))!
  108. expect(document.activeElement).toBe(targetProfile.element)
  109. await targetProfile.trigger('click')
  110. expect(wrapper.emitted('selectProfile')).toEqual([['ops:blue/team']])
  111. expect(desktop.find('.profile-menu').exists()).toBe(false)
  112. const settingsTrigger = desktop.get<HTMLButtonElement>('.sidebar-settings-trigger')
  113. expect(settingsTrigger.text()).toBe('')
  114. expect(settingsTrigger.attributes('aria-label')).toBe('设置与模式')
  115. await settingsTrigger.trigger('click')
  116. const actions = document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')
  117. expect([...actions].map(button => button.textContent?.trim())).toEqual(['我的设置', '关于', '检测更新', '帮助', '进入 Bot 模式'])
  118. actions[0]!.click()
  119. await wrapper.vm.$nextTick()
  120. const settings = wrapper.get('[data-testid="settings-center"]')
  121. expect(settings.attributes('data-page')).toBe('agent-identity')
  122. expect(desktop.find('.profile-menu').exists()).toBe(false)
  123. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  124. await wrapper.vm.$nextTick()
  125. expect(document.activeElement).toBe(settingsTrigger.element)
  126. const mobileNavigation = wrapper.get<HTMLButtonElement>('.mobile-header button[aria-label="打开导航"]')
  127. await mobileNavigation.trigger('click')
  128. await wrapper.vm.$nextTick()
  129. expect(wrapper.get('.mobile-header').attributes()).toHaveProperty('inert')
  130. expect(document.activeElement).toBe(wrapper.get('.mobile-drawer button[aria-label="关闭导航"]').element)
  131. await wrapper.get('.mobile-drawer .sidebar-settings-trigger').trigger('click')
  132. document.querySelector<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')!.click()
  133. await wrapper.vm.$nextTick()
  134. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  135. await wrapper.vm.$nextTick()
  136. expect(document.activeElement).toBe(mobileNavigation.element)
  137. expect(desktop.findAll('.sidebar-feature-nav button').map(button => button.text())).not.toContain('聊天')
  138. await settingsTrigger.trigger('click')
  139. document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')[4]!.click()
  140. await vi.waitFor(() => expect(wrapper.classes()).toContain('workspace-shell--conversations'))
  141. expect(document.querySelector('.workspace-settings-menu')).toBeNull()
  142. wrapper.unmount()
  143. })
  144. })
  145. it('uses the reference Bot list header and keeps mode changes in settings', async () => {
  146. const wrapper = await mountShell('/conversations')
  147. const rail = wrapper.get('.desktop-sidebar')
  148. expect(wrapper.find('.sidebar-settings-trigger').exists()).toBe(false)
  149. expect(rail.find('.sidebar-feature-nav').exists()).toBe(false)
  150. expect(rail.find('.sidebar-primary-action').exists()).toBe(false)
  151. expect(rail.find('.sidebar-footer').exists()).toBe(true)
  152. expect(rail.get('.sidebar-account-switcher__main').text()).toContain('owner')
  153. expect(rail.get('.account-initial-avatar').text()).toBe('O')
  154. expect(rail.find('.sidebar-account-switcher__main .agent-avatar').exists()).toBe(false)
  155. expect(rail.find('.bot-logo-trigger').exists()).toBe(true)
  156. expect(rail.find('.bot-account-trigger').exists()).toBe(false)
  157. expect(rail.find('.sidebar-collapse').exists()).toBe(false)
  158. expect(rail.find('.sidebar-context__heading').exists()).toBe(false)
  159. await rail.get('.sidebar-account-switcher__main').trigger('click')
  160. const accountItems = [...document.querySelectorAll<HTMLElement>('.workspace-settings-menu [role="menuitem"]')]
  161. expect(accountItems.map(item => item.textContent?.trim())).toEqual(['我的设置', '关于', '检测更新', '帮助', '进入聊天模式'])
  162. expect(document.querySelector<HTMLAnchorElement>('.workspace-settings-menu a')?.href).toBe('https://yaoyao.samien.cn/')
  163. accountItems[0]!.click(); await wrapper.vm.$nextTick()
  164. expect(wrapper.get('[data-testid="settings-center"]').attributes('data-page')).toBe('account-profile')
  165. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  166. await rail.get('.sidebar-create-trigger').trigger('click')
  167. const items = document.querySelectorAll<HTMLButtonElement>('.workspace-create-menu [role="menuitem"]')
  168. expect([...items].map(item => item.textContent?.trim())).toEqual(['新建 Bot', '新建群聊'])
  169. items[0]?.click()
  170. await wrapper.vm.$nextTick()
  171. expect(wrapper.emitted('createAgent')).toHaveLength(1)
  172. await rail.get('.sidebar-create-trigger').trigger('click')
  173. document.querySelectorAll<HTMLButtonElement>('.workspace-create-menu [role="menuitem"]')[1]?.click()
  174. await wrapper.vm.$nextTick()
  175. expect(wrapper.emitted('createGroup')).toHaveLength(1)
  176. expect(wrapper.emitted('createRemoteAgent')).toBeUndefined()
  177. await rail.get('.sidebar-search-trigger').trigger('click')
  178. document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search-close'))
  179. await wrapper.vm.$nextTick()
  180. wrapper.unmount()
  181. })
  182. it('shows Bot tools only in Bot mode and restores keyboard focus after closing', async () => {
  183. const normal = await mountShell('/chat')
  184. expect(normal.find('.sidebar-tools-trigger').exists()).toBe(false)
  185. normal.unmount()
  186. const wrapper = await mountShell('/conversations')
  187. const trigger = wrapper.get<HTMLButtonElement>('.desktop-sidebar .sidebar-tools-trigger')
  188. await trigger.trigger('click')
  189. const menu = document.querySelector<HTMLElement>('.workspace-tools-menu')!
  190. const items = [...menu.querySelectorAll<HTMLButtonElement>('[role="menuitem"]')]
  191. expect(items.map(b => b.textContent?.trim())).toEqual(['Bot 设置', '自动化', '已连接应用'])
  192. expect(document.activeElement).toBe(items[0])
  193. menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
  194. expect(document.activeElement).toBe(items[1])
  195. items[2]!.click(); await wrapper.vm.$nextTick()
  196. await flushPromises()
  197. expect(wrapper.find('[data-testid="settings-center"]').exists()).toBe(false)
  198. expect(wrapper.find('[data-testid="plugins-dialog"]').exists()).toBe(true)
  199. await wrapper.get('[data-testid="plugins-dialog"] button').trigger('click'); await wrapper.vm.$nextTick()
  200. expect(document.activeElement).toBe(trigger.element)
  201. wrapper.unmount()
  202. })
  203. it('opens dedicated Bot settings from tools and routes the existing VM shortcut there', async () => {
  204. const wrapper = await mountShell('/conversations')
  205. const trigger = wrapper.get<HTMLButtonElement>('.desktop-sidebar .sidebar-tools-trigger')
  206. await trigger.trigger('click')
  207. ;[...document.querySelectorAll<HTMLButtonElement>('.workspace-tools-menu button')].find(b => b.textContent?.trim() === 'Bot 设置')!.click()
  208. await flushPromises()
  209. expect(wrapper.get('[data-testid="bot-settings-dialog"]').attributes('data-page')).toBe('projects')
  210. expect(wrapper.find('[data-testid="settings-center"]').exists()).toBe(false)
  211. await wrapper.get('[data-testid="bot-settings-dialog"] button').trigger('click')
  212. expect(document.activeElement).toBe(trigger.element)
  213. window.dispatchEvent(new Event('yaoyao:local-vm-settings'))
  214. await flushPromises()
  215. expect(wrapper.get('[data-testid="bot-settings-dialog"]').attributes('data-page')).toBe('vm')
  216. expect(wrapper.find('[data-testid="settings-center"]').exists()).toBe(false)
  217. wrapper.unmount()
  218. })
  219. it.each(['/chat', '/history', '/conversations', '/kanban', '/files'])('opens an independent About dialog from the menu in %s', async path => {
  220. const wrapper = await mountShell(path)
  221. const trigger = wrapper.get<HTMLButtonElement>(path === '/conversations' ? '.desktop-sidebar .sidebar-account-switcher__main' : '.desktop-sidebar .sidebar-settings-trigger')
  222. await trigger.trigger('click')
  223. const item = [...document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu button')].find(b => b.textContent?.trim() === '关于')!
  224. item.click(); await flushPromises()
  225. expect(wrapper.find('[data-testid="about-dialog"]').exists()).toBe(true)
  226. expect(wrapper.find('[data-testid="settings-center"]').exists()).toBe(false)
  227. await wrapper.get('[data-testid="about-dialog"] button').trigger('click'); await flushPromises()
  228. expect(document.activeElement).toBe(trigger.element)
  229. wrapper.unmount()
  230. })
  231. it('navigates to the independent automation route without opening settings', async () => {
  232. const wrapper = await mountShell('/conversations')
  233. await wrapper.get('.desktop-sidebar .sidebar-tools-trigger').trigger('click')
  234. const item = [...document.querySelectorAll<HTMLButtonElement>('.workspace-tools-menu button')].find(b => b.textContent?.trim() === '自动化')!
  235. item.click(); await flushPromises()
  236. expect(wrapper.vm.$router.currentRoute.value.path).toBe('/conversations/automations')
  237. expect(wrapper.find('[data-testid="settings-center"]').exists()).toBe(false)
  238. wrapper.unmount()
  239. })
  240. it('checks Web updates from the Bot account menu and preserves focus through update settings', async () => {
  241. const wrapper = await mountShell('/conversations')
  242. const trigger = wrapper.get<HTMLButtonElement>('.desktop-sidebar .sidebar-account-switcher__main')
  243. await trigger.trigger('click')
  244. ;[...document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu button')].find(b => b.textContent?.trim() === '检测更新')!.click()
  245. await flushPromises()
  246. expect(wrapper.find('[data-testid="update-dialog"]').exists()).toBe(true)
  247. await wrapper.get('[data-testid="update-dialog"] button:nth-child(2)').trigger('click')
  248. expect(wrapper.get('[data-testid="settings-center"]').attributes('data-page')).toBe('system-update')
  249. await wrapper.get('[data-testid="close-settings"]').trigger('click')
  250. await flushPromises()
  251. expect(document.activeElement).toBe(trigger.element)
  252. wrapper.unmount()
  253. })
  254. it('opens the native updater, prevents duplicate clicks and lets a failed opening retry', async () => {
  255. const openUpdates = vi.fn().mockRejectedValueOnce(new Error('窗口打开失败')).mockResolvedValueOnce(undefined)
  256. window.yaoyaoDesktop = { openUpdates, openComputer: vi.fn(), computerClosed: vi.fn(), onComputerClose: vi.fn() }
  257. const wrapper = await mountShell('/conversations')
  258. await wrapper.get('.desktop-sidebar .sidebar-account-switcher__main').trigger('click')
  259. const item = [...document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu button')].find(b => b.textContent?.trim() === '检测更新')!
  260. item.click(); item.click()
  261. await flushPromises()
  262. expect(openUpdates).toHaveBeenCalledTimes(1)
  263. expect(document.querySelector('[role="alert"]')?.textContent).toContain('窗口打开失败')
  264. item.click(); await flushPromises()
  265. expect(openUpdates).toHaveBeenCalledTimes(2)
  266. expect(document.querySelector('.workspace-settings-menu')).toBeNull()
  267. expect(wrapper.find('[data-testid="update-dialog"]').exists()).toBe(false)
  268. wrapper.unmount()
  269. })
  270. it('keeps desktop downloads and restart in the same entry, including after closing the menu', async () => {
  271. vi.useFakeTimers()
  272. let state: DesktopUpdateState = { phase: 'checked', installMode: 'restart', currentVersion: '0.4.80', latestVersion: '0.4.81', available: true, received: 0, total: 100, message: '发现更新' }
  273. let finish!: (state: DesktopUpdateState) => void
  274. const openUpdates = vi.fn()
  275. const updateAction = vi.fn().mockImplementation(action => {
  276. if (action === 'download') {
  277. state = { ...state, phase: 'downloading', received: 42 }
  278. return new Promise<DesktopUpdateState>(resolve => { finish = resolve })
  279. }
  280. state = { ...state, phase: 'installing' }
  281. return Promise.resolve(state)
  282. })
  283. window.yaoyaoDesktop = { updateState: vi.fn(async () => state), updateAction, openUpdates, openComputer: vi.fn(), computerClosed: vi.fn(), onComputerClose: vi.fn() }
  284. const wrapper = await mountShell('/conversations')
  285. try {
  286. await wrapper.setProps({ isAdmin: false })
  287. await flushPromises()
  288. const entry = () => wrapper.get('.desktop-sidebar .desktop-update-entry')
  289. expect(entry().text()).toBe('可更新')
  290. await entry().get('button').trigger('click')
  291. await entry().get('button').trigger('click')
  292. await flushPromises()
  293. expect(updateAction).toHaveBeenCalledExactlyOnceWith('download')
  294. expect(entry().get('[role="progressbar"]').attributes('aria-valuenow')).toBe('42')
  295. await wrapper.get('.desktop-sidebar .sidebar-account-switcher__main').trigger('click')
  296. expect(document.querySelector('.workspace-settings-menu .desktop-update-entry')?.textContent).toContain('正在下载 · 42%')
  297. state = { ...state, phase: 'preparing', received: 100 }
  298. await vi.advanceTimersByTimeAsync(500)
  299. expect(document.querySelector('.workspace-settings-menu .desktop-update-entry')?.textContent).toContain('正在准备…')
  300. expect(document.querySelector('.workspace-settings-menu .desktop-update-entry button')?.hasAttribute('disabled')).toBe(true)
  301. await wrapper.get('.desktop-sidebar .sidebar-account-switcher__main').trigger('click')
  302. state = { ...state, phase: 'ready', available: false }
  303. finish(state); await flushPromises()
  304. expect(entry().text()).toBe('重新启动')
  305. expect(updateAction).toHaveBeenCalledTimes(1)
  306. await entry().get('button').trigger('click'); await flushPromises()
  307. expect(updateAction).toHaveBeenLastCalledWith('install')
  308. expect(openUpdates).not.toHaveBeenCalled()
  309. expect(wrapper.find('[data-testid="update-dialog"]').exists()).toBe(false)
  310. } finally { wrapper.unmount(); vi.useRealTimers() }
  311. })
  312. it('shows desktop check errors in place and allows retry without opening a window', async () => {
  313. const checked: DesktopUpdateState = { phase: 'checked', installMode: 'restart', currentVersion: '0.4.80', available: false, received: 0, total: 0, message: '已是最新版本' }
  314. const updateAction = vi.fn().mockRejectedValueOnce(new Error('连接失败')).mockResolvedValueOnce(checked)
  315. window.yaoyaoDesktop = { updateState: vi.fn(async () => ({ ...checked, phase: 'idle' as const })), updateAction, openComputer: vi.fn(), computerClosed: vi.fn(), onComputerClose: vi.fn() }
  316. const wrapper = await mountShell('/conversations')
  317. try {
  318. await flushPromises()
  319. await wrapper.get('.desktop-sidebar .sidebar-account-switcher__main').trigger('click')
  320. const button = () => document.querySelector<HTMLButtonElement>('.workspace-settings-menu .desktop-update-entry button')!
  321. button().click(); button().click(); await flushPromises()
  322. expect(updateAction).toHaveBeenCalledExactlyOnceWith('check')
  323. expect(document.querySelector('.workspace-settings-menu [role="alert"]')?.textContent).toBe('连接失败')
  324. button().click(); await flushPromises()
  325. expect(button().textContent).toContain('已是最新版本')
  326. expect(document.querySelector('.workspace-settings-menu')).not.toBeNull()
  327. expect(document.querySelector('.workspace-settings-menu [role="alert"]')).toBeNull()
  328. } finally { wrapper.unmount() }
  329. })
  330. it('does not expose server update controls to a non-admin browser account', async () => {
  331. const wrapper = await mountShell('/conversations')
  332. await wrapper.setProps({ isAdmin: false })
  333. await wrapper.get('.desktop-sidebar .sidebar-account-switcher__main').trigger('click')
  334. expect(document.querySelector('.workspace-settings-menu')?.textContent).not.toContain('检测更新')
  335. wrapper.unmount()
  336. })
  337. it('opens the unread center beside the sidebar and restores focus after Escape', async () => {
  338. const wrapper = await mountShell('/conversations')
  339. const trigger = wrapper.get<HTMLButtonElement>('.desktop-sidebar .unread-entry')
  340. vi.spyOn(trigger.element, 'getBoundingClientRect').mockReturnValue({ top: 86 } as DOMRect)
  341. vi.spyOn(wrapper.get('.desktop-sidebar').element, 'getBoundingClientRect').mockReturnValue({ right: 360 } as DOMRect)
  342. await trigger.trigger('click')
  343. expect(trigger.attributes('aria-expanded')).toBe('true')
  344. expect(wrapper.attributes()).toHaveProperty('inert')
  345. const panel = document.querySelector<HTMLElement>('#unread-center')!
  346. expect(panel).not.toBeNull()
  347. expect(document.activeElement).toBe(panel)
  348. expect(panel.parentElement?.style.getPropertyValue('--unread-left')).toBe('372px')
  349. expect(panel.parentElement?.style.getPropertyValue('--unread-top')).toBe('86px')
  350. await new DOMWrapper(panel).trigger('keydown', { key: 'Escape' })
  351. await flushPromises()
  352. expect(document.querySelector('#unread-center')).toBeNull()
  353. expect(wrapper.attributes()).not.toHaveProperty('inert')
  354. expect(document.activeElement).toBe(trigger.element)
  355. wrapper.unmount()
  356. })