240 lines
6.4 KiB
TypeScript
240 lines
6.4 KiB
TypeScript
/* @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' });
|
||
});
|
||
});
|