Skip to content

Commit 607e2d9

Browse files
authored
Merge branch 'main' into test/generatorclient-type-compiler
2 parents 15e879b + 0167fb3 commit 607e2d9

46 files changed

Lines changed: 2142 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ NEXT_ALLOWED_DEV_ORIGINS=
5757
# Find your GitHub user ID at https://api.github.com/users/<username> (the "id" field).
5858
# Example: ENTERPRISE_ADMIN_GITHUB_IDS=12345678,87654321
5959
ENTERPRISE_ADMIN_GITHUB_IDS=
60+
61+
# Spotify Integration (Optional)
62+
# Required for the "Currently Playing" SVG feature.
63+
# Follow the setup guide in the documentation to get these credentials.
64+
SPOTIFY_CLIENT_ID=
65+
SPOTIFY_CLIENT_SECRET=
66+
SPOTIFY_REFRESH_TOKEN=

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ jobs:
4747
- name: Run Unit Tests (Vitest)
4848
run: npm run test:coverage
4949

50+
- name: Upload Visual Regression Diffs
51+
if: failure()
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: visual-test-diffs
55+
path: tests/visual/diffs/
56+
if-no-files-found: ignore
57+
5058
build:
5159
name: Production Build
5260
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
# testing
1414
/coverage
15+
/tests/visual/diffs/
1516

1617
# next.js
1718
/.next/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ For advanced usage examples including custom gradient backgrounds, multi-user co
167167
CommitPulse transforms GitHub contribution data into visually engaging and highly customizable SVG badges.
168168

