diff --git a/app/contributors/page.massive-scaling.test.tsx b/app/contributors/page.massive-scaling.test.tsx index b55c32faf..7f3d3c4ab 100644 --- a/app/contributors/page.massive-scaling.test.tsx +++ b/app/contributors/page.massive-scaling.test.tsx @@ -237,7 +237,7 @@ describe('ContributorsPage - Massive Data Sets & High Bounds Scaling', () => { const endTime = performance.now(); const renderTime = endTime - startTime; - // Bounded higher to protect execution consistency against noisy neighbor/CPU constraints in the testing runner - expect(renderTime).toBeLessThan(process.env.CI ? 20000 : 15000); + // Rendering 500 mock cards should take less than 1500ms under virtual DOM + Vitest + expect(renderTime).toBeLessThan(process.env.CI ? 12000 : 5000); }); }); diff --git a/components/dashboard/DashboardClient.massive-scaling.test.tsx b/components/dashboard/DashboardClient.massive-scaling.test.tsx index 1d5d574f3..af97a4865 100644 --- a/components/dashboard/DashboardClient.massive-scaling.test.tsx +++ b/components/dashboard/DashboardClient.massive-scaling.test.tsx @@ -124,7 +124,7 @@ describe('DashboardClient - Massive Data Sets and Extreme High Bounds Scaling', const endTime = performance.now(); const executionTime = endTime - startTime; - expect(executionTime).toBeLessThan(3000); + expect(executionTime).toBeLessThan(1000); }); // Test Case 2: Extreme High Bounds Value Handling (No Layout Overflow) diff --git a/lib/svg/generator.additional.test.ts b/lib/svg/generator.additional.test.ts index 15ed6e892..2e003f3c7 100644 --- a/lib/svg/generator.additional.test.ts +++ b/lib/svg/generator.additional.test.ts @@ -1,4 +1,4 @@ -// lib/svg/generator.test.new.ts +// lib/svg/generator.additional.test.ts // New tests covering gaps in existing generator.test.ts coverage. // Covers: generateVersusSVG, neon theme bg, accent override, border param, org/repo title entity. @@ -762,3 +762,124 @@ describe('[Refactor] renderGhostTowers — shared helper consistency', () => { expect(rateLimitSvg).toContain(''); }); }); + +// ─── generateAutoThemeVersusSVG refactor — renderTowers consistency ────────── +// Verifies that after replacing the manual tower loops with renderTowers(), +// the auto-theme versus SVG produces the same CSS class output as other +// auto-theme paths. Any regression here means the refactor broke something. + +describe('[Refactor] generateAutoThemeVersusSVG — uses renderTowers consistency', () => { + const stats1: StreakStats = { + currentStreak: 5, + longestStreak: 10, + totalContributions: 120, + todayDate: '2024-06-12', + }; + + const stats2: StreakStats = { + currentStreak: 3, + longestStreak: 8, + totalContributions: 80, + todayDate: '2024-06-12', + }; + + const calendar1: ContributionCalendar = { + totalContributions: 120, + weeks: [ + { + contributionDays: [ + { contributionCount: 0, date: '2024-06-09' }, + { contributionCount: 5, date: '2024-06-10' }, + { contributionCount: 11, date: '2024-06-11' }, + { contributionCount: 6, date: '2024-06-12' }, + ], + }, + ], + }; + + const calendar2: ContributionCalendar = { + totalContributions: 80, + weeks: [ + { + contributionDays: [ + { contributionCount: 0, date: '2024-06-09' }, + { contributionCount: 3, date: '2024-06-10' }, + { contributionCount: 12, date: '2024-06-11' }, + { contributionCount: 2, date: '2024-06-12' }, + ], + }, + ], + }; + + const autoVersusParams: BadgeParams = { + user: 'chetan', + versus: 'rival', + bg: hexColor('0d1117'), + text: hexColor('ffffff'), + accent: hexColor('58a6ff'), + speed: '8s', + scale: 'linear', + autoTheme: true, + }; + + it('auto-theme versus SVG contains cp-accent-fill class for active towers', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + expect(svg).toContain('class="cp-accent-fill"'); + }); + + it('auto-theme versus SVG contains cp-text-fill class for ghost towers', () => { + const ghostCalendar: ContributionCalendar = { + totalContributions: 0, + weeks: [{ contributionDays: [{ contributionCount: 0, date: '2024-06-12' }] }], + }; + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, ghostCalendar, ghostCalendar); + expect(svg).toContain('class="cp-text-fill"'); + }); + + it('auto-theme versus SVG contains cp-bg CSS variable (from auto-theme style block)', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + expect(svg).toContain('--cp-bg'); + expect(svg).toContain('--cp-accent'); + expect(svg).toContain('prefers-color-scheme: dark'); + }); + + it('auto-theme versus SVG has heat particles for high-contribution days', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + expect(svg).toContain('class="heat-particles"'); + }); + + it('auto-theme versus SVG has today pulse animation', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + expect(svg).toContain('attributeName="opacity" values="1;0.4;1"'); + }); + + it('auto-theme versus SVG has staggered tower animation delays', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + expect(svg).toMatch(/style="animation-delay: \d+\.\d+s;"/); + }); + + it('auto-theme versus and static versus produce same structural tower count', () => { + const staticParams: BadgeParams = { + ...autoVersusParams, + autoTheme: false, + }; + const autoSvg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + const staticSvg = generateVersusSVG(stats1, stats2, staticParams, calendar1, calendar2); + + // Both should have the same number of tower groups + const autoTowers = [...autoSvg.matchAll(/class="cp-tower"/g)].length; + const staticTowers = [...staticSvg.matchAll(/class="cp-tower"/g)].length; + expect(autoTowers).toBe(staticTowers); + }); + + it('auto-theme versus does not contain inline hex fill colors on tower paths', () => { + const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2); + + // Auto-theme uses CSS classes — tower paths must NOT have fill="#hexvalue" + // (scan-line and other elements may have fill attrs, but not tower paths) + const towerSection = svg.match(/class="cp-tower"[\s\S]*?<\/g>/g) || []; + for (const tower of towerSection) { + expect(tower).not.toMatch(/fill="#[0-9a-fA-F]{6}"/); + } + }); +}); diff --git a/lib/svg/generator.deterministicRandom.test.ts b/lib/svg/generator.deterministicRandom.test.ts index 218879661..d5ccbdaad 100644 --- a/lib/svg/generator.deterministicRandom.test.ts +++ b/lib/svg/generator.deterministicRandom.test.ts @@ -46,7 +46,7 @@ describe('Helper Function: deterministicRandom', () => { const endTime = performance.now(); const duration = endTime - startTime; - // Bounded higher to protect execution consistency against execution timing spikes in virtual test configurations + // 10,000 FNV-1a hashes should easily complete under 50ms on modern hardware expect(duration).toBeLessThan(150); }); }); diff --git a/lib/svg/generator.ts b/lib/svg/generator.ts index 0672d2093..da37c782a 100644 --- a/lib/svg/generator.ts +++ b/lib/svg/generator.ts @@ -27,25 +27,16 @@ import { GRID_ORIGIN_X, GRID_ORIGIN_Y, TILE_HEIGHT_HALF, TILE_WIDTH_HALF } from import { SVG_WIDTH, SVG_HEIGHT } from './generatorConstants'; const FONT_MAP = { - // ── Pre-existing entries ──────────────────────────────────────────────── jetbrains: '"JetBrains Mono", monospace', fira: '"Fira Code", monospace', roboto: '"Roboto", sans-serif', - - // ── Previously missing — both fonts are in the unconditional @import ─── - // Without these entries, passing ?font=syncopate or ?font=spacegrotesk - // incorrectly triggers a duplicate dynamic Google Fonts fetch. syncopate: '"Syncopate", sans-serif', spacegrotesk: '"Space Grotesk", sans-serif', - 'space grotesk': '"Space Grotesk", sans-serif', // handles spaced user input - - // ── Aliases for common variations ─────────────────────────────────────── - firacode: '"Fira Code", monospace', // alias: fira is the canonical key - 'jetbrains mono': '"JetBrains Mono", monospace', // handles spaced user input - - // ── Legacy keys for backward compatibility ────────────────────────────── + 'space grotesk': '"Space Grotesk", sans-serif', + firacode: '"Fira Code", monospace', + 'jetbrains mono': '"JetBrains Mono", monospace', inter: '"Inter", sans-serif', - space: '"Space Grotesk", sans-serif', // old key for spacegrotesk + space: '"Space Grotesk", sans-serif', } as const; export function resolveFont(sanitizedFont?: string | null): string | null { @@ -64,7 +55,6 @@ function isBundledFont(sanitizedFont?: string | null): boolean { return fontKey in FONT_MAP && fontKey !== 'inter'; } -// helpers export function getSizeScale(size?: 'small' | 'medium' | 'large') { if (size === 'small') return 400 / SVG_WIDTH; if (size === 'large') return 800 / SVG_WIDTH; @@ -121,12 +111,6 @@ export interface TowerPaths { top: string; } -/** - * Builds the SVG path strings for the three faces of an isometric 3D tower. - * - * @param h - The height of the tower. - * @param scale - Optional scale factor (defaults to 1, which represents the standard 16x10 grid). - */ export function buildTowerPaths(h: number, scale: number = 1): TowerPaths { const tileHalfWidth = 16 * scale; const tileHalfHeight = 10 * scale; @@ -183,8 +167,6 @@ export function getInteractiveTowerCSS(accentColorExpr: string): string { `; } -// ── Section helpers for generateSVG ────────────────────────────────────── - function renderHeader( safeUser: string, stats: StreakStats, @@ -203,24 +185,14 @@ function renderHeader( ${renderDefs(sf, params)}`; } -/** - * Generates custom SVG gradient definitions from gradient_stops and gradient_dir parameters. - * Returns an object with gradient SVG elements and the gradient ID (or empty string if invalid). - * If custom stops are invalid or insufficient, returns { gradients: '', gradientId: '' }. - * Also stores the gradient ID on the params object for tower rendering to use. - */ function generateCustomGradients(params: BadgeParams): { gradients: string; gradientId: string } { const stops = parseGradientStops(params.gradient_stops); - // Require at least 2 valid colors for custom gradient if (stops.length < 2) { return { gradients: '', gradientId: '' }; } const coords = getGradientCoordinates(params.gradient_dir); - - // Create a deterministic gradient ID based on the color stops and direction - // This ensures consistent output and avoids random/duplicate IDs const gradientSignature = `${stops.join('-')}-${params.gradient_dir || 'vertical'}`; const gradientId = `custom-grad-${deterministicRandom(gradientSignature) .toString() @@ -228,19 +200,15 @@ function generateCustomGradients(params: BadgeParams): { gradients: string; grad let gradients = ''; - // Generate 4 gradient definitions (one for each intensity level) - // Each uses the same color stops but with different opacity progression for (let i = 0; i < 4; i++) { const level = i + 1; const levelId = `${gradientId}-level-${level}`; - // Build the stop elements let stopElements = ''; const stopCount = stops.length; stops.forEach((color, stopIdx) => { const offset = (stopIdx / (stopCount - 1)) * 100; - // Increase opacity with intensity level (0.4 to 0.8) const baseOpacity = 0.4 + i * 0.2; const stopOpacity = Math.min(1, baseOpacity + stopIdx * 0.1); @@ -255,9 +223,7 @@ ${stopElements} `; } - // Store the gradient ID on params for tower rendering to use params.__customGradientId = gradientId; - return { gradients, gradientId }; } @@ -266,13 +232,10 @@ function renderDefs(sf: number, params: BadgeParams): string { let gradients = ''; if (params.gradient) { - // Try to use custom gradient if gradient_stops is provided const result = generateCustomGradients(params); if (result.gradientId) { - // Custom gradient stops were valid and used gradients = result.gradients; } else { - // Fallback to default gradient behavior const bgStr = params.bg || '0d1117'; const bgHex = bgStr.startsWith('#') ? bgStr : `#${bgStr}`; @@ -476,7 +439,6 @@ function renderTowers( topFillAttr = leftRightFillAttr; } - // opacity scalar: clamp 0.1–1.0, applied globally to all tower faces let leftFaceOpacity = Math.round(t.faceOpacity.left * opacity * 100) / 100; let rightFaceOpacity = Math.round(t.faceOpacity.right * opacity * 100) / 100; let topFaceOpacity = Math.round(t.faceOpacity.top * opacity * 100) / 100; @@ -493,7 +455,6 @@ function renderTowers( let finalTopFillAttr = topFillAttr; if (!isGhost && t.intensityLevel > 0 && params.gradient === true) { - // Use custom gradient ID if available, otherwise use default gradient ID const customGradId = params.__customGradientId; const gradId = customGradId ? `${customGradId}-level-${t.intensityLevel}` @@ -662,11 +623,10 @@ const MONTH_NAMES = [ 'Dec', ]; -// Layout constants for 3D isometric label positioning const ISOMETRIC_VERTICAL_OFFSET = 20; - const MONTH_LABEL_ROW_OFFSET = 7.2; const WEEKDAY_LABEL_COL_OFFSET = -1.2; + function renderIsometricLabels( calendar: ContributionCalendar, params: BadgeParams, @@ -768,8 +728,6 @@ function renderMilestoneBadges(stats: StreakStats, params: BadgeParams, sf: numb return `${elements}`; } -// ── Main static-theme renderer ──────────────────────────────────────────── - export function generateSVG( stats: StreakStats, params: BadgeParams, @@ -946,15 +904,6 @@ ${renderMilestoneBadges(stats, params, sf)} `; } -/** - * Computes the formatted delta text string for the monthly stats badge. - * Shared between generateMonthlySVG (static theme) and - * generateAutoThemeMonthlySVG (auto theme) to prevent divergence. - * - * @param stats - Monthly contribution statistics - * @param deltaUnit - 'commits' or 'lines' depending on mode - * @param deltaFormat - 'percent' | 'absolute' | 'both' from URL params - */ function computeDeltaText( stats: MonthlyStats, deltaUnit: string, @@ -978,7 +927,6 @@ function computeDeltaText( : `0% (${stats.deltaAbsolute > 0 ? '+' : ''}${stats.deltaAbsolute})`; } - // percent (default) return stats.deltaPercentage === null ? 'N/A' : stats.deltaPercentage > 0 @@ -1026,7 +974,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st const deltaUnit = params.mode === 'loc' ? 'LINES (EST.)' : 'commits'; const deltaText = computeDeltaText(stats, deltaUnit, params.delta_format); - // Resolve negative color let negativeColor = '#ff4444'; const cleanBg = sanitizeHexColor(params.bg, '0d1117'); const matchedTheme = Object.values(themes).find( @@ -1036,7 +983,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st if (matchedTheme && matchedTheme.negative) { negativeColor = `#${matchedTheme.negative}`; } else { - // Dynamic fallback based on background luminance const luminance = getLuminance(cleanBg); negativeColor = luminance > 0.5 ? '#cf222e' : '#f85149'; } @@ -1085,10 +1031,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st `; } -/** - * Backwards-compatible alias used by some integrations/tests. - * Keeps the public API explicit: `generateMonthlyBadge` -> `generateMonthlySVG`. - */ export function generateMonthlyBadge(stats: MonthlyStats, params: BadgeParams): string { return generateMonthlySVG(stats, params); } @@ -1126,7 +1068,6 @@ export function generateWrappedSVG( ? `@import url('https://fonts.googleapis.com/css2?family=${googleFontUrlPart}&display=swap');` : ''; - // Format month name (e.g. "2025-11" -> "NOVEMBER") const MONTH_NAMES: Record = { '01': 'JANUARY', '02': 'FEBRUARY', @@ -1146,7 +1087,6 @@ export function generateWrappedSVG( ? MONTH_NAMES[monthPart] || stats.busiestMonth : stats.busiestMonth || 'N/A'; - // Format peak day (e.g. "2025-11-20" -> "Nov 20") function formatActiveDate(dateStr: string): string { if (!dateStr) return 'N/A'; const parts = dateStr.split('-'); @@ -1172,29 +1112,20 @@ export function generateWrappedSVG( } const formattedPeakDate = formatActiveDate(stats.mostActiveDate); - // Circular progress calculations for weekend grind - // Radius = 14, circumference = 2 * PI * 14 = 87.96 const radiusCircle = 14; - const circ = 2 * Math.PI * radiusCircle; // ~87.96 + const circ = 2 * Math.PI * radiusCircle; const clampedRatio = Math.max(0, Math.min(stats.weekendRatio || 0, 100)); const strokeDashoffset = circ - (clampedRatio / 100) * circ; - // Background Mini-Monolith calculations - // Get 14 weeks of towers and scale them down - const sf = 0.45; // scale down + const sf = 0.45; const rawTowers = computeTowers(calendar, params.scale, '', 'commits'); - // We want to translate them to align beautifully behind the total contributions count - // Scale raw coordinates: tx * sf, ty * sf let bgTowersMarkup = ''; const resolvedSolidColor = accent; for (const t of rawTowers) { - // Only draw towers that have contributions or represent ghost city landscape - // We scale down the height and coordinates const scaleHeight = t.h * sf; - const scaleX = Math.round(t.x * sf) - 50; // offset left to shift it to the background - const scaleY = Math.round(t.y * sf) + 80; // shift down slightly + const scaleX = Math.round(t.x * sf) - 50; + const scaleY = Math.round(t.y * sf) + 80; - // Extreme low opacity for elegant backdrop watermark const leftFaceOpacity = t.isGhost ? 0.01 : 0.015; const rightFaceOpacity = t.isGhost ? 0.005 : 0.01; const topFaceOpacity = t.isGhost ? 0.02 : 0.035; @@ -1210,7 +1141,6 @@ export function generateWrappedSVG( `; } - // Border override or default glow const borderAttr = params.border ? `stroke="#${sanitizeHexColor(params.border, '58a6ff')}" stroke-width="1.5"` : `stroke="${accent}" stroke-opacity="0.15" stroke-width="1.5"`; @@ -1430,8 +1360,6 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams): `; } -// ── Heatmap View ────────────────────────────────────────────────────────── - const HEATMAP_CELL_SIZE = 16; const HEATMAP_CELL_GAP = 3; const HEATMAP_CELL_RADIUS = 2; @@ -1482,7 +1410,6 @@ function renderHeatmapGrid( const originY = Math.round(HEATMAP_GRID_ORIGIN_Y * sf); const step = cellSize + cellGap; - // Find max contribution count for intensity calculation let maxCount = 0; weeks.forEach((week) => { week.contributionDays.forEach((day) => { @@ -1492,14 +1419,12 @@ function renderHeatmapGrid( }); }); - // Check if todayDate is in visible window const todayInWindow = weeks.some((w) => w.contributionDays.some((d) => d.date === todayDate)); let cells = ''; let monthHeaders = ''; let prevMonth = ''; - // Render grid cells weeks.forEach((week, col) => { week.contributionDays.forEach((day, row) => { const count = @@ -1518,8 +1443,6 @@ function renderHeatmapGrid( const tooltip = `${tooltipPrefix}${day.date}: ${count} ${unit}`; const fillAttr = isAutoTheme ? 'fill="var(--cp-accent)"' : `fill="${accent}"`; - - // Glow on high-intensity cells const filterAttr = intensity === 4 && glow !== false ? ' filter="url(#hm-glow)"' : ''; cells += ` @@ -1533,7 +1456,6 @@ function renderHeatmapGrid( `; }); - // Month header: detect month change from first day of each week if (week.contributionDays.length > 0) { const firstDay = week.contributionDays[0]; const monthNum = parseInt(firstDay.date.substring(5, 7), 10); @@ -1549,7 +1471,6 @@ function renderHeatmapGrid( } }); - // Weekday labels let weekdayLabels = ''; HEATMAP_WEEKDAY_LABELS.forEach((label, row) => { if (!label) return; @@ -1877,7 +1798,6 @@ function generateAutoThemeHeatmapSVG( `; } -// Fixed isometric tower layout for the not-found ghost city. const GHOST_LAYOUT: { col: number; row: number; h: number }[] = [ { col: 0, row: 0, h: 8 }, { col: 1, row: 0, h: 20 }, @@ -1929,16 +1849,6 @@ const GHOST_LAYOUT: { col: number; row: number; h: number }[] = [ { col: 7, row: 5, h: 6 }, ]; -/** - * Renders a list of ghost tower entries as isometric wireframe SVG paths. - * Shared by generateNotFoundSVG and generateRateLimitSVG to avoid duplicated - * ghost-city rendering logic. Any visual change to ghost tower geometry - * (stroke widths, fill-opacity, coordinate math) only needs to happen here. - * - * @param layout - Array of {col, row, h} tower descriptors - * @param accent - Hex color string (with #) for tower stroke and fill tint - * @returns SVG string: a wrapping all tower groups - */ function renderGhostTowers( layout: { col: number; row: number; h: number }[], accent: string @@ -2192,26 +2102,8 @@ function generateAutoThemeVersusSVG( sf ); - const towers1 = renderTowers( - towerData1, - params, - '', - '', - sf, - true, - params.opacity ?? 1.0, - params.animate ?? true - ); - const towers2 = renderTowers( - towerData2, - params, - '', - '', - sf, - true, - params.opacity ?? 1.0, - params.animate ?? true - ); + const towers1 = renderTowers(towerData1, params, '', '', sf, true, params.opacity ?? 1.0); + const towers2 = renderTowers(towerData2, params, '', '', sf, true, params.opacity ?? 1.0); const s = createScaler(sf); const fs = (n: number): number => Math.round(n * sf * 10) / 10; @@ -2317,7 +2209,6 @@ export function generatePulseSVG( const width = params.width || 800; const height = params.height || 170; - // Extract the last 30 days of contributions const days: number[] = []; calendar.weeks.forEach((week) => { week.contributionDays.forEach((day) => { @@ -3210,21 +3101,18 @@ export function generateLanguagesSVG( .sort((a, b) => b.percentage - a.percentage) .slice(0, 5); - // We use custom isometric pixel coordinates from a center origin - // to spread the 5 towers across the canvas. - const TOWER_SCALE = 2.5; // Make the 5 language towers much larger than daily contribution tiles + const TOWER_SCALE = 2.5; const V_SHAPE_COORDS = [ - { x: 0, y: 0, zIndex: 3 }, // 1st (Center, Front) - { x: -80, y: -45, zIndex: 2 }, // 2nd (Left, Mid) - { x: 80, y: -45, zIndex: 2 }, // 3rd (Right, Mid) - { x: -160, y: -90, zIndex: 1 }, // 4th (Far Left, Back) - { x: 160, y: -90, zIndex: 1 }, // 5th (Far Right, Back) + { x: 0, y: 0, zIndex: 3 }, + { x: -80, y: -45, zIndex: 2 }, + { x: 80, y: -45, zIndex: 2 }, + { x: -160, y: -90, zIndex: 1 }, + { x: 160, y: -90, zIndex: 1 }, ]; let towersHtml = ''; const maxPercent = languages[0]?.percentage || 100; - // Sort languages by zIndex so we render back-to-front (painter's algorithm) const sortedLanguages = languages .map((lang, idx) => ({ ...lang, @@ -3233,13 +3121,13 @@ export function generateLanguagesSVG( .sort((a, b) => a.coord.zIndex - b.coord.zIndex); sortedLanguages.forEach((lang, idx) => { - // W and H are already scaled by sf, so we only scale the coordinate offsets const scaledX = W / 2 + lang.coord.x * sf; const scaledY = H / 2 + (50 + lang.coord.y) * sf; const h = Math.max(30, (lang.percentage / maxPercent) * 140) * sf; const towerScale = TOWER_SCALE * sf; const paths = buildTowerPaths(h, towerScale); + const th = 10 * towerScale; const hexColor = lang.color.startsWith('#') ? lang.color : `#${lang.color}`; const delay = (idx * 0.15).toFixed(3); diff --git a/lib/svg/themes.test.ts b/lib/svg/themes.test.ts index 00dbb5a9a..3ec08515a 100644 --- a/lib/svg/themes.test.ts +++ b/lib/svg/themes.test.ts @@ -45,7 +45,7 @@ describe('theme count', () => { // If this fails, either a theme was added to themes.ts without updating // THEMES.md, or a theme was removed without updating the docs. // Update this count when intentionally adding/removing themes. - expect(themeNames).toHaveLength(25); + expect(themeNames).toHaveLength(26); }); it('contains all expected theme keys', () => {