Files
explore/lib/clusters/useClustersData.test.ts
T
tom goriunovGitHubRichard OhMax AlekseenkoisstuevIgor Stuevdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Richard J Oh
60a7d97164 Clusters name sevices integration (#2994)
* adds clusters api configs

* adds clusters api configs

* adds api types

* adds props routes and titles for cluster pages

* adds query param enum for clusters api

* adds clusters svg

* adds dynamic page routes

* adds clusters entity ui in ui/shared/entities

* adds functions to detect input type; will be used to check whether a user is searching for a cluster name vs a evm address

* adds stubs for loading skeleton view

* adds pagination hook

* adds query param hooks for clusters trpc endpoints

* adds query param hooks for clusters trpc endpoints

* adds components that make up the /clusters ui

* adds components for /cluster ui

* adds clusters api endpoint to query by cluster id

* implements call to cluster id endpoint to get ac
tive chain count

* adds clusters search types and in api category

* creates clusters search hook and wraps useQuickSearchQuery hook

* replaces useQuickSearchQuery for useSearchWithClusters hook for both functionality

* add search suggestion ui

* adds optional chaining to handle react re-render when search type changes

* adds cluster ui to ui/pages/address on address details

* prevents clicking on cluster suggestion if address is non EVM

* adds util functions for testability along with tests, for action bar component logic

* refactor - extract clusters display logic wit
h tests

* refactor: extract page utilities from Clusters.tsx with tests

* adds jest testing infra for react components

* adds hook testing coverage

* adds component and integration tests

* adds detectInputType util tests

* removes generic parameters from clusters pagination types

* replaces any type with proper cluster config types

* adds cluster routes to metadata and analytics mappings

* updates order and stacks action bar items vertically when mobile view

* repositions clusters logo

* refactors search logic to render community clusters in search sugge
st

* adding clusters env vars

- add NEXT_PUBLIC_CLUSTERS_API_HOST and NEXT_PUBLIC_CLUSTERS_CDN_URL env vars
- configure clusters api integration in configs/app/apis.ts and features/clusters.ts
- adds clusters env vars to test env (.env.jest and .env.localhost)
- update CI deploy configs for both review envs
- add env vars validation schema
- document new vars in ENVS.md with usage examples

* centers directory/leaderboard toggle in mobile view

* resizes and sets proper color to logo in entity component

* hides clusters nav when clusters env is missing

* removes duplicate trailing slashes in cluster community names

* Bump `react` to v19 (#2782)

Co-authored-by: tom <[email protected]>

* change links to ton explorer

* fix internal links in ENVS.md file

* tx info: tac operation stage could be an array

* fix display of ad banner

* tac ui updates

* add more "button click" events

* update clock icon

* update screenshots

* fix age filter reset

* change tac operation status once again

* fix address icon tooltip content

* [skip ci] label released issues workflow: don't override release tag if it exists

* puzzle-15 (#2471)

* puzzle-15

* puzzle-review

* puzzle game

* puzzle fixes

* separate puzzle feature in readme

* fix testnet and rollup stage badges

* New styles for pagination and "Add / Remove" buttons (#2791)

* update styles for add / remove buttons

* update pagination styles

* change border color for filled inputs

* change row gap for alerts

* update icons and screenshots

* fix tests

* update buttons for contract method form

* update screenshot

* Gas tracker: bug fixes and improvements (#2792)

* Gas tracker page: hide `Gas price history` if chart is not enabled

Fixes #2781

* change default value for NEXT_PUBLIC_GAS_TRACKER_UNITS for testnets

* fix env for pw tests

* Public tags form improvements (#2796)

* adjust validation of comment field

* add icon url to public tag form

* update screenshots

* fix ts

* NFT marketplaces: make `collection_url` and `instance_url ` optional (#2797)

Fixes #2614

* Bump brace-expansion from 1.1.11 to 1.1.12 in /deploy/tools/affected-tests (#2801)

Bump brace-expansion in /deploy/tools/affected-tests

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* allow toaster to render custom description

* Encode the address hash for links to the TON chain (#2813)

Encode the address hash for links to the TON chain.

* Support redirects for known address discrepancies (#2799)

* Support redirects for known address discrepancies

Fixes #2793

* fix redirect to internal txs in the block

* add address user ops redirect

* Celo: support L2 epochs (#2784)

* epochs page

* epoch rewards distribution

* epoch election rewards

* address epoch rewards

* block epoch tag

* add some  tests

* adjust epoch details for L1 epochs

* support non-finalized epochs

* fixes after design review

* add truncation to block numbers on mobile

* update epochs table layout

* update timestamp hint

* fix bug with subheading of first epoch

* fixes of mobile view

* update screenshots

* Icons in footer links (#2809)

* Icons in footer links

Fixes #2539

* fix envs validator

* Metadata: send address to fetchers once visited (#2814)

* Metadata: send address to fetchers once visited

Fixes #2723

* fix ts

* replaces jest-dom matchers with standard matchers

* makes /clusters page default view the directory instead of leaderboard, including jest coverage

* updates columnn name from backing to total backing, in leaderboard display

* alphabetically reorders clusters export

* adds Cluster section to the Table of Cont
ents at top of page

* removes redundant Flex component wrapper around ClustersEntity

* uses standard toolkit values for border radius and color

* uses getFeaturePayload util instead of type coercion

* extends ImageProps interface to ClusterIconProps, and uses getFeaturePayload util instead of type coercion

* adds evm address regexp in regexp.ts as single source of truth

* updates imports for files that used form validator address regexp to use single source of truth

* removes unused args, unnecessary type coercions and memoizations. upper snake case constant values.

* sets correct action bar height using ACTION_BAR_HEIGHT_DESKTOP constant

* implements TimeFormatToggle to Joined column

* simplifies conditionals and removes unnecessary Box component wrapper

* displays standard 404 error screen on  erroneous ClusterDetails query

* standardizes details page to display ETH value and time in line with rest of app

* removes redundant debouncing

* moves useQueryParams hook to lib/router and updates imports and test to account

* moves config.features.cluster
s.isEnabled to top of the function and utilizes select function of useQuery in useAp
iQuery

* removes unnecessary 3rd column and isContractVerified since it is always false

* checks clusters enable for placeholder

* makes isClusterClickable condition more generic for future repurpose

* splits isEvmAddress out to general purpose utility in lib/address, update imports and test

* moves all cluster hooks and tests to lib/clusters folder

* wraps action bar with standard button wrapper

* makes nextjs-routes mock condition stricter

* adds mock clusters api data

* adds pw tests for cluster detail page

* adds pw tests for clusters directory page

* adds pw tests for clustersentity ui component

* adds mock envs for pw tests

* adds clusters api host and cdn url to env.pw

* fixes resolve react-hooks/rules-of-hooks violation in cluster search while keeping search optimization

* throws error instead of using app error component

* uses semantic token to reference color as per standard method in chakra ui

* removes empty or loading state pw tests

* pw tests - uses mockAssetResponse for icons, removes redundant tests, and updates vie
wport to narrow test case

* migrates all clusters pw tests from page.route to mockApiResponse

* updates docker generated pw screenshots

* removes object cloning in useSearchWithClusters hook

* uses standardized chakra name for backgroundColor

* resolves Clusters.pw.tsx docker screen generation issues and updates screenshots

* removes unneeded screenshots for Cluster.pw.tsx

* standardizes mobile testing approach with +@mobile tag by removing explicit viewport config, updates screenshots

* reverts expanded pw test coverage for ClustersEntity

* removes all clusters pw screenshots

* removes nested describe which was resulting in a failed test

* uploads latest docker generated screenshots for pw

* force trigger for conflict check

* removes repeating var from blockchainNavItems array

* fix screenshot merge conflicts - use upstream versions

* updates yarn.lock with upstream lib

* fix jest tests by mocking useQuery directly to resolve QueryClient context issues

* creates global mock object and proper typing to reduce duplication

* updates clusters pw screenshots with docker generation. update helia to 3.2.0 and pinned esbuild to 0.21.5. update docker dep script.

* updates yarn.lock

* removes unused imports from upstream/main

* add vite aliases for @libp2p/utils and @helia/verified-fetch to resolve playwright build failures

* updates pw screenshots for hero banner, home page and desktop header

* downgrades helia to v2.6.12

* revert changes in yarn.lock file related to helia package

* updates yarn.lock with helia downgrade

* revert changes in yarn.lock

* move clusters to the tab on the name service page

* fix tests

* small tweaks

* update screenshots

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Richard Oh <[email protected]>
Co-authored-by: Max Alekseenko <[email protected]>
Co-authored-by: isstuev <[email protected]>
Co-authored-by: Igor Stuev <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Richard J Oh <[email protected]>
2025-09-08 19:21:15 +02:00

422 lines
13 KiB
TypeScript

import { renderHook } from '@testing-library/react';
import useApiQuery from 'lib/api/useApiQuery';
import { useClustersData } from './useClustersData';
jest.mock('lib/api/useApiQuery');
const mockUseApiQuery = useApiQuery as jest.MockedFunction<typeof useApiQuery>;
type MockQueryResult = ReturnType<typeof useApiQuery>;
jest.mock('lib/clusters/detectInputType', () => ({
detectInputType: jest.fn(),
}));
const mockDetectInputType = require('lib/clusters/detectInputType').detectInputType;
describe('useClustersData', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseApiQuery.mockReturnValue({
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult);
});
describe('input type detection logic', () => {
it('should default to cluster_name when no search term provided', () => {
renderHook(() => useClustersData('', 'leaderboard', 1));
expect(mockDetectInputType).not.toHaveBeenCalled();
});
it('should call detectInputType when search term exists', () => {
mockDetectInputType.mockReturnValue('address');
renderHook(() => useClustersData('0x123...', 'directory', 1));
expect(mockDetectInputType).toHaveBeenCalledWith('0x123...');
});
it('should memoize input type calculation', () => {
mockDetectInputType.mockReturnValue('cluster_name');
const { rerender } = renderHook(
({ searchTerm }) => useClustersData(searchTerm, 'directory', 1),
{ initialProps: { searchTerm: 'example.cluster' } },
);
rerender({ searchTerm: 'example.cluster' });
expect(mockDetectInputType).toHaveBeenCalledTimes(1);
});
});
describe('view mode determination', () => {
it('should show directory view when viewMode is directory', () => {
renderHook(() => useClustersData('', 'directory', 1));
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_directory', expect.any(Object));
});
it('should show directory view when search term exists regardless of viewMode', () => {
mockDetectInputType.mockReturnValue('cluster_name');
renderHook(() => useClustersData('search', 'leaderboard', 1));
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_directory', expect.any(Object));
});
it('should show leaderboard view when no search term and viewMode is leaderboard', () => {
renderHook(() => useClustersData('', 'leaderboard', 1));
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_leaderboard', expect.any(Object));
});
});
describe('API query configuration', () => {
it('should configure leaderboard query with correct pagination', () => {
renderHook(() => useClustersData('', 'leaderboard', 3));
const leaderboardCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_leaderboard',
);
expect(leaderboardCall).toBeDefined();
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"offset":100');
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"limit":50');
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"orderBy":"rank-asc"');
});
it('should configure directory query with search term', () => {
mockDetectInputType.mockReturnValue('cluster_name');
renderHook(() => useClustersData('example', 'directory', 2));
const directoryCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_directory',
);
expect(directoryCall).toBeDefined();
expect(directoryCall?.[1]?.queryParams?.input).toContain('"offset":50');
expect(directoryCall?.[1]?.queryParams?.input).toContain('"query":"example"');
});
it('should configure address query when input type is address', () => {
mockDetectInputType.mockReturnValue('address');
renderHook(() => useClustersData('0x123...', 'directory', 1));
const addressCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_clusters_by_address',
);
expect(addressCall).toBeDefined();
expect(addressCall?.[1]?.queryParams?.input).toContain('"address":"0x123..."');
});
it('should call cluster details query when cluster ID is available', () => {
mockDetectInputType.mockReturnValue('address');
mockUseApiQuery.mockImplementation((resource) => {
if (resource === 'clusters:get_clusters_by_address') {
return {
data: {
result: {
data: [ { clusterId: 'cluster-123' } ],
},
},
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
}
return {
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
});
renderHook(() => useClustersData('0x123...', 'directory', 1));
const clusterDetailsCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_cluster_by_id',
);
expect(clusterDetailsCall).toBeDefined();
expect(clusterDetailsCall?.[1]?.queryParams?.input).toContain('"id":"cluster-123"');
});
});
describe('dynamic ordering business logic', () => {
it('should use NAME_ASC ordering when search term exists', () => {
mockDetectInputType.mockReturnValue('cluster_name');
renderHook(() => useClustersData('search', 'directory', 1));
const directoryCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_directory',
);
expect(directoryCall?.[1]?.queryParams?.input).toContain('"orderBy":"name-asc"');
});
it('should use CREATED_AT_DESC ordering when no search term', () => {
renderHook(() => useClustersData('', 'directory', 1));
const directoryCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_directory',
);
expect(directoryCall?.[1]?.queryParams?.input).toContain('"orderBy":"createdAt-desc"');
});
it('should memoize directory order by logic', () => {
const { rerender } = renderHook(
({ searchTerm }) => useClustersData(searchTerm, 'directory', 1),
{ initialProps: { searchTerm: 'search' } },
);
jest.clearAllMocks();
rerender({ searchTerm: 'search' });
const expectedCallsPerRender = 4;
expect(mockUseApiQuery.mock.calls.length).toBe(expectedCallsPerRender);
});
});
describe('query selection logic', () => {
it('should return data from leaderboard query in leaderboard mode', () => {
const mockLeaderboardData = { result: { data: [] } };
mockUseApiQuery.mockImplementation((resource) => {
if (resource === 'clusters:get_leaderboard') {
return {
data: mockLeaderboardData,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
}
return {
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
});
const { result } = renderHook(() =>
useClustersData('', 'leaderboard', 1),
);
expect(result.current.data).toBe(mockLeaderboardData);
});
it('should return data from address query when input type is address', () => {
mockDetectInputType.mockReturnValue('address');
const mockAddressData = { result: { data: [] } };
mockUseApiQuery.mockImplementation((resource) => {
if (resource === 'clusters:get_clusters_by_address') {
return {
data: mockAddressData,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
}
return {
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
});
const { result } = renderHook(() =>
useClustersData('0x123...', 'directory', 1),
);
expect(result.current.data).toBe(mockAddressData);
});
it('should return data from directory query when input type is cluster_name', () => {
mockDetectInputType.mockReturnValue('cluster_name');
const mockDirectoryData = { result: { data: { items: [] } } };
mockUseApiQuery.mockImplementation((resource) => {
if (resource === 'clusters:get_directory') {
return {
data: mockDirectoryData,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
}
return {
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
});
const { result } = renderHook(() =>
useClustersData('example', 'directory', 1),
);
expect(result.current.data).toBe(mockDirectoryData);
});
});
describe('return value structure', () => {
it('should return correct data structure with all expected properties', () => {
const mockData = { result: { data: [] } };
const mockClusterDetails = { result: { data: { id: 'cluster-123' } } };
mockUseApiQuery.mockImplementation((resource) => {
if (resource === 'clusters:get_leaderboard') {
return {
data: mockData,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
}
if (resource === 'clusters:get_cluster_by_id') {
return {
data: mockClusterDetails,
isError: false,
isPlaceholderData: false,
isLoading: true,
} as unknown as MockQueryResult;
}
return {
data: null,
isError: false,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult;
});
const { result } = renderHook(() =>
useClustersData('', 'leaderboard', 1),
);
expect(result.current).toEqual({
data: mockData,
clusterDetails: mockClusterDetails,
isError: false,
isLoading: false,
isClusterDetailsLoading: true,
});
});
it('should handle error states correctly', () => {
mockUseApiQuery.mockReturnValue({
data: null,
isError: true,
isPlaceholderData: false,
isLoading: false,
} as unknown as MockQueryResult);
const { result } = renderHook(() =>
useClustersData('', 'leaderboard', 1),
);
expect(result.current.isError).toBe(true);
});
it('should handle loading states correctly', () => {
mockUseApiQuery.mockReturnValue({
data: null,
isError: false,
isPlaceholderData: true,
isLoading: false,
} as unknown as MockQueryResult);
const { result } = renderHook(() =>
useClustersData('', 'leaderboard', 1),
);
expect(result.current.isLoading).toBe(true);
});
});
describe('pagination calculations', () => {
it('should calculate correct offset for page 1', () => {
renderHook(() => useClustersData('', 'leaderboard', 1));
const leaderboardCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_leaderboard',
);
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"offset":0');
});
it('should calculate correct offset for page 5', () => {
renderHook(() => useClustersData('', 'leaderboard', 5));
const leaderboardCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_leaderboard',
);
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"offset":200');
});
it('should consistently use 50 items per page', () => {
renderHook(() => useClustersData('', 'leaderboard', 1));
const leaderboardCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_leaderboard',
);
expect(leaderboardCall?.[1]?.queryParams?.input).toContain('"limit":50');
});
});
describe('query enabling/disabling logic', () => {
it('should disable leaderboard query when in directory view', () => {
renderHook(() => useClustersData('search', 'directory', 1));
const leaderboardCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_leaderboard',
);
expect(leaderboardCall?.[1]?.queryOptions?.enabled).toBe(false);
});
it('should disable directory query when input type is address', () => {
mockDetectInputType.mockReturnValue('address');
renderHook(() => useClustersData('0x123...', 'directory', 1));
const directoryCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_directory',
);
expect(directoryCall?.[1]?.queryOptions?.enabled).toBe(false);
});
it('should disable address query when input type is not address', () => {
mockDetectInputType.mockReturnValue('cluster_name');
renderHook(() => useClustersData('example', 'directory', 1));
const addressCall = mockUseApiQuery.mock.calls.find(call =>
call[0] === 'clusters:get_clusters_by_address',
);
expect(addressCall?.[1]?.queryOptions?.enabled).toBe(false);
});
});
});