Skip to content

Commit 7416133

Browse files
Merge branch 'main' into test-validation-timezone-boundary
2 parents e60f4ca + e85dcd5 commit 7416133

25 files changed

Lines changed: 1050 additions & 230 deletions

app/api/streak/route.test.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ describe('GET /api/streak', () => {
235235
const body = await response.text();
236236

237237
expect(body).toContain('<svg');
238+
expect(body).toContain('viewBox');
239+
expect(body).toContain('xmlns="http://www.w3.org/2000/svg"');
238240
expect(body).toContain('</svg>');
239241
});
240242

@@ -558,10 +560,10 @@ describe('GET /api/streak', () => {
558560
expect(body.details.fieldErrors.year[0]).toContain('GitHub was founded in 2008');
559561
});
560562

561-
it('returns 200 for unknown ?date= parameter (not part of schema)', async () => {
562-
const response = await GET(makeRequest({ user: 'octocat', date: '2026-15-40' }));
563-
expect(response.status).toBe(200);
564-
});
563+
// it('returns 200 for unknown ?date= parameter (not part of schema)', async () => {
564+
// const response = await GET(makeRequest({ user: 'octocat', date: '2026-15-40' }));
565+
// expect(response.status).toBe(200);
566+
// });
565567

566568
it('returns 400 for malformed numeric year', async () => {
567569
const response = await GET(makeRequest({ user: 'octocat', year: '100000' }));
@@ -611,6 +613,16 @@ describe('GET /api/streak', () => {
611613

612614
expect(response.status).toBe(200);
613615
});
616+
617+
describe('date parameter', () => {
618+
it('returns 400 when an invalid ISO8601 calendar date format like "2026-15-40" is supplied', async () => {
619+
const response = await GET(makeRequest({ user: 'octocat', date: '2026-15-40' }));
620+
const body = await response.json();
621+
622+
expect(response.status).toBe(400);
623+
expect(body.details.fieldErrors.date[0]).toContain('Invalid "date" format');
624+
});
625+
});
614626
});
615627

616628
describe('radius parameter', () => {
@@ -722,9 +734,15 @@ describe('GET /api/streak', () => {
722734

723735
expect(response.status).toBe(400);
724736
});
737+
725738
it('returns 400 when an invalid hex color is passed as accent', async () => {
726-
// #ZZZZZZZ contains non-hex characters — schema must reject it with 400
727-
const response = await GET(makeRequest({ user: 'octocat', accent: '#ZZZZZZZ' }));
739+
// #ZZZZZZ contains non-hex characters — schema must reject it with 400
740+
const response = await GET(makeRequest({ user: 'octocat', accent: '#ZZZZZZ' }));
741+
742+
expect(response.status).toBe(400);
743+
});
744+
it('returns 400 when another invalid hex color is passed as accent (Variation 4)', async () => {
745+
const response = await GET(makeRequest({ user: 'octocat', accent: '#ZZZZZZ' }));
728746

729747
expect(response.status).toBe(400);
730748
});

app/api/streak/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export async function GET(request: Request) {
9292
gradient,
9393
tz: tzParam,
9494
disable_particles,
95+
glow,
9596
} = parseResult.data;
9697

9798
const themeName = theme || 'dark';
@@ -160,6 +161,7 @@ export async function GET(request: Request) {
160161
shading,
161162
gradient,
162163
disable_particles,
164+
glow,
163165
};
164166

165167
let calendar;

app/components/Icons.tsx

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,21 @@
1+
import { Copy, Zap, Box, Check, X } from 'lucide-react';
2+
13
export function CopyIcon() {
2-
return (
3-
<svg
4-
xmlns="http://www.w3.org/2000/svg"
5-
width="20"
6-
height="20"
7-
viewBox="0 0 24 24"
8-
fill="none"
9-
stroke="currentColor"
10-
strokeWidth="2"
11-
strokeLinecap="round"
12-
strokeLinejoin="round"
13-
>
14-
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
15-
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
16-
</svg>
17-
);
4+
return <Copy width={20} height={20} strokeWidth={2} />;
185
}
196

207
export function ZapIcon() {
21-
return (
22-
<svg
23-
xmlns="http://www.w3.org/2000/svg"
24-
width="24"
25-
height="24"
26-
viewBox="0 0 24 24"
27-
fill="none"
28-
stroke="currentColor"
29-
strokeWidth="2"
30-
strokeLinecap="round"
31-
strokeLinejoin="round"
32-
>
33-
<path d="M13 2 L3 14 L12 14 L11 22 L21 10 L12 10 L13 2 Z" />
34-
</svg>
35-
);
8+
return <Zap width={24} height={24} strokeWidth={2} />;
369
}
3710

3811
export function BoxIcon() {
39-
return (
40-
<svg
41-
xmlns="http://www.w3.org/2000/svg"
42-
width="24"
43-
height="24"
44-
viewBox="0 0 24 24"
45-
fill="none"
46-
stroke="currentColor"
47-
strokeWidth="2"
48-
strokeLinecap="round"
49-
strokeLinejoin="round"
50-
>
51-
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
52-
<path d="m7.5 4.27 9 5.15" />
53-
<path d="M3.29 7L12 12l8.71-5" />
54-
<path d="M12 22V12" />
55-
</svg>
56-
);
12+
return <Box width={24} height={24} strokeWidth={2} />;
5713
}
5814

5915
export function CheckIcon() {
60-
return (
61-
<svg
62-
xmlns="http://www.w3.org/2000/svg"
63-
width="20"
64-
height="20"
65-
viewBox="0 0 24 24"
66-
fill="none"
67-
stroke="#10b981"
68-
strokeWidth="3"
69-
strokeLinecap="round"
70-
strokeLinejoin="round"
71-
>
72-
<polyline points="20 6 9 17 4 12" />
73-
</svg>
74-
);
16+
return <Check width={20} height={20} strokeWidth={3} stroke="#10b981" />;
7517
}
7618

7719
export function CloseIcon() {
78-
return (
79-
<svg
80-
xmlns="http://www.w3.org/2000/svg"
81-
width="18"
82-
height="18"
83-
viewBox="0 0 24 24"
84-
fill="none"
85-
stroke="currentColor"
86-
strokeWidth="2.5"
87-
strokeLinecap="round"
88-
strokeLinejoin="round"
89-
>
90-
<line x1="18" y1="6" x2="6" y2="18" />
91-
<line x1="6" y1="6" x2="18" y2="18" />
92-
</svg>
93-
);
20+
return <X width={18} height={18} strokeWidth={2.5} />;
9421
}

app/customize/components/ThemeSelector.test.tsx

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -137,65 +137,57 @@ describe('ThemeSelector - Custom Variations (Variation 4)', () => {
137137
});
138138
});
139139

