diff --git a/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.spec.ts b/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.spec.ts new file mode 100644 index 0000000000..25ac9d25db --- /dev/null +++ b/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.spec.ts @@ -0,0 +1,71 @@ +import { renderHook, act } from '@testing-library/react-hooks' +import { Pages } from 'uiSrc/constants' +import useStartWizard from './useStartWizard' + +describe('useStartWizard', () => { + let mockPush: jest.Mock + let useHistoryMock: jest.SpyInstance + let useParamsMock: jest.SpyInstance + + beforeEach(() => { + jest.clearAllMocks() + mockPush = jest.fn() + + useHistoryMock = jest.spyOn(require('react-router-dom'), 'useHistory') + useHistoryMock.mockImplementation(() => ({ + push: mockPush, + })) + + useParamsMock = jest.spyOn(require('react-router-dom'), 'useParams') + useParamsMock.mockImplementation(() => ({ + instanceId: 'test-instance-id', + })) + }) + + afterEach(() => { + useHistoryMock.mockRestore() + useParamsMock.mockRestore() + }) + + it('should navigate to vector search create index page when start is called', () => { + const { result } = renderHook(() => useStartWizard()) + + act(() => { + result.current() + }) + + expect(mockPush).toHaveBeenCalledWith( + Pages.vectorSearchCreateIndex('test-instance-id'), + ) + expect(mockPush).toHaveBeenCalledTimes(1) + }) + + it('should use instanceId from useParams in navigation', () => { + const customInstanceId = 'custom-instance-123' + useParamsMock.mockImplementation(() => ({ instanceId: customInstanceId })) + + const { result } = renderHook(() => useStartWizard()) + + act(() => { + result.current() + }) + + expect(mockPush).toHaveBeenCalledWith( + Pages.vectorSearchCreateIndex(customInstanceId), + ) + }) + + it('should handle missing instanceId gracefully', () => { + useParamsMock.mockImplementation(() => ({ instanceId: undefined })) + + const { result } = renderHook(() => useStartWizard()) + + act(() => { + result.current() + }) + + expect(mockPush).toHaveBeenCalledWith( + Pages.vectorSearchCreateIndex(undefined as any), + ) + }) +}) diff --git a/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.ts b/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.ts new file mode 100644 index 0000000000..44e572fae9 --- /dev/null +++ b/redisinsight/ui/src/pages/vector-search/hooks/useStartWizard.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react' +import { useHistory, useParams } from 'react-router-dom' +import { Pages } from 'uiSrc/constants' + +const useStartWizard = () => { + const history = useHistory() + const { instanceId } = useParams<{ instanceId: string }>() + + return useCallback(() => { + history.push(Pages.vectorSearchCreateIndex(instanceId)) + }, [history, instanceId]) +} + +export default useStartWizard diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.spec.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.spec.tsx index 85794c08d1..06cc38aadf 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.spec.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.spec.tsx @@ -32,18 +32,21 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ const renderComponent = () => render() +const mockedRedisearchListSelector = redisearchListSelector as jest.Mock +const mockedConnectedInstanceSelector = connectedInstanceSelector as jest.Mock + describe('ManageIndexesList', () => { beforeEach(() => { cleanup() jest.clearAllMocks() // Reset mocks to default state before each test - ;(redisearchListSelector as jest.Mock).mockReturnValue({ + mockedRedisearchListSelector.mockReturnValue({ data: [], loading: false, error: '', }) - ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + mockedConnectedInstanceSelector.mockReturnValue({ id: 'test-instance-123', connectionType: 'STANDALONE', host: 'localhost', @@ -62,7 +65,7 @@ describe('ManageIndexesList', () => { }) it('should render Loader spinner while fetching data', async () => { - ;(redisearchListSelector as jest.Mock).mockReturnValue({ + mockedRedisearchListSelector.mockReturnValue({ data: [], loading: true, error: '', @@ -74,13 +77,28 @@ describe('ManageIndexesList', () => { expect(loader).toBeInTheDocument() }) + it('should render no indexes message when there are no indexes', async () => { + mockedRedisearchListSelector.mockReturnValue({ + data: [], + loading: false, + error: '', + }) + + renderComponent() + + const noIndexesMessage = await screen.getByText( + 'No indexes to display yet.', + ) + expect(noIndexesMessage).toBeInTheDocument() + }) + it('should render indexes boxes when data is available', () => { const mockIndexes = [ Buffer.from('test-index-1'), Buffer.from('test-index-2'), ] - ;(redisearchListSelector as jest.Mock).mockReturnValue({ + mockedRedisearchListSelector.mockReturnValue({ data: mockIndexes, loading: false, error: '', @@ -109,7 +127,7 @@ describe('ManageIndexesList', () => { it('should not render indexes boxes when there is no instanceHost', () => { // Mock connectedInstanceSelector to return no host // TODO: Potential candidate for a factory function to create mock instances - ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + mockedConnectedInstanceSelector.mockReturnValue({ id: 'test-instance-123', connectionType: 'STANDALONE', host: null, // No host means no instanceId @@ -136,7 +154,7 @@ describe('ManageIndexesList', () => { it('should not render indexes boxes when redisearch module is not available', () => { // Mock connectedInstanceSelector to return modules without redisearch // TODO: Potential candidate for a factory function to create mock instances - ;(connectedInstanceSelector as jest.Mock).mockReturnValue({ + mockedConnectedInstanceSelector.mockReturnValue({ id: 'test-instance-123', connectionType: 'STANDALONE', host: 'localhost', diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.tsx index 13038a4185..15d568d7f7 100644 --- a/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.tsx +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesList.tsx @@ -5,14 +5,18 @@ import { bufferToString } from 'uiSrc/utils' import { StyledManageIndexesListAction } from './ManageIndexesList.styles' import { IndexSection } from './IndexSection' import { useRedisearchListData } from '../useRedisearchListData' +import NoIndexesMessage from './NoIndexesMessage' export const ManageIndexesList = () => { const { data, loading } = useRedisearchListData() + const hasIndexes = !!data?.length return ( {loading && } + {!loading && !hasIndexes && } + {data.map((index) => ( ))} diff --git a/redisinsight/ui/src/pages/vector-search/manage-indexes/NoIndexesMessage.tsx b/redisinsight/ui/src/pages/vector-search/manage-indexes/NoIndexesMessage.tsx new file mode 100644 index 0000000000..d73a404fd5 --- /dev/null +++ b/redisinsight/ui/src/pages/vector-search/manage-indexes/NoIndexesMessage.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Button } from 'uiSrc/components/base/forms/buttons' +import { Col } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' +import useStartWizard from '../hooks/useStartWizard' + +const NoIndexesMessage = () => { + const start = useStartWizard() + + return ( + + No indexes to display yet. + + + Complete vector search onboarding to create your first index with sample + data, or create one manually. + + + + + ) +} + +export default NoIndexesMessage diff --git a/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.spec.tsx b/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.spec.tsx index 5804bb92d2..05183b52fc 100644 --- a/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.spec.tsx +++ b/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.spec.tsx @@ -1,10 +1,15 @@ import React from 'react' import { cleanup, render, screen, userEvent } from 'uiSrc/utils/test-utils' -import { Pages } from 'uiSrc/constants' - import { StartWizardButton } from './StartWizardButton' +import useStartWizard from '../hooks/useStartWizard' + +jest.mock('../hooks/useStartWizard', () => ({ + __esModule: true, + default: jest.fn(), +})) const renderComponent = () => render() +const mockedUseStartWizard = useStartWizard as jest.Mock describe('StartWizardButton', () => { beforeEach(() => { @@ -12,38 +17,22 @@ describe('StartWizardButton', () => { jest.clearAllMocks() }) - it('should navigate to vector search create index page when "Get started" is clicked', async () => { - const mockPush = jest.fn() - - const useHistoryMock = jest.spyOn(require('react-router-dom'), 'useHistory') - useHistoryMock.mockImplementation(() => ({ - push: mockPush, - })) + it('renders the CTA and calls the start function on click', async () => { + const startMock = jest.fn() + mockedUseStartWizard.mockReturnValue(startMock) renderComponent() - const getStartedButton = screen.getByText('Get started') - await userEvent.click(getStartedButton) - - expect(mockPush).toHaveBeenCalledWith( - Pages.vectorSearchCreateIndex('instanceId'), - ) - expect(mockPush).toHaveBeenCalledTimes(1) - - useHistoryMock.mockRestore() - }) - - it('should maintain callback reference stability with useCallback', () => { - const { rerender } = renderComponent() - - const firstRenderButton = screen.getByText('Get started') - - rerender() + expect( + screen.getByText( + 'Power fast, real-time semantic AI search with vector search.', + ), + ).toBeInTheDocument() - const secondRenderButton = screen.getByText('Get started') + const btn = screen.getByRole('button', { name: /get started/i }) + expect(btn).toBeInTheDocument() - // Both buttons should exist (they're the same element after rerender) - expect(firstRenderButton).toBeInTheDocument() - expect(secondRenderButton).toBeInTheDocument() + await userEvent.click(btn) + expect(startMock).toHaveBeenCalledTimes(1) }) }) diff --git a/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.tsx b/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.tsx index d787c8d172..ea8501587f 100644 --- a/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.tsx +++ b/redisinsight/ui/src/pages/vector-search/query/StartWizardButton.tsx @@ -1,15 +1,9 @@ -import React, { useCallback } from 'react' -import { useHistory, useParams } from 'react-router-dom' +import React from 'react' import { CallOut } from 'uiSrc/components/base/display/call-out/CallOut' -import { Pages } from 'uiSrc/constants' +import useStartWizard from '../hooks/useStartWizard' export const StartWizardButton = () => { - const history = useHistory() - const { instanceId } = useParams<{ instanceId: string }>() - - const startCreateIndexWizard = useCallback(() => { - history.push(Pages.vectorSearchCreateIndex(instanceId)) - }, [history, instanceId]) + const start = useStartWizard() return ( { actions={{ primary: { label: 'Get started', - onClick: startCreateIndexWizard, + onClick: start, }, }} >