Files
explore/lib/clusters/pageUtils.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

332 lines
9.4 KiB
TypeScript

import { ClustersOrderBy } from 'types/api/clusters';
import type { ClustersDirectoryObject } from 'types/api/clusters';
import {
getViewModeOrderBy,
shouldShowDirectoryView,
transformLeaderboardData,
transformAddressDataToDirectory,
transformFullDirectoryData,
applyDirectoryPagination,
calculateHasNextPage,
isValidViewMode,
getDefaultViewMode,
getCurrentDataLength,
} from './pageUtils';
describe('pageUtils', () => {
describe('getViewModeOrderBy', () => {
it('should return RANK_ASC for leaderboard view regardless of search', () => {
expect(getViewModeOrderBy('leaderboard', false)).toBe(ClustersOrderBy.RANK_ASC);
expect(getViewModeOrderBy('leaderboard', true)).toBe(ClustersOrderBy.RANK_ASC);
});
it('should return NAME_ASC for directory view with search term', () => {
expect(getViewModeOrderBy('directory', true)).toBe(ClustersOrderBy.NAME_ASC);
});
it('should return CREATED_AT_DESC for directory view without search term', () => {
expect(getViewModeOrderBy('directory', false)).toBe(ClustersOrderBy.CREATED_AT_DESC);
});
});
describe('shouldShowDirectoryView', () => {
it('should return true for directory view mode', () => {
expect(shouldShowDirectoryView('directory', false)).toBe(true);
expect(shouldShowDirectoryView('directory', true)).toBe(true);
});
it('should return true for leaderboard mode with search term', () => {
expect(shouldShowDirectoryView('leaderboard', true)).toBe(true);
});
it('should return false for leaderboard mode without search term', () => {
expect(shouldShowDirectoryView('leaderboard', false)).toBe(false);
});
});
describe('transformLeaderboardData', () => {
const mockLeaderboardData = {
result: {
data: [
{ name: 'cluster1', rank: 1 },
{ name: 'cluster2', rank: 2 },
],
},
};
it('should return empty array when showDirectoryView is true', () => {
expect(transformLeaderboardData(mockLeaderboardData, true)).toEqual([]);
});
it('should return empty array when data is null', () => {
expect(transformLeaderboardData(null, false)).toEqual([]);
});
it('should return transformed data when valid', () => {
const result = transformLeaderboardData(mockLeaderboardData, false);
expect(result).toEqual([
{ name: 'cluster1', rank: 1 },
{ name: 'cluster2', rank: 2 },
]);
});
it('should return empty array for invalid data structure', () => {
expect(transformLeaderboardData({ invalid: 'data' }, false)).toEqual([]);
expect(transformLeaderboardData({ result: { data: 'not-array' } }, false)).toEqual([]);
});
});
describe('transformAddressDataToDirectory', () => {
const mockAddressData = [
{
name: 'test-cluster',
isTestnet: false,
createdAt: '2023-01-01',
owner: '0x123',
totalWeiAmount: '1000',
updatedAt: '2023-01-02',
updatedBy: '0x456',
clusterId: 'test-cluster-id',
expiresAt: '2024-01-01',
},
];
const mockClusterDetails = {
result: {
data: {
wallets: [
{ chainIds: [ '1', '137' ] },
{ chainIds: [ '1', '56' ] },
],
},
},
};
it('should transform address data with unique chain IDs', () => {
const result = transformAddressDataToDirectory(mockAddressData, mockClusterDetails);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
name: 'test-cluster',
isTestnet: false,
createdAt: '2023-01-01',
owner: '0x123',
totalWeiAmount: '1000',
updatedAt: '2023-01-02',
updatedBy: '0x456',
chainIds: [ '1', '137', '56' ],
});
});
it('should handle missing cluster details', () => {
const result = transformAddressDataToDirectory(mockAddressData, null);
expect(result[0].chainIds).toEqual([]);
});
it('should handle empty wallets array', () => {
const emptyDetails = { result: { data: { wallets: [] } } };
const result = transformAddressDataToDirectory(mockAddressData, emptyDetails);
expect(result[0].chainIds).toEqual([]);
});
});
describe('transformFullDirectoryData', () => {
it('should return empty array when showDirectoryView is false', () => {
const result = transformFullDirectoryData({}, {}, 'address', false);
expect(result).toEqual([]);
});
it('should return empty array when data is null', () => {
const result = transformFullDirectoryData(null, {}, 'address', true);
expect(result).toEqual([]);
});
it('should transform address-type data', () => {
const mockData = {
result: {
data: [ { name: 'cluster1', owner: '0x123' } ],
},
};
const mockDetails = {
result: { data: { wallets: [ { chainIds: [ '1' ] } ] } },
};
const result = transformFullDirectoryData(mockData, mockDetails, 'address', true);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('cluster1');
});
it('should transform cluster_name-type data', () => {
const mockData = {
result: {
data: {
items: [ { name: 'cluster1' }, { name: 'cluster2' } ],
},
},
};
const result = transformFullDirectoryData(mockData, {}, 'cluster_name', true);
expect(result).toEqual([ { name: 'cluster1' }, { name: 'cluster2' } ]);
});
});
describe('applyDirectoryPagination', () => {
const mockData = Array.from({ length: 100 }, (_, i) => ({ name: `cluster${ i }` })) as Array<ClustersDirectoryObject>;
it('should apply pagination for address input type', () => {
const result = applyDirectoryPagination(mockData, 'address', 2, 20);
expect(result).toHaveLength(20);
expect(result[0].name).toBe('cluster20');
expect(result[19].name).toBe('cluster39');
});
it('should return all data for cluster_name input type', () => {
const result = applyDirectoryPagination(mockData, 'cluster_name', 2, 20);
expect(result).toHaveLength(100);
expect(result[0].name).toBe('cluster0');
});
it('should handle last page correctly', () => {
const result = applyDirectoryPagination(mockData, 'address', 5, 20);
expect(result).toHaveLength(20);
expect(result[0].name).toBe('cluster80');
});
it('should handle page beyond data length', () => {
const result = applyDirectoryPagination(mockData, 'address', 10, 20);
expect(result).toHaveLength(0);
});
});
describe('calculateHasNextPage', () => {
const mockDirectoryData = {
result: {
data: {
total: 100,
},
},
};
it('should return true for address type with more data', () => {
const result = calculateHasNextPage(
{},
0,
200,
true,
'address',
2,
false,
50,
);
expect(result).toBe(true);
});
it('should return false for address type at end', () => {
const result = calculateHasNextPage(
{},
0,
100,
true,
'address',
2,
false,
50,
);
expect(result).toBe(false);
});
it('should return false for cluster_name type with search term', () => {
const result = calculateHasNextPage(
mockDirectoryData,
0,
0,
true,
'cluster_name',
1,
true,
50,
);
expect(result).toBe(false);
});
it('should return true for cluster_name type without search and more pages', () => {
const result = calculateHasNextPage(
mockDirectoryData,
0,
0,
true,
'cluster_name',
1,
false,
50,
);
expect(result).toBe(true);
});
it('should return true for leaderboard with full page', () => {
const result = calculateHasNextPage(
{},
50,
0,
false,
'cluster_name',
1,
false,
50,
);
expect(result).toBe(true);
});
it('should return false for leaderboard with partial page', () => {
const result = calculateHasNextPage(
{},
25,
0,
false,
'cluster_name',
1,
false,
50,
);
expect(result).toBe(false);
});
});
describe('isValidViewMode', () => {
it('should return true for valid view modes', () => {
expect(isValidViewMode('leaderboard')).toBe(true);
expect(isValidViewMode('directory')).toBe(true);
});
it('should return false for invalid view modes', () => {
expect(isValidViewMode('invalid')).toBe(false);
expect(isValidViewMode('')).toBe(false);
expect(isValidViewMode('grid')).toBe(false);
});
});
describe('getDefaultViewMode', () => {
it('should return directory as default', () => {
expect(getDefaultViewMode()).toBe('directory');
});
});
describe('getCurrentDataLength', () => {
it('should return directory data length when showing directory view', () => {
expect(getCurrentDataLength(true, 25, 50)).toBe(25);
});
it('should return leaderboard data length when showing leaderboard view', () => {
expect(getCurrentDataLength(false, 25, 50)).toBe(50);
});
it('should handle zero lengths', () => {
expect(getCurrentDataLength(true, 0, 10)).toBe(0);
expect(getCurrentDataLength(false, 10, 0)).toBe(0);
});
});
});