mirror of
https://github.com/System-End/github-readme-stats.git
synced 2026-04-19 22:15:15 +00:00
Feature: Add gist card (#3064)
* Feature: Add gist card * dev * dev * dev * dev * dev * dev * e2e * e2e test timeout
This commit is contained in:
parent
48830adefa
commit
fe901dd337
10 changed files with 614 additions and 3 deletions
81
api/gist.js
Normal file
81
api/gist.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
renderError,
|
||||
parseBoolean,
|
||||
} from "../src/common/utils.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
import { renderGistCard } from "../src/cards/gist-card.js";
|
||||
import { fetchGist } from "../src/fetchers/gist-fetcher.js";
|
||||
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
id,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner,
|
||||
} = req.query;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(renderError("Something went wrong", "Language not found"));
|
||||
}
|
||||
|
||||
try {
|
||||
const gistData = await fetchGist(id);
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
|
||||
CONSTANTS.FOUR_HOURS,
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
cacheSeconds = process.env.CACHE_SECONDS
|
||||
? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds
|
||||
: cacheSeconds;
|
||||
|
||||
/*
|
||||
if star count & fork count is over 1k then we are kFormating the text
|
||||
and if both are zero we are not showing the stats
|
||||
so we can just make the cache longer, since there is no need to frequent updates
|
||||
*/
|
||||
const stars = gistData.starsCount;
|
||||
const forks = gistData.forksCount;
|
||||
const isBothOver1K = stars > 1000 && forks > 1000;
|
||||
const isBothUnder1 = stars < 1 && forks < 1;
|
||||
if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
|
||||
cacheSeconds = CONSTANTS.FOUR_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
renderGistCard(gistData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
43
readme.md
43
readme.md
|
|
@ -97,8 +97,11 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of
|
|||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Usage](#usage)
|
||||
- [Demo](#demo)
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
- [GitHub Gist Pins](#github-gist-pins)
|
||||
- [Usage](#usage-1)
|
||||
- [Demo](#demo-1)
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
- [Usage](#usage-2)
|
||||
- [Language stats algorithm](#language-stats-algorithm)
|
||||
- [Exclude individual repositories](#exclude-individual-repositories)
|
||||
- [Hide individual languages](#hide-individual-languages)
|
||||
|
|
@ -108,9 +111,9 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of
|
|||
- [Donut Vertical Chart Language Card Layout](#donut-vertical-chart-language-card-layout)
|
||||
- [Pie Chart Language Card Layout](#pie-chart-language-card-layout)
|
||||
- [Hide Progress Bars](#hide-progress-bars)
|
||||
- [Demo](#demo-1)
|
||||
- [Wakatime Stats Card](#wakatime-stats-card)
|
||||
- [Demo](#demo-2)
|
||||
- [Wakatime Stats Card](#wakatime-stats-card)
|
||||
- [Demo](#demo-3)
|
||||
- [All Demos](#all-demos)
|
||||
- [Quick Tip (Align The Cards)](#quick-tip-align-the-cards)
|
||||
- [Deploy on your own](#deploy-on-your-own)
|
||||
|
|
@ -328,6 +331,10 @@ You can provide multiple comma-separated values in the bg\_color option to rende
|
|||
|
||||
* `show_owner` - Shows the repo's owner name *(boolean)*. Default: `false`.
|
||||
|
||||
#### Gist Card Exclusive Options
|
||||
|
||||
* `show_owner` - Shows the gist's owner name *(boolean)*. Default: `false`.
|
||||
|
||||
#### Language Card Exclusive Options
|
||||
|
||||
* `hide` - Hides the languages specified from the card *(Comma-separated values)*. Default: `[] (blank array)`.
|
||||
|
|
@ -384,6 +391,28 @@ Use [show\_owner](#repo-card-exclusive-options) query option to include the repo
|
|||
|
||||

|
||||
|
||||
# GitHub Gist Pins
|
||||
|
||||
GitHub gist pins allow you to pin gists in your GitHub profile using a GitHub readme profile.
|
||||
|
||||
### Usage
|
||||
|
||||
Copy-paste this code into your readme and change the links.
|
||||
|
||||
Endpoint: `api/gist?id=bbfce31e0217a3689c8d961a356cb10d`
|
||||
|
||||
```md
|
||||
[](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||

|
||||
|
||||
Use [show\_owner](#gist-card-exclusive-options) query option to include the gist's owner username
|
||||
|
||||

|
||||
|
||||
# Top Languages Card
|
||||
|
||||
The top languages card shows a GitHub user's most frequently used languages.
|
||||
|
|
@ -592,6 +621,14 @@ Choose from any of the [default themes](#themes)
|
|||
|
||||

|
||||
|
||||
* Gist card
|
||||
|
||||

|
||||
|
||||
* Customizing gist card
|
||||
|
||||

|
||||
|
||||
* Top languages
|
||||
|
||||

|
||||
|
|
|
|||
180
src/cards/gist-card.js
Normal file
180
src/cards/gist-card.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// @ts-check
|
||||
|
||||
import {
|
||||
getCardColors,
|
||||
parseEmojis,
|
||||
wrapTextMultiline,
|
||||
encodeHTML,
|
||||
kFormatter,
|
||||
measureText,
|
||||
flexLayout,
|
||||
} from "../common/utils.js";
|
||||
import Card from "../common/Card.js";
|
||||
import { icons } from "../common/icons.js";
|
||||
|
||||
/** Import language colors.
|
||||
*
|
||||
* @description Here we use the workaround found in
|
||||
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
|
||||
* since vercel is using v16.14.0 which does not yet support json imports without the
|
||||
* --experimental-json-modules flag.
|
||||
*/
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const languageColors = require("../common/languageColors.json"); // now works
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const CARD_DEFAULT_WIDTH = 400;
|
||||
const HEADER_MAX_LENGTH = 35;
|
||||
|
||||
/**
|
||||
* Creates a node to display the primary programming language of the gist.
|
||||
*
|
||||
* @param {string} langName Language name.
|
||||
* @param {string} langColor Language color.
|
||||
* @returns {string} Language display SVG object.
|
||||
*/
|
||||
const createLanguageNode = (langName, langColor) => {
|
||||
return `
|
||||
<g data-testid="primary-lang">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an icon with label to display gist stats like forks, stars, etc.
|
||||
*
|
||||
* @param {string} icon The icon to display.
|
||||
* @param {number|string} label The label to display.
|
||||
* @param {string} testid The testid to assign to the label.
|
||||
* @returns {string} Icon with label SVG object.
|
||||
*/
|
||||
const iconWithLabel = (icon, label, testid) => {
|
||||
if (typeof label === "number" && label <= 0) return "";
|
||||
const iconSvg = `
|
||||
<svg
|
||||
class="icon"
|
||||
y="-12"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="${ICON_SIZE}"
|
||||
height="${ICON_SIZE}"
|
||||
>
|
||||
${icon}
|
||||
</svg>
|
||||
`;
|
||||
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
|
||||
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
|
||||
* @typedef {import('../fetchers/types').GistData} GistData Gist data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render gist card.
|
||||
*
|
||||
* @param {GistData} gistData Gist data.
|
||||
* @param {Partial<GistCardOptions>} options Gist card options.
|
||||
* @returns {string} Gist card.
|
||||
*/
|
||||
const renderGistCard = (gistData, options = {}) => {
|
||||
const { name, nameWithOwner, description, language, starsCount, forksCount } =
|
||||
gistData;
|
||||
const {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner = false,
|
||||
} = options;
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const lineWidth = 59;
|
||||
const linesLimit = 10;
|
||||
const desc = parseEmojis(description || "No description provided");
|
||||
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
|
||||
const descriptionLines = multiLineDescription.length;
|
||||
const descriptionSvg = multiLineDescription
|
||||
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
||||
.join("");
|
||||
|
||||
const lineHeight = descriptionLines > 3 ? 12 : 10;
|
||||
const height =
|
||||
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
|
||||
|
||||
const totalStars = kFormatter(starsCount);
|
||||
const totalForks = kFormatter(forksCount);
|
||||
const svgStars = iconWithLabel(icons.star, totalStars, "starsCount");
|
||||
const svgForks = iconWithLabel(icons.fork, totalForks, "forksCount");
|
||||
|
||||
const languageName = language || "Unspecified";
|
||||
const languageColor = languageColors[languageName] || "#858585";
|
||||
|
||||
const svgLanguage = createLanguageNode(languageName, languageColor);
|
||||
|
||||
const starAndForkCount = flexLayout({
|
||||
items: [svgLanguage, svgStars, svgForks],
|
||||
sizes: [
|
||||
measureText(languageName, 12),
|
||||
ICON_SIZE + measureText(`${totalStars}`, 12),
|
||||
ICON_SIZE + measureText(`${totalForks}`, 12),
|
||||
],
|
||||
gap: 25,
|
||||
}).join("");
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
|
||||
const card = new Card({
|
||||
defaultTitle:
|
||||
header.length > HEADER_MAX_LENGTH
|
||||
? `${header.slice(0, HEADER_MAX_LENGTH)}...`
|
||||
: header,
|
||||
titlePrefixIcon: icons.gist,
|
||||
width: CARD_DEFAULT_WIDTH,
|
||||
height,
|
||||
border_radius,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
`);
|
||||
|
||||
return card.render(`
|
||||
<text class="description" x="25" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderGistCard, HEADER_MAX_LENGTH };
|
||||
export default renderGistCard;
|
||||
4
src/cards/types.d.ts
vendored
4
src/cards/types.d.ts
vendored
|
|
@ -57,3 +57,7 @@ type WakaTimeOptions = CommonOptions & {
|
|||
layout: "compact" | "normal";
|
||||
langs_count: number;
|
||||
};
|
||||
|
||||
export type GistCardOptions = CommonOptions & {
|
||||
show_owner: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const icons = {
|
|||
reviews: `<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>`,
|
||||
discussions_started: `<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z" />`,
|
||||
discussions_answered: `<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />`,
|
||||
gist: `<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />`,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
106
src/fetchers/gist-fetcher.js
Normal file
106
src/fetchers/gist-fetcher.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// @ts-check
|
||||
|
||||
import { request } from "../common/utils.js";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
|
||||
/**
|
||||
* @typedef {import('axios').AxiosRequestHeaders} AxiosRequestHeaders Axios request headers.
|
||||
* @typedef {import('axios').AxiosResponse} AxiosResponse Axios response.
|
||||
*/
|
||||
|
||||
const QUERY = `
|
||||
query gistInfo($gistName: String!) {
|
||||
viewer {
|
||||
gist(name: $gistName) {
|
||||
description
|
||||
owner {
|
||||
login
|
||||
}
|
||||
stargazerCount
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
files {
|
||||
name
|
||||
language {
|
||||
name
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Gist data fetcher.
|
||||
*
|
||||
* @param {AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<AxiosResponse>} The response.
|
||||
*/
|
||||
const fetcher = async (variables, token) => {
|
||||
return await request(
|
||||
{ query: QUERY, variables },
|
||||
{ Authorization: `token ${token}` },
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').GistData} GistData Gist data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch GitHub gist information by given username and ID.
|
||||
*
|
||||
* @param {string} id Github gist ID.
|
||||
* @returns {Promise<GistData>} Gist data.
|
||||
*/
|
||||
const fetchGist = async (id) => {
|
||||
const res = await retryer(fetcher, { gistName: id });
|
||||
if (res.data.errors) throw new Error(res.data.errors[0].message);
|
||||
const data = res.data.data.viewer.gist;
|
||||
return {
|
||||
name: data.files[Object.keys(data.files)[0]].name,
|
||||
nameWithOwner: `${data.owner.login}/${
|
||||
data.files[Object.keys(data.files)[0]].name
|
||||
}`,
|
||||
description: data.description,
|
||||
language: calculatePrimaryLanguage(data.files),
|
||||
starsCount: data.stargazerCount,
|
||||
forksCount: data.forks.totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {{ name: string; language: { name: string; }, size: number }} GistFile Gist file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function calculates the primary language of a gist by files size.
|
||||
*
|
||||
* @param {GistFile[]} files Files.
|
||||
* @returns {string} Primary language.
|
||||
*/
|
||||
const calculatePrimaryLanguage = (files) => {
|
||||
const languages = {};
|
||||
for (const file of files) {
|
||||
if (file.language) {
|
||||
if (languages[file.language.name]) {
|
||||
languages[file.language.name] += file.size;
|
||||
} else {
|
||||
languages[file.language.name] = file.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
let primaryLanguage = Object.keys(languages)[0];
|
||||
for (const language in languages) {
|
||||
if (languages[language] > languages[primaryLanguage]) {
|
||||
primaryLanguage = language;
|
||||
}
|
||||
}
|
||||
return primaryLanguage;
|
||||
};
|
||||
|
||||
export { fetchGist };
|
||||
export default fetchGist;
|
||||
9
src/fetchers/types.d.ts
vendored
9
src/fetchers/types.d.ts
vendored
|
|
@ -1,3 +1,12 @@
|
|||
export type GistData = {
|
||||
name: string;
|
||||
nameWithOwner: string;
|
||||
description: string;
|
||||
language: string | null;
|
||||
starsCount: number;
|
||||
forksCount: number;
|
||||
};
|
||||
|
||||
export type RepositoryData = {
|
||||
name: string;
|
||||
nameWithOwner: string;
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import { renderRepoCard } from "../../src/cards/repo-card.js";
|
|||
import { renderStatsCard } from "../../src/cards/stats-card.js";
|
||||
import { renderTopLanguages } from "../../src/cards/top-languages-card.js";
|
||||
import { renderWakatimeCard } from "../../src/cards/wakatime-card.js";
|
||||
import { renderGistCard } from "../../src/cards/gist-card.js";
|
||||
import { expect, describe, beforeAll, test } from "@jest/globals";
|
||||
|
||||
const REPO = "curly-fiesta";
|
||||
const USER = "catelinemnemosyne";
|
||||
const GIST_ID = "372cef55fd897b31909fdeb3a7262758";
|
||||
|
||||
const STATS_DATA = {
|
||||
name: "Cateline Mnemosyne",
|
||||
totalPRs: 2,
|
||||
|
|
@ -81,6 +84,23 @@ const REPOSITORY_DATA = {
|
|||
starCount: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {GistData}
|
||||
*/
|
||||
const GIST_DATA = {
|
||||
name: "link.txt",
|
||||
nameWithOwner: "qwerty541/link.txt",
|
||||
description:
|
||||
"Trying to access this path on Windown 10 ver. 1803+ will breaks NTFS",
|
||||
language: "Text",
|
||||
starsCount: 1,
|
||||
forksCount: 0,
|
||||
};
|
||||
|
||||
const CACHE_BURST_STRING = `v=${new Date().getTime()}`;
|
||||
|
||||
describe("Fetch Cards", () => {
|
||||
|
|
@ -177,4 +197,26 @@ describe("Fetch Cards", () => {
|
|||
// Check if Repo card from deployment matches the local Repo card.
|
||||
expect(serverRepoSvg.data).toEqual(localRepoCardSVG);
|
||||
}, 15000);
|
||||
|
||||
test("retrieve gist card", async () => {
|
||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||
|
||||
// Check if the Vercel preview instance Gist function is up and running.
|
||||
await expect(
|
||||
axios.get(
|
||||
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
|
||||
),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
// Get local gist card.
|
||||
const localGistCardSVG = renderGistCard(GIST_DATA);
|
||||
|
||||
// Get the Vercel preview gist card response.
|
||||
const serverGistSvg = await axios.get(
|
||||
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
|
||||
);
|
||||
|
||||
// Check if Gist card from deployment matches the local Gist card.
|
||||
expect(serverGistSvg.data).toEqual(localGistCardSVG);
|
||||
}, 15000);
|
||||
});
|
||||
|
|
|
|||
72
tests/fetchGist.test.js
Normal file
72
tests/fetchGist.test.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import "@testing-library/jest-dom";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { expect, it, describe, afterEach } from "@jest/globals";
|
||||
import { fetchGist } from "../src/fetchers/gist-fetcher.js";
|
||||
|
||||
const gist_data = {
|
||||
data: {
|
||||
viewer: {
|
||||
gist: {
|
||||
description:
|
||||
"List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023",
|
||||
owner: {
|
||||
login: "Yizack",
|
||||
},
|
||||
stargazerCount: 33,
|
||||
forks: {
|
||||
totalCount: 11,
|
||||
},
|
||||
files: [
|
||||
{
|
||||
name: "countries.json",
|
||||
language: {
|
||||
name: "JSON",
|
||||
},
|
||||
size: 85858,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const gist_errors_data = {
|
||||
errors: [
|
||||
{
|
||||
message: "Some test GraphQL error",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test fetchGist", () => {
|
||||
it("should fetch gist correctly", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
||||
|
||||
let gist = await fetchGist("bbfce31e0217a3689c8d961a356cb10d");
|
||||
|
||||
expect(gist).toStrictEqual({
|
||||
name: "countries.json",
|
||||
nameWithOwner: "Yizack/countries.json",
|
||||
description:
|
||||
"List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023",
|
||||
language: "JSON",
|
||||
starsCount: 33,
|
||||
forksCount: 11,
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error if reaponse contains them", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_errors_data);
|
||||
|
||||
await expect(fetchGist("bbfce31e0217a3689c8d961a356cb10d")).rejects.toThrow(
|
||||
"Some test GraphQL error",
|
||||
);
|
||||
});
|
||||
});
|
||||
79
tests/renderGistCard.test.js
Normal file
79
tests/renderGistCard.test.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { renderGistCard } from "../src/cards/gist-card";
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { queryByTestId } from "@testing-library/dom";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
/**
|
||||
* @type {import("../src/fetchers/gist-fetcher").GistData}
|
||||
*/
|
||||
const data = {
|
||||
name: "test",
|
||||
nameWithOwner: "anuraghazra/test",
|
||||
description: "Small test repository with different Python programs.",
|
||||
language: "Python",
|
||||
starsCount: 163,
|
||||
forksCount: 19,
|
||||
};
|
||||
|
||||
describe("test renderGistCard", () => {
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderGistCard(data);
|
||||
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
|
||||
expect(header).toHaveTextContent("test");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Small test repository with different Python programs.",
|
||||
);
|
||||
expect(queryByTestId(document.body, "starsCount")).toHaveTextContent("163");
|
||||
expect(queryByTestId(document.body, "forksCount")).toHaveTextContent("19");
|
||||
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
|
||||
"Python",
|
||||
);
|
||||
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
|
||||
"fill",
|
||||
"#3572A5",
|
||||
);
|
||||
});
|
||||
|
||||
it("should display username in title if show_owner is true", () => {
|
||||
document.body.innerHTML = renderGistCard(data, { show_owner: true });
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
expect(header).toHaveTextContent("anuraghazra/test");
|
||||
});
|
||||
|
||||
it("should trim header if name is too long", () => {
|
||||
document.body.innerHTML = renderGistCard({
|
||||
...data,
|
||||
name: "some-really-long-repo-name-for-test-purposes",
|
||||
});
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
expect(header).toHaveTextContent("some-really-long-repo-name-for-test...");
|
||||
});
|
||||
|
||||
it("should trim description if description os too long", () => {
|
||||
document.body.innerHTML = renderGistCard({
|
||||
...data,
|
||||
description:
|
||||
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
|
||||
});
|
||||
expect(
|
||||
document.getElementsByClassName("description")[0].children[0].textContent,
|
||||
).toBe("The quick brown fox jumps over the lazy dog is an");
|
||||
|
||||
expect(
|
||||
document.getElementsByClassName("description")[0].children[1].textContent,
|
||||
).toBe("English-language pangram—a sentence that contains all");
|
||||
});
|
||||
|
||||
it("should not trim description if it is short", () => {
|
||||
document.body.innerHTML = renderGistCard({
|
||||
...data,
|
||||
description: "Small text should not trim",
|
||||
});
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Small text should not trim",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue