hostToolsPanel.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { flushPromises, mount } from '@vue/test-utils'
  2. import { beforeEach, expect, it, vi } from 'vitest'
  3. import HostToolsPanel from '@/components/app/HostToolsPanel.vue'
  4. import { apiRequest } from '@/api/client'
  5. vi.mock('@/api/client', () => ({ apiRequest: vi.fn() }))
  6. const request = vi.mocked(apiRequest)
  7. const endpoint = '/api/app/admin/desktop-hosts'
  8. const firstId = '1a111111-1111-4111-8111-111111111111'
  9. const secondId = '2b222222-2222-4222-8222-222222222222'
  10. let hosts: { id: string; name: string; displayName: string; enabled: boolean; online: boolean; createdAt: number }[]
  11. beforeEach(() => {
  12. request.mockReset()
  13. hosts = [
  14. { id: firstId, name: '同名电脑', displayName: '', enabled: true, online: false, createdAt: 1_700_000_000_000 },
  15. { id: secondId, name: '同名电脑', displayName: '', enabled: true, online: true, createdAt: 1_710_000_000_000 },
  16. { id: 'disabled-host', name: '已停用电脑', displayName: '', enabled: false, online: false, createdAt: 1 },
  17. ]
  18. request.mockImplementation(async (path, options) => {
  19. if (path === endpoint) return { hosts: structuredClone(hosts), localName: '' } as never
  20. if (path === '/api/app/settings/host-tools') return { approvalPolicy: 'allow', scriptMachine: true, serverComputer: true, vm: true, cloud: true } as never
  21. const host = hosts.find(item => path === `${endpoint}/${item.id}/name` || path === `${endpoint}/${item.id}`)
  22. if (host && options?.method === 'PUT') host.displayName = (options.body as { name: string }).name
  23. else if (host && options?.method === 'DELETE') host.enabled = false
  24. else throw new Error(`Unexpected request: ${path}`)
  25. return { ok: true } as never
  26. })
  27. })
  28. it('edits the shared transfer limit and blocks invalid values with an inline error', async () => {
  29. const wrapper = mount(HostToolsPanel)
  30. await flushPromises()
  31. const input = wrapper.get<HTMLInputElement>('#file-transfer-limit')
  32. expect(input.element.value).toBe('25')
  33. for (const invalid of ['0', '101', '1.5', '']) {
  34. await input.setValue(invalid)
  35. expect(input.attributes('aria-invalid')).toBe('true')
  36. expect(wrapper.get('#file-transfer-error').text()).toContain('1–100')
  37. expect(wrapper.get('button[type=submit]').attributes('disabled')).toBeDefined()
  38. }
  39. await input.setValue('100')
  40. await wrapper.get('form').trigger('submit')
  41. await flushPromises()
  42. expect(request).toHaveBeenCalledWith('/api/app/settings/host-tools', { method: 'PUT', body: expect.objectContaining({ fileTransferMaxMiB: 100 }) })
  43. wrapper.unmount()
  44. })
  45. it('keeps managed browsing opt-in and saves it independently of virtual machines', async () => {
  46. const wrapper = mount(HostToolsPanel)
  47. try {
  48. await flushPromises()
  49. const browser = wrapper.findAll('label.toggle').find(label => label.text().includes('托管浏览器'))!.get<HTMLInputElement>('input')
  50. expect(browser.element.checked).toBe(false)
  51. const vm = wrapper.findAll('label.toggle').find(label => label.text().includes('虚拟环境'))!.get<HTMLInputElement>('input')
  52. await vm.setValue(false)
  53. await browser.setValue(true)
  54. await wrapper.get('form').trigger('submit')
  55. await flushPromises()
  56. expect(request).toHaveBeenCalledWith('/api/app/settings/host-tools', { method: 'PUT', body: expect.objectContaining({ managedBrowser: true, vm: false }) })
  57. } finally { wrapper.unmount() }
  58. })
  59. it('distinguishes same-name registrations by status, ID and pairing time without merging them', async () => {
  60. const wrapper = mount(HostToolsPanel)
  61. await flushPromises()
  62. expect(wrapper.findAll('.computer')).toHaveLength(3)
  63. const first = wrapper.get(`[data-host-id="${firstId}"]`)
  64. const second = wrapper.get(`[data-host-id="${secondId}"]`)
  65. expect(first.text()).toContain('未连接')
  66. expect(second.text()).toContain('已连接')
  67. expect(first.text()).toContain('编号 1a111111')
  68. expect(second.text()).toContain('编号 2b222222')
  69. expect(first.text()).toContain('配对于')
  70. expect(wrapper.text()).toContain('有同名的配对记录')
  71. expect(wrapper.text()).not.toContain('已停用电脑')
  72. expect(wrapper.get('[data-host-id="local"]').find('.remove-host').exists()).toBe(false)
  73. expect(request.mock.calls.some(([, options]) => options?.method === 'DELETE')).toBe(false)
  74. wrapper.unmount()
  75. })
  76. it('only disables the explicitly confirmed registration, even when names match', async () => {
  77. const wrapper = mount(HostToolsPanel)
  78. await flushPromises()
  79. const first = wrapper.get(`[data-host-id="${firstId}"]`)
  80. await first.get('.remove-host').trigger('click')
  81. expect(request.mock.calls.some(([, options]) => options?.method === 'DELETE')).toBe(false)
  82. await first.get('.remove-confirmation .remove-host').trigger('click')
  83. await flushPromises()
  84. expect(request).toHaveBeenCalledWith(`${endpoint}/${firstId}`, { method: 'DELETE' })
  85. expect(wrapper.find(`[data-host-id="${firstId}"]`).exists()).toBe(false)
  86. expect(wrapper.find(`[data-host-id="${secondId}"]`).exists()).toBe(true)
  87. expect(wrapper.text()).toContain('这条配对记录已停用')
  88. wrapper.unmount()
  89. })
  90. it('keeps the record and shows an error when disabling fails', async () => {
  91. const wrapper = mount(HostToolsPanel)
  92. await flushPromises()
  93. request.mockRejectedValueOnce(new Error('服务暂时不可用'))
  94. const first = wrapper.get(`[data-host-id="${firstId}"]`)
  95. await first.get('.remove-host').trigger('click')
  96. await first.get('.remove-confirmation .remove-host').trigger('click')
  97. await flushPromises()
  98. expect(wrapper.find(`[data-host-id="${firstId}"]`).exists()).toBe(true)
  99. expect(wrapper.get('[role="alert"]').text()).toContain('服务暂时不可用')
  100. wrapper.unmount()
  101. })
  102. it('renames by registration ID and preserves another computer’s unsaved name', async () => {
  103. const wrapper = mount(HostToolsPanel)
  104. await flushPromises()
  105. const first = wrapper.get(`[data-host-id="${firstId}"]`)
  106. const second = wrapper.get(`[data-host-id="${secondId}"]`)
  107. await first.get('input').setValue('旧电脑')
  108. await second.get('input').setValue('新电脑')
  109. await first.get('.name-editor button').trigger('click')
  110. await flushPromises()
  111. expect(request).toHaveBeenCalledWith(`${endpoint}/${firstId}/name`, { method: 'PUT', body: { name: '旧电脑' } })
  112. expect(second.get<HTMLInputElement>('input').element.value).toBe('新电脑')
  113. hosts[1]!.online = false
  114. await wrapper.get('.names-heading button').trigger('click')
  115. await flushPromises()
  116. expect(second.text()).toContain('未连接')
  117. expect(second.get<HTMLInputElement>('input').element.value).toBe('新电脑')
  118. wrapper.unmount()
  119. })