Skip to content

Commit eef5bed

Browse files
committed
test: add component and page integration tests with mocking for core features and UI states
1 parent 4138047 commit eef5bed

6 files changed

Lines changed: 556 additions & 4 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4+
import DashboardPage, { generateMetadata } from './page';
5+
import { getFullDashboardData } from '@/lib/github';
6+
7+
vi.mock('@/lib/github', () => ({
8+
getFullDashboardData: vi.fn(),
9+
}));
10+
11+
// Mock the dashboard components to keep the test focused on the page rendering logic
12+
vi.mock('@/components/dashboard/ProfileCard', () => ({
13+
default: () => <div data-testid="profile-card">ProfileCard</div>,
14+
}));
15+
vi.mock('@/components/dashboard/ActivityLandscape', () => ({
16+
default: () => <div data-testid="activity-landscape">ActivityLandscape</div>,
17+
}));
18+
vi.mock('@/components/dashboard/StatsCard', () => ({
19+
default: ({ title, value }: any) => (
20+
<div data-testid="stats-card">
21+
{title}: {value}
22+
</div>
23+
),
24+
}));
25+
vi.mock('@/components/dashboard/LanguageChart', () => ({
26+
default: () => <div data-testid="language-chart">LanguageChart</div>,
27+
}));
28+
vi.mock('@/components/dashboard/CommitClock', () => ({
29+
default: () => <div data-testid="commit-clock">CommitClock</div>,
30+
}));
31+
vi.mock('@/components/dashboard/Heatmap', () => ({
32+
default: () => <div data-testid="heatmap">Heatmap</div>,
33+
}));
34+
vi.mock('@/components/dashboard/AIInsights', () => ({
35+
default: () => <div data-testid="ai-insights">AIInsights</div>,
36+
}));
37+
vi.mock('@/components/dashboard/Achievements', () => ({
38+
default: () => <div data-testid="achievements">Achievements</div>,
39+
}));
40+
41+
describe('DashboardPage', () => {
42+
const mockData = {
43+
profile: {
44+
username: 'octocat',
45+
name: 'The Octocat',
46+
avatarUrl: 'avatar.png',
47+
isPro: true,
48+
bio: 'Hello world',
49+
location: 'Earth',
50+
joinedDate: 'Jan 2020',
51+
developerScore: 90,
52+
stats: { repositories: 10, followers: 20, following: 5, stars: 100 },
53+
},
54+
stats: {
55+
currentStreak: 5,
56+
peakStreak: 15,
57+
totalContributions: 500,
58+
},
59+
languages: [{ name: 'TypeScript', percentage: 100, color: '#3178c6' }],
60+
activity: [],
61+
insights: [],
62+
commitClock: [],
63+
};
64+
65+
beforeEach(() => {
66+
vi.clearAllMocks();
67+
vi.mocked(getFullDashboardData).mockResolvedValue(mockData);
68+
});
69+
70+
afterEach(() => {
71+
vi.restoreAllMocks();
72+
});
73+
74+
describe('generateMetadata', () => {
75+
it('generates correct metadata for a given user', async () => {
76+
const metadata = await generateMetadata({ params: Promise.resolve({ username: 'octocat' }) });
77+
78+
expect(metadata.title).toBe("octocat's Commit Pulse");
79+
expect(metadata.description).toContain("octocat's GitHub contribution pulse");
80+
expect((metadata.openGraph?.images as any[])?.[0].url).toContain('api/og?username=octocat');
81+
});
82+
});
83+
84+
describe('DashboardPage rendering', () => {
85+
it('renders the dashboard components with the fetched data', async () => {
86+
const PageContent = await DashboardPage({ params: Promise.resolve({ username: 'octocat' }) });
87+
render(PageContent);
88+
89+
// Verify data fetching
90+
expect(getFullDashboardData).toHaveBeenCalledWith('octocat');
91+
92+
// Verify layout and component presence
93+
expect(screen.getByText('Generate Your Own Dashboard')).toBeDefined();
94+
expect(screen.getByTestId('profile-card')).toBeDefined();
95+
expect(screen.getByTestId('activity-landscape')).toBeDefined();
96+
expect(screen.getByTestId('language-chart')).toBeDefined();
97+
expect(screen.getByTestId('commit-clock')).toBeDefined();
98+
expect(screen.getByTestId('heatmap')).toBeDefined();
99+
expect(screen.getByTestId('ai-insights')).toBeDefined();
100+
expect(screen.getByTestId('achievements')).toBeDefined();
101+
102+
// Verify stats cards mapped correctly
103+
expect(screen.getByText('Current Streak: 5')).toBeDefined();
104+
expect(screen.getByText('Peak Streak: 15')).toBeDefined();
105+
expect(screen.getByText('Contributions: 500')).toBeDefined();
106+
});
107+
});
108+
});

