Skip to content
Open
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
16 changes: 8 additions & 8 deletions lib/svg/generator.getUsernameFontSize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ describe('getUsernameFontSize', () => {
});

it('scales down font size for usernames with length > 12', () => {
// len 13 -> 18 - (13-12)*0.5 = 17.5
expect(getUsernameFontSize('a'.repeat(13))).toBe(17.5);
// len 20 -> 18 - (20-12)*0.5 = 14
expect(getUsernameFontSize('a'.repeat(20))).toBe(14);
// len 23 (max truncated length) -> 18 - (23-12)*0.5 = 12.5
expect(getUsernameFontSize('a'.repeat(23))).toBe(12.5);
// len 13 -> 18 - (13-12)*0.3 = 17.7
expect(getUsernameFontSize('a'.repeat(13))).toBe(17.7);
// len 20 -> 18 - (20-12)*0.3 = 15.6
expect(getUsernameFontSize('a'.repeat(20))).toBe(15.6);
// len 39 (max GitHub username length) -> 18 - (39-12)*0.3 = 9.9 -> clamped to 10
expect(getUsernameFontSize('a'.repeat(39))).toBe(10);
});

it('clamps to a minimum font size of 10px', () => {
// len 39 -> 18 - (39-12)*0.5 = 18 - 13.5 = 4.5 -> clamped to 10
expect(getUsernameFontSize('a'.repeat(39))).toBe(10);
// len 50 -> 18 - (50-12)*0.3 = 6.6 -> clamped to 10
expect(getUsernameFontSize('a'.repeat(50))).toBe(10);
});
});
20 changes: 10 additions & 10 deletions lib/svg/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,17 +1070,17 @@ describe('generateSVG', () => {
});

it('verify boundary robustness of username length truncator (Variation 4)', () => {
const extendedLongUsername = 'abcdefghijklmnopqrstuvwxyz1234567890';
const extendedLongUsername = 'abcdefghijklmnopqrstuvwxyz12345678901234567890';
const extendedParams = {
user: extendedLongUsername,
hide_title: false,
} as unknown as BadgeParams;

const svg = generateSVG(mockStats, extendedParams, mockCalendar);

expect(extendedLongUsername.length).toBeGreaterThan(30);
expect(svg).toContain('ABCDEFGHIJKLMNOPQRST...');
expect(svg).not.toContain('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
expect(extendedLongUsername.length).toBeGreaterThan(39);
expect(svg).toContain('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123...');
expect(svg).not.toContain(extendedLongUsername.toUpperCase());
});
});

