import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Channels } from '@/pages/Channels/index';
const hostApiFetchMock = vi.fn();
const subscribeHostEventMock = vi.fn();
const { gatewayState } = vi.hoisted(() => ({
gatewayState: {
status: { state: 'running', port: 18789 },
},
}));
vi.mock('@/stores/gateway', () => ({
useGatewayStore: (selector: (state: typeof gatewayState) => unknown) => selector(gatewayState),
}));
vi.mock('@/lib/host-api', () => ({
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));
vi.mock('@/lib/host-events', () => ({
subscribeHostEvent: (...args: unknown[]) => subscribeHostEventMock(...args),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
},
}));
describe('Channels page status refresh', () => {
beforeEach(() => {
vi.clearAllMocks();
gatewayState.status = { state: 'running', port: 18789 };
hostApiFetchMock.mockImplementation(async (path: string) => {
if (path === '/api/channels/accounts') {
return {
success: true,
channels: [
{
channelType: 'feishu',
defaultAccountId: 'default',
status: 'connected',
accounts: [
{
accountId: 'default',
name: 'Primary Account',
configured: true,
status: 'connected',
isDefault: true,
},
],
},
],
};
}
if (path === '/api/agents') {
return {
success: true,
agents: [],
};
}
throw new Error(`Unexpected host API path: ${path}`);
});
});
it('refetches channel accounts when gateway channel-status events arrive', async () => {
let channelStatusHandler: (() => void) | undefined;
subscribeHostEventMock.mockImplementation((eventName: string, handler: () => void) => {
if (eventName === 'gateway:channel-status') {
channelStatusHandler = handler;
}
return vi.fn();
});
render();
await waitFor(() => {
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/channels/accounts');
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/agents');
});
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:channel-status', expect.any(Function));
await act(async () => {
channelStatusHandler?.();
});
await waitFor(() => {
const channelFetchCalls = hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/channels/accounts');
const agentFetchCalls = hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agents');
expect(channelFetchCalls).toHaveLength(2);
expect(agentFetchCalls).toHaveLength(2);
});
});
it('refetches when the gateway transitions to running after mount', async () => {
gatewayState.status = { state: 'starting', port: 18789 };
const { rerender } = render();
await waitFor(() => {
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/channels/accounts');
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/agents');
});
gatewayState.status = { state: 'running', port: 18789 };
await act(async () => {
rerender();
});
await waitFor(() => {
const channelFetchCalls = hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/channels/accounts');
const agentFetchCalls = hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agents');
expect(channelFetchCalls).toHaveLength(2);
expect(agentFetchCalls).toHaveLength(2);
});
});
it('treats WeChat accounts as plugin-managed QR accounts', async () => {
subscribeHostEventMock.mockImplementation(() => vi.fn());
hostApiFetchMock.mockImplementation(async (path: string) => {
if (path === '/api/channels/accounts') {
return {
success: true,
channels: [
{
channelType: 'wechat',
defaultAccountId: 'wx-bot-im-bot',
status: 'connected',
accounts: [
{
accountId: 'wx-bot-im-bot',
name: 'WeChat ClawBot',
configured: true,
status: 'connected',
isDefault: true,
},
],
},
],
};
}
if (path === '/api/agents') {
return {
success: true,
agents: [],
};
}
if (path === '/api/channels/wechat/cancel') {
return { success: true };
}
throw new Error(`Unexpected host API path: ${path}`);
});
render();
await waitFor(() => {
expect(screen.getByText('WeChat')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'account.add' }));
await waitFor(() => {
expect(screen.getByText('dialog.configureTitle')).toBeInTheDocument();
});
expect(screen.queryByLabelText('account.customIdLabel')).not.toBeInTheDocument();
});
});