app/page.test.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @next/next/no-img-element, jsx-a11y/alt-text */
3+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5+
import LandingPage from './page';
6+
7+
// Mock child components to isolate LandingPage testing
8+
vi.mock('./components/CustomizeCTA', () => ({
9+
CustomizeCTA: () => <div data-testid="customize-cta">Customize CTA</div>,
10+
}));
11+
12+
vi.mock('@/components/commitpulse-logo', () => ({
13+
CommitPulseLogo: () => <svg data-testid="commitpulse-logo"></svg>,
14+
}));
15+
16+
vi.mock('next/image', () => ({
17+
default: (props: any) => <img {...props} data-testid="next-image" />,
18+
}));
19+
20+
vi.mock('next/link', () => ({
21+
default: ({ children, href, ...props }: any) => (
22+
<a href={href} {...props} data-testid="next-link">
23+
{children}
24+
</a>
25+
),
26+
}));
27+
28+
// Mock framer-motion
29+
vi.mock('framer-motion', () => ({
30+
motion: {
31+
div: ({ children, className, ...props }: any) => (
32+
<div className={className} data-testid="motion-div" {...props}>
33+
{children}
34+
</div>
35+
),
36+
p: ({ children, className, ...props }: any) => (
37+
<p className={className} data-testid="motion-p" {...props}>
38+
{children}
39+
</p>
40+
),
41+
},
42+
AnimatePresence: ({ children }: any) => <>{children}</>,
43+
}));
44+
45+
describe('LandingPage', () => {
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
49+
// Mock navigator.clipboard
50+
Object.assign(navigator, {
51+
clipboard: {
52+
writeText: vi.fn().mockResolvedValue(undefined),
53+
},
54+
});
55+
56+
// Mock scrollIntoView
57+
window.HTMLElement.prototype.scrollIntoView = vi.fn();
58+
});
59+
60+
afterEach(() => {
61+
vi.restoreAllMocks();
62+
});
63+
64+
it('renders the main heading', () => {
65+
render(<LandingPage />);
66+
expect(screen.getByText(/Elevate Your/i)).toBeDefined();
67+
expect(screen.getByText(/Contribution Story/i)).toBeDefined();
68+
});
69+
70+
it('renders the input field with default username', () => {
71+
render(<LandingPage />);
72+
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
73+
expect(input).toBeDefined();
74+
expect(input.value).toBe('jhasourav07');
75+
});
76+
77+
it('updates the username when input changes', () => {
78+
render(<LandingPage />);
79+
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
80+
81+
fireEvent.change(input, { target: { value: 'octocat' } });
82+
expect(input.value).toBe('octocat');
83+
84+
// The image src should also update
85+
const image = screen.getByTestId('next-image') as HTMLImageElement;
86+
expect(image.src).toContain('user=octocat');
87+
});
88+
89+
it('handles copying to clipboard and showing the SuccessGuide', async () => {
90+
render(<LandingPage />);
91+
92+
const copyButton = screen.getByText('Copy Link').closest('button');
93+
fireEvent.click(copyButton!);
94+
95+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
96+
expect.stringContaining(
97+
'![CommitPulse](https://commitpulse.vercel.app/api/streak?user=jhasourav07)'
98+
)
99+
);
100+
101+
await waitFor(() => {
102+
// The button text should change to Copied
103+
expect(screen.getByText('Copied')).toBeDefined();
104+
// The SuccessGuide should appear
105+
expect(screen.getByText('Your Monolith is Ready - Deploy It in 4 Steps')).toBeDefined();
106+
});
107+
});
108+
109+
it('renders the FeatureCards', () => {
110+
render(<LandingPage />);
111+
expect(screen.getByText('Real-time Sync')).toBeDefined();
112+
expect(screen.getByText('Theme Engine')).toBeDefined();
113+
expect(screen.getByText('Isometric Math')).toBeDefined();
114+
});
115+
116+
it('renders the CustomizeCTA', () => {
117+
render(<LandingPage />);
118+
expect(screen.getByTestId('customize-cta')).toBeDefined();
119+
});
120+
121+
it('can dismiss the SuccessGuide', async () => {
122+
render(<LandingPage />);
123+
124+
// Trigger copy to show guide
125+
const copyButton = screen.getByText('Copy Link').closest('button');
126+
fireEvent.click(copyButton!);
127+
128+
await waitFor(() => {
129+
expect(screen.getByText('Your Monolith is Ready - Deploy It in 4 Steps')).toBeDefined();
130+
});
131+
132+
// Dismiss guide
133+
const dismissButton = screen.getByLabelText('Dismiss guide');
134+
fireEvent.click(dismissButton);
135+
136+
await waitFor(() => {
137+
expect(screen.queryByText('Your Monolith is Ready - Deploy It in 4 Steps')).toBeNull();
138+
});
139+
});
140+
});
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4+
import ShareSheet from './ShareSheet';
5+
6+
// Mock html-to-image
7+
vi.mock('html-to-image', () => ({
8+
toPng: vi.fn().mockResolvedValue('data:image/png;base64,mockdata'),
9+
}));
10+
11+
// Mock framer-motion to avoid animation issues in tests
12+
vi.mock('framer-motion', () => ({
13+
motion: {
14+
div: ({ children, className, onClick, ...props }: any) => (
15+
<div className={className} onClick={onClick} data-testid="motion-div" {...props}>
16+
{children}
17+
</div>
18+
),
19+
button: ({ children, className, onClick, disabled, ...props }: any) => (
20+
<button
21+
className={className}
22+
onClick={onClick}
23+
disabled={disabled}
24+
data-testid="motion-button"
25+
{...props}
26+
>
27+
{children}
28+
</button>
29+
),
30+
},
31+
AnimatePresence: ({ children }: any) => <>{children}</>,
32+
}));
33+
34+
describe('ShareSheet', () => {
35+
const defaultProps = {
36+
username: 'octocat',
37+
isOpen: true,
38+
onClose: vi.fn(),
39+
};
40+
41+
beforeEach(() => {
42+
vi.clearAllMocks();
43+
44+
// Mock navigator.clipboard
45+
Object.assign(navigator, {
46+
clipboard: {
47+
writeText: vi.fn().mockResolvedValue(undefined),
48+
},
49+
});
50+
51+
// Mock window.open
52+
vi.spyOn(window, 'open').mockImplementation(() => null);
53+
});
54+
55+
afterEach(() => {
56+
vi.restoreAllMocks();
57+
});
58+
59+
it('does not render when isOpen is false', () => {
60+
const { container } = render(<ShareSheet {...defaultProps} isOpen={false} />);
61+
expect(container.firstChild).toBeNull();
62+
});
63+
64+
it('renders correctly when isOpen is true', () => {
65+
render(<ShareSheet {...defaultProps} />);
66+
expect(screen.getByText('Share Pulse')).toBeDefined();
67+
expect(screen.getByText('@octocat')).toBeDefined();
68+
expect(screen.getByText('Copy Link')).toBeDefined();
69+
expect(screen.getByText('Share on X')).toBeDefined();
70+
});
71+
72+
it('calls onClose when close button is clicked', () => {
73+
render(<ShareSheet {...defaultProps} />);
74+
const closeButton = screen.getByRole('button', { name: /close/i });
75+
fireEvent.click(closeButton);
76+
expect(defaultProps.onClose).toHaveBeenCalled();
77+
});
78+
79+
it('calls onClose when overlay is clicked', () => {
80+
render(<ShareSheet {...defaultProps} />);
81+
const overlay = screen.getAllByTestId('motion-div')[0];
82+
fireEvent.click(overlay);
83+
expect(defaultProps.onClose).toHaveBeenCalled();
84+
});
85+
86+
it('handles Copy Link action', async () => {
87+
render(<ShareSheet {...defaultProps} />);
88+
const copyButton = screen.getByText('Copy Link').closest('button');
89+
fireEvent.click(copyButton!);
90+
91+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining('octocat'));
92+
93+
await waitFor(() => {
94+
expect(screen.getByText('Link Copied!')).toBeDefined();
95+
});
96+
});
97+
98+
it('handles Share on X action', () => {
99+
render(<ShareSheet {...defaultProps} />);
100+
const twitterButton = screen.getByText('Share on X').closest('button');
101+
fireEvent.click(twitterButton!);
102+
103+
expect(window.open).toHaveBeenCalledWith(
104+
expect.stringContaining('twitter.com/intent/tweet'),
105+
'_blank',
106+
'noopener'
107+
);
108+
expect(defaultProps.onClose).toHaveBeenCalled();
109+
});
110+
111+
it('handles Share on LinkedIn action', () => {
112+
render(<ShareSheet {...defaultProps} />);
113+
const linkedinButton = screen.getByText('Share on LinkedIn').closest('button');
114+
fireEvent.click(linkedinButton!);
115+
116+
expect(window.open).toHaveBeenCalledWith(
117+
expect.stringContaining('linkedin.com/sharing/share-offsite'),
118+
'_blank',
119+
'noopener'
120+
);
121+
expect(defaultProps.onClose).toHaveBeenCalled();
122+
});
123+
124+
it('handles Download PNG action', async () => {
125+
render(<ShareSheet {...defaultProps} />);
126+
127+
// Create a mock document element to satisfy the selector
128+
const mockRoot = document.createElement('div');
129+
mockRoot.id = 'dashboard-root';
130+
document.body.appendChild(mockRoot);
131+
132+
const downloadButton = screen.getByText('Download as PNG').closest('button');
133+
fireEvent.click(downloadButton!);
134+
135+
const { toPng } = await import('html-to-image');
136+
expect(toPng).toHaveBeenCalled();
137+
138+
await waitFor(() => {
139+
expect(screen.getByText('Downloaded!')).toBeDefined();
140+
});
141+
142+
document.body.removeChild(mockRoot);
143+
});
144+
});

0 commit comments

Comments
 (0)