This PR mainly refactors the code by extracting a "core" package which is then used by the "backend" and "frontend" apps. Apart from this the PR also contains several smaller changes like fixing dependabot, upgrading dependencies, aligned "package.json" files, added tests, ... addresses step 1 of #32 closes #136 --------- Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
467 lines
17 KiB
JavaScript
467 lines
17 KiB
JavaScript
import {
|
|
getByTestId,
|
|
queryAllByTestId,
|
|
queryByTestId,
|
|
} from "@testing-library/dom";
|
|
import { cssToObject } from "@uppercod/css-to-object";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { renderStatsCard } from "../src/cards/stats.js";
|
|
import { CustomError } from "../src/common/error.js";
|
|
import { themes } from "../src/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", "1");
|
|
|
|
// 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", "1");
|
|
|
|
// 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", "1");
|
|
|
|
// 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", "1");
|
|
|
|
// 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", "1");
|
|
});
|
|
|
|
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");
|
|
document.body.innerHTML = renderStatsCard(stats, { number_precision: 2 });
|
|
expect(getByTestId(document.body, "commits").textContent).toBe("2.00k");
|
|
document.body.innerHTML = renderStatsCard(stats, {
|
|
number_format: "long",
|
|
number_precision: 2,
|
|
});
|
|
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.",
|
|
),
|
|
);
|
|
});
|
|
});
|