kt-template-admin/test/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx

240 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* @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<string, unknown>,
},
}));
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' });
});
});