Skip to content

Commit 06bb8e1

Browse files
authored
merge: fix(ui): restore ThemeSelector and add error resilience testing (#8042)
## Description Fixes #6888 This PR addresses the issue where `ThemeSelector` was causing an "Element type is invalid: expected a string... but got: undefined" crash. The component file was accidentally overwritten during previous testing. This PR restores the component implementation and adds a dedicated error resilience test suite (`ThemeSelector.error-resilience.test.tsx`) to guarantee the component renders gracefully even with invalid props, undefined themes, or unhandled child exceptions. It also resolves a merge conflict in `route.ts`. ## Pillar - [x] 🎨 Pillar 1 — New Theme Design - [x] 📐 Pillar 2 — Geometric SVG Improvement - [x] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview *(No visual changes, fixes a crash)* ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 3cf5ca4 + 51dd3dd commit 06bb8e1

6 files changed

Lines changed: 481 additions & 11 deletions

File tree

app/api/student/resume/upload/route.mock-integrations.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,150 @@ describe('API Route: Student Resume Upload (Mock Integrations)', () => {
131131
expect(parseResume).not.toHaveBeenCalled();
132132
});
133133
});
134+
135+
// 1. Mock the External Modules and Service Layers
136+
vi.mock('@/utils/getClientIp', () => ({
137+
getClientIp: vi.fn(() => '127.0.0.1'),
138+
}));
139+
140+
vi.mock('@/lib/rate-limit', () => {
141+
const RateLimiterMock = vi.fn();
142+
RateLimiterMock.prototype.check = vi.fn().mockResolvedValue(true);
143+
RateLimiterMock.prototype.checkWithResult = vi
144+
.fn()
145+
.mockResolvedValue({ success: true, limit: 10, remaining: 9, reset: 0 });
146+
return {
147+
RateLimiter: RateLimiterMock,
148+
getRateLimitHeaders: vi.fn().mockReturnValue(new Headers()),
149+
};
150+
});
151+
152+
vi.mock('@/lib/resume-parser', () => ({
153+
parseResume: vi.fn(),
154+
hasValidFileSignature: vi.fn().mockReturnValue(true),
155+
ALLOWED_MIME_TYPES: [
156+
'application/pdf',
157+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
158+
],
159+
MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
160+
}));
161+
162+
describe('API Route: Student Resume Upload (Mock Integrations)', () => {
163+
beforeEach(() => {
164+
vi.clearAllMocks();
165+
});
166+
167+
// THE FIX: Explicitly mock formData() to bypass Vitest parsing issues
168+
const createMockRequest = (
169+
fileName = 'resume.pdf',
170+
fileType = 'application/pdf',
171+
fieldName = 'resume'
172+
) => {
173+
const formData = new FormData();
174+
const file = new File(['dummy pdf buffer content'], fileName, { type: fileType });
175+
if (fieldName) formData.append(fieldName, file);
176+
177+
const req = new Request('http://localhost/api/student/resume/upload', {
178+
method: 'POST',
179+
// We don't attach the body here to avoid parsing crashes
180+
});
181+
182+
// We force the request to resolve our formData object when asked
183+
req.formData = vi.fn().mockResolvedValue(formData) as unknown as () => Promise<FormData>;
184+
185+
return req;
186+
};
187+
188+
it('1. should test service loading paths to ensure successful parsing returns 200 (mock success)', async () => {
189+
// Arrange: Mock the async service to succeed
190+
const mockParsedData = {
191+
name: 'Priyanuj',
192+
email: 'test@example.com',
193+
phone: '1234567890',
194+
skills: ['React', 'Next.js'],
195+
education: [],
196+
experience: [],
197+
};
198+
vi.mocked(parseResume).mockResolvedValueOnce(mockParsedData);
199+
200+
const req = createMockRequest();
201+
202+
// Act
203+
const res = await POST(req);
204+
const json = await res.json();
205+
206+
// Assert
207+
expect(res.status).toBe(200);
208+
expect(json.success).toBe(true);
209+
expect(json.data).toEqual(mockParsedData);
210+
expect(parseResume).toHaveBeenCalledTimes(1);
211+
});
212+
213+
it('2. should assert local cache layers (RateLimiter) block requests before triggering async services', async () => {
214+
// Arrange: Mock the RateLimiter cache to block the user
215+
vi.mocked(RateLimiter.prototype.checkWithResult).mockResolvedValueOnce({
216+
success: false,
217+
limit: 10,
218+
remaining: 0,
219+
reset: 0,
220+
});
221+
222+
const req = createMockRequest();
223+
224+
// Act
225+
const res = await POST(req);
226+
const json = await res.json();
227+
228+
// Assert: Blocked with 429, and parseResume is NEVER called
229+
expect(res.status).toBe(429);
230+
expect(json.error).toMatch(/Too many requests/i);
231+
expect(parseResume).not.toHaveBeenCalled();
232+
});
233+
234+
it('3. should verify correct fallback procedures during fake endpoint/parsing errors', async () => {
235+
// Arrange: Simulate the PDF parser crashing or timing out
236+
vi.mocked(parseResume).mockRejectedValueOnce(new Error('Parser timeout'));
237+
238+
const req = createMockRequest();
239+
240+
// Act
241+
const res = await POST(req);
242+
const json = await res.json();
243+
244+
// Assert: Route catches the error safely and returns 422
245+
expect(res.status).toBe(422);
246+
expect(json.success).toBe(false);
247+
expect(json.error).toMatch(/Failed to parse resume/i);
248+
});
249+
250+
it('4. should block invalid file signatures without hitting the parser service', async () => {
251+
// Arrange: Simulate a user renaming a .exe file to .pdf
252+
vi.mocked(hasValidFileSignature).mockReturnValueOnce(false);
253+
254+
const req = createMockRequest();
255+
256+
// Act
257+
const res = await POST(req);
258+
const json = await res.json();
259+
260+
// Assert: Fails security check (400) before reaching the async parser
261+
expect(res.status).toBe(400);
262+
expect(json.error).toMatch(/File content does not match its type/i);
263+
expect(parseResume).not.toHaveBeenCalled();
264+
});
265+
266+
it('5. should reject requests with missing formData fields without processing', async () => {
267+
// Arrange: Send a request where the file is named 'wrong_field' instead of 'resume'
268+
const req = createMockRequest('test.pdf', 'application/pdf', 'wrong_field_name');
269+
270+
// Act
271+
const res = await POST(req);
272+
const json = await res.json();
273+
274+
// Assert: Immediately fails validation (400)
275+
expect(res.status).toBe(400);
276+
expect(json.error).toMatch(/No resume file provided/i);
277+
expect(RateLimiter.prototype.checkWithResult).toHaveBeenCalled();
278+
expect(parseResume).not.toHaveBeenCalled();
279+
});
280+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { ThemeSelector } from './ThemeSelector';
5+
6+
// 1. MOCK ALL DEPENDENCIES FULLY
7+
vi.mock('./ThemeQuickPresets', () => ({
8+
ThemeQuickPresets: () => <div data-testid="mock-presets">Presets</div>,
9+
}));
10+
11+
vi.mock('./SectionLabel', () => ({
12+
SectionLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
13+
}));
14+
15+
vi.mock('../../../lib/svg/themes', () => ({
16+
themes: {
17+
dracula: { bg: '000', accent: '000', text: '000' },
18+
neon: { bg: '000', accent: '000', text: '000' },
19+
ocean: { bg: '000', accent: '000', text: '000' },
20+
sunset: { bg: '000', accent: '000', text: '000' },
21+
light: { bg: '000', accent: '000', text: '000' },
22+
dark: { bg: '000', accent: '000', text: '000' },
23+
},
24+
}));
25+
26+
vi.mock('../types', () => ({
27+
THEME_KEYS: ['dracula', 'neon', 'ocean', 'sunset', 'auto', 'random'],
28+
}));
29+
30+
// 2. Error Boundary
31+
class TestErrorBoundary extends React.Component<
32+
{ children: React.ReactNode },
33+
{ hasError: boolean }
34+
> {
35+
constructor(props: { children: React.ReactNode }) {
36+
super(props);
37+
this.state = { hasError: false };
38+
}
39+
static getDerivedStateFromError() {
40+
return { hasError: true };
41+
}
42+
render() {
43+
if (this.state.hasError) return <div data-testid="error-fallback">Error</div>;
44+
return this.props.children;
45+
}
46+
}
47+
48+
describe('ThemeSelector Resilience', () => {
49+
it('should render successfully with a valid theme', () => {
50+
render(
51+
<TestErrorBoundary>
52+
<ThemeSelector theme="dracula" onThemeChange={vi.fn()} />
53+
</TestErrorBoundary>
54+
);
55+
expect(screen.getByRole('combobox')).toBeInTheDocument();
56+
expect(screen.queryByTestId('error-fallback')).not.toBeInTheDocument();
57+
});
58+
59+
it('should handle invalid theme strings gracefully without crashing', () => {
60+
// This tests if the component handles bad inputs without throwing exceptions
61+
render(
62+
<TestErrorBoundary>
63+
<ThemeSelector theme="not-a-real-theme" onThemeChange={vi.fn()} />
64+
</TestErrorBoundary>
65+
);
66+
expect(screen.queryByTestId('error-fallback')).not.toBeInTheDocument();
67+
});
68+
69+
it('should render gracefully with undefined theme prop', () => {
70+
render(
71+
<TestErrorBoundary>
72+
<ThemeSelector theme={undefined as unknown as string} onThemeChange={vi.fn()} />
73+
</TestErrorBoundary>
74+
);
75+
expect(screen.queryByTestId('error-fallback')).not.toBeInTheDocument();
76+
});
77+
78+
it('should catch crashes in children via Error Boundary', () => {
79+
// Force a child component to crash
80+
const CrashingChild = () => {
81+
throw new Error('Crash');
82+
};
83+
84+
render(
85+
<TestErrorBoundary>
86+
<ThemeSelector theme="dracula" onThemeChange={vi.fn()} />
87+
<CrashingChild />
88+
</TestErrorBoundary>
89+
);
90+
91+
expect(screen.getByTestId('error-fallback')).toBeInTheDocument();
92+
});
93+
});

app/generator/components/SectionCard.error-resilience.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ describe('SectionCard Error Resilience', () => {
117117
);
118118

119119
expect(telemetrySpy).toHaveBeenCalledTimes(1);
120-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
120+
121121
const [errorArg] = telemetrySpy.mock.calls[0];
122122
expect(errorArg).toBeInstanceOf(Error);
123-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
123+
124124
expect((errorArg as Error).message).toBe('Standard runtime exception');
125125

126126
consoleErrorSpy.mockRestore();

0 commit comments

Comments
 (0)