Skip to content

Commit 7278c4d

Browse files
committed
fix: address reviewer feedback on PR #11
- Remove manual Aryanbuha890 contributor card from README; keep anchors only - Add concurrency group to workflow to prevent race on simultaneous merges - Add unit tests for getContributionType, parseExistingContributors, generateTableHTML
1 parent 034db97 commit 7278c4d

3 files changed

Lines changed: 277 additions & 9 deletions

File tree

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/**
2+
* Tests for update-contributors.js
3+
*
4+
* Run with: node .github/scripts/update-contributors.test.js
5+
* (No extra dependencies needed — uses Node's built-in assert module)
6+
*/
7+
8+
'use strict';
9+
10+
const assert = require('assert');
11+
const { getContributionType, parseExistingContributors, generateTableHTML } = require('./update-contributors');
12+
13+
let passed = 0;
14+
let failed = 0;
15+
16+
function test(description, fn) {
17+
try {
18+
fn();
19+
console.log(` ✅ ${description}`);
20+
passed++;
21+
} catch (err) {
22+
console.error(` ❌ ${description}`);
23+
console.error(` ${err.message}`);
24+
failed++;
25+
}
26+
}
27+
28+
function makePR(labels = [], title = '') {
29+
return {
30+
pull_request: {
31+
labels: labels.map(name => ({ name })),
32+
title
33+
}
34+
};
35+
}
36+
37+
// ─────────────────────────────────────────────────────────────────────────────
38+
// getContributionType
39+
// ─────────────────────────────────────────────────────────────────────────────
40+
console.log('\ngetContributionType');
41+
42+
test('returns 🎨 Frontend for label "frontend"', () => {
43+
assert.strictEqual(getContributionType(makePR(['frontend'])), '🎨 Frontend');
44+
});
45+
46+
test('returns 🎨 Frontend for label "ui"', () => {
47+
assert.strictEqual(getContributionType(makePR(['ui'])), '🎨 Frontend');
48+
});
49+
50+
test('returns 🎨 Frontend for label "css"', () => {
51+
assert.strictEqual(getContributionType(makePR(['css'])), '🎨 Frontend');
52+
});
53+
54+
test('returns 💻 Code for label "backend"', () => {
55+
assert.strictEqual(getContributionType(makePR(['backend'])), '💻 Code');
56+
});
57+
58+
test('returns 💻 Code for label "api"', () => {
59+
assert.strictEqual(getContributionType(makePR(['api'])), '💻 Code');
60+
});
61+
62+
test('returns 📖 Docs for label "docs"', () => {
63+
assert.strictEqual(getContributionType(makePR(['docs'])), '📖 Docs');
64+
});
65+
66+
test('returns 📖 Docs for label "documentation"', () => {
67+
assert.strictEqual(getContributionType(makePR(['documentation'])), '📖 Docs');
68+
});
69+
70+
test('returns 🐛 Bug fix for label "bug"', () => {
71+
assert.strictEqual(getContributionType(makePR(['bug'])), '🐛 Bug fix');
72+
});
73+
74+
test('returns 🐛 Bug fix for label "fix"', () => {
75+
assert.strictEqual(getContributionType(makePR(['fix'])), '🐛 Bug fix');
76+
});
77+
78+
test('returns 🧪 Tests for label "test"', () => {
79+
assert.strictEqual(getContributionType(makePR(['test'])), '🧪 Tests');
80+
});
81+
82+
test('returns 🧪 Tests for label "jest"', () => {
83+
assert.strictEqual(getContributionType(makePR(['jest'])), '🧪 Tests');
84+
});
85+
86+
test('returns ⚡ Performance for label "performance"', () => {
87+
assert.strictEqual(getContributionType(makePR(['performance'])), '⚡ Performance');
88+
});
89+
90+
test('returns 🔒 Security for label "security"', () => {
91+
assert.strictEqual(getContributionType(makePR(['security'])), '🔒 Security');
92+
});
93+
94+
test('returns 🚇 Infrastructure for label "ci"', () => {
95+
assert.strictEqual(getContributionType(makePR(['ci'])), '🚇 Infrastructure');
96+
});
97+
98+
test('returns 🚇 Infrastructure for label "docker"', () => {
99+
assert.strictEqual(getContributionType(makePR(['docker'])), '🚇 Infrastructure');
100+
});
101+
102+
test('returns ♿ Accessibility for label "a11y"', () => {
103+
assert.strictEqual(getContributionType(makePR(['a11y'])), '♿ Accessibility');
104+
});
105+
106+
// Label takes priority over title
107+
test('label takes priority over title prefix', () => {
108+
assert.strictEqual(getContributionType(makePR(['docs'], 'feat: add new thing')), '📖 Docs');
109+
});
110+
111+
// Title fallback tests (no labels)
112+
test('title starting with "fix" returns 🐛 Bug fix', () => {
113+
assert.strictEqual(getContributionType(makePR([], 'fix: correct typo')), '🐛 Bug fix');
114+
});
115+
116+
test('title starting with "docs" returns 📖 Docs', () => {
117+
assert.strictEqual(getContributionType(makePR([], 'docs: update README')), '📖 Docs');
118+
});
119+
120+
test('title starting with "test" returns 🧪 Tests', () => {
121+
assert.strictEqual(getContributionType(makePR([], 'test: add unit tests')), '🧪 Tests');
122+
});
123+
124+
test('title with "ui" keyword returns 🎨 Frontend', () => {
125+
assert.strictEqual(getContributionType(makePR([], 'feat: improve ui layout')), '🎨 Frontend');
126+
});
127+
128+
test('generic feat title defaults to 💻 Code', () => {
129+
assert.strictEqual(getContributionType(makePR([], 'feat: some backend feature')), '💻 Code');
130+
});
131+
132+
test('returns 💻 Code as ultimate fallback (no labels, unknown title)', () => {
133+
assert.strictEqual(getContributionType(makePR([], 'chore: misc update')), '💻 Code');
134+
});
135+
136+
// ─────────────────────────────────────────────────────────────────────────────
137+
// parseExistingContributors
138+
// ─────────────────────────────────────────────────────────────────────────────
139+
console.log('\nparseExistingContributors');
140+
141+
const SINGLE_ENTRY_HTML = `
142+
<td align="center">
143+
<a href="https://github.com/alice">
144+
<img src="https://github.com/alice.png" width="80px" />
145+
<br />
146+
<sub><b>Alice Smith</b></sub>
147+
<br />
148+
<sub>💻 Code</sub>
149+
</a>
150+
</td>`;
151+
152+
const MULTI_ENTRY_HTML = `
153+
<td align="center">
154+
<a href="https://github.com/alice">
155+
<img src="https://github.com/alice.png" width="80px" />
156+
<br />
157+
<sub><b>Alice Smith</b></sub>
158+
<br />
159+
<sub>💻 Code</sub>
160+
</a>
161+
</td>
162+
<td align="center">
163+
<a href="https://github.com/bob">
164+
<img src="https://github.com/bob.png" width="80px" />
165+
<br />
166+
<sub><b>Bob Jones</b></sub>
167+
<br />
168+
<sub>📖 Docs</sub>
169+
</a>
170+
</td>`;
171+
172+
test('returns empty array for empty section', () => {
173+
const result = parseExistingContributors('');
174+
assert.deepStrictEqual(result, []);
175+
});
176+
177+
test('parses a single contributor entry correctly', () => {
178+
const result = parseExistingContributors(SINGLE_ENTRY_HTML);
179+
assert.strictEqual(result.length, 1);
180+
assert.strictEqual(result[0].username, 'alice');
181+
assert.strictEqual(result[0].avatarUrl, 'https://github.com/alice.png');
182+
assert.strictEqual(result[0].name, 'Alice Smith');
183+
assert.strictEqual(result[0].role, '💻 Code');
184+
});
185+
186+
test('parses multiple contributor entries', () => {
187+
const result = parseExistingContributors(MULTI_ENTRY_HTML);
188+
assert.strictEqual(result.length, 2);
189+
assert.strictEqual(result[0].username, 'alice');
190+
assert.strictEqual(result[1].username, 'bob');
191+
assert.strictEqual(result[1].role, '📖 Docs');
192+
});
193+
194+
test('is case-insensitive for username matching (regex)', () => {
195+
// The regex uses the gi flag; verify duplicates found regardless of case
196+
const result = parseExistingContributors(SINGLE_ENTRY_HTML);
197+
const found = result.some(c => c.username.toLowerCase() === 'alice');
198+
assert.ok(found);
199+
});
200+
201+
// ─────────────────────────────────────────────────────────────────────────────
202+
// generateTableHTML
203+
// ─────────────────────────────────────────────────────────────────────────────
204+
console.log('\ngenerateTableHTML');
205+
206+
const ONE_CONTRIBUTOR = [
207+
{ username: 'alice', avatarUrl: 'https://github.com/alice.png', name: 'Alice Smith', role: '💻 Code' }
208+
];
209+
210+
const SIX_CONTRIBUTORS = Array.from({ length: 6 }, (_, i) => ({
211+
username: `user${i}`,
212+
avatarUrl: `https://github.com/user${i}.png`,
213+
name: `User ${i}`,
214+
role: '💻 Code'
215+
}));
216+
217+
test('returns a string', () => {
218+
assert.strictEqual(typeof generateTableHTML(ONE_CONTRIBUTOR), 'string');
219+
});
220+
221+
test('contains <table> and </table> tags', () => {
222+
const html = generateTableHTML(ONE_CONTRIBUTOR);
223+
assert.ok(html.includes('<table>'), 'Missing <table>');
224+
assert.ok(html.includes('</table>'), 'Missing </table>');
225+
});
226+
227+
test('contains correct github profile link for contributor', () => {
228+
const html = generateTableHTML(ONE_CONTRIBUTOR);
229+
assert.ok(html.includes('https://github.com/alice'));
230+
});
231+
232+
test('contains contributor avatar img', () => {
233+
const html = generateTableHTML(ONE_CONTRIBUTOR);
234+
assert.ok(html.includes('https://github.com/alice.png'));
235+
});
236+
237+
test('contains contributor display name', () => {
238+
const html = generateTableHTML(ONE_CONTRIBUTOR);
239+
assert.ok(html.includes('Alice Smith'));
240+
});
241+
242+
test('contains contributor role', () => {
243+
const html = generateTableHTML(ONE_CONTRIBUTOR);
244+
assert.ok(html.includes('💻 Code'));
245+
});
246+
247+
test('wraps into multiple rows for 6 contributors (max 5 per row)', () => {
248+
const html = generateTableHTML(SIX_CONTRIBUTORS);
249+
// Should have at least 2 <tr> tags
250+
const trCount = (html.match(/<tr>/g) || []).length;
251+
assert.ok(trCount >= 2, `Expected ≥2 <tr> tags, got ${trCount}`);
252+
});
253+
254+
test('first row has exactly 5 entries for 6 contributors', () => {
255+
const html = generateTableHTML(SIX_CONTRIBUTORS);
256+
// Count td elements — 6 total across 2 rows
257+
const tdCount = (html.match(/<td align="center">/g) || []).length;
258+
assert.strictEqual(tdCount, 6, `Expected 6 <td> entries, got ${tdCount}`);
259+
});
260+
261+
test('empty contributors array returns table with no rows', () => {
262+
const html = generateTableHTML([]);
263+
assert.ok(!html.includes('<tr>'), 'Should have no <tr> for empty input');
264+
});
265+
266+
// ─────────────────────────────────────────────────────────────────────────────
267+
// Summary
268+
// ─────────────────────────────────────────────────────────────────────────────
269+
console.log(`\n${'─'.repeat(50)}`);
270+
console.log(`Results: ${passed} passed, ${failed} failed`);
271+
if (failed > 0) {
272+
process.exit(1);
273+
}

.github/workflows/update-contributors.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
permissions:
99
contents: write
1010

11+
concurrency:
12+
group: update-contributors
13+
cancel-in-progress: false
14+
1115
jobs:
1216
update-readme:
1317
if: github.event.pull_request.merged == true

README.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,6 @@ Contributors who submit PRs will be added to the table below.
379379
<sub>🎨 Frontend</sub>
380380
</a>
381381
</td>
382-
<td align="center">
383-
<a href="https://github.com/Aryanbuha890">
384-
<img src="https://github.com/Aryanbuha890.png" width="80px" />
385-
<br />
386-
<sub><b>Aryan Buha</b></sub>
387-
<br />
388-
<sub>🎨 Frontend</sub>
389-
</a>
390-
</td>
391382
</tr>
392383
</tbody>
393384
</table>

0 commit comments

Comments
 (0)