mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
* 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]>
422 lines
13 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|