Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/api/streak/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export async function GET(request: Request) {
labels,
labelColor,
versus,
shading,
gradient,
} = parseResult.data;

const themeName = theme || 'dark';
Expand Down Expand Up @@ -133,6 +135,8 @@ export async function GET(request: Request) {
labels,
labelColor,
versus,
shading,
gradient,
};

let calendar;
Expand Down Expand Up @@ -204,7 +208,13 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo
message.toLowerCase().includes('could not resolve');

const errBg = `#${(parseResult.success && parseResult.data.bg) || '0d1117'}`;
const errAccent = `#${(parseResult.success && parseResult.data.accent) || '58a6ff'}`;
const errAccent = `#${
(parseResult.success &&
(Array.isArray(parseResult.data.accent)
? parseResult.data.accent[parseResult.data.accent.length - 1]
: parseResult.data.accent)) ||
'58a6ff'
}`;
const errText = `#${(parseResult.success && parseResult.data.text) || 'c9d1d9'}`;
const errRadius = parseResult.success
? (() => {
Expand Down
6 changes: 0 additions & 6 deletions components/dashboard/ComparisonStatsCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,6 @@ describe('ComparisonStatsCard', () => {
/>
);

const progressSegments = container.querySelectorAll(
'.w-full.bg-gray-700\\/50 div, .relative div'
);

const allDivs = Array.from(container.querySelectorAll('div'));

const emeraldElement =
container.querySelector('[className*="emerald"]') ||
container.querySelector('.text-emerald-400');
Expand Down
3 changes: 3 additions & 0 deletions lib/export3d.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('generateMonolithSTL', () => {
strokeWidth: 1,
row: 0,
col: 0,
intensityLevel: 2,
},
{
x: 0,
Expand All @@ -37,6 +38,7 @@ describe('generateMonolithSTL', () => {
strokeWidth: 1,
row: 1,
col: 1,
intensityLevel: 0,
},
];

Expand Down Expand Up @@ -67,6 +69,7 @@ it('generates structurally valid ASCII STL facets', () => {
strokeWidth: 1,
row: 0,
col: 0,
intensityLevel: 2,
},
];

Expand Down
152 changes: 152 additions & 0 deletions lib/svg/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,158 @@ describe('generateMonthlySVG', () => {
});
});

describe('shading and gradients', () => {
const mockStats: StreakStats = {
currentStreak: 5,
longestStreak: 10,
totalContributions: 100,
todayDate: '2024-06-12',
};

const mockCalendar = {
weeks: [
{
contributionDays: [
{ contributionCount: 0, date: '2024-06-10' },
{ contributionCount: 5, date: '2024-06-11' },
{ contributionCount: 15, date: '2024-06-12' },
],
},
],
} as ContributionCalendar;

it('renders linearGradient definitions when gradient=true', () => {
const svg = generateSVG(
mockStats,
{ user: 'avi', gradient: true } as unknown as BadgeParams,
mockCalendar
);
expect(svg).toContain('<linearGradient id="tower-grad-level-1"');
expect(svg).toContain('<linearGradient id="tower-grad-level-4"');
expect(svg).toContain('fill="url(#tower-grad-level-');
});

it('does not render linearGradient definitions when gradient=false', () => {
const svg = generateSVG(
mockStats,
{ user: 'avi', gradient: false } as unknown as BadgeParams,
mockCalendar
);
expect(svg).not.toContain('<linearGradient id="tower-grad-level-1"');
});

it('supports mapping colors in array accent lists to towers', () => {
const calendarWithAllQuartiles = {
weeks: [
{
contributionDays: [
{ contributionCount: 2, date: '2024-06-10' }, // Level 1 (2/15 <= 0.25)
{ contributionCount: 6, date: '2024-06-11' }, // Level 2 (6/15 <= 0.5)
{ contributionCount: 10, date: '2024-06-12' }, // Level 3 (10/15 <= 0.75)
{ contributionCount: 15, date: '2024-06-13' }, // Level 4
],
},
],
} as ContributionCalendar;

const svg = generateSVG(
mockStats,
{ user: 'avi', accent: ['111111', '222222', '333333', '444444'] } as unknown as BadgeParams,
calendarWithAllQuartiles
);
expect(svg).toContain('fill="#111111"');
expect(svg).toContain('fill="#222222"');
expect(svg).toContain('fill="#333333"');
expect(svg).toContain('fill="#444444"');
});

it('gracefully handles and clamps accent color arrays with fewer than 4 items without crashing', () => {
const calendarWithAllQuartiles = {
weeks: [
{
contributionDays: [
{ contributionCount: 2, date: '2024-06-10' },
{ contributionCount: 6, date: '2024-06-11' },
{ contributionCount: 10, date: '2024-06-12' },
{ contributionCount: 15, date: '2024-06-13' },
],
},
],
} as ContributionCalendar;

const svg = generateSVG(
mockStats,
{ user: 'avi', accent: ['111111'] } as unknown as BadgeParams,
calendarWithAllQuartiles
);
expect(svg).toContain('fill="#111111"');
});
});

describe('shading', () => {
// A calendar with a mix of contribution counts to produce towers at different
// intensity levels — needed so that shading multipliers actually apply.
const shadingCalendar = {
weeks: [
{
contributionDays: [
{ contributionCount: 2, date: '2024-06-10' }, // low intensity
{ contributionCount: 10, date: '2024-06-11' }, // mid intensity
{ contributionCount: 15, date: '2024-06-12' }, // high intensity
],
},
],
} as ContributionCalendar;

const shadingStats: StreakStats = {
currentStreak: 3,
longestStreak: 10,
totalContributions: 27,
todayDate: '2024-06-12',
};

it('applies reduced face-opacity (shading) when shading is not disabled', () => {
// With shading on, low-intensity towers use opacity multiplier 0.4, so their
// face-opacity should be lower than the unshaded base value.
const svgShading = generateSVG(
shadingStats,
{ user: 'avi', shading: true } as unknown as BadgeParams,
shadingCalendar
);
// The shaded SVG should still contain the tower paths
expect(svgShading).toContain('class="cp-tower"');
// For level 1 (mult=0.4), base top face opacity 0.7 becomes 0.28
// Check for that specific derived value to ensure shading actually multiplied it.
expect(svgShading).toContain('fill-opacity="0.28"');
});

it('does not apply shading multipliers when shading=false', () => {
const svgShading = generateSVG(
shadingStats,
{ user: 'avi', shading: true } as unknown as BadgeParams,
shadingCalendar
);
const svgNoShading = generateSVG(
shadingStats,
{ user: 'avi', shading: false } as unknown as BadgeParams,
shadingCalendar
);
// The two renders must differ — shading changes face opacities
expect(svgShading).not.toBe(svgNoShading);
});

it('falls back to default accent #00ffaa when accent array is empty', () => {
const svg = generateSVG(
shadingStats,
// Simulate what validation returns for accent=,,, (empty array is
// now normalised to undefined, but if it somehow reached the renderer
// as [] the fallback should still fire without crashing).
{ user: 'avi', accent: [] } as unknown as BadgeParams,
shadingCalendar
);
expect(svg).toContain('00ffaa');
});
});
describe('escapeXML', () => {
it('escapes ampersands (&)', () => {
expect(escapeXML('foo & bar')).toBe('foo &amp; bar');
Expand Down
Loading
Loading