Skip to content

Commit b76119b

Browse files
committed
Enhance LinkCard component with improved testing and documentation
- Add comprehensive unit tests for LinkCard component - Update component documentation with new features and testing guidelines - Improve component props handling and add data-testid for testing - Enhance description truncation and expansion functionality - Update README with testing examples and component details
1 parent 470776a commit b76119b

File tree

3 files changed

+237
-11
lines changed

3 files changed

+237
-11
lines changed

src/components/LinkCard/LinkCard.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@ function renderTextWithLineBreaks(text) {
1717
}
1818

1919
/**
20-
* A component that displays a link with an associated description
21-
* @param {Object} props - The component props
22-
* @param {string} props.href - The URL that the link points to
23-
* @param {string} props.text - The text content of the link
24-
* @param {string} props.description - A description of the link that appears below it
25-
* @param {string} [props.className='link-card--default'] - Additional CSS class names to apply to the component
26-
* @returns {JSX.Element} A div containing a link and description
20+
* LinkCard component displays a link with a description that can be expanded/collapsed.
21+
*
22+
* Features:
23+
* - Renders a link with customizable text
24+
* - Displays a description that can be truncated based on screen width
25+
* - Provides Show More/Show Less toggle for longer descriptions
26+
* - Handles line breaks in descriptions
27+
* - Responsive truncation based on viewport size
28+
*
29+
* @param {Object} props - Component props
30+
* @param {string} props.href - URL for the link
31+
* @param {string} props.text - Link text to display
32+
* @param {string} props.description - Description text (can contain line breaks with \n)
33+
* @param {string} [props.className='link-card--default'] - Optional additional CSS class
34+
* @param {string} [props.variant='default'] - Visual variant of the card ('default', 'compact', etc.)
35+
* @returns {JSX.Element} Rendered LinkCard component
2736
*/
28-
const LinkCard = ({ href, text, description, className = 'link-card--default' }) => {
37+
const LinkCard = ({ href, text, description, className = 'link-card--default', ...props }) => {
2938
const [isExpanded, setIsExpanded] = useState(false);
3039
const [maxChars, setMaxChars] = useState(getContentLength(window.innerWidth));
3140

@@ -46,7 +55,7 @@ const LinkCard = ({ href, text, description, className = 'link-card--default' })
4655
: description;
4756

4857
return (
49-
<div className={`link-card ${className || ''}`}>
58+
<div className={`link-card ${className || ''}`} {...props}>
5059
<a
5160
href={href}
5261
className="link-card__link"
@@ -55,7 +64,7 @@ const LinkCard = ({ href, text, description, className = 'link-card--default' })
5564
>
5665
{text}
5766
</a>
58-
<div className="link-card__description">
67+
<div className="link-card__description" data-testid="link-card-description">
5968
{renderTextWithLineBreaks(truncatedText)}
6069
{needsTruncation && (
6170
<button
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import React from 'react';
4+
5+
import { getContentLength } from './config';
6+
import LinkCard from './LinkCard';
7+
8+
// Mock the config module
9+
jest.mock('./config', () => ({
10+
getContentLength: jest.fn()
11+
}));
12+
13+
/**
14+
* Test suite for the LinkCard component.
15+
*
16+
* This test suite verifies that the LinkCard component correctly:
17+
* - Renders with required props (href, text, description)
18+
* - Renders the link with proper attributes (href, target, rel)
19+
* - Displays the description text
20+
* - Truncates long descriptions based on window width
21+
* - Shows/hides text properly when the toggle button is clicked
22+
* - Handles line breaks in the description
23+
*/
24+
describe('LinkCard', () => {
25+
const defaultProps = {
26+
href: 'https://example.com',
27+
text: 'Example Link',
28+
description: 'This is a sample description.'
29+
};
30+
31+
beforeEach(() => {
32+
// Reset mocks
33+
jest.clearAllMocks();
34+
// Mock the getContentLength to return a fixed value
35+
getContentLength.mockReturnValue(20);
36+
// Mock window.innerWidth
37+
Object.defineProperty(window, 'innerWidth', {
38+
writable: true,
39+
configurable: true,
40+
value: 1024
41+
});
42+
});
43+
44+
it('renders with required props', () => {
45+
// Override getContentLength for this test to avoid truncation
46+
getContentLength.mockReturnValue(100);
47+
48+
render(<LinkCard {...defaultProps} />);
49+
50+
expect(screen.getByText('Example Link')).toBeInTheDocument();
51+
expect(screen.getByText('This is a sample description.')).toBeInTheDocument();
52+
});
53+
54+
it('renders the link with proper attributes', () => {
55+
render(<LinkCard {...defaultProps} />);
56+
57+
const linkElement = screen.getByText('Example Link');
58+
expect(linkElement.tagName).toBe('A');
59+
expect(linkElement).toHaveAttribute('href', 'https://example.com');
60+
expect(linkElement).toHaveAttribute('target', '_blank');
61+
expect(linkElement).toHaveAttribute('rel', 'noopener noreferrer');
62+
expect(linkElement).toHaveClass('link-card__link');
63+
});
64+
65+
it('applies custom className when provided', () => {
66+
render(
67+
<LinkCard
68+
{...defaultProps}
69+
className="custom-class"
70+
data-testid="link-card-container"
71+
/>
72+
);
73+
74+
// Use getByTestId to find the container instead of direct node access
75+
const cardElement = screen.getByTestId('link-card-container');
76+
expect(cardElement).toHaveClass('custom-class');
77+
});
78+
79+
it('truncates long description and shows "Show More" button', () => {
80+
const longDescription = 'This is a very long description that should be truncated because it exceeds the maximum character limit.';
81+
82+
render(<LinkCard {...defaultProps} description={longDescription} />);
83+
84+
// Description should be truncated
85+
expect(screen.queryByText(longDescription)).not.toBeInTheDocument();
86+
87+
// "Show More" button should be visible
88+
const showMoreButton = screen.getByText('Show More');
89+
expect(showMoreButton).toBeInTheDocument();
90+
});
91+
92+
it('expands truncated description when "Show More" is clicked', () => {
93+
const longDescription = 'This is a very long description that should be truncated because it exceeds the maximum character limit.';
94+
95+
render(<LinkCard {...defaultProps} description={longDescription} />);
96+
97+
// Click "Show More" button
98+
fireEvent.click(screen.getByText('Show More'));
99+
100+
// Full description should now be visible
101+
expect(screen.getByText(longDescription)).toBeInTheDocument();
102+
103+
// Button text should change to "Show Less"
104+
expect(screen.getByText('Show Less')).toBeInTheDocument();
105+
});
106+
107+
it('collapses expanded description when "Show Less" is clicked', () => {
108+
const longDescription = 'This is a very long description that should be truncated because it exceeds the maximum character limit.';
109+
110+
render(<LinkCard {...defaultProps} description={longDescription} />);
111+
112+
// First click to expand
113+
fireEvent.click(screen.getByText('Show More'));
114+
115+
// Second click to collapse
116+
fireEvent.click(screen.getByText('Show Less'));
117+
118+
// Description should be truncated again
119+
expect(screen.queryByText(longDescription)).not.toBeInTheDocument();
120+
121+
// Button text should change back to "Show More"
122+
expect(screen.getByText('Show More')).toBeInTheDocument();
123+
});
124+
125+
it('handles line breaks in description', () => {
126+
const descriptionWithLineBreaks = 'Line 1\nLine 2\nLine 3';
127+
128+
// Override getContentLength for this test to avoid truncation
129+
getContentLength.mockReturnValue(100);
130+
131+
render(<LinkCard {...defaultProps} description={descriptionWithLineBreaks} />);
132+
133+
// Use the data-testid to directly access the description container
134+
const descriptionElement = screen.getByTestId('link-card-description');
135+
136+
// Check if the description container includes all the lines
137+
expect(descriptionElement).toHaveTextContent('Line 1');
138+
expect(descriptionElement).toHaveTextContent('Line 2');
139+
expect(descriptionElement).toHaveTextContent('Line 3');
140+
});
141+
142+
it('updates maxChars when window resizes', () => {
143+
render(<LinkCard {...defaultProps} />);
144+
145+
// Initially called during component mount
146+
expect(getContentLength).toHaveBeenCalledWith(1024);
147+
148+
// Simulate window resize
149+
Object.defineProperty(window, 'innerWidth', {
150+
value: 768
151+
});
152+
153+
// Trigger resize event
154+
fireEvent(window, new Event('resize'));
155+
156+
// Should be called again with new width
157+
expect(getContentLength).toHaveBeenCalledWith(768);
158+
});
159+
160+
it('handles descriptions that are shorter than maxChars', () => {
161+
const shortDescription = 'Short description';
162+
getContentLength.mockReturnValue(100); // More than the description length
163+
164+
render(<LinkCard {...defaultProps} description={shortDescription} />);
165+
166+
// Description should be displayed without truncation
167+
expect(screen.getByText(shortDescription)).toBeInTheDocument();
168+
169+
// No "Show More" button should be present
170+
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
171+
});
172+
173+
it('cleans up event listeners on unmount', () => {
174+
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
175+
176+
const { unmount } = render(<LinkCard {...defaultProps} />);
177+
unmount();
178+
179+
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
180+
181+
removeEventListenerSpy.mockRestore();
182+
});
183+
});

src/components/LinkCard/README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function MyComponent() {
3636
| `text` | string | Yes | - | The text content/title of the link |
3737
| `description` | string | Yes | - | A description of the link that appears below it |
3838
| `className` | string | No | 'link-card--default' | Additional CSS class names to apply to the component |
39+
| `...props` | - | No | - | Additional props are passed to the root div element |
3940

4041
## Responsive Behavior
4142

@@ -63,7 +64,8 @@ The component uses the `LinkCard.scss` file for styling. You can customize the a
6364

6465
## Internal Helpers
6566

66-
The component uses a `renderTextWithLineBreaks` utility function to properly format text with line breaks, converting newline characters into React's `<br />` elements.
67+
The component uses a `renderTextWithLineBreaks` utility function to properly format text with line breaks,
68+
converting newline characters into React's `<br />` elements.
6769

6870
## Accessibility
6971

@@ -74,6 +76,38 @@ The component follows accessibility best practices:
7476
- Interactive elements are keyboard navigable
7577
- Sufficient color contrast for text elements
7678

79+
## Testing
80+
81+
The component includes specific data attributes to facilitate testing:
82+
83+
- `data-testid="link-card-description"` on the description container
84+
85+
When testing the component, you can:
86+
87+
- Pass your own test IDs through props to the root element
88+
- Use the built-in data-testid for targeting the description container
89+
- Test the truncation and expansion behavior using the show more/less functionality
90+
91+
Example test for checking description content:
92+
93+
```jsx
94+
import { render, screen } from '@testing-library/react';
95+
import LinkCard from './LinkCard';
96+
97+
it('displays the description text', () => {
98+
render(
99+
<LinkCard
100+
href="https://example.com"
101+
text="Example Link"
102+
description="Description text"
103+
/>
104+
);
105+
106+
const descriptionElement = screen.getByTestId('link-card-description');
107+
expect(descriptionElement).toHaveTextContent('Description text');
108+
});
109+
```
110+
77111
### Custom Styling Example
78112

79113
```scss

0 commit comments

Comments
 (0)