forked from mirrored/github-readme-stats
* add custom year * edit fetcher * add tests * add documentation * Update vercel.json * Update fetchStats.test.js * Update readme.md * Update readme.md Co-authored-by: Rick Staa <rick.staa@outlook.com> * test: fix tests * style: format code * fix: fix commit stats time frame description #1053 This commit makes sure the last year label is shown when `include_all_commits` or `year` is not provided. See #1053 for more information. * resolved merge conflict * Update stats-fetcher.js * resolve comments * Update src/cards/types.d.ts Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> * Update src/fetchers/stats-fetcher.js Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> * graphQL year only to commit * change argument name to commits_year * Update readme.md Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> * Update readme.md Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> * Update readme.md Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> * wrap label into function * review * more review * fix test * Update readme.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs --------- Co-authored-by: Touch Sungkawichai <talay@10ninox.com> Co-authored-by: Rick Staa <rick.staa@outlook.com> Co-authored-by: Alexandr Garbuzov <qwerty541zxc@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
469 lines
17 KiB
JavaScript
469 lines
17 KiB
JavaScript
import {
|
|
getByTestId,
|
|
queryAllByTestId,
|
|
queryByTestId,
|
|
} from "@testing-library/dom";
|
|
import { cssToObject } from "@uppercod/css-to-object";
|
|
import { renderStatsCard } from "../src/cards/stats.js";
|
|
import { expect, it, describe } from "@jest/globals";
|
|
import { CustomError } from "../src/common/utils.js";
|
|
|
|
// adds special assertions like toHaveTextContent
|
|
import "@testing-library/jest-dom";
|
|
|
|
import { themes } from "../themes/index.js";
|
|
|
|
const stats = {
|
|
name: "Anurag Hazra",
|
|
totalStars: 100,
|
|
totalCommits: 200,
|
|
totalIssues: 300,
|
|
totalPRs: 400,
|
|
totalPRsMerged: 320,
|
|
mergedPRsPercentage: 80,
|
|
totalReviews: 50,
|
|
totalDiscussionsStarted: 10,
|
|
totalDiscussionsAnswered: 50,
|
|
contributedTo: 500,
|
|
rank: { level: "A+", percentile: 40 },
|
|
};
|
|
|
|
describe("Test renderStatsCard", () => {
|
|
it("should render correctly", () => {
|
|
document.body.innerHTML = renderStatsCard(stats);
|
|
|
|
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
|
"Anurag Hazra's GitHub Stats",
|
|
);
|
|
|
|
expect(
|
|
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
|
|
).toBe("195");
|
|
expect(getByTestId(document.body, "stars").textContent).toBe("100");
|
|
expect(getByTestId(document.body, "commits").textContent).toBe("200");
|
|
expect(getByTestId(document.body, "issues").textContent).toBe("300");
|
|
expect(getByTestId(document.body, "prs").textContent).toBe("400");
|
|
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
|
|
expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
|
|
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
|
|
|
|
// Default hidden stats
|
|
expect(queryByTestId(document.body, "reviews")).not.toBeInTheDocument();
|
|
expect(
|
|
queryByTestId(document.body, "discussions_started"),
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
queryByTestId(document.body, "discussions_answered"),
|
|
).not.toBeInTheDocument();
|
|
expect(queryByTestId(document.body, "prs_merged")).not.toBeInTheDocument();
|
|
expect(
|
|
queryByTestId(document.body, "prs_merged_percentage"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should have proper name apostrophe", () => {
|
|
document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" });
|
|
|
|
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
|
"Anil Das' GitHub Stats",
|
|
);
|
|
|
|
document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" });
|
|
|
|
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
|
"Felix's GitHub Stats",
|
|
);
|
|
});
|
|
|
|
it("should hide individual stats", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
hide: ["issues", "prs", "contribs"],
|
|
});
|
|
|
|
expect(
|
|
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
|
|
).toBe("150"); // height should be 150 because we clamped it.
|
|
|
|
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
|
expect(queryByTestId(document.body, "commits")).toBeDefined();
|
|
expect(queryByTestId(document.body, "issues")).toBeNull();
|
|
expect(queryByTestId(document.body, "prs")).toBeNull();
|
|
expect(queryByTestId(document.body, "contribs")).toBeNull();
|
|
expect(queryByTestId(document.body, "reviews")).toBeNull();
|
|
expect(queryByTestId(document.body, "discussions_started")).toBeNull();
|
|
expect(queryByTestId(document.body, "discussions_answered")).toBeNull();
|
|
expect(queryByTestId(document.body, "prs_merged")).toBeNull();
|
|
expect(queryByTestId(document.body, "prs_merged_percentage")).toBeNull();
|
|
});
|
|
|
|
it("should show additional stats", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
show: [
|
|
"reviews",
|
|
"discussions_started",
|
|
"discussions_answered",
|
|
"prs_merged",
|
|
"prs_merged_percentage",
|
|
],
|
|
});
|
|
|
|
expect(
|
|
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
|
|
).toBe("320");
|
|
|
|
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
|
expect(queryByTestId(document.body, "commits")).toBeDefined();
|
|
expect(queryByTestId(document.body, "issues")).toBeDefined();
|
|
expect(queryByTestId(document.body, "prs")).toBeDefined();
|
|
expect(queryByTestId(document.body, "contribs")).toBeDefined();
|
|
expect(queryByTestId(document.body, "reviews")).toBeDefined();
|
|
expect(queryByTestId(document.body, "discussions_started")).toBeDefined();
|
|
expect(queryByTestId(document.body, "discussions_answered")).toBeDefined();
|
|
expect(queryByTestId(document.body, "prs_merged")).toBeDefined();
|
|
expect(queryByTestId(document.body, "prs_merged_percentage")).toBeDefined();
|
|
});
|
|
|
|
it("should hide_rank", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, { hide_rank: true });
|
|
|
|
expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should render with custom width set", () => {
|
|
document.body.innerHTML = renderStatsCard(stats);
|
|
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
|
|
|
|
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
|
|
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
|
|
});
|
|
|
|
it("should render with custom width set and limit minimum width", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
|
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
|
|
|
// Test default minimum card width without rank circle.
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
card_width: 1,
|
|
hide_rank: true,
|
|
});
|
|
expect(document.querySelector("svg")).toHaveAttribute(
|
|
"width",
|
|
"305.81250000000006",
|
|
);
|
|
|
|
// Test minimum card width with rank and icons.
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
card_width: 1,
|
|
hide_rank: true,
|
|
show_icons: true,
|
|
});
|
|
expect(document.querySelector("svg")).toHaveAttribute(
|
|
"width",
|
|
"322.81250000000006",
|
|
);
|
|
|
|
// Test minimum card width with icons but without rank.
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
card_width: 1,
|
|
hide_rank: false,
|
|
show_icons: true,
|
|
});
|
|
expect(document.querySelector("svg")).toHaveAttribute("width", "437");
|
|
|
|
// Test minimum card width without icons or rank.
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
card_width: 1,
|
|
hide_rank: false,
|
|
show_icons: false,
|
|
});
|
|
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
|
});
|
|
|
|
it("should render default colors properly", () => {
|
|
document.body.innerHTML = renderStatsCard(stats);
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.textContent);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe("#2f80ed");
|
|
expect(statClassStyles.fill.trim()).toBe("#434d58");
|
|
expect(iconClassStyles.fill.trim()).toBe("#4c71f2");
|
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
|
"fill",
|
|
"#fffefe",
|
|
);
|
|
});
|
|
|
|
it("should render custom colors properly", () => {
|
|
const customColors = {
|
|
title_color: "5a0",
|
|
icon_color: "1b998b",
|
|
text_color: "9991",
|
|
bg_color: "252525",
|
|
};
|
|
|
|
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.innerHTML);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
|
|
expect(statClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
|
|
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
|
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
|
"fill",
|
|
"#252525",
|
|
);
|
|
});
|
|
|
|
it("should render custom colors with themes", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
title_color: "5a0",
|
|
theme: "radical",
|
|
});
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.innerHTML);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe("#5a0");
|
|
expect(statClassStyles.fill.trim()).toBe(`#${themes.radical.text_color}`);
|
|
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
|
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
|
"fill",
|
|
`#${themes.radical.bg_color}`,
|
|
);
|
|
});
|
|
|
|
it("should render with all the themes", () => {
|
|
Object.keys(themes).forEach((name) => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
theme: name,
|
|
});
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.innerHTML);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe(
|
|
`#${themes[name].title_color}`,
|
|
);
|
|
expect(statClassStyles.fill.trim()).toBe(`#${themes[name].text_color}`);
|
|
expect(iconClassStyles.fill.trim()).toBe(`#${themes[name].icon_color}`);
|
|
const backgroundElement = queryByTestId(document.body, "card-bg");
|
|
const backgroundElementFill = backgroundElement.getAttribute("fill");
|
|
expect([`#${themes[name].bg_color}`, "url(#gradient)"]).toContain(
|
|
backgroundElementFill,
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should render custom colors with themes and fallback to default colors if invalid", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
title_color: "invalid color",
|
|
text_color: "invalid color",
|
|
theme: "radical",
|
|
});
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.innerHTML);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe(
|
|
`#${themes.default.title_color}`,
|
|
);
|
|
expect(statClassStyles.fill.trim()).toBe(`#${themes.default.text_color}`);
|
|
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
|
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
|
"fill",
|
|
`#${themes.radical.bg_color}`,
|
|
);
|
|
});
|
|
|
|
it("should render custom ring_color properly", () => {
|
|
const customColors = {
|
|
title_color: "5a0",
|
|
ring_color: "0000ff",
|
|
icon_color: "1b998b",
|
|
text_color: "9991",
|
|
bg_color: "252525",
|
|
};
|
|
|
|
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
|
|
|
|
const styleTag = document.querySelector("style");
|
|
const stylesObject = cssToObject(styleTag.innerHTML);
|
|
|
|
const headerClassStyles = stylesObject[":host"][".header "];
|
|
const statClassStyles = stylesObject[":host"][".stat "];
|
|
const iconClassStyles = stylesObject[":host"][".icon "];
|
|
const rankCircleStyles = stylesObject[":host"][".rank-circle "];
|
|
const rankCircleRimStyles = stylesObject[":host"][".rank-circle-rim "];
|
|
|
|
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
|
|
expect(statClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
|
|
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
|
|
expect(rankCircleStyles.stroke.trim()).toBe(`#${customColors.ring_color}`);
|
|
expect(rankCircleRimStyles.stroke.trim()).toBe(
|
|
`#${customColors.ring_color}`,
|
|
);
|
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
|
"fill",
|
|
"#252525",
|
|
);
|
|
});
|
|
|
|
it("should render icons correctly", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
show_icons: true,
|
|
});
|
|
|
|
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
|
|
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
|
expect(
|
|
queryByTestId(document.body, "stars").previousElementSibling, // the label
|
|
).toHaveAttribute("x", "25");
|
|
});
|
|
|
|
it("should not have icons if show_icons is false", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
|
|
|
|
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
|
|
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
|
expect(
|
|
queryByTestId(document.body, "stars").previousElementSibling, // the label
|
|
).not.toHaveAttribute("x");
|
|
});
|
|
|
|
it("should auto resize if hide_rank is true", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
hide_rank: true,
|
|
});
|
|
|
|
expect(
|
|
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
|
).toBe("305.81250000000006");
|
|
});
|
|
|
|
it("should auto resize if hide_rank is true & custom_title is set", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
hide_rank: true,
|
|
custom_title: "Hello world",
|
|
});
|
|
|
|
expect(
|
|
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
|
).toBe("287");
|
|
});
|
|
|
|
it("should render translations", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, { locale: "cn" });
|
|
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
|
"Anurag Hazra 的 GitHub 统计数据",
|
|
);
|
|
expect(
|
|
document.querySelector(
|
|
'g[transform="translate(0, 0)"]>.stagger>.stat.bold',
|
|
).textContent,
|
|
).toMatchInlineSnapshot(`"获标星数:"`);
|
|
expect(
|
|
document.querySelector(
|
|
'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
|
|
).textContent,
|
|
).toMatchInlineSnapshot(`"累计提交总数 (去年):"`);
|
|
expect(
|
|
document.querySelector(
|
|
'g[transform="translate(0, 50)"]>.stagger>.stat.bold',
|
|
).textContent,
|
|
).toMatchInlineSnapshot(`"发起的 PR 总数:"`);
|
|
expect(
|
|
document.querySelector(
|
|
'g[transform="translate(0, 75)"]>.stagger>.stat.bold',
|
|
).textContent,
|
|
).toMatchInlineSnapshot(`"提出的 issue 总数:"`);
|
|
expect(
|
|
document.querySelector(
|
|
'g[transform="translate(0, 100)"]>.stagger>.stat.bold',
|
|
).textContent,
|
|
).toMatchInlineSnapshot(`"贡献的项目数(去年):"`);
|
|
});
|
|
|
|
it("should render without rounding", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, { border_radius: "0" });
|
|
expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
|
|
document.body.innerHTML = renderStatsCard(stats, {});
|
|
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
|
|
});
|
|
|
|
it("should shorten values", () => {
|
|
stats["totalCommits"] = 1999;
|
|
|
|
document.body.innerHTML = renderStatsCard(stats);
|
|
expect(getByTestId(document.body, "commits").textContent).toBe("2k");
|
|
document.body.innerHTML = renderStatsCard(stats, { number_format: "long" });
|
|
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
|
|
});
|
|
|
|
it("should render default rank icon with level A+", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
rank_icon: "default",
|
|
});
|
|
expect(queryByTestId(document.body, "level-rank-icon")).toBeDefined();
|
|
expect(
|
|
queryByTestId(document.body, "level-rank-icon").textContent.trim(),
|
|
).toBe("A+");
|
|
});
|
|
|
|
it("should render github rank icon", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
rank_icon: "github",
|
|
});
|
|
expect(queryByTestId(document.body, "github-rank-icon")).toBeDefined();
|
|
});
|
|
|
|
it("should show the rank percentile", () => {
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
rank_icon: "percentile",
|
|
});
|
|
expect(queryByTestId(document.body, "percentile-top-header")).toBeDefined();
|
|
expect(
|
|
queryByTestId(document.body, "percentile-top-header").textContent.trim(),
|
|
).toBe("Top");
|
|
expect(queryByTestId(document.body, "rank-percentile-text")).toBeDefined();
|
|
expect(
|
|
queryByTestId(document.body, "percentile-rank-value").textContent.trim(),
|
|
).toBe(stats.rank.percentile.toFixed(1) + "%");
|
|
});
|
|
|
|
it("should throw error if all stats and rank icon are hidden", () => {
|
|
expect(() =>
|
|
renderStatsCard(stats, {
|
|
hide: ["stars", "commits", "prs", "issues", "contribs"],
|
|
hide_rank: true,
|
|
}),
|
|
).toThrow(
|
|
new CustomError(
|
|
"Could not render stats card.",
|
|
"Either stats or rank are required.",
|
|
),
|
|
);
|
|
});
|
|
});
|