Skip to content

Commit 45941f6

Browse files
committedJul 11, 2020
test: added test & refactored files
1 parent 223bd65 commit 45941f6

15 files changed

+5665
-272
lines changed
 

‎api/index.js

+4-136
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,6 @@
1-
const axios = require("axios");
2-
const { renderError, kFormatter } = require("../utils");
3-
require("dotenv").config();
4-
5-
async function fetchStats(username) {
6-
if (!username) throw Error("Invalid username");
7-
8-
const res = await axios({
9-
url: "https://api.github.com/graphql",
10-
method: "post",
11-
headers: {
12-
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
13-
},
14-
data: {
15-
query: `
16-
query userInfo($login: String!) {
17-
user(login: $login) {
18-
name
19-
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
20-
totalCount
21-
}
22-
contributionsCollection {
23-
totalCommitContributions
24-
}
25-
pullRequests(first: 100) {
26-
totalCount
27-
}
28-
issues(first: 100) {
29-
totalCount
30-
}
31-
repositories(first: 100) {
32-
nodes {
33-
stargazers {
34-
totalCount
35-
}
36-
}
37-
}
38-
}
39-
}
40-
`,
41-
variables: {
42-
login: username,
43-
},
44-
},
45-
});
46-
47-
const stats = {
48-
name: "",
49-
totalPRs: 0,
50-
totalCommits: 0,
51-
totalIssues: 0,
52-
totalStars: 0,
53-
contributedTo: 0,
54-
};
55-
56-
if (res.data.errors) {
57-
console.log(res.data.errors);
58-
throw Error("Could not fetch user");
59-
}
60-
61-
const user = res.data.data.user;
62-
63-
stats.name = user.name;
64-
stats.totalIssues = user.issues.totalCount;
65-
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
66-
stats.totalPRs = user.pullRequests.totalCount;
67-
stats.contributedTo = user.repositoriesContributedTo.totalCount;
68-
69-
stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
70-
return prev + curr.stargazers.totalCount;
71-
}, 0);
72-
73-
return stats;
74-
}
75-
76-
const createTextNode = (icon, label, value, lheight) => {
77-
const classname = icon === "★" && "star-icon";
78-
return `
79-
<tspan x="25" dy="${lheight}" class="stat bold">
80-
<tspan class="icon ${classname}" fill="#4C71F2">${icon}</tspan> ${label}:</tspan>
81-
<tspan x="155" dy="0" class="stat">${kFormatter(value)}</tspan>
82-
`;
83-
};
84-
85-
const renderSVG = (stats, options) => {
86-
const {
87-
name,
88-
totalStars,
89-
totalCommits,
90-
totalIssues,
91-
totalPRs,
92-
contributedTo,
93-
} = stats;
94-
const { hide, show_icons, hide_border, line_height } = options || {};
95-
96-
const lheight = line_height || 25;
97-
98-
const STAT_MAP = {
99-
stars: createTextNode("★", "Total Stars", totalStars, lheight),
100-
commits: createTextNode("🕗", "Total Commits", totalCommits, lheight),
101-
prs: createTextNode("🔀", "Total PRs", totalPRs, lheight),
102-
issues: createTextNode("ⓘ", "Total Issues", totalIssues, lheight),
103-
contribs: createTextNode("📕", "Contributed to", contributedTo, lheight),
104-
};
105-
106-
const statItems = Object.keys(STAT_MAP)
107-
.filter((key) => !hide.includes(key))
108-
.map((key) => STAT_MAP[key]);
109-
110-
const height = 45 + (statItems.length + 1) * lheight;
111-
112-
return `
113-
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
114-
<style>
115-
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
116-
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
117-
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
118-
.bold { font-weight: 700 }
119-
.icon {
120-
display: none;
121-
${!!show_icons && "display: block"}
122-
}
123-
</style>
124-
${
125-
!hide_border &&
126-
`<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>`
127-
}
128-
129-
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
130-
<text y="45">
131-
${statItems}
132-
</text>
133-
</svg>
134-
`;
135-
};
1+
const { renderError } = require("../src/utils");
2+
const fetchStats = require("../src/fetchStats");
3+
const renderStatsCard = require("../src/renderStatsCard");
1364

