systemUpdatePanel.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { flushPromises, mount } from '@vue/test-utils'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import SystemUpdatePanel from '@/components/app/SystemUpdatePanel.vue'
  4. const api = vi.hoisted(() => ({
  5. applySystemUpdate: vi.fn(),
  6. checkSystemUpdate: vi.fn(),
  7. rollbackSystemUpdate: vi.fn(),
  8. systemUpdateJob: vi.fn(),
  9. systemUpdateStatus: vi.fn(),
  10. }))
  11. vi.mock('@/api/systemUpdate', () => api)
  12. const manifest = {
  13. schemaVersion: 1 as const,
  14. releaseVersion: '0.2.17',
  15. webVersion: '0.2.17',
  16. pluginVersion: '1.7.3',
  17. gitTag: 'v0.2.17',
  18. }
  19. const activeJob = {
  20. id: 'job-1',
  21. operation: 'update' as const,
  22. state: 'verifying' as const,
  23. message: '正在验证',
  24. createdAt: '2026-08-29T00:00:00Z',
  25. updatedAt: '2026-08-29T00:00:01Z',
  26. }
  27. const doneJob = { ...activeJob, state: 'succeeded' as const, message: '更新完成' }
  28. const readyStatus = {
  29. current: manifest,
  30. installedPluginVersion: '1.7.3',
  31. versionsMatch: true,
  32. installationMode: 'release' as const,
  33. latest: manifest,
  34. updateAvailable: false,
  35. supported: true,
  36. canRollback: true,
  37. }
  38. beforeEach(() => {
  39. vi.useFakeTimers()
  40. api.systemUpdateStatus.mockReset()
  41. api.checkSystemUpdate.mockReset()
  42. api.systemUpdateJob.mockReset()
  43. api.applySystemUpdate.mockReset()
  44. api.rollbackSystemUpdate.mockReset()
  45. api.systemUpdateStatus.mockResolvedValue(readyStatus)
  46. api.checkSystemUpdate.mockResolvedValue(readyStatus)
  47. api.systemUpdateJob.mockResolvedValue(doneJob)
  48. })
  49. afterEach(() => {
  50. document.body.innerHTML = ''
  51. vi.clearAllTimers()
  52. vi.useRealTimers()
  53. vi.restoreAllMocks()
  54. })
  55. describe('SystemUpdatePanel', () => {
  56. it('shows GitHub failures without claiming the installed version is latest', async () => {
  57. api.systemUpdateStatus.mockResolvedValue({ ...readyStatus, releaseSource: 'https://github.com/Lsamien/hermes-yaoyao.git', releasePageUrl: 'https://github.com/Lsamien/hermes-yaoyao/releases' })
  58. api.checkSystemUpdate.mockRejectedValue(new Error('GitHub 请求受限,请稍后重试'))
  59. const wrapper = mount(SystemUpdatePanel, { global: { stubs: { AppIcon: true } } })
  60. await flushPromises()
  61. expect(wrapper.text()).toContain('GitHub 请求受限')
  62. expect(wrapper.text()).not.toContain('已是最新版本')
  63. expect(wrapper.get('.solid-button').attributes('disabled')).toBeDefined()
  64. expect(wrapper.get('a').attributes('href')).toBe('https://github.com/Lsamien/hermes-yaoyao/releases')
  65. wrapper.unmount()
  66. })
  67. it('allows Web updates with unknown plugin state and explains the offline boundary', async () => {
  68. const offlineStatus = { ...readyStatus, installedPluginVersion: undefined, versionsMatch: false, updateAvailable: true,
  69. latest: { ...manifest, releaseVersion: '0.3.0', webVersion: '0.3.0', gitTag: 'v0.3.0' } }
  70. api.systemUpdateStatus.mockResolvedValue(offlineStatus)
  71. api.checkSystemUpdate.mockResolvedValue(offlineStatus)
  72. api.applySystemUpdate.mockResolvedValue(doneJob)
  73. const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true)
  74. const wrapper = mount(SystemUpdatePanel, { global: { stubs: { AppIcon: true } } })
  75. await flushPromises()
  76. expect(wrapper.text()).toContain('查看 Web 版本、检查更新或回滚到上一个版本')
  77. expect(wrapper.text()).not.toContain('当前插件')
  78. expect(wrapper.find('.version-warning').exists()).toBe(false)
  79. const apply = wrapper.find<HTMLButtonElement>('.solid-button')
  80. expect(apply.element.disabled).toBe(false)
  81. await apply.trigger('click'); await flushPromises()
  82. expect(confirm).toHaveBeenCalledWith('将 Web 升级到 0.3.0?')
  83. expect(api.applySystemUpdate).toHaveBeenCalledWith('0.3.0')
  84. expect(wrapper.text()).toContain('更新完成')
  85. wrapper.unmount()
  86. })
  87. it('resumes polling an existing non-terminal job and releases the settings lock', async () => {
  88. api.systemUpdateStatus
  89. .mockResolvedValueOnce({ ...readyStatus, job: activeJob })
  90. .mockResolvedValueOnce({ ...readyStatus, job: doneJob })
  91. const wrapper = mount(SystemUpdatePanel, {
  92. props: { active: true },
  93. global: { stubs: { AppIcon: true } },
  94. })
  95. await flushPromises()
  96. expect(wrapper.emitted('lock-change')?.some(event => event[0] === true)).toBe(true)
  97. await vi.advanceTimersByTimeAsync(1_000)
  98. await flushPromises()
  99. expect(api.systemUpdateJob).toHaveBeenCalledWith('job-1')
  100. expect(wrapper.text()).toContain('更新完成')
  101. expect(wrapper.emitted('lock-change')?.at(-1)).toEqual([false])
  102. expect(api.checkSystemUpdate).not.toHaveBeenCalled()
  103. })
  104. it('locks while an update mutation is starting and ignores a stale response after deactivation', async () => {
  105. const updateAvailable = {
  106. ...readyStatus,
  107. latest: { ...manifest, releaseVersion: '0.2.18', webVersion: '0.2.18', gitTag: 'v0.2.18' },
  108. updateAvailable: true,
  109. }
  110. api.systemUpdateStatus.mockResolvedValue(updateAvailable)
  111. api.checkSystemUpdate.mockResolvedValue(updateAvailable)
  112. let resolveApply: ((job: typeof activeJob) => void) | undefined
  113. api.applySystemUpdate.mockReturnValue(new Promise(resolve => { resolveApply = resolve }))
  114. vi.spyOn(window, 'confirm').mockReturnValue(true)
  115. const wrapper = mount(SystemUpdatePanel, {
  116. props: { active: true },
  117. global: { stubs: { AppIcon: true } },
  118. })
  119. await flushPromises()
  120. const apply = wrapper.findAll<HTMLButtonElement>('button').find(button => button.text().includes('升级 Web'))!
  121. await apply.trigger('click')
  122. await wrapper.vm.$nextTick()
  123. expect(wrapper.emitted('lock-change')?.at(-1)).toEqual([true])
  124. const check = wrapper.findAll<HTMLButtonElement>('button').find(button => button.text().includes('检查更新'))!
  125. expect(check.element.disabled).toBe(true)
  126. await wrapper.setProps({ active: false })
  127. resolveApply?.(activeJob)
  128. await flushPromises()
  129. expect(api.systemUpdateJob).not.toHaveBeenCalled()
  130. expect(wrapper.emitted('lock-change')?.at(-1)).toEqual([false])
  131. })
  132. })