diff --git a/lib/svg/generator.test.ts b/lib/svg/generator.test.ts index eb43cb4a..6c3d582f 100644 --- a/lib/svg/generator.test.ts +++ b/lib/svg/generator.test.ts @@ -8,6 +8,7 @@ import { escapeXML, getSizeScale, truncateUsername, + deterministicRandom, } from './generator'; import type { BadgeParams, ContributionCalendar, StreakStats, MonthlyStats } from '../../types'; import { hexColor } from './sanitizer'; @@ -1444,3 +1445,24 @@ describe('Radar Scan Line Animation Alignment', () => { expect(geometryLong).toEqual(geometryBaseline); }); }); +describe('deterministicRandom', () => { + it('returns the same value for the same seed (determinism)', () => { + const seed = 'test-seed-42'; + expect(deterministicRandom(seed)).toBe(deterministicRandom(seed)); + }); + + it('result is always in the range [0, 1)', () => { + const seeds = ['hello', 'world', '', 'abc:def:0:offsetX', '12345']; + for (const seed of seeds) { + const result = deterministicRandom(seed); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThan(1); + } + }); + + it('returns different values for different seeds', () => { + const a = deterministicRandom('seed-alpha'); + const b = deterministicRandom('seed-beta'); + expect(a).not.toBe(b); + }); +}); diff --git a/lib/svg/generator.ts b/lib/svg/generator.ts index 7a8343b1..72e979c4 100644 --- a/lib/svg/generator.ts +++ b/lib/svg/generator.ts @@ -26,7 +26,7 @@ export function truncateUsername(username: string): string { return username.length > 12 ? `${username.slice(0, 12)}...` : username; } -function deterministicRandom(seed: string): number { +export function deterministicRandom(seed: string): number { let hash = 2166136261; for (let i = 0; i < seed.length; i++) { hash ^= seed.charCodeAt(i); @@ -930,13 +930,13 @@ export function generateWrappedSVG( .title-user { font-family: ${selectedFont || '"Syncopate", sans-serif'}; font-weight: 700; font-size: 13px; letter-spacing: 2.5px; opacity: 0.85; } .title-wrapped { font-family: ${selectedFont || '"Syncopate", sans-serif'}; font-weight: 700; font-size: 13px; letter-spacing: 2.5px; } - + .total-commits { font-family: ${statsFont}; font-size: 46px; font-weight: 700; } .total-label { font-family: "Roboto", sans-serif; font-size: 9.5px; font-weight: 700; letter-spacing: 1.5px; opacity: 0.5; } - + .grid-label { font-family: "Roboto", sans-serif; font-size: 9px; font-weight: 700; letter-spacing: 1.5px; opacity: 0.5; } .grid-val { font-family: ${statsFont}; font-size: 14.5px; font-weight: 600; } - + .scan-line { animation: scan-sweep var(--scan-speed, 8s) linear infinite; transform-box: fill-box; @@ -1085,11 +1085,11 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams): ${googleFontsImport} :root { --cp-bg: #${light.bg}; --cp-text: #${light.text}; --cp-accent: #${light.accent}; --cp-negative: #${light.negative || 'cf222e'}; } @media (prefers-color-scheme: dark) { :root { --cp-bg: #${dark.bg}; --cp-text: #${dark.text}; --cp-accent: #${dark.accent}; --cp-negative: #${dark.negative || 'f85149'}; } } - .cp-bg-fill { fill: var(--cp-bg); } - .cp-text-fill { fill: var(--cp-text); color: var(--cp-text); } + .cp-bg-fill { fill: var(--cp-bg); } + .cp-text-fill { fill: var(--cp-text); color: var(--cp-text); } .cp-accent-fill { fill: var(--cp-accent); color: var(--cp-accent); } .cp-delta-fill { fill: ${stats.deltaAbsolute >= 0 ? 'var(--cp-accent)' : 'var(--cp-negative)'}; } - + .title { font-family: ${selectedFont || '"Syncopate", sans-serif'}; fill: var(--cp-text); font-size: 14px; letter-spacing: 2px; font-weight: 400; opacity: 0.8; } .stats { font-family: ${statsFont}; fill: var(--cp-accent); font-size: 36px; font-weight: 600; letter-spacing: 0; } .label { font-family: "Roboto", sans-serif; fill: var(--cp-text); font-size: 10px; font-weight: 400; letter-spacing: 1px; opacity: 0.7; } @@ -1776,7 +1776,7 @@ export function generateVersusSVG( ${renderDefs(sf, params)} ${renderStyle(selectedFont, statsFont, googleFontsImport, text, accent, sf, bg)} - + ${towers1} ${renderIsometricLabels(calendar1, params, text, sf)} @@ -1790,7 +1790,7 @@ export function generateVersusSVG( - + VS @@ -1911,7 +1911,7 @@ function generateAutoThemeVersusSVG( CommitPulse Versus Stats: ${safeUser1} vs ${safeUser2} ${safeUser1} has ${stats1.totalContributions} ${unit}. ${safeUser2} has ${stats2.totalContributions} ${unit}. ${renderDefs(sf, params)} - + - + ${towers1} ${renderIsometricLabels(calendar1, params, '', sf)} @@ -1958,7 +1958,7 @@ function generateAutoThemeVersusSVG( - + VS