| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- import { flushPromises, mount } from '@vue/test-utils'
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
- import QRCode from 'qrcode'
- import NodePairingPanel from '@/components/app/NodePairingPanel.vue'
- import { accountPairingStatus, createAccountPairing } from '@/api/accountPairing'
- import {
- createPairing,
- pairChildNode,
- pairedDevices,
- pairingStatus,
- revokePairedDevice,
- type NodeScope,
- } from '@/api/pairing'
- vi.mock('qrcode', () => ({
- default: {
- toDataURL: vi.fn(async () => 'data:image/png;base64,cXItY29kZQ=='),
- },
- }))
- vi.mock('@/api/accountPairing', () => ({
- accountPairingStatus: vi.fn(),
- createAccountPairing: vi.fn(),
- }))
- vi.mock('@/api/pairing', () => ({
- createPairing: vi.fn(),
- pairChildNode: vi.fn(),
- pairedDevices: vi.fn(),
- pairingStatus: vi.fn(),
- revokePairedDevice: vi.fn(),
- }))
- const allScopes: NodeScope[] = [
- 'agents.read',
- 'history.read',
- 'sessions.execute',
- 'groups.read',
- 'groups.execute',
- ]
- beforeEach(() => {
- vi.useFakeTimers()
- vi.setSystemTime(new Date('2026-08-29T04:00:00.000Z'))
- vi.mocked(createAccountPairing).mockResolvedValue({
- protocolVersion: 1,
- serviceType: 'hermes-yaoyao',
- pairingId: 'account-pairing-1',
- expiresAt: Date.now() + 60_000,
- qrPayload: 'yaoyao://login?v=1&id=account-pairing-1',
- })
- vi.mocked(accountPairingStatus).mockResolvedValue({ state: 'pending' })
- vi.mocked(pairedDevices).mockResolvedValue({
- nodeId: 'node-12345678',
- fingerprint: 'fingerprint-1',
- devices: [],
- })
- vi.mocked(createPairing).mockResolvedValue({
- protocolVersion: 1,
- pairingId: 'node-pairing-1',
- nodeId: 'node-12345678',
- fingerprint: 'fingerprint-1',
- scopes: [...allScopes],
- expiresAt: Date.now() + 60_000,
- qrPayload: 'yaoyao://pair?v=1&id=node-pairing-1',
- })
- vi.mocked(pairingStatus).mockResolvedValue({ state: 'pending' })
- vi.mocked(pairChildNode).mockResolvedValue(undefined)
- vi.mocked(revokePairedDevice).mockResolvedValue(undefined)
- })
- afterEach(() => {
- document.body.innerHTML = ''
- vi.clearAllTimers()
- vi.useRealTimers()
- vi.clearAllMocks()
- vi.restoreAllMocks()
- vi.unstubAllGlobals()
- Object.defineProperty(navigator, 'mediaDevices', { configurable: true, value: undefined })
- })
- describe('NodePairingPanel', () => {
- it('account 模式只创建账号登录二维码,并在停用时清理轮询', async () => {
- const wrapper = mount(NodePairingPanel, {
- attachTo: document.body,
- props: { active: true, mode: 'account', isAdmin: true, userName: 'admin' },
- global: { stubs: { AppIcon: true } },
- })
- await flushPromises()
- expect(createAccountPairing).toHaveBeenCalledOnce()
- expect(QRCode.toDataURL).toHaveBeenCalledWith(
- 'yaoyao://login?v=1&id=account-pairing-1',
- expect.objectContaining({ width: 296, margin: 2, errorCorrectionLevel: 'M' }),
- )
- expect(pairedDevices).not.toHaveBeenCalled()
- expect(createPairing).not.toHaveBeenCalled()
- expect(wrapper.text()).toContain('在 iOS 登录页选择扫码')
- expect(vi.getTimerCount()).toBe(1)
- await wrapper.setProps({ active: false })
- await flushPromises()
- expect(vi.getTimerCount()).toBe(0)
- wrapper.unmount()
- })
- it('nodes 模式先只加载设备,生成二维码时传递完整授权范围,并在卸载时清理轮询', async () => {
- const wrapper = mount(NodePairingPanel, {
- attachTo: document.body,
- props: { active: true, mode: 'nodes', isAdmin: true },
- global: { stubs: { AppIcon: true } },
- })
- await flushPromises()
- expect(pairedDevices).toHaveBeenCalledOnce()
- expect(createAccountPairing).not.toHaveBeenCalled()
- expect(createPairing).not.toHaveBeenCalled()
- expect(QRCode.toDataURL).not.toHaveBeenCalled()
- expect(wrapper.text()).toContain('还没有已授权设备')
- expect(vi.getTimerCount()).toBe(0)
- await wrapper.get('button.create-pairing').trigger('click')
- await flushPromises()
- expect(createPairing).toHaveBeenCalledWith(allScopes)
- expect(QRCode.toDataURL).toHaveBeenCalledWith(
- 'yaoyao://pair?v=1&id=node-pairing-1',
- expect.objectContaining({ width: 296, margin: 2, errorCorrectionLevel: 'M' }),
- )
- expect(vi.getTimerCount()).toBe(1)
- wrapper.unmount()
- expect(vi.getTimerCount()).toBe(0)
- })
- it('stops a camera stream whose startup becomes stale before video playback completes', async () => {
- let resolvePlay: (() => void) | undefined
- const play = new Promise<void>(resolve => { resolvePlay = resolve })
- const track = { stop: vi.fn() }
- const getUserMedia = vi.fn(async () => ({ getTracks: () => [track] }) as unknown as MediaStream)
- Object.defineProperty(navigator, 'mediaDevices', { configurable: true, value: { getUserMedia } })
- vi.stubGlobal('BarcodeDetector', class {
- detect = vi.fn(async () => [])
- })
- const playSpy = vi.spyOn(HTMLMediaElement.prototype, 'play').mockReturnValue(play)
- const wrapper = mount(NodePairingPanel, {
- attachTo: document.body,
- props: { active: true, mode: 'nodes', isAdmin: true },
- global: { stubs: { AppIcon: true } },
- })
- await flushPromises()
- const scan = wrapper.findAll<HTMLButtonElement>('button').find(button => button.text().includes('使用摄像头扫码'))!
- await scan.trigger('click')
- await flushPromises()
- await vi.advanceTimersByTimeAsync(0)
- await flushPromises()
- expect(getUserMedia).toHaveBeenCalledOnce()
- expect(playSpy).toHaveBeenCalledOnce()
- const stop = wrapper.findAll<HTMLButtonElement>('button').find(button => button.text().includes('停止扫码'))!
- await stop.trigger('click')
- expect(track.stop).toHaveBeenCalledOnce()
- resolvePlay?.()
- await flushPromises()
- expect(vi.getTimerCount()).toBe(0)
- expect(wrapper.text()).not.toContain('停止扫码')
- wrapper.unmount()
- })
- })
|