mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(docs): self-host the Star History chart (#6755)
Problem: The README's third-party Star History image stopped rendering after GitHub restricted access to timestamped stargazer history. Root cause: The unauthenticated chart service can no longer read this repository's detailed stargazer data, so GitHub Camo receives an upstream failure. Fix: Generate accessible light and dark SVG charts in a trusted secret-backed workflow, publish them to a dedicated asset branch, and reference those repository-hosted images from both READMEs. Verification: - chart generator syntax and 5 unit tests passed - actionlint and git diff checks passed - full repository CI passed on Linux, macOS, and Windows - race, CodeQL, desktop, site, and security checks passed
This commit is contained in:
1 parent
9cd6c67a6e
commit
3a815ed35d
5 files changed
+462
-6
No files matched your search
@@ -0,0 +1,74 @@
|
||||
name: Update Star History chart
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main-v2
|
||||
paths:
|
||||
- '.github/workflows/update-star-history.yml'
|
||||
- 'scripts/update-star-history.mjs'
|
||||
- 'scripts/update-star-history.test.mjs'
|
||||
schedule:
|
||||
- cron: '23 */3 * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: update-star-history
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.repository == 'esengine/DeepSeek-Reasonix'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Test chart generator
|
||||
run: node --test scripts/update-star-history.test.mjs
|
||||
- name: Generate chart assets
|
||||
env:
|
||||
STAR_HISTORY_GITHUB_TOKEN: ${{ secrets.STAR_HISTORY_GITHUB_TOKEN }}
|
||||
STAR_HISTORY_OUTPUT_DIR: ${{ runner.temp }}/star-history
|
||||
run: node scripts/update-star-history.mjs
|
||||
- name: Publish chart assets
|
||||
env:
|
||||
OUTPUT_DIR: ${{ runner.temp }}/star-history
|
||||
TARGET_BRANCH: star-history
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
if git fetch origin "$TARGET_BRANCH"; then
|
||||
git switch --force-create "$TARGET_BRANCH" "origin/$TARGET_BRANCH"
|
||||
else
|
||||
git switch --create "$TARGET_BRANCH"
|
||||
fi
|
||||
|
||||
mkdir -p assets/star-history
|
||||
cp "$OUTPUT_DIR/star-history-light.svg" assets/star-history/star-history-light.svg
|
||||
cp "$OUTPUT_DIR/star-history-dark.svg" assets/star-history/star-history-dark.svg
|
||||
git add assets/star-history
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "Star History chart is already current."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit \
|
||||
-m "docs: update Star History chart" \
|
||||
-m "Automated update:
|
||||
Refresh the repository-hosted light and dark Star History SVGs from the authenticated GitHub stargazers API.
|
||||
|
||||
Verification:
|
||||
- chart generator tests passed
|
||||
- both SVG assets were rendered from timestamped stargazer data"
|
||||
git push origin HEAD:"$TARGET_BRANCH"
|
||||
@@ -150,9 +150,9 @@ For advanced CLI usage and configuration, see the **[CLI reference](./docs/CLI.m
|
||||
|
||||
<a href="https://www.star-history.com/?repos=esengine%2FDeepSeek-Reasonix&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-dark.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-light.svg" />
|
||||
<img alt="Star History Chart" src="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-light.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
+3
-3
@@ -144,9 +144,9 @@ CLI 进阶用法和详细配置见 **[CLI 命令参考](./docs/CLI.zh-CN.md)**
|
||||
|
||||
<a href="https://www.star-history.com/?repos=esengine%2FDeepSeek-Reasonix&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=esengine/DeepSeek-Reasonix&type=date&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-dark.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-light.svg" />
|
||||
<img alt="Star History Chart" src="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-light.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const API_VERSION = '2026-03-10';
|
||||
const DEFAULT_REPOSITORY = 'esengine/DeepSeek-Reasonix';
|
||||
|
||||
export function parseLinkHeader(value) {
|
||||
const links = {};
|
||||
for (const part of (value || '').split(',')) {
|
||||
const match = part.trim().match(/^<([^>]+)>;\s*rel="([^"]+)"$/);
|
||||
if (match) links[match[2]] = match[1];
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
async function requestPage(url, token, fetchImpl) {
|
||||
const retryable = new Set([429, 500, 502, 503, 504]);
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.star+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'DeepSeek-Reasonix-star-history-updater',
|
||||
'X-GitHub-Api-Version': API_VERSION,
|
||||
},
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < 3) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1_000));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.ok) return response;
|
||||
|
||||
const detail = (await response.text()).trim().slice(0, 300);
|
||||
lastError = new Error(`GitHub API returned ${response.status}: ${detail}`);
|
||||
if (!retryable.has(response.status) || attempt === 3) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1_000));
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function readStargazerPage(response, page) {
|
||||
const entries = await response.json();
|
||||
if (!Array.isArray(entries)) {
|
||||
throw new Error(`GitHub stargazers page ${page} was not an array`);
|
||||
}
|
||||
|
||||
return entries.map((entry, index) => {
|
||||
if (typeof entry?.starred_at !== 'string') {
|
||||
throw new Error(
|
||||
`GitHub stargazers page ${page} item ${index + 1} did not include starred_at`,
|
||||
);
|
||||
}
|
||||
return entry.starred_at;
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAllStargazers({
|
||||
repository = DEFAULT_REPOSITORY,
|
||||
token,
|
||||
fetchImpl = fetch,
|
||||
}) {
|
||||
if (!token) throw new Error('STAR_HISTORY_GITHUB_TOKEN is required');
|
||||
|
||||
const endpoint = new URL(`https://api.github.com/repos/${repository}/stargazers`);
|
||||
endpoint.searchParams.set('per_page', '100');
|
||||
endpoint.searchParams.set('page', '1');
|
||||
|
||||
const firstResponse = await requestPage(endpoint, token, fetchImpl);
|
||||
const links = parseLinkHeader(firstResponse.headers.get('link'));
|
||||
const lastPage = links.last
|
||||
? Number.parseInt(new URL(links.last).searchParams.get('page') || '1', 10)
|
||||
: 1;
|
||||
|
||||
if (!Number.isInteger(lastPage) || lastPage < 1) {
|
||||
throw new Error(`GitHub returned an invalid last page: ${lastPage}`);
|
||||
}
|
||||
|
||||
const timestamps = await readStargazerPage(firstResponse, 1);
|
||||
for (let page = 2; page <= lastPage; page += 1) {
|
||||
endpoint.searchParams.set('page', String(page));
|
||||
const response = await requestPage(endpoint, token, fetchImpl);
|
||||
timestamps.push(...(await readStargazerPage(response, page)));
|
||||
|
||||
if (page % 50 === 0 || page === lastPage) {
|
||||
console.log(`Fetched stargazer page ${page}/${lastPage}`);
|
||||
}
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
export function buildDailySeries(timestamps, asOf = new Date()) {
|
||||
if (timestamps.length === 0) throw new Error('The repository has no stargazers');
|
||||
|
||||
const byDay = new Map();
|
||||
for (const timestamp of timestamps) {
|
||||
const instant = new Date(timestamp);
|
||||
if (Number.isNaN(instant.getTime())) {
|
||||
throw new Error(`Invalid starred_at timestamp: ${timestamp}`);
|
||||
}
|
||||
const day = instant.toISOString().slice(0, 10);
|
||||
byDay.set(day, (byDay.get(day) || 0) + 1);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const series = [...byDay.entries()]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([date, count]) => {
|
||||
total += count;
|
||||
return { date, count: total };
|
||||
});
|
||||
|
||||
const today = asOf.toISOString().slice(0, 10);
|
||||
if (today > series.at(-1).date) series.push({ date: today, count: total });
|
||||
return series;
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function niceMaximum(value) {
|
||||
const magnitude = 10 ** Math.floor(Math.log10(Math.max(1, value)));
|
||||
return Math.ceil(value / magnitude) * magnitude;
|
||||
}
|
||||
|
||||
function formatDateTick(timestamp, spanDays) {
|
||||
const date = new Date(timestamp);
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
...(spanDays > 365 ? { year: 'numeric' } : { day: 'numeric' }),
|
||||
timeZone: 'UTC',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function renderStarHistorySvg(
|
||||
series,
|
||||
{ repository = DEFAULT_REPOSITORY, theme = 'light' } = {},
|
||||
) {
|
||||
if (series.length === 0) throw new Error('Cannot render an empty star history series');
|
||||
|
||||
const palette =
|
||||
theme === 'dark'
|
||||
? {
|
||||
background: '#0d1117',
|
||||
border: '#30363d',
|
||||
grid: '#21262d',
|
||||
line: '#58a6ff',
|
||||
muted: '#8b949e',
|
||||
text: '#f0f6fc',
|
||||
}
|
||||
: {
|
||||
background: '#ffffff',
|
||||
border: '#d0d7de',
|
||||
grid: '#d8dee4',
|
||||
line: '#0969da',
|
||||
muted: '#57606a',
|
||||
text: '#24292f',
|
||||
};
|
||||
|
||||
const width = 960;
|
||||
const height = 480;
|
||||
const margin = { top: 78, right: 34, bottom: 62, left: 76 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
const firstTime = Date.parse(`${series[0].date}T00:00:00Z`);
|
||||
const lastTime = Date.parse(`${series.at(-1).date}T00:00:00Z`);
|
||||
const timeSpan = Math.max(86_400_000, lastTime - firstTime);
|
||||
const spanDays = timeSpan / 86_400_000;
|
||||
const total = series.at(-1).count;
|
||||
const yMaximum = niceMaximum(total);
|
||||
const xFor = (timestamp) => margin.left + ((timestamp - firstTime) / timeSpan) * plotWidth;
|
||||
const yFor = (count) => margin.top + plotHeight - (count / yMaximum) * plotHeight;
|
||||
|
||||
const points = series.map(({ date, count }) => ({
|
||||
x: xFor(Date.parse(`${date}T00:00:00Z`)),
|
||||
y: yFor(count),
|
||||
}));
|
||||
const linePath = points
|
||||
.map(({ x, y }, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`)
|
||||
.join(' ');
|
||||
const areaPath = `${linePath} L${points.at(-1).x.toFixed(2)},${(
|
||||
margin.top + plotHeight
|
||||
).toFixed(2)} L${points[0].x.toFixed(2)},${(margin.top + plotHeight).toFixed(2)} Z`;
|
||||
|
||||
const yTicks = Array.from({ length: 6 }, (_, index) => {
|
||||
const value = (yMaximum / 5) * index;
|
||||
const y = yFor(value);
|
||||
return `
|
||||
<line x1="${margin.left}" y1="${y.toFixed(2)}" x2="${width - margin.right}" y2="${y.toFixed(2)}" stroke="${palette.grid}" stroke-width="1" />
|
||||
<text x="${margin.left - 12}" y="${(y + 4).toFixed(2)}" text-anchor="end" fill="${palette.muted}" font-size="12">${Math.round(value).toLocaleString('en-US')}</text>`;
|
||||
}).join('');
|
||||
|
||||
const xTicks = Array.from({ length: 6 }, (_, index) => {
|
||||
const timestamp = firstTime + (timeSpan * index) / 5;
|
||||
const x = xFor(timestamp);
|
||||
return `
|
||||
<line x1="${x.toFixed(2)}" y1="${margin.top}" x2="${x.toFixed(2)}" y2="${margin.top + plotHeight}" stroke="${palette.grid}" stroke-width="1" />
|
||||
<text x="${x.toFixed(2)}" y="${height - 28}" text-anchor="middle" fill="${palette.muted}" font-size="12">${escapeXml(formatDateTick(timestamp, spanDays))}</text>`;
|
||||
}).join('');
|
||||
|
||||
const lastPoint = points.at(-1);
|
||||
const title = `${repository} Star History`;
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="title description">
|
||||
<title id="title">${escapeXml(title)}</title>
|
||||
<desc id="description">${total.toLocaleString('en-US')} current GitHub stargazers through ${escapeXml(series.at(-1).date)}.</desc>
|
||||
<rect width="${width}" height="${height}" rx="12" fill="${palette.background}" stroke="${palette.border}" />
|
||||
<text x="${margin.left}" y="37" fill="${palette.text}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="22" font-weight="600">Star History</text>
|
||||
<text x="${margin.left}" y="60" fill="${palette.muted}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13">${escapeXml(repository)}</text>
|
||||
<g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
|
||||
${yTicks}
|
||||
${xTicks}
|
||||
<path d="${areaPath}" fill="${palette.line}" opacity="0.12" />
|
||||
<path d="${linePath}" fill="none" stroke="${palette.line}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="${lastPoint.x.toFixed(2)}" cy="${lastPoint.y.toFixed(2)}" r="5" fill="${palette.line}" stroke="${palette.background}" stroke-width="2" />
|
||||
<g transform="translate(${width - margin.right - 280}, 25)">
|
||||
<circle cx="0" cy="0" r="5" fill="${palette.line}" />
|
||||
<text x="12" y="5" fill="${palette.text}" font-size="14" font-weight="600">${escapeXml(repository)}</text>
|
||||
<text x="12" y="24" fill="${palette.muted}" font-size="12">${total.toLocaleString('en-US')} stars</text>
|
||||
</g>
|
||||
<text x="${width - margin.right}" y="${height - 14}" text-anchor="end" fill="${palette.muted}" font-size="11">Updated automatically from GitHub stargazer history · ${escapeXml(series.at(-1).date)} UTC</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const repository = process.env.STAR_HISTORY_REPOSITORY || DEFAULT_REPOSITORY;
|
||||
const outputDirectory = process.env.STAR_HISTORY_OUTPUT_DIR || 'assets/star-history';
|
||||
const token = process.env.STAR_HISTORY_GITHUB_TOKEN;
|
||||
|
||||
console.log(`Fetching GitHub stargazer history for ${repository}`);
|
||||
const timestamps = await fetchAllStargazers({ repository, token });
|
||||
const series = buildDailySeries(timestamps);
|
||||
console.log(`Rendering ${timestamps.length.toLocaleString('en-US')} current stargazers`);
|
||||
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
`${outputDirectory}/star-history-light.svg`,
|
||||
renderStarHistorySvg(series, { repository, theme: 'light' }),
|
||||
'utf8',
|
||||
),
|
||||
writeFile(
|
||||
`${outputDirectory}/star-history-dark.svg`,
|
||||
renderStarHistorySvg(series, { repository, theme: 'dark' }),
|
||||
'utf8',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
if (isMain) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildDailySeries,
|
||||
fetchAllStargazers,
|
||||
parseLinkHeader,
|
||||
renderStarHistorySvg,
|
||||
} from './update-star-history.mjs';
|
||||
|
||||
test('parseLinkHeader extracts pagination relationships', () => {
|
||||
assert.deepEqual(
|
||||
parseLinkHeader(
|
||||
'<https://api.github.com/example?page=2>; rel="next", <https://api.github.com/example?page=4>; rel="last"',
|
||||
),
|
||||
{
|
||||
next: 'https://api.github.com/example?page=2',
|
||||
last: 'https://api.github.com/example?page=4',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchAllStargazers follows every page and requests starred_at timestamps', async () => {
|
||||
const requests = [];
|
||||
const fakeFetch = async (url, options) => {
|
||||
requests.push({ url: String(url), headers: options.headers });
|
||||
const page = new URL(url).searchParams.get('page');
|
||||
const body =
|
||||
page === '1'
|
||||
? [{ starred_at: '2026-07-19T01:00:00Z' }]
|
||||
: [{ starred_at: '2026-07-20T01:00:00Z' }];
|
||||
return new Response(JSON.stringify(body), {
|
||||
headers:
|
||||
page === '1'
|
||||
? {
|
||||
link: '<https://api.github.com/repos/example/repo/stargazers?per_page=100&page=2>; rel="next", <https://api.github.com/repos/example/repo/stargazers?per_page=100&page=2>; rel="last"',
|
||||
}
|
||||
: {},
|
||||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
await fetchAllStargazers({
|
||||
repository: 'example/repo',
|
||||
token: 'fake-token',
|
||||
fetchImpl: fakeFetch,
|
||||
}),
|
||||
['2026-07-19T01:00:00Z', '2026-07-20T01:00:00Z'],
|
||||
);
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(requests[0].headers.Accept, 'application/vnd.github.star+json');
|
||||
assert.equal(requests[0].headers.Authorization, 'Bearer fake-token');
|
||||
});
|
||||
|
||||
test('fetchAllStargazers does not retry permission failures', async () => {
|
||||
let requests = 0;
|
||||
const fakeFetch = async () => {
|
||||
requests += 1;
|
||||
return new Response('{"message":"Forbidden"}', { status: 403 });
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
fetchAllStargazers({
|
||||
repository: 'example/repo',
|
||||
token: 'fake-token',
|
||||
fetchImpl: fakeFetch,
|
||||
}),
|
||||
/GitHub API returned 403/,
|
||||
);
|
||||
assert.equal(requests, 1);
|
||||
});
|
||||
|
||||
test('buildDailySeries aggregates days and extends the line to the current date', () => {
|
||||
assert.deepEqual(
|
||||
buildDailySeries(
|
||||
[
|
||||
'2026-07-19T01:00:00Z',
|
||||
'2026-07-19T18:00:00Z',
|
||||
'2026-07-20T01:00:00Z',
|
||||
],
|
||||
new Date('2026-07-21T12:00:00Z'),
|
||||
),
|
||||
[
|
||||
{ date: '2026-07-19', count: 2 },
|
||||
{ date: '2026-07-20', count: 3 },
|
||||
{ date: '2026-07-21', count: 3 },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('renderStarHistorySvg produces stable accessible light and dark charts', () => {
|
||||
const series = [
|
||||
{ date: '2026-07-19', count: 1 },
|
||||
{ date: '2026-07-21', count: 3 },
|
||||
];
|
||||
const light = renderStarHistorySvg(series, { repository: 'example/repo', theme: 'light' });
|
||||
const dark = renderStarHistorySvg(series, { repository: 'example/repo', theme: 'dark' });
|
||||
|
||||
assert.match(light, /<title id="title">example\/repo Star History<\/title>/);
|
||||
assert.match(light, /3 stars/);
|
||||
assert.match(light, /#ffffff/);
|
||||
assert.match(dark, /#0d1117/);
|
||||
assert.doesNotMatch(light, /NaN|undefined/);
|
||||
});
|
||||
Reference in new issue
Block a user