140-
describe('ThemeSelector - Custom Variations (Variation 4)', () => {
140+
describe('ThemeSelector responsive rendering', () => {
141141
const onThemeChange = vi.fn();
142142

143143
afterEach(() => {
144144
vi.clearAllMocks();
145145
});
146146

147-
it('renders ThemeSelector successfully and layout structure exists', () => {
148-
const { container } = render(<ThemeSelector theme="dark" onThemeChange={onThemeChange} />);
147+
it('check if the root container holds its flex-col layout when switch from Dracula -> Neon', () => {
148+
const { container, rerender } = render(
149+
<ThemeSelector theme="dracula" onThemeChange={onThemeChange} />
150+
);
151+
const root = container.firstChild as HTMLElement;
152+
expect(root.className).toContain('flex-col');
149153

150-
// ThemeSelector renders successfully
151-
expect(screen.getByRole('combobox')).toBeTruthy();
152-
expect(screen.getByText('Theme Preset')).toBeTruthy();
153-
154-
// Layout structure exists
155-
const mainContainer = container.firstChild as HTMLElement;
156-
expect(mainContainer).toBeTruthy();
157-
expect(mainContainer.className).toContain('flex');
158-
expect(mainContainer.className).toContain('flex-col');
159-
expect(mainContainer.className).toContain('gap-1.5');
154+
rerender(<ThemeSelector theme="neon" onThemeChange={onThemeChange} />);
155+
expect(root.className).toContain('flex-col');
160156
});
161157

162-
it('verifies that selecting Dracula preset calls onThemeChange("dracula")', async () => {
163-
const user = userEvent.setup();
164-
render(<ThemeSelector theme="dark" onThemeChange={onThemeChange} />);
158+
it('check if active preset button updates when switching from Dracula to Neon', () => {
159+
const { rerender } = render(<ThemeSelector theme="dracula" onThemeChange={onThemeChange} />);
165160

166-
// Select Dracula preset via accessibility query
167-
const draculaBtn = screen.getByRole('button', { name: /apply dracula theme/i });
168-
expect(draculaBtn).toBeTruthy();
161+
expect(
162+
screen.getByRole('button', { name: /apply dracula theme/i }).getAttribute('aria-pressed')
163+
).toBe('true');
169164

170-
// Click it
171-
await user.click(draculaBtn);
165+
rerender(<ThemeSelector theme="neon" onThemeChange={onThemeChange} />);
172166

173-
// Verify onThemeChange was called with 'dracula'
174-
expect(onThemeChange).toHaveBeenCalledWith('dracula');
167+
expect(
168+
screen.getByRole('button', { name: /apply neon theme/i }).getAttribute('aria-pressed')
169+
).toBe('true');
170+
expect(
171+
screen.getByRole('button', { name: /apply dracula theme/i }).getAttribute('aria-pressed')
172+
).toBe('false');
175173
});
176174

177-
it('verifies that selecting Neon preset calls onThemeChange("neon")', async () => {
178-
const user = userEvent.setup();
179-
render(<ThemeSelector theme="dark" onThemeChange={onThemeChange} />);
175+
it('check if select value reflects the active theme after switch Dracula -> Neon', () => {
176+
const { rerender } = render(<ThemeSelector theme="dracula" onThemeChange={onThemeChange} />);
177+
const select = screen.getByRole('combobox') as HTMLSelectElement;
178+
expect(select.value).toBe('dracula');
180179

181-
// Select Neon preset via accessibility query
182-
const neonBtn = screen.getByRole('button', { name: /apply neon theme/i });
183-
expect(neonBtn).toBeTruthy();
184-
185-
// Click it
186-
await user.click(neonBtn);
187-
188-
// Verify onThemeChange was called with 'neon'
189-
expect(onThemeChange).toHaveBeenCalledWith('neon');
180+
rerender(<ThemeSelector theme="neon" onThemeChange={onThemeChange} />);
181+
expect(select.value).toBe('neon');
190182
});
191183

192-
it('verifies the select dropdown calls onThemeChange with the selected theme', async () => {
193-
const user = userEvent.setup();
194-
render(<ThemeSelector theme="dark" onThemeChange={onThemeChange} />);
195-
196-
const select = screen.getByRole('combobox');
197-
await user.selectOptions(select, 'sunset');
184+
it('colour swatches update to the new theme after switching', () => {
185+
const { rerender } = render(<ThemeSelector theme="dracula" onThemeChange={onThemeChange} />);
186+
const draculaSwatches = screen.getAllByTitle(/^(bg|accent|text):/i);
187+
expect(draculaSwatches.length).toBe(3);
198188

199-
expect(onThemeChange).toHaveBeenCalledWith('sunset');
189+
rerender(<ThemeSelector theme="neon" onThemeChange={onThemeChange} />);
190+
// swatches should still be 3, now reflecting neon's palette
191+
expect(screen.getAllByTitle(/^(bg|accent|text):/i).length).toBe(3);
200192
});
201193
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { it, expect, vi } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import ActivityLandscape from './ActivityLandscape';
4+
5+
vi.mock('framer-motion', async () => {
6+
const actual = await vi.importActual<typeof import('framer-motion')>('framer-motion');
7+
8+
return {
9+
...actual,
10+
motion: {
11+
...actual.motion,
12+
div: ({ children, ...props }: React.ComponentProps<'div'>) => (
13+
<div {...props}>{children}</div>
14+
),
15+
},
16+
};
17+
});
18+
const mockData = Array.from({ length: 100 }, (_, i) => ({
19+
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
20+
count: i + 1,
21+
intensity: (i % 5) as 0 | 1 | 2 | 3 | 4,
22+
}));
23+
it('renders Activity Landscape heading', () => {
24+
render(<ActivityLandscape data={mockData} />);
25+
26+
expect(screen.getByText('Activity Landscape')).toBeTruthy();
27+
});
28+
it('renders all tab buttons', () => {
29+
render(<ActivityLandscape data={mockData} />);
30+
31+
expect(screen.getByText('1W')).toBeTruthy();
32+
expect(screen.getByText('1M')).toBeTruthy();
33+
expect(screen.getByText('3M')).toBeTruthy();
34+
expect(screen.getByText('1Y')).toBeTruthy();
35+
});
36+
it('has 3M active by default', () => {
37+
render(<ActivityLandscape data={mockData} />);
38+
39+
const tab = screen.getByText('3M');
40+
41+
expect(tab.className).toContain('bg-black');
42+
});
43+
it('activates 1W tab when clicked', () => {
44+
render(<ActivityLandscape data={mockData} />);
45+
46+
const tab = screen.getByText('1W');
47+
48+
fireEvent.click(tab);
49+
50+
expect(tab.className).toContain('bg-black');
51+
});
52+
it('renders activity chart', () => {
53+
render(<ActivityLandscape data={mockData} />);
54+
55+
expect(screen.getByText('Activity Landscape')).toBeTruthy();
56+
expect(screen.getByText('Commit frequency over time')).toBeTruthy();
57+
});
58+
it('renders with empty data without crashing', () => {
59+
render(<ActivityLandscape data={[]} />);
60+
61+
expect(screen.getByText('Activity Landscape')).toBeTruthy();
62+
});

0 commit comments

Comments
 (0)