Skip to content

Commit

Permalink
chore: update tests for disabled cases, move to children instead of l…
Browse files Browse the repository at this point in the history
…abel
  • Loading branch information
thekidnamedkd committed Jan 26, 2024
1 parent 62808f8 commit 73cba9e
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 78 deletions.
6 changes: 0 additions & 6 deletions src/assets/icons/upper-right-arrow.svg

This file was deleted.

4 changes: 1 addition & 3 deletions src/components/icon/iconList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import TxFailure from '../../assets/icons/tx-failure.svg';
import TxSmartContract from '../../assets/icons/tx-smart-contract.svg';
import TxWithdraw from '../../assets/icons/tx-withdraw.svg';
import Update from '../../assets/icons/update.svg';
import UpperRightArrow from '../../assets/icons/upper-right-arrow.svg';
import Warning from '../../assets/icons/warning.svg';
import WysiwygBold from '../../assets/icons/wysiwyg-bold.svg';
import WysiwygItalic from '../../assets/icons/wysiwyg-italic.svg';
Expand All @@ -61,7 +60,7 @@ import WysiwygListOrdered from '../../assets/icons/wysiwyg-list-ordered.svg';
import WysiwygListUnordered from '../../assets/icons/wysiwyg-list-unordered.svg';
import { IconType } from './iconType';

export type IconComponent = React.FC<SVGProps<SVGSVGElement>>;
type IconComponent = React.FC<SVGProps<SVGSVGElement>>;

