Skip to content

Commit 062f032

Browse files
authored
Merge branch 'main' into issue-resume-preview-error-resilience
2 parents 27e54ae + adc7f67 commit 062f032

17 files changed

Lines changed: 963 additions & 93 deletions

README.md

Lines changed: 74 additions & 73 deletions
Large diffs are not rendered by default.

app/api/og/route.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const displayDomain = (() => {
2121

2222
function getLuminance(hex: string) {
2323
let normalizedHex = hex.trim();
24-
// Normalize short hex (e.g., #fff or #ffff) to #rrggbb (alpha is ignored for luminance)
2524
if (normalizedHex.length === 4 || normalizedHex.length === 5) {
2625
normalizedHex = `#${normalizedHex[1]}${normalizedHex[1]}${normalizedHex[2]}${normalizedHex[2]}${normalizedHex[3]}${normalizedHex[3]}`;
2726
}
@@ -37,12 +36,20 @@ function getLuminance(hex: string) {
3736

3837
export async function GET(req: NextRequest) {
3938
const { searchParams } = new URL(req.url);
40-
const parsed = ogParamsSchema.parse(Object.fromEntries(searchParams.entries()));
41-
let { user } = parsed;
42-
const { theme, bg, text, accent } = parsed;
4339

44-
// Sanitize user: limit to 39 chars (GitHub max length) and strip invalid chars
45-
user = user.slice(0, 39).replace(/[^a-zA-Z0-9-]/g, '');
40+
const parseResult = ogParamsSchema.safeParse(Object.fromEntries(searchParams.entries()));
41+
42+
if (!parseResult.success) {
43+
return new Response(
44+
JSON.stringify({ error: 'Invalid parameters', details: parseResult.error.flatten() }),
45+
{
46+
status: 400,
47+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
48+
}
49+
);
50+
}
51+
52+
const { user, theme, bg, text, accent, refresh } = parseResult.data;
4653

4754
const selectedTheme = themes[theme] || themes.dark;
4855
const resolvedBg = `#${bg || selectedTheme.bg}`;
@@ -59,19 +66,23 @@ export async function GET(req: NextRequest) {
5966
let longestStreak = 0;
6067
let currentStreak = 0;
6168

62-
// Only the data fetching is wrapped in try/catch — not the JSX rendering.
6369
try {
64-
const userData = await fetchGitHubContributions(user, { bypassCache: true });
65-
const calendar = userData.calendar;
66-
const stats = calculateStreak(calendar);
70+
// bypassCache mirrors the ?refresh=true pattern used by /api/stats and /api/streak.
71+
// Without this, every link-preview bot crawl fires a fresh GitHub GraphQL request,
72+
// burning API quota on an endpoint that is embedded in every page's <meta> tag.
73+
const data = await fetchGitHubContributions(user, { bypassCache: refresh });
74+
const stats = calculateStreak(data.calendar ?? data);
6775
totalCommits = stats.totalContributions;
6876
longestStreak = stats.longestStreak;
6977
currentStreak = stats.currentStreak;
7078
} catch (err) {
7179
console.error('[OG] stats fetch failed:', err);
72-
// fallback to zeros if GitHub is unreachable
7380
}
7481

82+
const cacheControl = refresh
83+
? 'no-cache, no-store, must-revalidate'
84+
: 'public, max-age=3600, stale-while-revalidate=86400';
85+
7586
return new ImageResponse(
7687
<div
7788
style={{
@@ -112,7 +123,6 @@ export async function GET(req: NextRequest) {
112123
{`@${user}`}
113124
</div>
114125
<div style={{ display: 'flex', gap: '48px' }}>
115-
{/* Total Commits */}
116126
<div
117127
style={{
118128
display: 'flex',
@@ -133,7 +143,6 @@ export async function GET(req: NextRequest) {
133143
Total Commits
134144
</div>
135145
</div>
136-
{/* Longest Streak */}
137146
<div
138147
style={{
139148
display: 'flex',
@@ -154,7 +163,6 @@ export async function GET(req: NextRequest) {
154163
{'Longest Streak 🔥'}
155164
</div>
156165
</div>
157-
{/* Current Streak */}
158166
<div
159167
style={{
160168
display: 'flex',
@@ -192,7 +200,7 @@ export async function GET(req: NextRequest) {
192200
width: 1200,
193201
height: 630,
194202
headers: {
195-
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
203+
'Cache-Control': cacheControl,
196204
},
197205
}
198206
);

app/api/streak/route.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,12 @@ export async function GET(request: Request) {
197197
width,
198198
height,
199199
size,
200-
grace,
200+
201+
grace: Math.max(
202+
0,
203+
Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10))
204+
),
205+
201206
mode,
202207
repo,
203208
org,
@@ -208,7 +213,12 @@ export async function GET(request: Request) {
208213
gradient,
209214
gradient_stops,
210215
gradient_dir,
211-
opacity,
216+
217+
opacity: Math.max(
218+
0.1,
219+
Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0)))
220+
),
221+
212222
disable_particles,
213223
glow,
214224
animate,

app/components/navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default function Navbar() {
7474
};
7575

7676
return (
77-
<header className="relative z-50 px-4 pt-4 sm:px-6 w-full">
77+
<header className="relative px-4 pt-4 sm:px-6 w-full">
7878
<div className="mx-auto max-w-6xl">
7979
<div
8080
ref={shellRef}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import React, { Component, ErrorInfo, ReactNode } from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4+
import '@testing-library/jest-dom';
5+
import Template from './template';
6+
7+
// Mock telemetry tracker
8+
const mockTelemetryLogger = vi.fn();
9+
10+
// Localized Error Boundary for testing exception safety and fallbacks
11+
interface ErrorBoundaryProps {
12+
children: ReactNode;
13+
onReset: () => void;
14+
telemetryLogger?: (error: Error, errorInfo: ErrorInfo) => void;
15+
}
16+
17+
interface ErrorBoundaryState {
18+
hasError: boolean;
19+
error: Error | null;
20+
}
21+
22+
class LocalErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
23+
constructor(props: ErrorBoundaryProps) {
24+
super(props);
25+
this.state = { hasError: false, error: null };
26+
}
27+
28+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
29+
return { hasError: true, error };
30+
}
31+
32+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
33+
if (this.props.telemetryLogger) {
34+
this.props.telemetryLogger(error, errorInfo);
35+
}
36+
}
37+
38+
render() {
39+
if (this.state.hasError) {
40+
return (
41+
<div data-testid="error-boundary-fallback">
42+
<h1>Something went wrong.</h1>
43+
<p>{this.state.error?.message}</p>
44+
<button
45+
onClick={() => {
46+
this.setState({ hasError: false, error: null });
47+
this.props.onReset();
48+
}}
49+
>
50+
Try again
51+
</button>
52+
</div>
53+
);
54+
}
55+
56+
return this.props.children;
57+
}
58+
}
59+
60+
// Dummy components to simulate varied conditions
61+
const BuggyRuntimeChild = () => {
62+
throw new Error('Unexpected runtime exception!');
63+
};
64+
65+
const BuggyDatabaseChild = () => {
66+
throw new Error('Database connection timeout or anomaly!');
67+
};
68+
69+
// Start testing suite
70+
describe('Template Component: Hydration Stability, Exception Safety & Error Fallbacks', () => {
71+
let mockReset: ReturnType<typeof vi.fn>;
72+
73+
beforeEach(() => {
74+
mockReset = vi.fn();
75+
mockTelemetryLogger.mockClear();
76+
77+
// Suppress console.error in tests to keep output clean when errors are thrown intentionally
78+
vi.spyOn(console, 'error').mockImplementation(() => {});
79+
});
80+
81+
afterEach(() => {
82+
vi.restoreAllMocks();
83+
});
84+
85+
it('Test 1: Hydration Stability - renders valid children without crashing', () => {
86+
render(
87+
// @ts-expect-error Mock does not perfectly match the function signature but is callable
88+
<LocalErrorBoundary onReset={mockReset} telemetryLogger={mockTelemetryLogger}>
89+
<Template>
90+
<div data-testid="valid-child">Valid Content</div>
91+
</Template>
92+
</LocalErrorBoundary>
93+
);
94+
95+
// Assert that the target modules render cleanly
96+
expect(screen.getByTestId('valid-child')).toBeInTheDocument();
97+
expect(screen.queryByTestId('error-boundary-fallback')).not.toBeInTheDocument();
98+
});
99+
100+
it('Test 2: Exception Safety - catches unexpected runtime exceptions and renders localized boundary element', () => {
101+
render(
102+
// @ts-expect-error Mock does not perfectly match the function signature but is callable
103+
<LocalErrorBoundary onReset={mockReset} telemetryLogger={mockTelemetryLogger}>
104+
<Template>
105+
<BuggyRuntimeChild />
106+
</Template>
107+
</LocalErrorBoundary>
108+
);
109+
110+
// Assert that the site does not crash, but rather displays the localized error recovery UI
111+
expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument();
112+
expect(screen.getByText('Something went wrong.')).toBeInTheDocument();
113+
expect(screen.getByText('Unexpected runtime exception!')).toBeInTheDocument();
114+
});
115+
116+
it('Test 3: Dev-Telemetry - verifies exceptions are logged to dev-telemetry trackers appropriately', () => {
117+
render(
118+
// @ts-expect-error Mock does not perfectly match the function signature but is callable
119+
<LocalErrorBoundary onReset={mockReset} telemetryLogger={mockTelemetryLogger}>
120+
<Template>
121+
<BuggyRuntimeChild />
122+
</Template>
123+
</LocalErrorBoundary>
124+
);
125+
126+
// Assert that the mock telemetry logger was called with the error object
127+
expect(mockTelemetryLogger).toHaveBeenCalledTimes(1);
128+
const [errorArg] = mockTelemetryLogger.mock.calls[0];
129+
expect(errorArg).toBeInstanceOf(Error);
130+
expect(errorArg.message).toBe('Unexpected runtime exception!');
131+
});
132+
133+
it('Test 4: Error Fallbacks - isolates and handles mocked database connectivity errors properly', () => {
134+
render(
135+
// @ts-expect-error Mock does not perfectly match the function signature but is callable
136+
<LocalErrorBoundary onReset={mockReset} telemetryLogger={mockTelemetryLogger}>
137+
<Template>
138+
<BuggyDatabaseChild />
139+
</Template>
140+
</LocalErrorBoundary>
141+
);
142+
143+
// Assert that database connectivity anomalies trigger the error recovery fallback
144+
expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument();
145+
expect(screen.getByText('Database connection timeout or anomaly!')).toBeInTheDocument();
146+
});
147+
148+
it('Test 5: Reset/Reload Paths - ensures user reset/reload paths are available on recovery panels and functioning', () => {
149+
render(
150+
// @ts-expect-error Mock does not perfectly match the function signature but is callable
151+
<LocalErrorBoundary onReset={mockReset} telemetryLogger={mockTelemetryLogger}>
152+
<Template>
153+
<BuggyRuntimeChild />
154+
</Template>
155+
</LocalErrorBoundary>
156+
);
157+
158+
// Verify recovery panel is present
159+
expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument();
160+
161+
// Find and click the 'Try again' reset button
162+
const resetButton = screen.getByRole('button', { name: /try again/i });
163+
expect(resetButton).toBeInTheDocument();
164+
165+
fireEvent.click(resetButton);
166+
167+
// Verify the mockReset callback is executed when the user initiates a reset path
168+
expect(mockReset).toHaveBeenCalledTimes(1);
169+
});
170+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom/vitest';
4+
import CherryBlossom from './CherryBlossom';
5+
import type React from 'react';
6+
7+
vi.mock('framer-motion', () => ({
8+
motion: {
9+
div: ({
10+
children,
11+
...props
12+
}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) => (
13+
<div data-testid="motion-div" {...props}>
14+
{children}
15+
</div>
16+
),
17+
},
18+
}));
19+
20+
describe('CherryBlossom accessibility', () => {
21+
beforeEach(() => {
22+
vi.restoreAllMocks();
23+
});
24+
25+
it('renders as a decorative non-interactive overlay', async () => {
26+
const { container } = render(<CherryBlossom />);
27+
28+
await waitFor(() => {
29+
const overlay = container.querySelector('.pointer-events-none');
30+
expect(overlay).toBeInTheDocument();
31+
});
32+
});
33+
34+
it('contains no focusable interactive controls', async () => {
35+
const { container } = render(<CherryBlossom />);
36+
37+
await waitFor(() => {
38+
expect(container.querySelectorAll('button,input,textarea,select,a,[tabindex]')).toHaveLength(
39+
0
40+
);
41+
});
42+
});
43+
44+
it('does not expose interactive motion petals', async () => {
45+
render(<CherryBlossom />);
46+
47+
await waitFor(() => {
48+
const petals = screen.getAllByTestId('motion-div');
49+
50+
petals.forEach((petal) => {
51+
expect(petal).not.toHaveAttribute('role', 'button');
52+
});
53+
});
54+
});
55+
56+
it('maintains normal document accessibility flow', async () => {
57+
const { container } = render(<CherryBlossom />);
58+
59+
await waitFor(() => {
60+
const overlay = container.querySelector('.fixed.inset-0');
61+
expect(overlay).toBeInTheDocument();
62+
expect(overlay).toHaveClass('pointer-events-none');
63+
});
64+
});
65+
66+
it('renders decorative SVG content without accessibility violations', async () => {
67+
const { container } = render(<CherryBlossom />);
68+
69+
await waitFor(() => {
70+
const svgs = container.querySelectorAll('svg');
71+
expect(svgs.length).toBeGreaterThanOrEqual(27);
72+
});
73+
});
74+
});

0 commit comments

Comments
 (0)