diff --git a/src/components/App/SideBar/Latest/__test__/index.tsx b/src/components/App/SideBar/Latest/__test__/index.tsx new file mode 100644 index 000000000..2f25b35ea --- /dev/null +++ b/src/components/App/SideBar/Latest/__test__/index.tsx @@ -0,0 +1,70 @@ +import { fireEvent, render, waitFor } from '@testing-library/react' +import * as React from 'react' +import { LatestView } from '..' +import { useDataStore } from '../../../../../stores/useDataStore' +import { useUserStore } from '../../../../../stores/useUserStore' + +const mockedUseDataStore = useDataStore as jest.MockedFunction +const mockedUseUserStore = useUserStore as jest.MockedFunction + +jest.mock('~/stores/useDataStore') +jest.mock('~/stores/useUserStore') + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}) + +describe('LatestView Component', () => { + beforeEach(() => { + jest.clearAllMocks() + mockedUseDataStore.mockReturnValue([jest.fn()]) + mockedUseUserStore.mockReturnValue([0, jest.fn(), jest.fn()]) + }) + + test('renders button correctly when new data added', () => { + const { getByText } = render() + const galleryIcon = document.querySelector('.heading__icon') as Node + + expect(getByText('Latest')).toBeInTheDocument() + expect(galleryIcon).toBeInTheDocument() + }) + + test('does not show the latest button when there are no nodes', () => { + mockedUseUserStore.mockReturnValue([0, jest.fn(), jest.fn()]) + + const { queryByText } = render() + + expect(queryByText('See Latest (0)')).toBeNull() + }) + + test('calls latest endpoint with param on button click', async () => { + const fetchDataMock = jest.fn() + + mockedUseDataStore.mockReturnValue([fetchDataMock]) + + const setNodeCountMock = jest.fn() + + const setBudgetMock = jest.fn() + + mockedUseUserStore.mockReturnValue([5, setNodeCountMock, setBudgetMock]) + + const { getByText } = render() + + fireEvent.click(getByText('See Latest (5)')) + + await waitFor(() => { + expect(fetchDataMock).toHaveBeenCalledWith(setBudgetMock, { skip_cache: 'true' }) + expect(setNodeCountMock).toHaveBeenCalledWith('CLEAR') + }) + }) +}) diff --git a/src/components/App/SideBar/Latest/index.tsx b/src/components/App/SideBar/Latest/index.tsx index 6d0b811b9..b7ccb4910 100644 --- a/src/components/App/SideBar/Latest/index.tsx +++ b/src/components/App/SideBar/Latest/index.tsx @@ -1,9 +1,9 @@ import { Button } from '@mui/material' import { memo } from 'react' import styled from 'styled-components' -import { Flex } from '~/components/common/Flex' import BrowseGalleryIcon from '~/components/Icons/BrowseGalleryIcon' import DownloadIcon from '~/components/Icons/DownloadIcon' +import { Flex } from '~/components/common/Flex' import { useDataStore } from '~/stores/useDataStore' import { useUserStore } from '~/stores/useUserStore' import { colors } from '~/utils/colors' @@ -16,14 +16,14 @@ type Props = { // eslint-disable-next-line no-underscore-dangle const _View = ({ isSearchResult }: Props) => { const [nodeCount, setNodeCount, setBudget] = useUserStore((s) => [s.nodeCount, s.setNodeCount, s.setBudget]) - const [fetchData] = [useDataStore((s) => s.fetchData)] + const [fetchData] = useDataStore((s) => [s.fetchData]) const getLatest = async () => { if (nodeCount < 1) { return } - await fetchData(setBudget) + await fetchData(setBudget, { skip_cache: 'true' }) setNodeCount('CLEAR') } diff --git a/src/components/App/SideBar/Relevance/index.tsx b/src/components/App/SideBar/Relevance/index.tsx index 643b9f2e3..f45c79577 100644 --- a/src/components/App/SideBar/Relevance/index.tsx +++ b/src/components/App/SideBar/Relevance/index.tsx @@ -22,8 +22,7 @@ export const Relevance = ({ isSearchResult }: Props) => { const [setSelectedNode, setSelectedTimestamp] = useDataStore((s) => [s.setSelectedNode, s.setSelectedTimestamp]) - const [setSidebarOpen] = useAppStore((s) => [s.setSidebarOpen]) - const setRelevanceSelected = useAppStore((s) => s.setRelevanceSelected) + const [setSidebarOpen, setRelevanceSelected] = useAppStore((s) => [s.setSidebarOpen, s.setRelevanceSelected]) const [currentPage, setCurrentPage] = useState(0) @@ -32,12 +31,12 @@ export const Relevance = ({ isSearchResult }: Props) => { const startSlice = currentPage * pageSize const endSlice = startSlice + pageSize - const hasNext = filteredNodes.length - 1 > endSlice + const hasNext = filteredNodes && filteredNodes.length > 0 ? filteredNodes.length - 1 > endSlice : false const isMobile = useIsMatchBreakpoint('sm', 'down') const currentNodes = useMemo( - () => [...filteredNodes].sort((a, b) => (b.date || 0) - (a.date || 0)).slice(0, endSlice), + () => filteredNodes && [...filteredNodes].sort((a, b) => (b.date || 0) - (a.date || 0)).slice(0, endSlice), [filteredNodes, endSlice], ) @@ -55,7 +54,7 @@ export const Relevance = ({ isSearchResult }: Props) => { return ( <> - {currentNodes.map((n, index) => { + {(currentNodes ?? []).map((n, index) => { const { image_url: imageUrl, date, diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 28ba6b531..4913d5c67 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -90,7 +90,7 @@ export const App = () => { }) const runSearch = useCallback(async () => { - await fetchData(setBudget, searchTerm) + await fetchData(setBudget, { word: searchTerm ?? '' }) setSidebarOpen(true) if (searchTerm) { diff --git a/src/network/fetchGraphData/index.ts b/src/network/fetchGraphData/index.ts index ca7d26f89..50dc5ed4c 100644 --- a/src/network/fetchGraphData/index.ts +++ b/src/network/fetchGraphData/index.ts @@ -9,6 +9,7 @@ import { } from '~/constants' import { mock } from '~/mocks/getMockGraphData/mockResponse' import { api } from '~/network/api' +import { FetchNodeParams } from '~/stores/useDataStore' import { FetchDataResponse, FetchSentimentResponse, @@ -60,21 +61,29 @@ const shouldIncludeTopics = true const maxScale = 26 export const fetchGraphData = async ( - search: string, graphStyle: 'split' | 'force' | 'sphere' | 'earth', setBudget: (value: number | null) => void, + params: FetchNodeParams, ) => { try { - return getGraphData(search, graphStyle, setBudget) + return getGraphData(graphStyle, setBudget, params) } catch (e) { return defaultData } } -const fetchNodes = async (search: string, setBudget: (value: number | null) => void): Promise => { - if (!search) { +const fetchNodes = async ( + setBudget: (value: number | null) => void, + params: FetchNodeParams, +): Promise => { + const args = new URLSearchParams({ + ...(isDevelopment || isE2E ? { free: 'true' } : {}), + ...params, + }).toString() + + if (!params.word) { try { - const response = await api.get(`/prediction/content/latest`) + const response = await api.get(`/prediction/content/latest?${args}`) return response } catch (e) { @@ -85,7 +94,7 @@ const fetchNodes = async (search: string, setBudget: (value: number | null) => v } if (isDevelopment || isE2E) { - const response = await api.get(`/v2/search?word=${search}&free=true`) + const response = await api.get(`/v2/search?${args}`) return response } @@ -93,7 +102,7 @@ const fetchNodes = async (search: string, setBudget: (value: number | null) => v const lsatToken = await getLSat() try { - const response = await api.get(`/v2/search?word=${search}`, { + const response = await api.get(`/v2/search?${args}`, { Authorization: lsatToken, }) @@ -103,7 +112,7 @@ const fetchNodes = async (search: string, setBudget: (value: number | null) => v if (error.status === 402) { await payLsat(setBudget) - return fetchNodes(search, setBudget) + return fetchNodes(setBudget, params) } throw error @@ -334,14 +343,14 @@ const generateGuestsMap = ( } export const getGraphData = async ( - searchterm: string, graphStyle: 'split' | 'force' | 'sphere' | 'earth', setBudget: (value: number | null) => void, + params: FetchNodeParams, ) => { try { - const dataInit = await fetchNodes(searchterm, setBudget) + const dataInit = await fetchNodes(setBudget, params) - return formatFetchNodes(dataInit, searchterm, graphStyle) + return formatFetchNodes(dataInit, params?.word || '', graphStyle) } catch (e) { console.error(e) diff --git a/src/stores/useDataStore/index.ts b/src/stores/useDataStore/index.ts index 66442a9ec..ccbe097a9 100644 --- a/src/stores/useDataStore/index.ts +++ b/src/stores/useDataStore/index.ts @@ -9,7 +9,13 @@ export type GraphStyle = 'sphere' | 'force' | 'split' | 'earth' export const graphStyles: GraphStyle[] = ['sphere', 'force', 'split', 'earth'] -type DataStore = { +export type FetchNodeParams = { + word?: string + skip_cache?: string + free?: string +} + +export type DataStore = { scrollEventsDisabled: boolean categoryFilter: NodeType | null disableCameraRotation: boolean @@ -41,7 +47,7 @@ type DataStore = { setScrollEventsDisabled: (scrollEventsDisabled: boolean) => void setCategoryFilter: (categoryFilter: NodeType | null) => void setDisableCameraRotation: (rotation: boolean) => void - fetchData: (setBudget: (value: number | null) => void, search?: string | null) => void + fetchData: (setBudget: (value: number | null) => void, params?: FetchNodeParams) => void setData: (data: GraphData) => void setGraphStyle: (graphStyle: GraphStyle) => void setGraphRadius: (graphRadius?: number | null) => void @@ -115,16 +121,16 @@ const defaultData: Omit< export const useDataStore = create((set, get) => ({ ...defaultData, - fetchData: async (setBudget, search) => { + fetchData: async (setBudget, params) => { if (get().isFetching) { return } set({ isFetching: true, sphinxModalIsOpen: true }) - const data = await fetchGraphData(search || '', get().graphStyle, setBudget) + const data = await fetchGraphData(get().graphStyle, setBudget, params ?? {}) - if (search) { + if (params?.word) { await saveSearchTerm() }