export const iconList: Record<IconType, IconComponent> = {
[IconType.ADD]: Add,
Expand Down Expand Up @@ -115,7 +114,6 @@ export const iconList: Record<IconType, IconComponent> = {
[IconType.TX_SMART_CONTRACT]: TxSmartContract,
[IconType.TX_WITHDRAW]: TxWithdraw,
[IconType.UPDATE]: Update,
[IconType.UPPER_RIGHT_ARROW]: UpperRightArrow,
[IconType.WARNING]: Warning,
[IconType.REMOVE]: Remove,
[IconType.WYSIWYG_BOLD]: WysiwygBold,
Expand Down
1 change: 0 additions & 1 deletion src/components/icon/iconType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export enum IconType {
TX_SMART_CONTRACT = 'TX_SMART_CONTRACT',
TX_WITHDRAW = 'TX_WITHDRAW',
UPDATE = 'UPDATE',
UPPER_RIGHT_ARROW = 'UPPER_RIGHT_ARROW',
WARNING = 'WARNING',
WYSIWYG_BOLD = 'WYSIWYG_BOLD',
WYSIWYG_ITALIC = 'WYSIWYG_ITALIC',
Expand Down
5 changes: 2 additions & 3 deletions src/components/link/link.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ export interface ILinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/**
* Whether the link is disabled.
*/

disabled?: boolean;
/**
* Whether the link is external (opens in a new tab).
*/
external?: boolean;
/**
* The label or text content of the link. The only required prop
* This effectivly label or text content of the link. The only required prop and must be a string.
*/
label: string;
children: string;
/**
* Optional description text.
*/
Expand Down
12 changes: 7 additions & 5 deletions src/components/link/link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ const meta: Meta<LinkStoryProps> = {
},
},
args: {
label: 'Label',
children: 'Label',
variant: 'primary',
description: 'Description',
disabled: false,
external: true,
href: '#',
iconRight: IconType.LINK_EXTERNAL,
/** only used for testing the icon right prop */
showIconRight: true,
},
Expand All @@ -56,11 +57,12 @@ type Story = StoryObj<LinkStoryProps>;
// can swap out the IconType for any icon in the iconList
export const Default: Story = {
args: {
variant: 'primary',
iconRight: IconType.UPPER_RIGHT_ARROW,
children: 'Label',
},
render: ({ showIconRight, iconRight, ...props }) => (
<Link {...props} iconRight={showIconRight ? iconRight : undefined} />
render: ({ showIconRight, iconRight, children, ...props }) => (
<Link {...props} iconRight={showIconRight ? iconRight : undefined}>
{children}
</Link>
),
};

Expand Down
73 changes: 22 additions & 51 deletions src/components/link/link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,51 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { IconType } from '../icon';
import { Link } from './link';
import { Link, type ILinkProps } from '.';

describe('<Link /> component', () => {
const createTestComponent = (props?: Partial<ILinkProps>) => {
const completeProps: ILinkProps = { children: 'Default children', href: 'http://default.com', ...props };

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

describe('Link', () => {
it('renders correctly with minimum props', () => {
render(<Link label="Example" href="http://example.com" />);
const linkElement = screen.getByTestId('link');
render(createTestComponent({ children: 'Example', href: 'http://example.com' }));
const linkElement = screen.getByRole('link', { name: 'Example' });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', 'http://example.com');
expect(screen.getByText('Example')).toBeInTheDocument();
});

it('applies correct classes based on disabled prop', () => {
const { rerender } = render(<Link label="Primary Link" variant="primary" />);
let linkElement = screen.getByTestId('link');

rerender(<Link label="Disabled Link" disabled />);
linkElement = screen.getByTestId('link');
render(createTestComponent({ children: 'Disabled Link', disabled: true }));
const linkElement = screen.getByLabelText('Disabled Link');
expect(linkElement).toHaveClass('truncate text-neutral-300 cursor-not-allowed');
});

it('prevents default behavior when clicked and disabled', () => {
const handleClick = jest.fn();
render(<Link label="Disabled Link" disabled onClick={handleClick} />);
const linkElement = screen.getByTestId('link');
render(createTestComponent({ children: 'Disabled Link', disabled: true, onClick: handleClick }));
const linkElement = screen.getByLabelText('Disabled Link');
fireEvent.click(linkElement);
expect(handleClick).not.toHaveBeenCalled();
});

it('does not have href attribute when disabled', () => {
render(<Link label="Disabled Link" disabled href="http://example.com" />);
const linkElement = screen.getByTestId('link');
render(createTestComponent({ children: 'Disabled Link', disabled: true, href: 'http://example.com' }));
const linkElement = screen.getByLabelText('Disabled Link');
expect(linkElement).not.toHaveAttribute('href', 'http://example.com');
});

it('has correct attributes when link is external', () => {
render(<Link label="External Link" external href="http://example.com" />);
const linkElement = screen.getByTestId('link');
expect(linkElement).toHaveAttribute('target', '_blank');
expect(linkElement).toHaveAttribute('rel', 'noopener noreferrer');
});

it('has correct aria attributes when disabled', () => {
render(<Link label="Disabled Link" disabled />);
const linkElement = screen.getByTestId('link');
render(createTestComponent({ children: 'Disabled Link', disabled: true }));
const linkElement = screen.getByLabelText('Disabled Link');
expect(linkElement).toHaveAttribute('aria-disabled', 'true');
expect(linkElement).toHaveAttribute('tabIndex', '-1');
});

it('fires click event when not disabled', () => {
const handleClick = jest.fn();
render(<Link label="Clickable Link" href="http://example.com" onClick={handleClick} />);
const linkElement = screen.getByTestId('link');
fireEvent.click(linkElement);
expect(handleClick).toHaveBeenCalled();
});

it.each(Object.values(IconType))('renders correctly with %s icon', async (icon) => {
render(<Link label={`Link with ${icon} icon`} href="http://example.com" iconRight={icon} />);
const iconElement = await screen.findByTestId(icon);
expect(iconElement).toBeInTheDocument();
expect(screen.getByText(`Link with ${icon} icon`)).toBeInTheDocument();
});

it('renders correctly with no icon', () => {
const label = 'Link without icon';
render(<Link label={label} href="http://example.com" />);
const linkElement = screen.getByTestId('link');
expect(linkElement).toHaveTextContent(label);
});

it('receives focus when tabbed to', () => {
render(<Link label="Clickable Link" href="http://example.com" />);
const linkElement = screen.getByTestId('link');
fireEvent.keyDown(linkElement, { key: 'Tab', code: 'Tab' });
expect(linkElement).toHaveClass('test-focus');
const children = 'Link without icon';
render(createTestComponent({ children, href: 'http://example.com' }));
const linkElement = screen.getByRole('link', { name: 'Link without icon' });
expect(linkElement).toHaveTextContent(children);
});
});
17 changes: 8 additions & 9 deletions src/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,26 @@ const disabledStyle = 'truncate text-neutral-300 cursor-not-allowed';
export const Link = React.forwardRef<HTMLAnchorElement, ILinkProps>(
(
{
children,
disabled = false,
external = true,
variant = 'primary',
description,
label,
href,
iconRight,
onClick,
className,
target,
...props
},
ref,
) => {
// disabling eslint rule to throw in custom 'test-focs' class for focus ring test on tab selection
// eslint-disable-next-line tailwindcss/no-custom-classname
const linkClassName = classNames(
'gap-y-1/2 inline-flex max-w-fit flex-col truncate rounded text-sm leading-tight focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset md:gap-y-1 md:text-base',
'inline-flex max-w-fit flex-col gap-y-0.5 truncate rounded text-sm leading-tight focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset md:gap-y-1 md:text-base',
className,
disabled ? disabledStyle : variantToLabelClassNames[variant],
'test-focus',
);
const descriptionClassName = classNames('truncate', disabled ? disabledStyle : 'text-neutral-500');
const linkRel = target === '_blank' ? 'noopener noreferrer' : '';

return (
<a
Expand All @@ -51,12 +49,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, ILinkProps>(
href={disabled ? undefined : href}
className={linkClassName}
{...(disabled && { tabIndex: -1, 'aria-disabled': 'true' })}
{...(external && !disabled && { target: '_blank', rel: 'noopener noreferrer' })}
target={target}
rel={linkRel}
aria-label={children}
{...props}
data-testid="link"
>
<div className="flex items-center gap-x-2 truncate">
{label}
{children}
{iconRight && <Icon icon={iconRight} size="sm" />}
</div>
{description && <p className={descriptionClassName}>{description}</p>}
Expand Down

0 comments on commit 73cba9e

Please sign in to comment.