@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
427 lines (326 loc) • 13.1 kB
text/typescript
import { act, renderHook, waitFor } from '@testing-library/react';
import { major, minor } from 'semver';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { withSWR } from '~test-utils';
import { CURRENT_VERSION } from '@/const/version';
import { globalService } from '@/services/global';
import { useGlobalStore } from '@/store/global/index';
import { initialState } from '@/store/global/initialState';
vi.mock('zustand/traditional');
vi.mock('@/utils/client/switchLang', () => ({
switchLang: vi.fn(),
}));
vi.mock('swr', async (importOriginal) => {
const modules = await importOriginal();
return {
...(modules as any),
mutate: vi.fn(),
};
});
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('createPreferenceSlice', () => {
describe('toggleChatSideBar', () => {
it('should toggle chat sidebar', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.getState().updateSystemStatus({ showChatSideBar: false });
result.current.toggleChatSideBar();
});
expect(result.current.status.showChatSideBar).toBe(true);
});
it('should set chat sidebar to specified value', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.toggleChatSideBar(true);
});
expect(result.current.status.showChatSideBar).toBe(true);
act(() => {
result.current.toggleChatSideBar(false);
});
expect(result.current.status.showChatSideBar).toBe(false);
});
});
describe('toggleExpandSessionGroup', () => {
it('should toggle expand session group', () => {
const { result } = renderHook(() => useGlobalStore());
const groupId = 'group-id';
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.toggleExpandSessionGroup(groupId, true);
});
expect(result.current.status.expandSessionGroupKeys).toContain(groupId);
});
const groupId = 'group-id';
const anotherGroupId = 'another-group-id';
beforeEach(() => {
// 确保每个测试前状态都是已初始化的
useGlobalStore.setState({ isStatusInit: true });
});
it('should add group id when expanding and id not exists', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
result.current.toggleExpandSessionGroup(groupId, true);
});
expect(result.current.status.expandSessionGroupKeys).toEqual(['pinned', 'default', groupId]);
});
it('should not add duplicate group id when expanding', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
// 先添加一个组
result.current.toggleExpandSessionGroup(groupId, true);
// 再次尝试添加同一个组
result.current.toggleExpandSessionGroup(groupId, true);
});
// 确保数组中只有一个实例
expect(result.current.status.expandSessionGroupKeys).toEqual(['pinned', 'default', groupId]);
});
it('should remove group id when collapsing', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
// 先设置初始状态为展开
result.current.toggleExpandSessionGroup(groupId, true);
result.current.toggleExpandSessionGroup(anotherGroupId, true);
// 验证初始状态
// 收起第一个组
result.current.toggleExpandSessionGroup(groupId, false);
});
// 验证只移除了指定的组
expect(result.current.status.expandSessionGroupKeys).toEqual([
'pinned',
'default',
anotherGroupId,
]);
});
it('should do nothing when collapsing non-existent group', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
// 先添加一个组
result.current.toggleExpandSessionGroup(groupId, true);
// 尝试收起一个不存在的组
result.current.toggleExpandSessionGroup('non-existent-id', false);
});
// 验证原有的组没有受影响
expect(result.current.status.expandSessionGroupKeys).toEqual(['pinned', 'default', groupId]);
});
it('should handle multiple groups correctly', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
// 添加多个组
result.current.toggleExpandSessionGroup(groupId, true);
result.current.toggleExpandSessionGroup(anotherGroupId, true);
result.current.toggleExpandSessionGroup('third-group', true);
});
expect(result.current.status.expandSessionGroupKeys).toEqual([
'pinned',
'default',
groupId,
anotherGroupId,
'third-group',
]);
act(() => {
// 收起中间的组
result.current.toggleExpandSessionGroup(anotherGroupId, false);
});
expect(result.current.status.expandSessionGroupKeys).toEqual([
'pinned',
'default',
groupId,
'third-group',
]);
});
it('should save to localStorage when groups are toggled', () => {
const { result } = renderHook(() => useGlobalStore());
const saveToLocalStorageSpy = vi.spyOn(result.current.statusStorage, 'saveToLocalStorage');
act(() => {
result.current.toggleExpandSessionGroup(groupId, true);
});
expect(saveToLocalStorageSpy).toHaveBeenCalledWith(
expect.objectContaining({
expandSessionGroupKeys: ['pinned', 'default', groupId],
}),
);
});
});
describe('toggleMobileTopic', () => {
it('should toggle mobile topic', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.toggleMobileTopic();
});
expect(result.current.status.mobileShowTopic).toBe(true);
});
});
describe('toggleMobilePortal', () => {
it('should toggle mobile topic', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.toggleMobilePortal();
});
expect(result.current.status.mobileShowPortal).toBe(true);
});
});
describe('toggleZenMode', () => {
it('should toggle zen mode', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.setState({ isStatusInit: true });
// 初始值应该是 false
expect(result.current.status.zenMode).toBe(false);
result.current.toggleZenMode();
});
expect(result.current.status.zenMode).toBe(true);
act(() => {
result.current.toggleZenMode();
});
expect(result.current.status.zenMode).toBe(false);
});
});
describe('toggleSystemRole', () => {
it('should toggle system role', () => {
const { result } = renderHook(() => useGlobalStore());
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.toggleSystemRole(true);
});
expect(result.current.status.showSystemRole).toBe(true);
});
});
describe('updatePreference', () => {
it('should update status', () => {
const { result } = renderHook(() => useGlobalStore());
const status = { inputHeight: 200 };
act(() => {
result.current.updateSystemStatus(status);
});
expect(result.current.status.inputHeight).toEqual(200);
});
});
describe('switchBackToChat', () => {
it('should switch back to chat', () => {
const { result } = renderHook(() => useGlobalStore());
const sessionId = 'session-id';
const router = { push: vi.fn() } as any;
act(() => {
useGlobalStore.setState({ router });
result.current.switchBackToChat(sessionId);
});
expect(router.push).toHaveBeenCalledWith('/chat?session=session-id');
});
});
describe('useCheckLatestVersion', () => {
it('should set hasNewVersion to false if there is no new version', async () => {
const latestVersion = '0.0.1';
vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toBe(latestVersion);
});
expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
expect(useGlobalStore.getState().latestVersion).toBeUndefined();
});
it('should set hasNewVersion to true if there is a new version', async () => {
const latestVersion = '10000000.0.0';
vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toBe(latestVersion);
});
expect(useGlobalStore.getState().hasNewVersion).toBe(true);
expect(useGlobalStore.getState().latestVersion).toBe(latestVersion);
});
it('should set hasNewVersion to false if the version is same minor', async () => {
const latestVersion = `${major(CURRENT_VERSION)}.${minor(CURRENT_VERSION)}.9999999`;
vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toBe(latestVersion);
});
expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
expect(useGlobalStore.getState().latestVersion).toBeUndefined();
});
it('should set hasNewVersion to true if there is a minor version', async () => {
const latestVersion = `${major(CURRENT_VERSION)}.${minor(CURRENT_VERSION) + 10}.0`;
vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toBe(latestVersion);
});
expect(useGlobalStore.getState().hasNewVersion).toBe(true);
expect(useGlobalStore.getState().latestVersion).toBe(latestVersion);
});
it('should handle invalid latest version', async () => {
const latestVersion = 'invalid.version';
vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toBe(latestVersion);
});
expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
expect(useGlobalStore.getState().latestVersion).toBeUndefined();
});
it('should not fetch version when check is disabled', () => {
const getLatestVersionSpy = vi.spyOn(globalService, 'getLatestVersion');
renderHook(() => useGlobalStore().useCheckLatestVersion(false), {
wrapper: withSWR,
});
expect(getLatestVersionSpy).not.toHaveBeenCalled();
});
});
describe('useInitGlobalPreference', () => {
it('should init global status if there is empty object', async () => {
vi.spyOn(useGlobalStore.getState().statusStorage, 'getFromLocalStorage').mockReturnValueOnce(
{} as any,
);
const { result } = renderHook(() => useGlobalStore().useInitSystemStatus(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(result.current.data).toEqual({});
});
expect(useGlobalStore.getState().status).toEqual(initialState.status);
});
it('should update with data', async () => {
const { result } = renderHook(() => useGlobalStore());
vi.spyOn(useGlobalStore.getState().statusStorage, 'getFromLocalStorage').mockReturnValueOnce({
inputHeight: 300,
} as any);
const { result: hooks } = renderHook(() => result.current.useInitSystemStatus(), {
wrapper: withSWR,
});
await waitFor(() => {
expect(hooks.current.data).toEqual({ inputHeight: 300 });
});
expect(result.current.status.inputHeight).toEqual(300);
});
});
describe('switchThemeMode', () => {
it('should switch theme mode', async () => {
const { result } = renderHook(() => useGlobalStore());
// Perform the action
act(() => {
useGlobalStore.setState({ isStatusInit: true });
result.current.switchThemeMode('light');
});
// Assert that updateUserSettings was called with the correct theme mode
expect(result.current.status.themeMode).toEqual('light');
});
});
});