Expand Down Expand Up @@ -1764,8 +1764,8 @@ describe('Radar Scan Line Animation Alignment', () => {
expect(result.endsWith('...')).toBe(true);

// 4. Assert: Verify the string was actually truncated
// If it caps at 30 chars and adds '...', the max length is 33.
expect(result.length).toBeLessThanOrEqual(33);
// If it caps at 39 chars and adds '...', the max length is 42.
expect(result.length).toBeLessThanOrEqual(42);

// 5. Assert: Ensure the original string was actually modified
expect(result).not.toEqual(longUsername);
Expand Down Expand Up @@ -1827,7 +1827,7 @@ describe('Radar Scan Line Animation Alignment', () => {
// 1. Arrange: Create usernames (one short baseline, one strictly > 30 chars)
const shortUsername = 'avi';
const longUsername = 'ThisIsAVeryLongUsernameThatExceedsThirtyCharacters';
const expectedTruncated = longUsername.slice(0, 20) + '...';
const expectedTruncated = longUsername.slice(0, 39) + '...';

const paramsBaseline = {
user: shortUsername,
Expand Down Expand Up @@ -1872,9 +1872,9 @@ describe('Radar Scan Line Animation Alignment', () => {
expect(geometryLong).toEqual(geometryBaseline);
});

it('truncates usernames longer than 20 characters and adds an ellipsis in generateSVG', () => {
const longUsername = 'averylongusernamethatexceeds20chars'; // 35 characters
const expectedTruncated = 'AVERYLONGUSERNAMETHA...'; // 20 characters + '...' (in uppercase)
it('truncates usernames longer than 39 characters and adds an ellipsis in generateSVG', () => {
const longUsername = 'averylongusernamethatexceeds39characterlimit'; // 44 characters
const expectedTruncated = 'AVERYLONGUSERNAMETHATEXCEEDS39CHARACTER...'; // 39 characters + '...' (in uppercase)

const svg = generateSVG(
mockStats,
Expand Down
17 changes: 10 additions & 7 deletions lib/svg/generator.truncateUsername.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ import { describe, expect, it } from 'vitest';
import { truncateUsername } from './generator';

describe('truncateUsername', () => {
it('returns original username when length is less than 20', () => {
it('returns original username when length is less than 39', () => {
expect(truncateUsername('sonalkathuria')).toBe('sonalkathuria');
});

it('returns original username when length is exactly 20', () => {
expect(truncateUsername('abcdefghijklmnopqrst')).toBe('abcdefghijklmnopqrst');
it('returns original username when length is exactly 39', () => {
const user39 = 'abcdefghijklmnopqrstuvwxyz0123456789123';
expect(truncateUsername(user39)).toBe(user39);
});

it('truncates username longer than 20 characters with ellipsis', () => {
expect(truncateUsername('abcdefghijklmnopqrstuvwxyz')).toBe('abcdefghijklmnopqrst...');
it('truncates username longer than 39 characters with ellipsis', () => {
const user42 = 'abcdefghijklmnopqrstuvwxyz0123456789123456';
expect(truncateUsername(user42)).toBe('abcdefghijklmnopqrstuvwxyz0123456789123...');
});

it('handles empty string input', () => {
expect(truncateUsername('')).toBe('');
});

it('preserves spaces and special characters in the first 20 chars before ellipsis', () => {
expect(truncateUsername('john doe_user+tag12345678')).toBe('john doe_user+tag123...');
it('preserves spaces and special characters in the first 39 chars before ellipsis', () => {
const longString = 'john doe_user+tag123456789012345678901234567890123';
expect(truncateUsername(longString)).toBe(longString.slice(0, 39) + '...');
});
});
2 changes: 1 addition & 1 deletion lib/svg/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function truncateLabel(label: string): string {
export function getUsernameFontSize(username: string): number {
const len = username.length;
if (len <= 12) return 18;
return Math.max(10, 18 - (len - 12) * 0.5);
return Math.max(10, Number((18 - (len - 12) * 0.3).toFixed(1)));
}

/**
Expand Down
27 changes: 9 additions & 18 deletions lib/svg/generatorConstants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,12 @@ describe('MAX_USERNAME_DISPLAY_LENGTH', () => {
expect(Number.isInteger(MAX_USERNAME_DISPLAY_LENGTH)).toBe(true);
});

it('equals 20 β€” the increased value supporting longer usernames', () => {
expect(MAX_USERNAME_DISPLAY_LENGTH).toBe(20);
it('equals 39 β€” supporting full GitHub usernames', () => {
expect(MAX_USERNAME_DISPLAY_LENGTH).toBe(39);
});

it('is less than GitHub max username length of 39 characters', () => {
// Sanity: display truncation must be shorter than the max possible username
expect(MAX_USERNAME_DISPLAY_LENGTH).toBeLessThan(39);
});

it('is coordinated with SVG_WIDTH β€” truncation prevents title overflow', () => {
// At Syncopate 18px with letter-spacing 6px, each character is ~24px wide.
// MAX_USERNAME_DISPLAY_LENGTH * 24 should be safely within SVG_WIDTH.
const estimatedTextWidth = MAX_USERNAME_DISPLAY_LENGTH * 24;
expect(estimatedTextWidth).toBeLessThan(SVG_WIDTH);
it('matches GitHub max username length of 39 characters', () => {
expect(MAX_USERNAME_DISPLAY_LENGTH).toBe(39);
});
});

Expand All @@ -239,7 +231,7 @@ describe('truncateUsername β€” uses MAX_USERNAME_DISPLAY_LENGTH constant', () =>
};

it('username exactly at MAX_USERNAME_DISPLAY_LENGTH is not truncated', () => {
const exactLengthUser = 'a'.repeat(MAX_USERNAME_DISPLAY_LENGTH); // 'aaaaaaaaaaaa'
const exactLengthUser = 'a'.repeat(MAX_USERNAME_DISPLAY_LENGTH);
const svg = generateSVG(
mockStats,
{ user: exactLengthUser } as unknown as BadgeParams,
Expand All @@ -250,24 +242,23 @@ describe('truncateUsername β€” uses MAX_USERNAME_DISPLAY_LENGTH constant', () =>
});

it('username one character over MAX_USERNAME_DISPLAY_LENGTH is truncated with ...', () => {
const longUser = 'a'.repeat(MAX_USERNAME_DISPLAY_LENGTH + 1); // 13 chars
const longUser = 'a'.repeat(MAX_USERNAME_DISPLAY_LENGTH + 1);
const svg = generateSVG(mockStats, { user: longUser } as unknown as BadgeParams, mockCalendar);
expect(svg).toContain('...');
// The displayed portion should be exactly MAX_USERNAME_DISPLAY_LENGTH chars
const truncated = 'A'.repeat(MAX_USERNAME_DISPLAY_LENGTH) + '...';
expect(svg).toContain(truncated);
});

it('very long GitHub username (39 chars) is truncated to MAX_USERNAME_DISPLAY_LENGTH', () => {
it('very long GitHub username (39 chars) is not truncated', () => {
const maxGitHubUser = 'a'.repeat(39);
const svg = generateSVG(
mockStats,
{ user: maxGitHubUser } as unknown as BadgeParams,
mockCalendar
);
expect(svg).toContain('...');
// Should show exactly 12 chars + '...'
expect(svg).toContain('A'.repeat(MAX_USERNAME_DISPLAY_LENGTH) + '...');
expect(svg).toContain('A'.repeat(39));
expect(svg).not.toContain('...');
});

it('short username (under MAX_USERNAME_DISPLAY_LENGTH) is never truncated', () => {
Expand Down
11 changes: 4 additions & 7 deletions lib/svg/generatorConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ export function isFontKey(font: string): font is FontKey {

/**
* Maximum number of characters displayed in the username title before
* truncation with '...'. Chosen to fit within SVG_WIDTH=600 at the
* Syncopate font size of 18px with letter-spacing of 6px β€” approximately
* 550px of text width, leaving comfortable edge margin on all badge sizes.
*
* Increasing this value may cause title overflow on small (400px) and
* medium (600px) badges. Coordinate any change with SVG_WIDTH in this file.
* truncation with '...'. Set to 39 to accommodate full GitHub usernames
* (which can be up to 39 characters long) without truncation, utilizing dynamic
* font scaling to fit safely within SVG cards.
*/
export const MAX_USERNAME_DISPLAY_LENGTH = 20;
export const MAX_USERNAME_DISPLAY_LENGTH = 39;
Loading