-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Added Link.tsx and Link.types.d.ts. - Wrapped MuiLink in new Link component. - Added test suite for link, Link.test.tsx.
- Loading branch information
ishaan bhalla
authored and
ishaan bhalla
committed
Oct 11, 2024
1 parent
a605ffd
commit 983150e
Showing
3 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
'use client'; | ||
|
||
import { LinkProps } from '@components/atoms/Link'; | ||
import { Link as MuiLink } from '@mui/material'; | ||
import React from 'react'; | ||
|
||
const Link: React.FC<LinkProps> = ({ | ||
href, | ||
color = 'primary', | ||
underline = 'always', | ||
variant = 'inherit', | ||
target, | ||
rel, | ||
onClick, | ||
sx = {}, | ||
tabIndex = 0, | ||
children, | ||
ariaLabel, | ||
role, | ||
component, | ||
...props | ||
}) => { | ||
if (!href && component !== 'button') { | ||
throw new Error( | ||
'The `href` prop is required unless `component="button"` is used.' | ||
); | ||
} | ||
|
||
const computedRel = | ||
target === '_blank' | ||
? rel | ||
? `${rel} noopener noreferrer` | ||
: 'noopener noreferrer' | ||
: rel; | ||
|
||
const linkProps = { | ||
...(component !== 'button' && { href }), | ||
target, | ||
rel: computedRel, | ||
onClick, | ||
'aria-label': ariaLabel, | ||
role, | ||
tabIndex, | ||
component, | ||
}; | ||
|
||
return ( | ||
<MuiLink | ||
color={color} | ||
sx={sx} | ||
underline={underline} | ||
variant={variant} | ||
{...linkProps} | ||
{...props} | ||
> | ||
{children} | ||
</MuiLink> | ||
); | ||
}; | ||
|
||
export default Link; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
declare module '@components/atoms/Link' { | ||
import { ElementType, FC, ReactNode } from 'react'; | ||
import { SxProps, Theme } from '@mui/system'; | ||
import { LinkProps as MuiLinkProps } from '@mui/material/Link'; | ||
|
||
export interface LinkProps | ||
extends Omit< | ||
MuiLinkProps, | ||
'color' | 'variant' | 'underline' | 'href' | 'onClick' | ||
> { | ||
href?: string; | ||
color?: | ||
| 'initial' | ||
| 'inherit' | ||
| 'primary' | ||
| 'secondary' | ||
| 'textPrimary' | ||
| 'textSecondary' | ||
| 'error'; | ||
underline?: 'none' | 'hover' | 'always'; | ||
variant?: | ||
| 'inherit' | ||
| 'h1' | ||
| 'h2' | ||
| 'h3' | ||
| 'h4' | ||
| 'h5' | ||
| 'h6' | ||
| 'subtitle1' | ||
| 'subtitle2' | ||
| 'body1' | ||
| 'body2' | ||
| 'caption' | ||
| 'button' | ||
| 'overline'; | ||
target?: React.HTMLAttributeAnchorTarget; | ||
rel?: string; | ||
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>; | ||
sx?: SxProps<Theme>; | ||
tabIndex?: number; | ||
children: ReactNode; | ||
ariaLabel?: string; | ||
role?: string; | ||
component?: ElementType; | ||
} | ||
|
||
declare const Link: FC<LinkProps>; | ||
export default Link; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import '@testing-library/jest-dom'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import Link from '../../../src/components/atoms/Link'; | ||
import React from 'react'; | ||
|
||
describe('Link component', () => { | ||
const defaultProps = { | ||
href: 'https://www.example.com', | ||
children: 'Example Link', | ||
}; | ||
|
||
it('renders the link with correct href and text', () => { | ||
render(<Link {...defaultProps} />); | ||
const linkElement = screen.getByRole('link', { name: 'Example Link' }); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', defaultProps.href); | ||
}); | ||
|
||
it('includes rel="noopener noreferrer" when target="_blank"', () => { | ||
render(<Link {...defaultProps} target="_blank" />); | ||
const linkElement = screen.getByRole('link', { name: 'Example Link' }); | ||
expect(linkElement).toHaveAttribute('target', '_blank'); | ||
expect(linkElement).toHaveAttribute('rel', 'noopener noreferrer'); | ||
}); | ||
|
||
it('includes custom rel attribute when provided with target="_blank"', () => { | ||
render(<Link {...defaultProps} rel="nofollow" target="_blank" />); | ||
const linkElement = screen.getByRole('link', { name: 'Example Link' }); | ||
expect(linkElement).toHaveAttribute('rel', 'nofollow noopener noreferrer'); | ||
}); | ||
|
||
it('does not include rel when target is not "_blank"', () => { | ||
render(<Link {...defaultProps} rel="nofollow" />); | ||
const linkElement = screen.getByRole('link', { name: 'Example Link' }); | ||
expect(linkElement).toHaveAttribute('rel', 'nofollow'); | ||
}); | ||
|
||
it('handles onClick event', () => { | ||
const handleClick = jest.fn(); | ||
render( | ||
<Link {...defaultProps} onClick={handleClick}> | ||
Clickable Link | ||
</Link> | ||
); | ||
const linkElement = screen.getByRole('link', { name: 'Clickable Link' }); | ||
fireEvent.click(linkElement); | ||
expect(handleClick).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('renders as a button when component="button"', () => { | ||
const handleClick = jest.fn(); | ||
render( | ||
<Link component="button" onClick={handleClick}> | ||
Button Link | ||
</Link> | ||
); | ||
const buttonElement = screen.getByRole('button', { name: 'Button Link' }); | ||
expect(buttonElement.tagName).toBe('BUTTON'); | ||
expect(buttonElement).not.toHaveAttribute('href'); | ||
fireEvent.click(buttonElement); | ||
expect(handleClick).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('applies aria-label when provided', () => { | ||
render(<Link {...defaultProps} ariaLabel="Custom Aria Label" />); | ||
const linkElement = screen.getByLabelText('Custom Aria Label'); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', defaultProps.href); | ||
}); | ||
|
||
it('does not have aria-label when not provided', () => { | ||
render(<Link {...defaultProps} />); | ||
const linkElement = screen.getByRole('link', { name: 'Example Link' }); | ||
expect(linkElement).not.toHaveAttribute('aria-label'); | ||
}); | ||
|
||
it('passes additional props to the underlying component', () => { | ||
render(<Link {...defaultProps} data-testid="custom-link" />); | ||
const linkElement = screen.getByTestId('custom-link'); | ||
expect(linkElement).toBeInTheDocument(); | ||
}); | ||
|
||
it('supports custom components via the "component" prop', () => { | ||
const CustomComponent = React.forwardRef< | ||
HTMLAnchorElement, | ||
React.AnchorHTMLAttributes<HTMLAnchorElement> | ||
>((props, ref) => ( | ||
<a data-testid="custom-component" ref={ref} {...props} /> | ||
)); | ||
|
||
CustomComponent.displayName = 'CustomComponent'; | ||
|
||
render( | ||
<Link component={CustomComponent} href="/custom"> | ||
Custom Component Link | ||
</Link> | ||
); | ||
|
||
const customElement = screen.getByTestId('custom-component'); | ||
expect(customElement).toBeInTheDocument(); | ||
expect(customElement).toHaveAttribute('href', '/custom'); | ||
expect(customElement).toHaveTextContent('Custom Component Link'); | ||
}); | ||
|
||
it('handles missing href when component is "button"', () => { | ||
render(<Link component="button">Button without href</Link>); | ||
const buttonElement = screen.getByRole('button', { | ||
name: 'Button without href', | ||
}); | ||
expect(buttonElement).toBeInTheDocument(); | ||
expect(buttonElement.tagName).toBe('BUTTON'); | ||
expect(buttonElement).not.toHaveAttribute('href'); | ||
}); | ||
|
||
it('throws an error when href is missing and component is not "button"', () => { | ||
console.error = jest.fn(); | ||
expect(() => { | ||
render(<Link>Link without href</Link>); | ||
}).toThrow(); | ||
}); | ||
}); |