/* @vitest-environment happy-dom */ /* eslint-disable vue/one-component-per-file, vue/require-default-prop */ import { flushPromises, mount } from '@vue/test-utils'; import { defineComponent, h } from 'vue'; import QqBotAccountNapcatWebui from '@test-source/apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createQqbotNapcatWebuiSession, heartbeatQqbotNapcatWebuiSession, revokeQqbotNapcatWebuiSession, } from '#/api/qqbot/napcat'; const testState = vi.hoisted(() => ({ intervalControls: { callback: undefined as (() => unknown) | undefined, pause: vi.fn(), resume: vi.fn(), }, pushSpy: vi.fn(), route: { params: { accountId: 'account-1', } as Record, }, })); const { intervalControls, pushSpy, route } = testState; vi.mock('vue-router', () => ({ useRoute: () => route, useRouter: () => ({ push: pushSpy, }), })); vi.mock('@vben/common-ui', () => ({ Page: defineComponent({ name: 'MockPage', setup(_, { slots }) { return () => h('main', slots.default?.()); }, }), })); vi.mock('@vben/icons', () => ({ ArrowLeft: defineComponent({ name: 'MockArrowLeft', setup() { return () => h('i', { 'data-testid': 'arrow-left' }); }, }), })); vi.mock('antdv-next', () => ({ Alert: defineComponent({ name: 'MockAlert', props: { message: String, type: String, }, setup(props) { return () => h('div', { role: 'alert' }, [props.message, props.type as string]); }, }), Button: defineComponent({ name: 'MockButton', props: { danger: Boolean, disabled: Boolean, type: String, }, emits: ['click'], setup(props, { emit, slots }) { return () => h( 'button', { disabled: props.disabled, type: 'button', onClick: () => emit('click'), }, slots.default?.(), ); }, }), Space: defineComponent({ name: 'MockSpace', setup(_, { slots }) { return () => h('div', slots.default?.()); }, }), Spin: defineComponent({ name: 'MockSpin', props: { spinning: Boolean, }, setup(props, { slots }) { return () => h( 'div', { 'data-spinning': String(props.spinning) }, slots.default?.(), ); }, }), Tag: defineComponent({ name: 'MockTag', props: { color: String, }, setup(_, { slots }) { return () => h('span', slots.default?.()); }, }), })); vi.mock('@vueuse/core', () => ({ useIntervalFn: vi.fn((callback: () => unknown) => { intervalControls.callback = callback; return { pause: intervalControls.pause, resume: intervalControls.resume, }; }), })); vi.mock('#/api/qqbot/napcat', () => ({ createQqbotNapcatWebuiSession: vi.fn(), heartbeatQqbotNapcatWebuiSession: vi.fn(), revokeQqbotNapcatWebuiSession: vi.fn(), })); function createSessionFixture(overrides = {}) { return { account: { id: 'account-1', name: '主账号', selfId: '10001', }, container: { webuiStatus: 'online' as const, }, expiresAt: new Date('2026-06-24T15:30:00+08:00').getTime(), iframeUrl: '/qqbot/napcat/webui/session/session-1/', sessionId: 'session-1', ...overrides, }; } describe('qqbot account NapCat WebUI page', () => { beforeEach(() => { route.params.accountId = 'account-1'; pushSpy.mockReset(); intervalControls.callback = undefined; intervalControls.pause.mockReset(); intervalControls.resume.mockReset(); vi.clearAllMocks(); vi.mocked(createQqbotNapcatWebuiSession).mockResolvedValue( createSessionFixture(), ); vi.mocked(heartbeatQqbotNapcatWebuiSession).mockResolvedValue({ expiresAt: new Date('2026-06-24T15:40:00+08:00').getTime(), sessionId: 'session-1', status: 'active', }); vi.mocked(revokeQqbotNapcatWebuiSession).mockResolvedValue({ sessionId: 'session-1', status: 'revoked', }); }); it('creates a session for the route account and renders the gateway iframe', async () => { const wrapper = mount(QqBotAccountNapcatWebui); await flushPromises(); expect(createQqbotNapcatWebuiSession).toHaveBeenCalledWith({ accountId: 'account-1', }); expect(intervalControls.resume).toHaveBeenCalledTimes(1); expect(wrapper.find('iframe').attributes('src')).toBe( '/qqbot/napcat/webui/session/session-1/', ); expect(wrapper.find('.qqbot-napcat-webui__header').exists()).toBe(false); expect(wrapper.find('.qqbot-napcat-webui__meta').exists()).toBe(false); expect(wrapper.find('.qqbot-napcat-webui__floating-card').exists()).toBe( true, ); expect(wrapper.text()).toContain('主账号(10001)'); expect(wrapper.text()).toContain('NapCat WebUI'); expect(wrapper.text()).not.toContain('kt-qqbot-napcat'); }); it('revokes the active session and pauses heartbeat on unmount', async () => { const wrapper = mount(QqBotAccountNapcatWebui); await flushPromises(); wrapper.unmount(); await flushPromises(); expect(intervalControls.pause).toHaveBeenCalled(); expect(revokeQqbotNapcatWebuiSession).toHaveBeenCalledWith('session-1'); }); it('uses the captured heartbeat callback to extend expiry while ready', async () => { const wrapper = mount(QqBotAccountNapcatWebui); await flushPromises(); await intervalControls.callback?.(); await flushPromises(); expect(heartbeatQqbotNapcatWebuiSession).toHaveBeenCalledWith('session-1'); expect(wrapper.text()).toContain('2026-06-24 15:40:00'); }); it('renders an error state and hides iframe when session creation fails', async () => { vi.mocked(createQqbotNapcatWebuiSession).mockRejectedValueOnce( new Error('Gateway unavailable'), ); const wrapper = mount(QqBotAccountNapcatWebui); await flushPromises(); expect(wrapper.find('iframe').exists()).toBe(false); expect(wrapper.text()).toContain('Gateway unavailable'); expect(intervalControls.resume).not.toHaveBeenCalled(); }); it('routes back to the QQBot account list from the back button', async () => { const wrapper = mount(QqBotAccountNapcatWebui); await flushPromises(); await wrapper.find('button').trigger('click'); expect(pushSpy).toHaveBeenCalledWith({ name: 'QqBotAccount' }); }); });