UNPKG

@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
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'); }); }); });