169169
- **🎨 Theme & Customization**: Multiple built-in themes, custom colors (`bg`, `accent`, `text`), dynamic font selection, adjustable dimensions, border radius, opacity, and system-aware `auto` light/dark theme.
170+
- **🎵 Spotify "Currently Playing"**: Showcase your current Spotify playback on your GitHub profile with a customizable, near real-time SVG card.
170171
- **📈 Contribution Analytics**: Current streak and longest streak tracking, monthly contribution summaries, historical year-by-year viewing, and custom grace period configurations.
171172
- **🔥 Visualization Modes**: Isometric 3D monolith rendering (with ghost city blueprint foundations), GitHub-style heatmap, monthly statistics view, and radar chart view.
172173
- **🌍 Localization & Accessibility**: Multi-language support (e.g. English, Hindi, Simplified Chinese, Portuguese), timezone-aware calculations, and high-contrast accessibility themes.
@@ -182,6 +183,7 @@ To keep the repository clean and readable, technical details have been modulariz
182183
- **[🎨 Customization Guide & Parameters](docs/customization.md)**: Explore the list of over 30 URL parameters including `theme`, `view` (e.g. `skyline`, `heatmap`, `radar`, `monthly`), `radius`, `grace`, `tz`, `entrance`, `versus`, and layout dimensions to style your monolith.
183184
- **[🏛️ Architecture & Design Philosophy](docs/architecture.md)**: Read about why we built isometric 3D monolith landscapes instead of flat meters, and check out our Next.js 16 Edge computing pipeline.
184185
- **[🚀 Self-Hosting & Deployment](docs/self_hosting.md)**: Step-by-step instructions to clone, configure `.env.local` with GitHub Personal Access Tokens (PAT), set up MongoDB tracking, and deploy to Vercel with one click.
186+
- **[🎵 Spotify Setup Guide](docs/SPOTIFY_SETUP.md)**: Instructions for setting up Spotify integration for the Currently Playing feature.
185187
- **[🤖 Automated Contributor Workflow](docs/contributor_workflow.md)**: Overview of GSSoC contribution automation, self-claiming comments `/claim`, anti-hoarding rules, stale unassign scripts, and Gemini AI-powered semantic issue duplication check.
186188
- **[🎯 Real-Time Accuracy & Caching](docs/accuracy.md)**: Deep dive into the "off-by-N contributions" problem and how CommitPulse solves it with UTC midnight CDN expiration and no-store GraphQL fetches.
187189
- **[❓ FAQ & Troubleshooting](docs/faq.md)**: Answers to common questions regarding timezone overrides, private contribution visibility, GitHub API rate limits, and troubleshooting.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
vi.mock('@/lib/github', async (importOriginal) => {
4+
const actual = await importOriginal<typeof import('@/lib/github')>();
5+
return {
6+
...actual,
7+
getFullDashboardData: vi.fn(),
8+
};
9+
});
10+
11+
vi.mock('@/lib/githubtoken', () => ({
12+
getUserGitHubToken: vi.fn().mockResolvedValue(undefined),
13+
}));
14+
15+
vi.mock('@/lib/rate-limit', () => ({
16+
RateLimiter: vi.fn().mockImplementation(function () {
17+
return { check: vi.fn().mockResolvedValue(true) };
18+
}),
19+
}));
20+
21+
import { GET } from './route';
22+
import { getFullDashboardData } from '@/lib/github';
23+
24+
/**
25+
* Helper: build a mobile-flavoured request. We attach viewport hint headers
26+
* and mobile User-Agent strings so we can prove the /api/compare route is
27+
* device-agnostic — the JSON response must be identical and unclipped no
28+
* matter how narrow the client viewport is.
29+
*/
30+
function makeMobileRequest(search: string, headers: Record<string, string> = {}): Request {
31+
return new Request(`http://localhost:3000/api/compare?${search}`, {
32+
headers: new Headers(headers),
33+
});
34+
}
35+
36+
describe('Responsive Multi-device Columns & Mobile Viewport Layouts (Issue #6759)', () => {
37+
beforeEach(() => {
38+
vi.clearAllMocks();
39+
vi.mocked(getFullDashboardData).mockResolvedValue({
40+
calendar: { totalContributions: 50, weeks: [] },
41+
} as never);
42+
});
43+
44+
// Test 1: Standard 375px mobile viewport (iPhone SE / 12 / 13 mini reference).
45+
// The comparison payload for two users must arrive intact — no clipping when
46+
// the client advertises a narrow viewport via the standard hint headers.
47+
it('serves a clean 200 JSON comparison for a 375px mobile viewport request', async () => {
48+
const res = await GET(
49+
makeMobileRequest('user1=alice&user2=bob', {
50+
'Viewport-Width': '375',
51+
'Sec-CH-Viewport-Width': '375',
52+
})
53+
);
54+
55+
expect(res.status).toBe(200);
56+
expect(res.headers.get('content-type')).toContain('application/json');
57+
58+
const body = await res.json();
59+
// "Columns reflow" here means both user payloads are still present —
60+
// nothing was dropped or truncated because of the mobile viewport hint.
61+
expect(body.user1).toBeDefined();
62+
expect(body.user2).toBeDefined();
63+
});
64+
65+
// Test 2: Mobile User-Agent (iPhone Safari). The route must not branch on
66+
// user-agent and must return a flat JSON shape with no absolute-width fields
67+
// that would push the mobile UI into a horizontal scrollbar.
68+
it('returns a scrollbar-safe payload when called from an iPhone User-Agent', async () => {
69+
const iphoneUA =
70+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
71+
72+
const res = await GET(makeMobileRequest('user1=alice&user2=bob', { 'user-agent': iphoneUA }));
73+
74+
expect(res.status).toBe(200);
75+
const body = await res.json();
76+
expect(typeof body).toBe('object');
77+
// Payload must not carry fixed pixel widths that would break mobile layout.
78+
expect(body).not.toHaveProperty('width');
79+
expect(body).not.toHaveProperty('minWidth');
80+
expect(body.user1).toBeDefined();
81+
expect(body.user2).toBeDefined();
82+
});
83+
84+
// Test 3: Navigation-like re-fetch from a mobile client. When the user pulls
85+
// to refresh on Android, the client will re-issue the request with the same
86+
// ETag it already has. The route must scale down gracefully and return a
87+
// 304 Not Modified — no wasted bandwidth on a mobile connection.
88+
it('returns 304 for a mobile pull-to-refresh with a matching If-None-Match', async () => {
89+
const androidUA =
90+
'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36';
91+
92+
// First mobile request — captures the ETag.
93+
const first = await GET(
94+
makeMobileRequest('user1=alice&user2=bob', {
95+
'user-agent': androidUA,
96+
'Viewport-Width': '412',
97+
})
98+
);
99+
const etag = first.headers.get('ETag');
100+
expect(etag).toBeTruthy();
101+
102+
// Second mobile request (pull-to-refresh) — sends the ETag back.
103+
const second = await GET(
104+
makeMobileRequest('user1=alice&user2=bob', {
105+
'user-agent': androidUA,
106+
'Viewport-Width': '412',
107+
'if-none-match': etag!,
108+
})
109+
);
110+
111+
expect(second.status).toBe(304);
112+
expect(second.body).toBeNull();
113+
expect(second.headers.get('Cache-Control')).toBe('public, s-maxage=1');
114+
});
115+
116+
// Test 4: Mobile-specific toggle state (no cache header sent). When the
117+
// mobile client leaves the "use cache" toggle OFF and issues a fresh
118+
// request, the route must respond cleanly with a fresh ETag and the
119+
// standard Cache-Control — no state leaked from previous requests.
120+
it('responds with a fresh ETag when mobile client omits If-None-Match', async () => {
121+
const mobileUA =
122+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1';
123+
124+
const res = await GET(
125+
makeMobileRequest('user1=alice&user2=bob', {
126+
'user-agent': mobileUA,
127+
'Viewport-Width': '375',
128+
})
129+
);
130+
131+
expect(res.status).toBe(200);
132+
expect(res.headers.get('ETag')).toBeTruthy();
133+
expect(res.headers.get('Cache-Control')).toBe('public, s-maxage=1');
134+
});
135+
136+
// Test 5: Multi-device consistency — small phone (320px), standard phone
137+
// (375px), large phone (414px). All three breakpoints must produce an
138+
// identical 200 response with the same ETag (same underlying data) —
139+
// proving the route scales predictably across mobile widths.
140+
it('returns consistent behavior across 320px / 375px / 414px mobile breakpoints', async () => {
141+
const breakpoints = ['320', '375', '414'];
142+
const results: Array<{ status: number; etag: string | null }> = [];
143+
144+
for (const width of breakpoints) {
145+
const res = await GET(
146+
makeMobileRequest('user1=alice&user2=bob', {
147+
'Viewport-Width': width,
148+
'Sec-CH-Viewport-Width': width,
149+
})
150+
);
151+
results.push({
152+
status: res.status,
153+
etag: res.headers.get('ETag'),
154+
});
155+
}
156+
157+
// All three widths must produce a 200 with a valid ETag — and since the
158+
// underlying data is deterministic, the ETag must be identical.
159+
expect(results[0].status).toBe(200);
160+
expect(results[1].status).toBe(200);
161+
expect(results[2].status).toBe(200);
162+
expect(results[0].etag).toBeTruthy();
163+
expect(results[0].etag).toBe(results[1].etag);
164+
expect(results[1].etag).toBe(results[2].etag);
165+
});
166+
});

0 commit comments

Comments
 (0)