1375
module.exports = async (req, res) => {
1386
const { username, hide, hide_border, show_icons, line_height } = req.query;
@@ -146,7 +14,7 @@ module.exports = async (req, res) => {
14614
}
14715

14816
res.send(
149-
renderSVG(stats, {
17+
renderStatsCard(stats, {
15018
hide: JSON.parse(hide || "[]"),
15119
show_icons,
15220
hide_border,

‎api/pin.js

+3-115
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,8 @@
1-
const axios = require("axios");
2-
const { renderError, kFormatter, encodeHTML } = require("../utils");
1+
const { renderError } = require("../src/utils");
2+
const fetchRepo = require("../src/fetchRepo");
3+
const renderRepoCard = require("../src/renderRepoCard");
34
require("dotenv").config();
45

5-
async function fetchRepo(username, reponame) {
6-
const res = await axios({
7-
url: "https://api.github.com/graphql",
8-
method: "post",
9-
headers: {
10-
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
11-
},
12-
data: {
13-
query: `
14-
fragment RepoInfo on Repository {
15-
name
16-
stargazers {
17-
totalCount
18-
}
19-
description
20-
primaryLanguage {
21-
color
22-
id
23-
name
24-
}
25-
forkCount
26-
}
27-
query getRepo($login: String!, $repo: String!) {
28-
user(login: $login) {
29-
repository(name: $repo) {
30-
...RepoInfo
31-
}
32-
}
33-
organization(login: $login) {
34-
repository(name: $repo) {
35-
...RepoInfo
36-
}
37-
}
38-
}
39-
`,
40-
variables: {
41-
login: username,
42-
repo: reponame,
43-
},
44-
},
45-
});
46-
47-
const data = res.data.data;
48-
49-
if (!data.user && !data.organization) {
50-
throw new Error("Not found");
51-
}
52-
53-
if (data.organization === null && data.user) {
54-
if (!data.user.repository) {
55-
throw new Error("User Repository Not found");
56-
}
57-
return data.user.repository;
58-
}
59-
60-
if (data.user === null && data.organization) {
61-
if (!data.organization.repository) {
62-
throw new Error("Organization Repository Not found");
63-
}
64-
return data.organization.repository;
65-
}
66-
}
67-
68-
const renderRepoCard = (repo) => {
69-
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
70-
const height = 120;
71-
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
72-
73-
let desc = description || "No description provided";
74-
if (desc.length > 55) {
75-
desc = `${description.slice(0, 55)}..`;
76-
}
77-
78-
return `
79-
<svg width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
80-
<style>
81-
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
82-
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
83-
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
84-
.bold { font-weight: 700 }
85-
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
86-
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
87-
</style>
88-
<rect x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
89-
<svg x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
90-
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
91-
</svg>
92-
93-
<text x="50" y="38" class="header">${name}</text>
94-
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
95-
96-
<g transform="translate(30, 100)">
97-
<circle cx="0" cy="-5" r="6" fill="${primaryLanguage.color}" />
98-
<text class="gray" x="15">${primaryLanguage.name}</text>
99-
</g>
100-
101-
<g transform="translate(${155 - shiftText}, 100)">
102-
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
103-
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
104-
</svg>
105-
<text class="gray" x="25">${kFormatter(stargazers.totalCount)}</text>
106-
</g>
107-
108-
<g transform="translate(${220 - shiftText}, 100)">
109-
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
110-
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
111-
</svg>
112-
<text class="gray" x="25">${kFormatter(forkCount)}</text>
113-
</g>
114-
</svg>
115-
`;
116-
};
117-
1186
module.exports = async (req, res) => {
1197
const { username, repo } = req.query;
1208

‎jest.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
clearMocks: true,
3+
};

‎package-lock.json

+5,025-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
{
22
"name": "github-readme-stats",
33
"version": "1.0.0",
4-
"description": "",
4+
"description": "Dynamically generate stats for your github readmes",
55
"main": "index.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "jest",
8+
"test:watch": "jest --watch"
89
},
910
"author": "Anurag Hazra",
1011
"license": "MIT",
1112
"devDependencies": {
12-
"axios": "^0.19.2"
13+
"@testing-library/dom": "^7.20.0",
14+
"@testing-library/jest-dom": "^5.11.0",
15+
"axios": "^0.19.2",
16+
"axios-mock-adapter": "^1.18.1",
17+
"jest": "^26.1.0"
1318
},
1419
"dependencies": {
1520
"dotenv": "^8.2.0"

‎src/fetchRepo.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const axios = require("axios");
2+
3+
async function fetchRepo(username, reponame) {
4+
if (!username || !reponame) {
5+
throw new Error("Invalid username or reponame");
6+
}
7+
8+
const res = await axios({
9+
url: "https://api.github.com/graphql",
10+
method: "post",
11+
headers: {
12+
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
13+
},
14+
data: {
15+
query: `
16+
fragment RepoInfo on Repository {
17+
name
18+
stargazers {
19+
totalCount
20+
}
21+
description
22+
primaryLanguage {
23+
color
24+
id
25+
name
26+
}
27+
forkCount
28+
}
29+
query getRepo($login: String!, $repo: String!) {
30+
user(login: $login) {
31+
repository(name: $repo) {
32+
...RepoInfo
33+
}
34+
}
35+
organization(login: $login) {
36+
repository(name: $repo) {
37+
...RepoInfo
38+
}
39+
}
40+
}
41+
`,
42+
variables: {
43+
login: username,
44+
repo: reponame,
45+
},
46+
},
47+
});
48+
49+
const data = res.data.data;
50+
51+
if (!data.user && !data.organization) {
52+
throw new Error("Not found");
53+
}
54+
55+
if (data.organization === null && data.user) {
56+
if (!data.user.repository) {
57+
throw new Error("User Repository Not found");
58+
}
59+
return data.user.repository;
60+
}
61+
62+
if (data.user === null && data.organization) {
63+
if (!data.organization.repository) {
64+
throw new Error("Organization Repository Not found");
65+
}
66+
return data.organization.repository;
67+
}
68+
}
69+
70+
module.exports = fetchRepo;

‎src/fetchStats.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const axios = require("axios");
2+
require("dotenv").config();
3+
4+
async function fetchStats(username) {
5+
if (!username) throw Error("Invalid username");
6+
7+
const res = await axios({
8+
url: "https://api.github.com/graphql",
9+
method: "post",
10+
headers: {
11+
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
12+
},
13+
data: {
14+
query: `
15+
query userInfo($login: String!) {
16+
user(login: $login) {
17+
name
18+
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
19+
totalCount
20+
}
21+
contributionsCollection {
22+
totalCommitContributions
23+
}
24+
pullRequests(first: 100) {
25+
totalCount
26+
}
27+
issues(first: 100) {
28+
totalCount
29+
}
30+
repositories(first: 100) {
31+
nodes {
32+
stargazers {
33+
totalCount
34+
}
35+
}
36+
}
37+
}
38+
}
39+
`,
40+
variables: {
41+
login: username,
42+
},
43+
},
44+
});
45+
46+
const stats = {
47+
name: "",
48+
totalPRs: 0,
49+
totalCommits: 0,
50+
totalIssues: 0,
51+
totalStars: 0,
52+
contributedTo: 0,
53+
};
54+
55+
if (res.data.errors) {
56+
console.log(res.data.errors);
57+
throw Error("Could not fetch user");
58+
}
59+
60+
const user = res.data.data.user;
61+
62+
stats.name = user.name;
63+
stats.totalIssues = user.issues.totalCount;
64+
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
65+
stats.totalPRs = user.pullRequests.totalCount;
66+
stats.contributedTo = user.repositoriesContributedTo.totalCount;
67+
68+
stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
69+
return prev + curr.stargazers.totalCount;
70+
}, 0);
71+
72+
return stats;
73+
}
74+
75+
module.exports = fetchStats;

‎src/renderRepoCard.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const { kFormatter, encodeHTML } = require("../src/utils");
2+
3+
const renderRepoCard = (repo) => {
4+
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
5+
const height = 120;
6+
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
7+
8+
let desc = description || "No description provided";
9+
if (desc.length > 55) {
10+
desc = `${description.slice(0, 55)}..`;
11+
}
12+
13+
const totalStars = kFormatter(stargazers.totalCount);
14+
const totalForks = kFormatter(forkCount);
15+
return `
16+
<svg width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
17+
<style>
18+
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
19+
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
20+
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
21+
.bold { font-weight: 700 }
22+
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
23+
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
24+
</style>
25+
<rect x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
26+
<svg x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
27+
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
28+
</svg>
29+
30+
<text x="50" y="38" class="header">${name}</text>
31+
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
32+
33+
<g transform="translate(30, 100)">
34+
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${
35+
primaryLanguage.color
36+
}" />
37+
<text data-testid="lang" class="gray" x="15">${
38+
primaryLanguage.name
39+
}</text>
40+
</g>
41+
42+
<g transform="translate(${155 - shiftText}, 100)">
43+
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
44+
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
45+
</svg>
46+
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
47+
</g>
48+
49+
<g transform="translate(${220 - shiftText}, 100)">
50+
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
51+
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
52+
</svg>
53+
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
54+
</g>
55+
</svg>
56+
`;
57+
};
58+
59+
module.exports = renderRepoCard;

‎src/renderStatsCard.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const { kFormatter } = require("../src/utils");
2+
3+
const createTextNode = ({ icon, label, value, lineHeight, id }) => {
4+
const classname = icon === "★" && "star-icon";
5+
const kValue = kFormatter(value);
6+
return `
7+
<tspan x="25" dy="${lineHeight}" class="stat bold">
8+
<tspan data-testid="icon" class="icon ${classname}" fill="#4C71F2">${icon}</tspan> ${label}:</tspan>
9+
<tspan data-testid="${id}" x="155" dy="0" class="stat">${kValue}</tspan>
10+
`;
11+
};
12+
13+
const renderStatsCard = (stats = {}, options = { hide: [] }) => {
14+
const {
15+
name,
16+
totalStars,
17+
totalCommits,
18+
totalIssues,
19+
totalPRs,
20+
contributedTo,
21+
} = stats;
22+
const {
23+
hide = [],
24+
show_icons = false,
25+
hide_border = false,
26+
line_height = 25,
27+
} = options;
28+
29+
const lheight = parseInt(line_height);
30+
31+
const STAT_MAP = {
32+
stars: createTextNode({
33+
icon: "★",
34+
label: "Total Stars",
35+
value: totalStars,
36+
lineHeight: lheight,
37+
id: "stars",
38+
}),
39+
commits: createTextNode({
40+
icon: "🕗",
41+
label: "Total Commits",
42+
value: totalCommits,
43+
lineHeight: lheight,
44+
id: "commits",
45+
}),
46+
prs: createTextNode({
47+
icon: "🔀",
48+
label: "Total PRs",
49+
value: totalPRs,
50+
lineHeight: lheight,
51+
id: "prs",
52+
}),
53+
issues: createTextNode({
54+
icon: "ⓘ",
55+
label: "Total Issues",
56+
value: totalIssues,
57+
lineHeight: lheight,
58+
id: "issues",
59+
}),
60+
contribs: createTextNode({
61+
icon: "📕",
62+
label: "Contributed to",
63+
value: contributedTo,
64+
lineHeight: lheight,
65+
id: "contribs",
66+
}),
67+
};
68+
69+
const statItems = Object.keys(STAT_MAP)
70+
.filter((key) => !hide.includes(key))
71+
.map((key) => STAT_MAP[key]);
72+
73+
const height = 45 + (statItems.length + 1) * lheight;
74+
75+
const border = `<rect data-testid="card-border" x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>`;
76+
return `
77+
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
78+
<style>
79+
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
80+
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
81+
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
82+
.bold { font-weight: 700 }
83+
.icon {
84+
display: ${!!show_icons ? "block" : "none"};
85+
}
86+
</style>
87+
${hide_border ? "" : border}
88+
89+
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
90+
<text y="45">
91+
${statItems.toString().replace(/\,/gm, "")}
92+
</text>
93+
</svg>
94+
`;
95+
};
96+
97+
module.exports = renderStatsCard;

‎utils.js renamed to ‎src/utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const renderError = (message) => {
77
</style>
88
<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
99
<text x="25" y="45" class="text">Something went wrong! file an issue at https://git.io/JJmN9</text>
10-
<text x="25" y="65" class="text small">${message}</text>
10+
<text id="message" x="25" y="65" class="text small">${message}</text>
1111
</svg>
1212
`;
1313
};

‎tests/fetchRepo.test.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
require("@testing-library/jest-dom");
2+
const axios = require("axios");
3+
const MockAdapter = require("axios-mock-adapter");
4+
const fetchRepo = require("../src/fetchRepo");
5+
6+
const data_repo = {
7+
repository: {
8+
name: "convoychat",
9+
stargazers: { totalCount: 38000 },
10+
description: "Help us take over the world! React + TS + GraphQL Chat App",
11+
primaryLanguage: {
12+
color: "#2b7489",
13+
id: "MDg6TGFuZ3VhZ2UyODc=",
14+
name: "TypeScript",
15+
},
16+
forkCount: 100,
17+
},
18+
};
19+
20+
const data_user = {
21+
data: {
22+
user: { repository: data_repo },
23+
organization: null,
24+
},
25+
};
26+
const data_org = {
27+
data: {
28+
user: null,
29+
organization: { repository: data_repo },
30+
},
31+
};
32+
33+
const mock = new MockAdapter(axios);
34+
35+
afterEach(() => {
36+
mock.reset();
37+
});
38+
39+
describe("Test fetchRepo", () => {
40+
it("should fetch correct user repo", async () => {
41+
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
42+
43+
let repo = await fetchRepo("anuraghazra", "convoychat");
44+
expect(repo).toStrictEqual(data_repo);
45+
});
46+
47+
it("should fetch correct org repo", async () => {
48+
mock.onPost("https://api.github.com/graphql").reply(200, data_org);
49+
50+
let repo = await fetchRepo("anuraghazra", "convoychat");
51+
expect(repo).toStrictEqual(data_repo);
52+
});
53+
54+
it("should throw error if user is found but repo is null", async () => {
55+
mock
56+
.onPost("https://api.github.com/graphql")
57+
.reply(200, { data: { user: { repository: null }, organization: null } });
58+
59+
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
60+
"User Repository Not found"
61+
);
62+
});
63+
64+
it("should throw error if org is found but repo is null", async () => {
65+
mock
66+
.onPost("https://api.github.com/graphql")
67+
.reply(200, { data: { user: null, organization: { repository: null } } });
68+
69+
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
70+
"Organization Repository Not found"
71+
);
72+
});
73+
74+
it("should throw error if both user & org data not found", async () => {
75+
mock
76+
.onPost("https://api.github.com/graphql")
77+
.reply(200, { data: { user: null, organization: null } });
78+
79+
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
80+
"Not found"
81+
);
82+
});
83+
});

‎tests/fetchStats.test.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
require("@testing-library/jest-dom");
2+
const axios = require("axios");
3+
const MockAdapter = require("axios-mock-adapter");
4+
const fetchStats = require("../src/fetchStats");
5+
6+
const data = {
7+
data: {
8+
user: {
9+
name: "Anurag Hazra",
10+
repositoriesContributedTo: { totalCount: 61 },
11+
contributionsCollection: { totalCommitContributions: 100 },
12+
pullRequests: { totalCount: 300 },
13+
issues: { totalCount: 200 },
14+
repositories: {
15+
nodes: [
16+
{ stargazers: { totalCount: 100 } },
17+
{ stargazers: { totalCount: 100 } },
18+
{ stargazers: { totalCount: 100 } },
19+
{ stargazers: { totalCount: 50 } },
20+
{ stargazers: { totalCount: 50 } },
21+
],
22+
},
23+
},
24+
},
25+
};
26+
27+
const error = {
28+
errors: [
29+
{
30+
type: "NOT_FOUND",
31+
path: ["user"],
32+
locations: [],
33+
message: "Could not resolve to a User with the login of 'noname'.",
34+
},
35+
],
36+
};
37+
38+
const mock = new MockAdapter(axios);
39+
40+
afterEach(() => {
41+
mock.reset();
42+
});
43+
44+
describe("Test fetchStats", () => {
45+
it("should fetch correct stats", async () => {
46+
mock.onPost("https://api.github.com/graphql").reply(200, data);
47+
48+
let stats = await fetchStats("anuraghazra");
49+
expect(stats).toStrictEqual({
50+
contributedTo: 61,
51+
name: "Anurag Hazra",
52+
totalCommits: 100,
53+
totalIssues: 200,
54+
totalPRs: 300,
55+
totalStars: 400,
56+
});
57+
});
58+
59+
it("should throw error", async () => {
60+
mock.onPost("https://api.github.com/graphql").reply(200, error);
61+
62+
await expect(fetchStats("anuraghazra")).rejects.toThrow(
63+
"Could not fetch user"
64+
);
65+
});
66+
});

‎tests/renderRepoCard.test.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
require("@testing-library/jest-dom");
2+
const renderRepoCard = require("../src/renderRepoCard");
3+
4+
const { queryByTestId } = require("@testing-library/dom");
5+
6+
const data_repo = {
7+
repository: {
8+
name: "convoychat",
9+
stargazers: { totalCount: 38000 },
10+
description: "Help us take over the world! React + TS + GraphQL Chat App",
11+
primaryLanguage: {
12+
color: "#2b7489",
13+
id: "MDg6TGFuZ3VhZ2UyODc=",
14+
name: "TypeScript",
15+
},
16+
forkCount: 100,
17+
},
18+
};
19+
20+
describe("Test renderRepoCard", () => {
21+
it("should render correctly", () => {
22+
document.body.innerHTML = renderRepoCard(data_repo.repository);
23+
24+
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
25+
"convoychat"
26+
);
27+
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
28+
"Help us take over the world! React + TS + GraphQL Chat .."
29+
);
30+
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
31+
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
32+
expect(queryByTestId(document.body, "lang")).toHaveTextContent(
33+
"TypeScript"
34+
);
35+
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
36+
"fill",
37+
"#2b7489"
38+
);
39+
});
40+
41+
it("should trim description", () => {
42+
document.body.innerHTML = renderRepoCard({
43+
...data_repo.repository,
44+
description:
45+
"Very long long long long long long long long text it should trim it",
46+
});
47+
48+
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
49+
"Very long long long long long long long long text it sh.."
50+
);
51+
52+
// Should not trim
53+
document.body.innerHTML = renderRepoCard({
54+
...data_repo.repository,
55+
description: "Small text should not trim",
56+
});
57+
58+
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
59+
"Small text should not trim"
60+
);
61+
});
62+
63+
it("should shift the text position depending on language length", () => {
64+
document.body.innerHTML = renderRepoCard({
65+
...data_repo.repository,
66+
primaryLanguage: {
67+
...data_repo.repository.primaryLanguage,
68+
name: "Jupyter Notebook",
69+
},
70+
});
71+
72+
expect(document.getElementsByTagName("g")[1]).toHaveAttribute(
73+
"transform",
74+
"translate(155, 100)"
75+
);
76+
77+
// Small lang
78+
document.body.innerHTML = renderRepoCard({
79+
...data_repo.repository,
80+
primaryLanguage: {
81+
...data_repo.repository.primaryLanguage,
82+
name: "Ruby",
83+
},
84+
});
85+
86+
expect(document.getElementsByTagName("g")[1]).toHaveAttribute(
87+
"transform",
88+
"translate(125, 100)"
89+
);
90+
});
91+
});

‎tests/renderStatsCard.test.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require("@testing-library/jest-dom");
2+
const renderStatsCard = require("../src/renderStatsCard");
3+
4+
const { getByTestId, queryByTestId } = require("@testing-library/dom");
5+
6+
describe("Test renderStatsCard", () => {
7+
const stats = {
8+
name: "Anurag Hazra",
9+
totalStars: 100,
10+
totalCommits: 200,
11+
totalIssues: 300,
12+
totalPRs: 400,
13+
contributedTo: 500,
14+
};
15+
16+
it("should render correctly", () => {
17+
document.body.innerHTML = renderStatsCard(stats);
18+
19+
expect(document.getElementsByClassName("header")[0].textContent).toBe(
20+
"Anurag Hazra's GitHub Stats"
21+
);
22+
23+
expect(
24+
document.body.getElementsByTagName("svg")[0].getAttribute("height")
25+
).toBe("195");
26+
expect(getByTestId(document.body, "stars").textContent).toBe("100");
27+
expect(getByTestId(document.body, "commits").textContent).toBe("200");
28+
expect(getByTestId(document.body, "issues").textContent).toBe("300");
29+
expect(getByTestId(document.body, "prs").textContent).toBe("400");
30+
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
31+
expect(queryByTestId(document.body, "card-border")).toBeInTheDocument();
32+
});
33+
34+
it("should hide individual stats", () => {
35+
document.body.innerHTML = renderStatsCard(stats, {
36+
hide: "['issues', 'prs', 'contribs']",
37+
});
38+
39+
expect(
40+
document.body.getElementsByTagName("svg")[0].getAttribute("height")
41+
).toBe("120");
42+
expect(queryByTestId(document.body, "stars")).toBeDefined();
43+
expect(queryByTestId(document.body, "commits")).toBeDefined();
44+
expect(queryByTestId(document.body, "issues")).toBeNull();
45+
expect(queryByTestId(document.body, "prs")).toBeNull();
46+
expect(queryByTestId(document.body, "contribs")).toBeNull();
47+
});
48+
49+
it("should hide_border", () => {
50+
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
51+
52+
expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument();
53+
});
54+
});

‎tests/utils.test.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
2+
3+
describe("Test utils.js", () => {
4+
it("should test kFormatter", () => {
5+
expect(kFormatter(1)).toBe(1);
6+
expect(kFormatter(-1)).toBe(-1);
7+
expect(kFormatter(500)).toBe(500);
8+
expect(kFormatter(1000)).toBe("1k");
9+
expect(kFormatter(10000)).toBe("10k");
10+
expect(kFormatter(12345)).toBe("12.3k");
11+
expect(kFormatter(9900000)).toBe("9900k");
12+
});
13+
14+
it("should test encodeHTML", () => {
15+
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
16+
"&#60;html&#62;hello world&#60;,.#4^&#38;^@%!))"
17+
);
18+
});
19+
20+
it("should test renderError", () => {
21+
document.body.innerHTML = renderError("Something went wrong");
22+
expect(document.getElementById("message").textContent).toBe(
23+
"Something went wrong"
24+
);
25+
});
26+
});

0 commit comments

Comments
 (0)
Please sign in to comment.