Skip to content

Commit

Permalink
chore: Add missing tests on core and module components (#402)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth authored Feb 9, 2025
1 parent 1686067 commit 720bc54
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 104 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Bump `actions/setup-python` to 5.4.0
- Update minor and patch NPM dependencies
- Improve code coverage on core and modules components

### Fixed

Expand Down
8 changes: 0 additions & 8 deletions src/core/components/alerts/alertCard/alertCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ const alertVariantToMessageClassNames: Record<AlertVariant, string> = {
warning: 'text-warning-800',
};

/**
* AlertCard Component
*
* Displays an alert card with an icon, a main message, and an optional description.
*
* @param {IAlertCardProps} props - Component properties.
* @returns {React.ReactElement} Rendered AlertCard component.
*/
export const AlertCard: React.FC<IAlertCardProps> = (props) => {
const { className, description, message, variant = 'info', ...otherProps } = props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { AlertInline, type IAlertInlineProps } from './alertInline';
describe('<AlertInline />', () => {
const createTestComponent = (props?: Partial<IAlertInlineProps>) => {
const completeProps: IAlertInlineProps = {
variant: 'critical',
message: 'Message',
...props,
};
Expand Down
2 changes: 0 additions & 2 deletions src/core/components/alerts/alertInline/alertInline.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import classNames from 'classnames';
import type React from 'react';
import { type HTMLAttributes } from 'react';

import { Icon } from '../../icon';
import { alertVariantToIconType, type AlertVariant } from '../alertUtils';

Expand Down Expand Up @@ -31,7 +30,6 @@ const variantToTextClassNames: Record<AlertVariant, string> = {
warning: 'text-warning-800',
};

/** AlertInline UI Component */
export const AlertInline: React.FC<IAlertInlineProps> = (props) => {
const { className, message, variant = 'info', ...rest } = props;

Expand Down
18 changes: 15 additions & 3 deletions src/core/components/avatars/avatarIcon/avatarIcon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ import { AvatarIcon, type IAvatarIconProps } from './avatarIcon';

describe('<AvatarIcon /> component', () => {
const createTestComponent = (props?: Partial<IAvatarIconProps>) => {
const completeProps: IAvatarIconProps = { icon: IconType.PLUS, ...props };
const completeProps: IAvatarIconProps = {
icon: IconType.PLUS,
...props,
};

return <AvatarIcon {...completeProps} />;
};

it('renders the specified icon', () => {
render(createTestComponent({ icon: IconType.APP_ASSETS }));
expect(screen.getByTestId(IconType.APP_ASSETS)).toBeInTheDocument();
const icon = IconType.APP_DASHBOARD;
render(createTestComponent({ icon }));
expect(screen.getByTestId(icon)).toBeInTheDocument();
});

it('renders correct style for background-white variant', () => {
const backgroundWhite = true;
const icon = IconType.PLUS;
render(createTestComponent({ backgroundWhite, icon }));
// eslint-disable-next-line testing-library/no-node-access
expect(screen.getByTestId(icon).parentElement?.classList).toContain('bg-neutral-0');
});
});
15 changes: 10 additions & 5 deletions src/core/components/button/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import type { IButtonProps } from './button.api';

describe('<Button /> component', () => {
const createTestComponent = (props?: Partial<IButtonProps>) => {
const completeProps: IButtonProps = {
variant: 'primary',
size: 'md',
...props,
};
const completeProps: IButtonProps = { ...props };

return <Button {...completeProps} />;
};
Expand Down Expand Up @@ -101,6 +97,15 @@ describe('<Button /> component', () => {
expect(onClick).toHaveBeenCalled();
});

it('does not throw error when clicking on button and onClick property is not set on link variant', async () => {
// Suppress "Not implemented: navigation" warning
testLogger.suppressErrors();
const user = userEvent.setup();
const href = '/test';
render(createTestComponent({ href }));
await expect(user.click(screen.getByRole('link'))).resolves.toBeUndefined();
});

it('sets button type by default', () => {
render(createTestComponent());
expect(screen.getByRole<HTMLButtonElement>('button').type).toEqual('button');
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/cards/cardSummary/cardSummary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('<CardSummary /> component', () => {
const label = 'action-test';
const onClick = jest.fn();
const action = { label, onClick };
render(createTestComponent({ action }));
render(createTestComponent({ action, isStacked: false }));

const button = screen.getByRole('button', { name: label });
expect(button).toBeInTheDocument();
Expand Down
7 changes: 2 additions & 5 deletions src/core/components/cards/cardSummary/cardSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ export const CardSummary: React.FC<ICardSummaryProps> = (props) => {

const containerClassNames = classNames(
'grid grid-cols-[auto_max-content] items-center gap-4 p-4 md:px-6 md:py-5',
{ 'grid-cols-[auto_max-content] md:gap-5': isStacked },
{
'grid-cols-[auto_max-content] md:grid-flow-col md:grid-cols-[auto_1fr_1fr_max-content] md:gap-6':
!isStacked,
},
{ 'md:gap-5': isStacked },
{ 'md:grid-flow-col md:grid-cols-[auto_1fr_1fr_max-content] md:gap-6': !isStacked },
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react';
import { DateFormat, formatterUtils, NumberFormat } from '../../../../../core';
import { render, screen } from '@testing-library/react';
import { DateFormat, formatterUtils, IconType, NumberFormat } from '../../../../../core';
import * as useBlockExplorer from '../../../../hooks';
import { TransactionDataListItemStructure } from './transactionDataListItemStructure';
import {
Expand All @@ -14,9 +14,7 @@ describe('<TransactionDataListItem.Structure /> component', () => {
beforeEach(() => {
useBlockExplorerSpy.mockReturnValue({
buildEntityUrl: jest.fn(),
getBlockExplorer: jest.fn(),
blockExplorer: undefined,
});
} as unknown as useBlockExplorer.IUseBlockExplorerReturn);
});

afterEach(() => {
Expand All @@ -34,14 +32,13 @@ describe('<TransactionDataListItem.Structure /> component', () => {
return <TransactionDataListItemStructure {...defaultProps} />;
};

it('renders the transaction type', () => {
it('renders the transaction type heading', () => {
const type = TransactionType.ACTION;
render(createTestComponent({ type }));
const transactionTypeHeading = screen.getByText('Smart contract action');
expect(transactionTypeHeading).toBeInTheDocument();
expect(screen.getByText('Smart contract action')).toBeInTheDocument();
});

it('renders the token value and symbol in a deposit', () => {
it('renders the token value and symbol for a deposit transaction', () => {
const tokenSymbol = 'ETH';
const tokenAmount = 10;
const type = TransactionType.DEPOSIT;
Expand All @@ -54,19 +51,22 @@ describe('<TransactionDataListItem.Structure /> component', () => {
const amountUsd = '123.21';
const tokenAmount = 10;
const type = TransactionType.DEPOSIT;
const usdPrice = formatterUtils.formatNumber(amountUsd, {
format: NumberFormat.FIAT_TOTAL_SHORT,
})!;
const usdPrice = formatterUtils.formatNumber(amountUsd, { format: NumberFormat.FIAT_TOTAL_SHORT })!;
render(createTestComponent({ amountUsd, tokenAmount, type }));
expect(screen.getByText(usdPrice)).toBeInTheDocument();
});

it('renders a failed transaction indicator alongside the transaction type', () => {
render(createTestComponent({ type: TransactionType.DEPOSIT, status: TransactionStatus.FAILED }));
const failedTransactionText = screen.getByText('Deposit');
expect(failedTransactionText).toBeInTheDocument();
const closeIcon = screen.getByTestId('CLOSE');
expect(closeIcon).toBeInTheDocument();
it('renders a failed icon when transaction is failed', () => {
const status = TransactionStatus.FAILED;
render(createTestComponent({ status }));
expect(screen.getByTestId('CLOSE')).toBeInTheDocument();
});

it('renders the related transaction type icon when transaction is successful', () => {
const status = TransactionStatus.SUCCESS;
const type = TransactionType.DEPOSIT;
render(createTestComponent({ status, type }));
expect(screen.getByTestId(IconType.DEPOSIT)).toBeInTheDocument();
});

it('renders the provided date correctly', () => {
Expand All @@ -76,20 +76,20 @@ describe('<TransactionDataListItem.Structure /> component', () => {
expect(screen.getByText(formattedDate)).toBeInTheDocument();
});

it('renders with the correct block explorer URL', async () => {
it('renders with the correct block explorer URL when href property is not defined', () => {
const chainId = 1;
const hash = '0x123';
const explorerUrl = 'https://etherscan.io/tx/0x123';
useBlockExplorerSpy.mockReturnValue({
buildEntityUrl: () => 'https://etherscan.io/tx/0x123',
getBlockExplorer: jest.fn(),
blockExplorer: undefined,
});

buildEntityUrl: () => explorerUrl,
} as unknown as useBlockExplorer.IUseBlockExplorerReturn);
render(createTestComponent({ chainId, hash }));
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://etherscan.io/tx/0x123');
});

await waitFor(() => {
const linkElement = screen.getByRole<HTMLAnchorElement>('link');
expect(linkElement).toHaveAttribute('href', 'https://etherscan.io/tx/0x123');
});
it('does not override href property when defined', () => {
const href = 'https://custom.com/0x123';
render(createTestComponent({ href }));
expect(screen.getByRole('link')).toHaveAttribute('href', href);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ import {
type ITransactionDataListItemProps,
} from './transactionDataListItemStructure.api';

const txHeadingStringList: Record<TransactionType, string> = {
const typeToHeading: Record<TransactionType, string> = {
[TransactionType.DEPOSIT]: 'Deposit',
[TransactionType.WITHDRAW]: 'Withdraw',
[TransactionType.ACTION]: 'Smart contract action',
};

const txIconTypeList: Record<TransactionType, IconType> = {
const typeToIcon: Record<TransactionType, IconType> = {
[TransactionType.DEPOSIT]: IconType.DEPOSIT,
[TransactionType.WITHDRAW]: IconType.WITHDRAW,
[TransactionType.ACTION]: IconType.BLOCKCHAIN_SMARTCONTRACT,
};

const txVariantList: Record<TransactionType, AvatarIconVariant> = {
const typeToIconVariant: Record<TransactionType, AvatarIconVariant> = {
[TransactionType.DEPOSIT]: 'success',
[TransactionType.WITHDRAW]: 'warning',
[TransactionType.ACTION]: 'info',
Expand All @@ -53,13 +53,16 @@ export const TransactionDataListItemStructure: React.FC<ITransactionDataListItem
const blockExplorerHref = buildEntityUrl({ type: ChainEntityType.TRANSACTION, id: hash });
const processedHref = 'href' in otherProps && otherProps.href != null ? otherProps.href : blockExplorerHref;

const formattedTokenValue = formatterUtils.formatNumber(tokenAmount, { format: NumberFormat.TOKEN_AMOUNT_SHORT });
const formattedTokenPrice = formatterUtils.formatNumber(amountUsd, {
const formattedTokenAmount = formatterUtils.formatNumber(tokenAmount, { format: NumberFormat.TOKEN_AMOUNT_SHORT });
const formattedTransactionValue = formatterUtils.formatNumber(amountUsd, {
format: NumberFormat.FIAT_TOTAL_SHORT,
fallback: '-',
});
const formattedTokenAmount =
type === TransactionType.ACTION || formattedTokenValue == null ? '-' : `${formattedTokenValue} ${tokenSymbol}`;

const processedTokenAmount =
type === TransactionType.ACTION || formattedTokenAmount == null
? '-'
: `${formattedTokenAmount} ${tokenSymbol}`;

return (
<DataList.Item
Expand All @@ -68,7 +71,7 @@ export const TransactionDataListItemStructure: React.FC<ITransactionDataListItem
{...otherProps}
>
{status === TransactionStatus.SUCCESS && (
<AvatarIcon variant={txVariantList[type]} icon={txIconTypeList[type]} responsiveSize={{ md: 'md' }} />
<AvatarIcon variant={typeToIconVariant[type]} icon={typeToIcon[type]} responsiveSize={{ md: 'md' }} />
)}
{status === TransactionStatus.FAILED && (
<AvatarIcon variant="critical" icon={IconType.CLOSE} responsiveSize={{ md: 'md' }} />
Expand All @@ -79,7 +82,7 @@ export const TransactionDataListItemStructure: React.FC<ITransactionDataListItem
</div>
)}
<div className="flex w-full flex-col items-start gap-y-1 self-center">
<span className="leading-tight text-neutral-800 md:text-lg">{txHeadingStringList[type]}</span>
<span className="leading-tight text-neutral-800 md:text-lg">{typeToHeading[type]}</span>
{date && (
<p className="text-sm leading-tight text-neutral-500 md:text-base">
{formatterUtils.formatDate(date, { format: DateFormat.YEAR_MONTH_DAY_TIME })}
Expand All @@ -88,8 +91,8 @@ export const TransactionDataListItemStructure: React.FC<ITransactionDataListItem
</div>

<div className="flex shrink-0 flex-col items-end gap-y-1 truncate">
<span className="leading-tight text-neutral-800 md:text-lg">{formattedTokenAmount}</span>
<span className="text-sm leading-tight text-neutral-500 md:text-base">{formattedTokenPrice}</span>
<span className="leading-tight text-neutral-800 md:text-lg">{processedTokenAmount}</span>
<span className="text-sm leading-tight text-neutral-500 md:text-base">{formattedTransactionValue}</span>
</div>
</DataList.Item>
);
Expand Down
4 changes: 3 additions & 1 deletion src/modules/components/wallet/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const Wallet: React.FC<IWalletProps> = (props) => {
chainId,
config: wagmiConfig,
});

const resolvedUserHandle = user?.name ?? ensName ?? addressUtils.truncateAddress(user?.address);
const resolvedUserTitle = user?.name ?? ensName ?? user?.address;

const buttonClassName = classNames(
'flex items-center gap-3 rounded-full border border-neutral-100 bg-neutral-0 text-neutral-500 transition-all',
Expand All @@ -40,7 +42,7 @@ export const Wallet: React.FC<IWalletProps> = (props) => {
{!user && copy.wallet.connect}
{user && isEnsLoading && <StateSkeletonBar className="hidden md:block" size="lg" width={56} />}
{user && !isEnsLoading && (
<span title={user.name ?? ensName ?? user.address} className="hidden max-w-24 truncate md:block">
<span title={resolvedUserTitle} className="hidden max-w-24 truncate md:block">
{resolvedUserHandle}
</span>
)}
Expand Down
6 changes: 4 additions & 2 deletions src/modules/hooks/useBlockExplorer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { useBlockExplorer } from './useBlockExplorer';
export {
ChainEntityType,
useBlockExplorer,
type IBlockExplorerDefinitions,
type IBuildEntityUrlParams,
type IUseBlockExplorerParams,
} from './useBlockExplorer';
type IUseBlockExplorerReturn,
} from './useBlockExplorer.api';
62 changes: 62 additions & 0 deletions src/modules/hooks/useBlockExplorer/useBlockExplorer.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Config } from 'wagmi';

export enum ChainEntityType {
ADDRESS = 'address',
TRANSACTION = 'tx',
TOKEN = 'token',
}

export interface IUseBlockExplorerParams {
/**
* Chains definitions to use for returning the block explorer definitions and building the URLs. Defaults to the
* chains defined on the Wagmi context provider.
*/
chains?: Config['chains'];
/**
* Uses the block explorer definition of the specified Chain ID when set. Defaults to the ID of the first chain on
* the chains list.
*/
chainId?: number;
}

export interface IBlockExplorerDefinitions {
/**
* Name of the block explorer.
*/
name: string;
/**
* URL of the block explorer.
*/
url: string;
}

export interface IUseBlockExplorerReturn {
/**
* Definitions for the requested block explorer.
*/
blockExplorer?: IBlockExplorerDefinitions;
/**
* Function to retrieve the block explorer from the given chain id. Defaults to the first block explorer of the
* chain defined on the hook params or on the Wagmi context provider.
*/
getBlockExplorer: (chainId?: number) => IBlockExplorerDefinitions | undefined;
/**
* Function to build the url for the given entity.
*/
buildEntityUrl: (params: IBuildEntityUrlParams) => string | undefined;
}

export interface IBuildEntityUrlParams {
/**
* The type of the entity (e.g. address, transaction, token)
*/
type: ChainEntityType;
/**
* ID of the chain related to the entity. When set, overrides the chainId set as hook parameter.
*/
chainId?: number;
/**
* The ID of the entity (e.g. transaction hash for a transaction)
*/
id?: string;
}
Loading

0 comments on commit 720bc54

Please sign in to comment.