Files
Hermes-ui/tests/client/copilot-login-modal.test.ts
T

134 lines
4.1 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
const mockApi = vi.hoisted(() => ({
startCopilotLogin: vi.fn(),
pollCopilotLogin: vi.fn(),
}))
const mockMessage = vi.hoisted(() => ({
success: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
}))
vi.mock('@/api/hermes/copilot-auth', () => mockApi)
vi.mock('@/utils/clipboard', () => ({ copyToClipboard: vi.fn(async () => true) }))
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (k: string) => k }),
}))
vi.mock('naive-ui', () => ({
NModal: { template: '<div><slot /><slot name="footer" /></div>' },
NButton: { template: '<button @click="$emit(\'click\')"><slot /></button>' },
NSpin: { template: '<span class="spin" />' },
useMessage: () => mockMessage,
}))
import CopilotLoginModal from '@/components/hermes/models/CopilotLoginModal.vue'
function mountModal() {
return mount(CopilotLoginModal)
}
describe('CopilotLoginModal device-flow state machine', () => {
beforeEach(() => {
vi.useFakeTimers()
mockApi.startCopilotLogin.mockReset()
mockApi.pollCopilotLogin.mockReset()
mockMessage.success.mockReset()
mockMessage.warning.mockReset()
mockMessage.error.mockReset()
})
it('启动后进入 waiting 并显示 user_code', async () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-1',
user_code: 'ABCD-1234',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'pending', error: null })
const wrapper = mountModal()
await flushPromises()
expect(wrapper.text()).toContain('ABCD-1234')
expect(mockApi.startCopilotLogin).toHaveBeenCalledTimes(1)
})
it('approved 时 emit success 且消息为 copilotApproved', async () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-2',
user_code: 'WXYZ-9999',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'approved', error: null })
const wrapper = mountModal()
await flushPromises()
// 推动一次 poll timer
await vi.advanceTimersByTimeAsync(3000)
await flushPromises()
expect(mockMessage.success).toHaveBeenCalledWith('models.copilotApproved')
// approved 后 1s 自动关闭
await vi.advanceTimersByTimeAsync(1500)
await flushPromises()
expect(wrapper.emitted('success')).toBeTruthy()
})
it('expired 时进入 expired 状态并显示重试按钮', async () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-3',
user_code: 'EXPI-RED!',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'expired', error: null })
const wrapper = mountModal()
await flushPromises()
await vi.advanceTimersByTimeAsync(3000)
await flushPromises()
expect(wrapper.text()).toContain('models.copilotExpired')
expect(wrapper.emitted('success')).toBeFalsy()
})
it('startCopilotLogin 抛错时显示 error 且不 emit success', async () => {
mockApi.startCopilotLogin.mockRejectedValue(new Error('boom'))
const wrapper = mountModal()
await flushPromises()
expect(mockMessage.error).toHaveBeenCalled()
expect(wrapper.emitted('success')).toBeFalsy()
})
it('denied 时进入 error 状态', async () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-4',
user_code: 'NOPE',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'denied', error: null })
const wrapper = mountModal()
await flushPromises()
await vi.advanceTimersByTimeAsync(3000)
await flushPromises()
expect(wrapper.text()).toContain('models.copilotDenied')
expect(wrapper.emitted('success')).toBeFalsy()
})
})