Skip to content

Commit 4e2177a

Browse files
authored
Merge branch 'main' into test/student-profile-timezone-boundaries
2 parents 5b7c801 + c9602dd commit 4e2177a

5 files changed

Lines changed: 218 additions & 59 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 ProfileOptimizerModal from './ProfileOptimizerModal';
5+
6+
vi.mock('framer-motion', () => ({
7+
motion: {
8+
div: ({ children, className, ...props }: any) => {
9+
const clean = { ...props };
10+
delete clean.initial;
11+
delete clean.animate;
12+
delete clean.exit;
13+
delete clean.transition;
14+
delete clean.onClick;
15+
return (
16+
<div className={className} {...clean}>
17+
{children}
18+
</div>
19+
);
20+
},
21+
p: ({ children, className, ...props }: any) => {
22+
const clean = { ...props };
23+
delete clean.initial;
24+
delete clean.animate;
25+
delete clean.exit;
26+
return (
27+
<p className={className} {...clean}>
28+
{children}
29+
</p>
30+
);
31+
},
32+
},
33+
AnimatePresence: ({ children }: any) => <>{children}</>,
34+
}));
35+
36+
vi.mock('next/link', () => ({
37+
default: ({ children, href }: any) => <a href={href}>{children}</a>,
38+
}));
39+
40+
vi.mock('lucide-react', () => ({
41+
X: () => <span data-testid="icon-x">X</span>,
42+
Download: () => <span>Download</span>,
43+
Copy: () => <span>Copy</span>,
44+
CheckCircle: () => <span>CheckCircle</span>,
45+
TrendingUp: () => <span>TrendingUp</span>,
46+
AlertCircle: () => <span>AlertCircle</span>,
47+
}));
48+
49+
const mockUserData = {
50+
profile: {
51+
developerScore: 60,
52+
bio: 'Full-stack developer',
53+
stats: { repositories: 10, followers: 50 },
54+
},
55+
languages: [{ name: 'TypeScript' }, { name: 'Python' }],
56+
stats: { totalContributions: 200 },
57+
};
58+
59+
describe('ProfileOptimizerModal — Responsive Multi-device Columns & Mobile Viewport Layouts', () => {
60+
const originalInnerWidth = window.innerWidth;
61+
62+
beforeEach(() => {
63+
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 375 });
64+
window.dispatchEvent(new Event('resize'));
65+
});
66+
67+
afterEach(() => {
68+
Object.defineProperty(window, 'innerWidth', {
69+
writable: true,
70+
configurable: true,
71+
value: originalInnerWidth,
72+
});
73+
});
74+
75+
it('renders modal container without fixed absolute widths that cause horizontal scroll', () => {
76+
const { container } = render(
77+
<ProfileOptimizerModal isOpen={true} onClose={vi.fn()} userData={mockUserData} />
78+
);
79+
const modalDiv = container.querySelector('.max-w-3xl');
80+
expect(modalDiv).not.toBeNull();
81+
expect(modalDiv?.className).toContain('w-full');
82+
expect(modalDiv?.className).not.toContain('w-[');
83+
});
84+
85+
it('renders close button accessible on mobile viewport', () => {
86+
render(<ProfileOptimizerModal isOpen={true} onClose={vi.fn()} userData={mockUserData} />);
87+
const closeButton = screen.getByTestId('icon-x');
88+
expect(closeButton).toBeDefined();
89+
});
90+
91+
it('renders modal with vertical flex layout on small screens', () => {
92+
const { container } = render(
93+
<ProfileOptimizerModal isOpen={true} onClose={vi.fn()} userData={mockUserData} />
94+
);
95+
const flexModal = container.querySelector('.flex.flex-col');
96+
expect(flexModal).not.toBeNull();
97+
});
98+
99+
it('renders loading state with centered vertical layout on mobile', () => {
100+
const { container } = render(
101+
<ProfileOptimizerModal isOpen={true} onClose={vi.fn()} userData={mockUserData} />
102+
);
103+
const centered = container.querySelector('.flex-col.items-center.justify-center');
104+
expect(centered).not.toBeNull();
105+
});
106+
107+
it('does not render modal content when isOpen is false', () => {
108+
const { container } = render(
109+
<ProfileOptimizerModal isOpen={false} onClose={vi.fn()} userData={mockUserData} />
110+
);
111+
expect(container.firstChild).toBeNull();
112+
});
113+
});

components/dashboard/ResumePreviewForm.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState } from 'react';
44
import { motion } from 'framer-motion';
5-
import { Check, X, Loader2, Plus, Trash2 } from 'lucide-react';
5+
import { Check, Loader2, Plus } from 'lucide-react';
66
import { toast } from 'sonner';
77
import type { ParsedResume, Education, Experience } from '@/types/student';
88

@@ -194,7 +194,21 @@ export default function ResumePreviewForm({
194194
className="w-24 bg-transparent text-sm text-gray-900 outline-none dark:text-white"
195195
/>
196196
<button onClick={() => removeSkill(i)} className="text-red-400 hover:text-red-500">
197-
<X size={12} />
197+
<svg
198+
xmlns="http://www.w3.org/2000/svg"
199+
width="12"
200+
height="12"
201+
viewBox="0 0 24 24"
202+
fill="none"
203+
stroke="currentColor"
204+
strokeWidth="2"
205+
strokeLinecap="round"
206+
strokeLinejoin="round"
207+
className="lucide lucide-x"
208+
>
209+
<path d="M18 6 6 18" />
210+
<path d="m6 6 12 12" />
211+
</svg>
198212
</button>
199213
</div>
200214
))}
@@ -224,7 +238,24 @@ export default function ResumePreviewForm({
224238
onClick={() => removeEducation(i)}
225239
className="text-red-400 hover:text-red-500"
226240
>
227-
<Trash2 size={14} />
241+
<svg
242+
xmlns="http://www.w3.org/2000/svg"
243+
width="14"
244+
height="14"
245+
viewBox="0 0 24 24"
246+
fill="none"
247+
stroke="currentColor"
248+
strokeWidth="2"
249+
strokeLinecap="round"
250+
strokeLinejoin="round"
251+
className="lucide lucide-trash-2"
252+
>
253+
<path d="M3 6h18" />
254+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
255+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
256+
<line x1="10" x2="10" y1="11" y2="17" />
257+
<line x1="14" x2="14" y1="11" y2="17" />
258+
</svg>
228259
</button>
229260
</div>
230261
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
@@ -294,7 +325,24 @@ export default function ResumePreviewForm({
294325
onClick={() => removeExperience(i)}
295326
className="text-red-400 hover:text-red-500"
296327
>
297-
<Trash2 size={14} />
328+
<svg
329+
xmlns="http://www.w3.org/2000/svg"
330+
width="14"
331+
height="14"
332+
viewBox="0 0 24 24"
333+
fill="none"
334+
stroke="currentColor"
335+
strokeWidth="2"
336+
strokeLinecap="round"
337+
strokeLinejoin="round"
338+
className="lucide lucide-trash-2"
339+
>
340+
<path d="M3 6h18" />
341+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
342+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
343+
<line x1="10" x2="10" y1="11" y2="17" />
344+
<line x1="14" x2="14" y1="11" y2="17" />
345+
</svg>
298346
</button>
299347
</div>
300348
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">

0 commit comments

Comments
 (0)