From 3db2209d2a9df3c27c1c800231cd93a3b5fc7449 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 25 Jan 2026 11:17:47 +0100 Subject: [PATCH] formatter tests --- .../formatters/AchievementFormatter.test.ts | 80 ++++ .../formatters/ActivityLevelFormatter.test.ts | 44 +++ .../lib/formatters/AvatarFormatter.test.ts | 75 ++++ .../formatters/CountryFlagFormatter.test.ts | 65 ++++ .../lib/formatters/CurrencyFormatter.test.ts | 76 ++++ .../lib/formatters/DateFormatter.test.ts | 98 +++++ ...DriverRegistrationStatusFormatter.test.tsx | 34 ++ .../lib/formatters/DurationFormatter.test.ts | 57 +++ .../lib/formatters/FinishFormatter.test.ts | 60 +++ .../formatters/HealthAlertFormatter.test.ts | 91 +++++ .../HealthComponentFormatter.test.ts | 84 ++++ .../formatters/HealthMetricFormatter.test.ts | 125 ++++++ .../formatters/HealthStatusFormatter.test.ts | 121 ++++++ .../LeagueCreationStatusFormatter.test.ts | 14 + .../lib/formatters/LeagueFormatter.test.ts | 26 ++ .../formatters/LeagueRoleFormatter.test.ts | 60 +++ .../formatters/LeagueTierFormatter.test.ts | 36 ++ .../LeagueWizardValidationMessages.test.ts | 52 +++ .../lib/formatters/MedalFormatter.test.ts | 100 +++++ .../lib/formatters/MemberFormatter.test.ts | 48 +++ .../MembershipFeeTypeFormatter.test.ts | 26 ++ .../lib/formatters/MemoryFormatter.test.ts | 46 +++ .../lib/formatters/NumberFormatter.test.ts | 82 ++++ .../OnboardingStatusFormatter.test.ts | 48 +++ .../lib/formatters/PayerTypeFormatter.test.ts | 28 ++ .../formatters/PaymentTypeFormatter.test.ts | 30 ++ .../lib/formatters/PercentFormatter.test.ts | 62 +++ .../lib/formatters/PrizeTypeFormatter.test.ts | 26 ++ .../lib/formatters/ProfileFormatter.test.ts | 359 ++++++++++++++++++ .../formatters/RaceStatusFormatter.test.ts | 70 ++++ .../formatters/RatingTrendFormatter.test.ts | 62 +++ .../formatters/RelativeTimeFormatter.test.ts | 83 ++++ .../formatters/SeasonStatusFormatter.test.ts | 33 ++ .../formatters/SkillLevelFormatter.test.ts | 92 +++++ .../SkillLevelIconFormatter.test.ts | 31 ++ .../lib/formatters/StatusFormatter.test.ts | 62 +++ .../TeamCreationStatusFormatter.test.ts | 14 + .../lib/formatters/TimeFormatter.test.ts | 41 ++ .../TransactionTypeFormatter.test.ts | 28 ++ .../lib/formatters/UserRoleFormatter.test.ts | 26 ++ .../formatters/UserStatusFormatter.test.ts | 94 +++++ .../lib/formatters/WinRateFormatter.test.ts | 54 +++ 42 files changed, 2743 insertions(+) create mode 100644 apps/website/lib/formatters/AchievementFormatter.test.ts create mode 100644 apps/website/lib/formatters/ActivityLevelFormatter.test.ts create mode 100644 apps/website/lib/formatters/AvatarFormatter.test.ts create mode 100644 apps/website/lib/formatters/CountryFlagFormatter.test.ts create mode 100644 apps/website/lib/formatters/CurrencyFormatter.test.ts create mode 100644 apps/website/lib/formatters/DateFormatter.test.ts create mode 100644 apps/website/lib/formatters/DriverRegistrationStatusFormatter.test.tsx create mode 100644 apps/website/lib/formatters/DurationFormatter.test.ts create mode 100644 apps/website/lib/formatters/FinishFormatter.test.ts create mode 100644 apps/website/lib/formatters/HealthAlertFormatter.test.ts create mode 100644 apps/website/lib/formatters/HealthComponentFormatter.test.ts create mode 100644 apps/website/lib/formatters/HealthMetricFormatter.test.ts create mode 100644 apps/website/lib/formatters/HealthStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/LeagueCreationStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/LeagueFormatter.test.ts create mode 100644 apps/website/lib/formatters/LeagueRoleFormatter.test.ts create mode 100644 apps/website/lib/formatters/LeagueTierFormatter.test.ts create mode 100644 apps/website/lib/formatters/LeagueWizardValidationMessages.test.ts create mode 100644 apps/website/lib/formatters/MedalFormatter.test.ts create mode 100644 apps/website/lib/formatters/MemberFormatter.test.ts create mode 100644 apps/website/lib/formatters/MembershipFeeTypeFormatter.test.ts create mode 100644 apps/website/lib/formatters/MemoryFormatter.test.ts create mode 100644 apps/website/lib/formatters/NumberFormatter.test.ts create mode 100644 apps/website/lib/formatters/OnboardingStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/PayerTypeFormatter.test.ts create mode 100644 apps/website/lib/formatters/PaymentTypeFormatter.test.ts create mode 100644 apps/website/lib/formatters/PercentFormatter.test.ts create mode 100644 apps/website/lib/formatters/PrizeTypeFormatter.test.ts create mode 100644 apps/website/lib/formatters/ProfileFormatter.test.ts create mode 100644 apps/website/lib/formatters/RaceStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/RatingTrendFormatter.test.ts create mode 100644 apps/website/lib/formatters/RelativeTimeFormatter.test.ts create mode 100644 apps/website/lib/formatters/SeasonStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/SkillLevelFormatter.test.ts create mode 100644 apps/website/lib/formatters/SkillLevelIconFormatter.test.ts create mode 100644 apps/website/lib/formatters/StatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/TeamCreationStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/TimeFormatter.test.ts create mode 100644 apps/website/lib/formatters/TransactionTypeFormatter.test.ts create mode 100644 apps/website/lib/formatters/UserRoleFormatter.test.ts create mode 100644 apps/website/lib/formatters/UserStatusFormatter.test.ts create mode 100644 apps/website/lib/formatters/WinRateFormatter.test.ts diff --git a/apps/website/lib/formatters/AchievementFormatter.test.ts b/apps/website/lib/formatters/AchievementFormatter.test.ts new file mode 100644 index 000000000..541064834 --- /dev/null +++ b/apps/website/lib/formatters/AchievementFormatter.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { AchievementFormatter } from './AchievementFormatter'; + +describe('AchievementFormatter', () => { + describe('getRarityVariant', () => { + it('should format common rarity correctly', () => { + const result = AchievementFormatter.getRarityVariant('common'); + expect(result).toEqual({ + text: 'low', + surface: 'rarity-common', + iconIntent: 'low', + }); + }); + + it('should format rare rarity correctly', () => { + const result = AchievementFormatter.getRarityVariant('rare'); + expect(result).toEqual({ + text: 'primary', + surface: 'rarity-rare', + iconIntent: 'primary', + }); + }); + + it('should format epic rarity correctly', () => { + const result = AchievementFormatter.getRarityVariant('epic'); + expect(result).toEqual({ + text: 'primary', + surface: 'rarity-epic', + iconIntent: 'primary', + }); + }); + + it('should format legendary rarity correctly', () => { + const result = AchievementFormatter.getRarityVariant('legendary'); + expect(result).toEqual({ + text: 'warning', + surface: 'rarity-legendary', + iconIntent: 'warning', + }); + }); + + it('should handle case-insensitive rarity', () => { + const result = AchievementFormatter.getRarityVariant('COMMON'); + expect(result).toEqual({ + text: 'low', + surface: 'rarity-common', + iconIntent: 'low', + }); + }); + + it('should default to common for unknown rarity', () => { + const result = AchievementFormatter.getRarityVariant('unknown'); + expect(result).toEqual({ + text: 'low', + surface: 'rarity-common', + iconIntent: 'low', + }); + }); + }); + + describe('formatDate', () => { + it('should format date correctly', () => { + const date = new Date('2026-01-15'); + const result = AchievementFormatter.formatDate(date); + expect(result).toBe('Jan 15, 2026'); + }); + + it('should format date with different months', () => { + const date = new Date('2026-12-25'); + const result = AchievementFormatter.formatDate(date); + expect(result).toBe('Dec 25, 2026'); + }); + + it('should handle single digit days', () => { + const date = new Date('2026-01-05'); + const result = AchievementFormatter.formatDate(date); + expect(result).toBe('Jan 5, 2026'); + }); + }); +}); diff --git a/apps/website/lib/formatters/ActivityLevelFormatter.test.ts b/apps/website/lib/formatters/ActivityLevelFormatter.test.ts new file mode 100644 index 000000000..9fcf77499 --- /dev/null +++ b/apps/website/lib/formatters/ActivityLevelFormatter.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { ActivityLevelFormatter } from './ActivityLevelFormatter'; + +describe('ActivityLevelFormatter', () => { + describe('levelLabel', () => { + it('should return "Low" for engagement rate below 20', () => { + expect(ActivityLevelFormatter.levelLabel(0)).toBe('Low'); + expect(ActivityLevelFormatter.levelLabel(10)).toBe('Low'); + expect(ActivityLevelFormatter.levelLabel(19.9)).toBe('Low'); + }); + + it('should return "Medium" for engagement rate between 20 and 50', () => { + expect(ActivityLevelFormatter.levelLabel(20)).toBe('Medium'); + expect(ActivityLevelFormatter.levelLabel(35)).toBe('Medium'); + expect(ActivityLevelFormatter.levelLabel(49.9)).toBe('Medium'); + }); + + it('should return "High" for engagement rate 50 or above', () => { + expect(ActivityLevelFormatter.levelLabel(50)).toBe('High'); + expect(ActivityLevelFormatter.levelLabel(75)).toBe('High'); + expect(ActivityLevelFormatter.levelLabel(100)).toBe('High'); + }); + }); + + describe('levelValue', () => { + it('should return "low" for engagement rate below 20', () => { + expect(ActivityLevelFormatter.levelValue(0)).toBe('low'); + expect(ActivityLevelFormatter.levelValue(10)).toBe('low'); + expect(ActivityLevelFormatter.levelValue(19.9)).toBe('low'); + }); + + it('should return "medium" for engagement rate between 20 and 50', () => { + expect(ActivityLevelFormatter.levelValue(20)).toBe('medium'); + expect(ActivityLevelFormatter.levelValue(35)).toBe('medium'); + expect(ActivityLevelFormatter.levelValue(49.9)).toBe('medium'); + }); + + it('should return "high" for engagement rate 50 or above', () => { + expect(ActivityLevelFormatter.levelValue(50)).toBe('high'); + expect(ActivityLevelFormatter.levelValue(75)).toBe('high'); + expect(ActivityLevelFormatter.levelValue(100)).toBe('high'); + }); + }); +}); diff --git a/apps/website/lib/formatters/AvatarFormatter.test.ts b/apps/website/lib/formatters/AvatarFormatter.test.ts new file mode 100644 index 000000000..7e8d338de --- /dev/null +++ b/apps/website/lib/formatters/AvatarFormatter.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { AvatarFormatter } from './AvatarFormatter'; + +describe('AvatarFormatter', () => { + describe('bufferToBase64', () => { + it('should convert ArrayBuffer to base64 string', () => { + const buffer = new ArrayBuffer(3); + const view = new Uint8Array(buffer); + view[0] = 72; // 'H' + view[1] = 101; // 'e' + view[2] = 108; // 'l' + + const result = AvatarFormatter.bufferToBase64(buffer); + expect(result).toBe('SGVs'); + }); + + it('should handle empty buffer', () => { + const buffer = new ArrayBuffer(0); + const result = AvatarFormatter.bufferToBase64(buffer); + expect(result).toBe(''); + }); + + it('should handle buffer with special characters', () => { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view[0] = 255; // ÿ + view[1] = 254; // Þ + view[2] = 253; // Ý + view[3] = 252; // Ü + + const result = AvatarFormatter.bufferToBase64(buffer); + expect(result).toBe('/v7+/v4='); + }); + }); + + describe('hasValidData', () => { + it('should return true for valid buffer and content type', () => { + expect(AvatarFormatter.hasValidData('base64data', 'image/png')).toBe(true); + }); + + it('should return false for empty buffer', () => { + expect(AvatarFormatter.hasValidData('', 'image/png')).toBe(false); + }); + + it('should return false for empty content type', () => { + expect(AvatarFormatter.hasValidData('base64data', '')).toBe(false); + }); + + it('should return false for both empty', () => { + expect(AvatarFormatter.hasValidData('', '')).toBe(false); + }); + }); + + describe('formatContentType', () => { + it('should format image/png to PNG', () => { + expect(AvatarFormatter.formatContentType('image/png')).toBe('PNG'); + }); + + it('should format image/jpeg to JPEG', () => { + expect(AvatarFormatter.formatContentType('image/jpeg')).toBe('JPEG'); + }); + + it('should format image/gif to GIF', () => { + expect(AvatarFormatter.formatContentType('image/gif')).toBe('GIF'); + }); + + it('should handle content type without slash', () => { + expect(AvatarFormatter.formatContentType('png')).toBe('png'); + }); + + it('should handle content type with multiple slashes', () => { + expect(AvatarFormatter.formatContentType('image/png/test')).toBe('PNG'); + }); + }); +}); diff --git a/apps/website/lib/formatters/CountryFlagFormatter.test.ts b/apps/website/lib/formatters/CountryFlagFormatter.test.ts new file mode 100644 index 000000000..b6fbe9da4 --- /dev/null +++ b/apps/website/lib/formatters/CountryFlagFormatter.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { CountryFlagFormatter } from './CountryFlagFormatter'; + +describe('CountryFlagFormatter', () => { + describe('fromCountryCode', () => { + it('should return flag emoji for valid 2-letter country code', () => { + const formatter = CountryFlagFormatter.fromCountryCode('US'); + expect(formatter.toString()).toBe('🇺🇸'); + }); + + it('should handle lowercase country codes', () => { + const formatter = CountryFlagFormatter.fromCountryCode('us'); + expect(formatter.toString()).toBe('🇺🇸'); + }); + + it('should return default flag for null', () => { + const formatter = CountryFlagFormatter.fromCountryCode(null); + expect(formatter.toString()).toBe('🏁'); + }); + + it('should return default flag for undefined', () => { + const formatter = CountryFlagFormatter.fromCountryCode(undefined); + expect(formatter.toString()).toBe('🏁'); + }); + + it('should return default flag for empty string', () => { + const formatter = CountryFlagFormatter.fromCountryCode(''); + expect(formatter.toString()).toBe('🏁'); + }); + + it('should return default flag for invalid length code', () => { + const formatter = CountryFlagFormatter.fromCountryCode('USA'); + expect(formatter.toString()).toBe('🏁'); + }); + + it('should return default flag for single character code', () => { + const formatter = CountryFlagFormatter.fromCountryCode('U'); + expect(formatter.toString()).toBe('🏁'); + }); + + it('should handle various country codes', () => { + expect(CountryFlagFormatter.fromCountryCode('GB').toString()).toBe('🇬🇧'); + expect(CountryFlagFormatter.fromCountryCode('DE').toString()).toBe('🇩🇪'); + expect(CountryFlagFormatter.fromCountryCode('FR').toString()).toBe('🇫🇷'); + expect(CountryFlagFormatter.fromCountryCode('IT').toString()).toBe('🇮🇹'); + expect(CountryFlagFormatter.fromCountryCode('ES').toString()).toBe('🇪🇸'); + expect(CountryFlagFormatter.fromCountryCode('JP').toString()).toBe('🇯🇵'); + expect(CountryFlagFormatter.fromCountryCode('AU').toString()).toBe('🇦🇺'); + expect(CountryFlagFormatter.fromCountryCode('CA').toString()).toBe('🇨🇦'); + expect(CountryFlagFormatter.fromCountryCode('BR').toString()).toBe('🇧🇷'); + }); + }); + + describe('toString', () => { + it('should return the flag emoji', () => { + const formatter = CountryFlagFormatter.fromCountryCode('US'); + expect(formatter.toString()).toBe('🇺🇸'); + }); + + it('should return the default flag for invalid codes', () => { + const formatter = CountryFlagFormatter.fromCountryCode('XX'); + expect(formatter.toString()).toBe('🏁'); + }); + }); +}); diff --git a/apps/website/lib/formatters/CurrencyFormatter.test.ts b/apps/website/lib/formatters/CurrencyFormatter.test.ts new file mode 100644 index 000000000..4fb817e0b --- /dev/null +++ b/apps/website/lib/formatters/CurrencyFormatter.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; +import { CurrencyFormatter } from './CurrencyFormatter'; + +describe('CurrencyFormatter', () => { + describe('format', () => { + it('should format USD with dollar sign and commas', () => { + expect(CurrencyFormatter.format(1234.56, 'USD')).toBe('$1,234.56'); + expect(CurrencyFormatter.format(1000000, 'USD')).toBe('$1,000,000.00'); + }); + + it('should format EUR with euro sign and dots as thousands separator', () => { + expect(CurrencyFormatter.format(1234.56, 'EUR')).toBe('€1.234,56'); + expect(CurrencyFormatter.format(1000000, 'EUR')).toBe('€1.000.000,00'); + }); + + it('should format with custom currency symbol', () => { + expect(CurrencyFormatter.format(1234.56, 'GBP')).toBe('GBP 1,234.56'); + expect(CurrencyFormatter.format(1234.56, 'JPY')).toBe('JPY 1,234.56'); + }); + + it('should use USD as default currency', () => { + expect(CurrencyFormatter.format(1234.56)).toBe('$1,234.56'); + }); + + it('should handle zero amount', () => { + expect(CurrencyFormatter.format(0, 'USD')).toBe('$0.00'); + expect(CurrencyFormatter.format(0, 'EUR')).toBe('€0,00'); + }); + + it('should handle negative amounts', () => { + expect(CurrencyFormatter.format(-1234.56, 'USD')).toBe('$-1,234.56'); + expect(CurrencyFormatter.format(-1234.56, 'EUR')).toBe('€-1.234,56'); + }); + + it('should handle amounts with many decimal places', () => { + expect(CurrencyFormatter.format(1234.5678, 'USD')).toBe('$1,234.57'); + expect(CurrencyFormatter.format(1234.5678, 'EUR')).toBe('€1.234,57'); + }); + }); + + describe('formatCompact', () => { + it('should format USD with dollar sign and no decimals', () => { + expect(CurrencyFormatter.formatCompact(1234.56, 'USD')).toBe('$1,235'); + expect(CurrencyFormatter.formatCompact(1000000, 'USD')).toBe('$1,000,000'); + }); + + it('should format EUR with euro sign and dots as thousands separator', () => { + expect(CurrencyFormatter.formatCompact(1234.56, 'EUR')).toBe('€1.235'); + expect(CurrencyFormatter.formatCompact(1000000, 'EUR')).toBe('€1.000.000'); + }); + + it('should format with custom currency symbol', () => { + expect(CurrencyFormatter.formatCompact(1234.56, 'GBP')).toBe('GBP 1,235'); + expect(CurrencyFormatter.formatCompact(1234.56, 'JPY')).toBe('JPY 1,235'); + }); + + it('should use USD as default currency', () => { + expect(CurrencyFormatter.formatCompact(1234.56)).toBe('$1,235'); + }); + + it('should handle zero amount', () => { + expect(CurrencyFormatter.formatCompact(0, 'USD')).toBe('$0'); + expect(CurrencyFormatter.formatCompact(0, 'EUR')).toBe('€0'); + }); + + it('should handle negative amounts', () => { + expect(CurrencyFormatter.formatCompact(-1234.56, 'USD')).toBe('$-1,235'); + expect(CurrencyFormatter.formatCompact(-1234.56, 'EUR')).toBe('€-1.235'); + }); + + it('should round amounts correctly', () => { + expect(CurrencyFormatter.formatCompact(1234.4, 'USD')).toBe('$1,234'); + expect(CurrencyFormatter.formatCompact(1234.6, 'USD')).toBe('$1,235'); + }); + }); +}); diff --git a/apps/website/lib/formatters/DateFormatter.test.ts b/apps/website/lib/formatters/DateFormatter.test.ts new file mode 100644 index 000000000..76c4afbd6 --- /dev/null +++ b/apps/website/lib/formatters/DateFormatter.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest'; +import { DateFormatter } from './DateFormatter'; + +describe('DateFormatter', () => { + describe('formatShort', () => { + it('should format date as "Jan 18, 2026"', () => { + const date = new Date('2026-01-18T12:00:00Z'); + expect(DateFormatter.formatShort(date)).toBe('Jan 18, 2026'); + }); + + it('should handle string input', () => { + expect(DateFormatter.formatShort('2026-01-18T12:00:00Z')).toBe('Jan 18, 2026'); + }); + + it('should format different months correctly', () => { + expect(DateFormatter.formatShort(new Date('2026-02-15T12:00:00Z'))).toBe('Feb 15, 2026'); + expect(DateFormatter.formatShort(new Date('2026-12-25T12:00:00Z'))).toBe('Dec 25, 2026'); + }); + + it('should handle single digit days', () => { + expect(DateFormatter.formatShort(new Date('2026-01-05T12:00:00Z'))).toBe('Jan 5, 2026'); + }); + }); + + describe('formatMonthYear', () => { + it('should format date as "Jan 2026"', () => { + const date = new Date('2026-01-18T12:00:00Z'); + expect(DateFormatter.formatMonthYear(date)).toBe('Jan 2026'); + }); + + it('should handle string input', () => { + expect(DateFormatter.formatMonthYear('2026-01-18T12:00:00Z')).toBe('Jan 2026'); + }); + + it('should format different months correctly', () => { + expect(DateFormatter.formatMonthYear(new Date('2026-02-15T12:00:00Z'))).toBe('Feb 2026'); + expect(DateFormatter.formatMonthYear(new Date('2026-12-25T12:00:00Z'))).toBe('Dec 2026'); + }); + }); + + describe('formatTime', () => { + it('should format time as "15:00"', () => { + const date = new Date('2026-01-18T15:00:00Z'); + expect(DateFormatter.formatTime(date)).toBe('15:00'); + }); + + it('should handle string input', () => { + expect(DateFormatter.formatTime('2026-01-18T15:00:00Z')).toBe('15:00'); + }); + + it('should pad single digit hours and minutes', () => { + expect(DateFormatter.formatTime(new Date('2026-01-18T05:09:00Z'))).toBe('05:09'); + }); + + it('should handle midnight', () => { + expect(DateFormatter.formatTime(new Date('2026-01-18T00:00:00Z'))).toBe('00:00'); + }); + }); + + describe('formatMonthDay', () => { + it('should format date as "Jan 18"', () => { + const date = new Date('2026-01-18T12:00:00Z'); + expect(DateFormatter.formatMonthDay(date)).toBe('Jan 18'); + }); + + it('should handle string input', () => { + expect(DateFormatter.formatMonthDay('2026-01-18T12:00:00Z')).toBe('Jan 18'); + }); + + it('should format different months correctly', () => { + expect(DateFormatter.formatMonthDay(new Date('2026-02-15T12:00:00Z'))).toBe('Feb 15'); + expect(DateFormatter.formatMonthDay(new Date('2026-12-25T12:00:00Z'))).toBe('Dec 25'); + }); + + it('should handle single digit days', () => { + expect(DateFormatter.formatMonthDay(new Date('2026-01-05T12:00:00Z'))).toBe('Jan 5'); + }); + }); + + describe('formatDateTime', () => { + it('should format date and time as "Jan 18, 15:00"', () => { + const date = new Date('2026-01-18T15:00:00Z'); + expect(DateFormatter.formatDateTime(date)).toBe('Jan 18, 15:00'); + }); + + it('should handle string input', () => { + expect(DateFormatter.formatDateTime('2026-01-18T15:00:00Z')).toBe('Jan 18, 15:00'); + }); + + it('should pad single digit hours and minutes', () => { + expect(DateFormatter.formatDateTime(new Date('2026-01-18T05:09:00Z'))).toBe('Jan 18, 05:09'); + }); + + it('should handle midnight', () => { + expect(DateFormatter.formatDateTime(new Date('2026-01-18T00:00:00Z'))).toBe('Jan 18, 00:00'); + }); + }); +}); diff --git a/apps/website/lib/formatters/DriverRegistrationStatusFormatter.test.tsx b/apps/website/lib/formatters/DriverRegistrationStatusFormatter.test.tsx new file mode 100644 index 000000000..9b882d8bd --- /dev/null +++ b/apps/website/lib/formatters/DriverRegistrationStatusFormatter.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import { DriverRegistrationStatusFormatter } from './DriverRegistrationStatusFormatter'; + +describe('DriverRegistrationStatusFormatter', () => { + describe('statusMessage', () => { + it('should return "Registered for this race" when registered', () => { + expect(DriverRegistrationStatusFormatter.statusMessage(true)).toBe('Registered for this race'); + }); + + it('should return "Not registered" when not registered', () => { + expect(DriverRegistrationStatusFormatter.statusMessage(false)).toBe('Not registered'); + }); + }); + + describe('statusBadgeVariant', () => { + it('should return "success" when registered', () => { + expect(DriverRegistrationStatusFormatter.statusBadgeVariant(true)).toBe('success'); + }); + + it('should return "warning" when not registered', () => { + expect(DriverRegistrationStatusFormatter.statusBadgeVariant(false)).toBe('warning'); + }); + }); + + describe('registrationButtonText', () => { + it('should return "Withdraw" when registered', () => { + expect(DriverRegistrationStatusFormatter.registrationButtonText(true)).toBe('Withdraw'); + }); + + it('should return "Register" when not registered', () => { + expect(DriverRegistrationStatusFormatter.registrationButtonText(false)).toBe('Register'); + }); + }); +}); diff --git a/apps/website/lib/formatters/DurationFormatter.test.ts b/apps/website/lib/formatters/DurationFormatter.test.ts new file mode 100644 index 000000000..9d5f2e5f9 --- /dev/null +++ b/apps/website/lib/formatters/DurationFormatter.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { DurationFormatter } from './DurationFormatter'; + +describe('DurationFormatter', () => { + describe('formatMs', () => { + it('should format milliseconds with 2 decimal places', () => { + expect(DurationFormatter.formatMs(123.456)).toBe('123.46ms'); + expect(DurationFormatter.formatMs(123.454)).toBe('123.45ms'); + }); + + it('should handle zero milliseconds', () => { + expect(DurationFormatter.formatMs(0)).toBe('0.00ms'); + }); + + it('should handle large milliseconds', () => { + expect(DurationFormatter.formatMs(123456.789)).toBe('123456.79ms'); + }); + + it('should handle negative milliseconds', () => { + expect(DurationFormatter.formatMs(-123.456)).toBe('-123.46ms'); + }); + }); + + describe('formatSeconds', () => { + it('should format seconds as "M:SS.mmm"', () => { + expect(DurationFormatter.formatSeconds(65.123)).toBe('1:05.123'); + expect(DurationFormatter.formatSeconds(125.456)).toBe('2:05.456'); + }); + + it('should handle zero seconds', () => { + expect(DurationFormatter.formatSeconds(0)).toBe('0:00.000'); + }); + + it('should handle less than 60 seconds', () => { + expect(DurationFormatter.formatSeconds(5.123)).toBe('0:05.123'); + expect(DurationFormatter.formatSeconds(59.999)).toBe('0:59.999'); + }); + + it('should handle exactly 60 seconds', () => { + expect(DurationFormatter.formatSeconds(60)).toBe('1:00.000'); + }); + + it('should handle multiple minutes', () => { + expect(DurationFormatter.formatSeconds(125.123)).toBe('2:05.123'); + expect(DurationFormatter.formatSeconds(365.456)).toBe('6:05.456'); + }); + + it('should pad seconds with leading zeros', () => { + expect(DurationFormatter.formatSeconds(5.123)).toBe('0:05.123'); + expect(DurationFormatter.formatSeconds(0.123)).toBe('0:00.123'); + }); + + it('should handle negative seconds', () => { + expect(DurationFormatter.formatSeconds(-65.123)).toBe('-1:05.123'); + }); + }); +}); diff --git a/apps/website/lib/formatters/FinishFormatter.test.ts b/apps/website/lib/formatters/FinishFormatter.test.ts new file mode 100644 index 000000000..91e3997fb --- /dev/null +++ b/apps/website/lib/formatters/FinishFormatter.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { FinishFormatter } from './FinishFormatter'; + +describe('FinishFormatter', () => { + describe('format', () => { + it('should format position as "P1"', () => { + expect(FinishFormatter.format(1)).toBe('P1'); + }); + + it('should format position as "P2"', () => { + expect(FinishFormatter.format(2)).toBe('P2'); + }); + + it('should format position as "P10"', () => { + expect(FinishFormatter.format(10)).toBe('P10'); + }); + + it('should handle null value', () => { + expect(FinishFormatter.format(null)).toBe('—'); + }); + + it('should handle undefined value', () => { + expect(FinishFormatter.format(undefined)).toBe('—'); + }); + + it('should handle decimal positions', () => { + expect(FinishFormatter.format(5.5)).toBe('P5'); + }); + + it('should handle large positions', () => { + expect(FinishFormatter.format(100)).toBe('P100'); + }); + }); + + describe('formatAverage', () => { + it('should format average as "P5.4"', () => { + expect(FinishFormatter.formatAverage(5.4)).toBe('P5.4'); + }); + + it('should format average as "P10.0"', () => { + expect(FinishFormatter.formatAverage(10.0)).toBe('P10.0'); + }); + + it('should handle null value', () => { + expect(FinishFormatter.formatAverage(null)).toBe('—'); + }); + + it('should handle undefined value', () => { + expect(FinishFormatter.formatAverage(undefined)).toBe('—'); + }); + + it('should handle decimal averages', () => { + expect(FinishFormatter.formatAverage(5.123)).toBe('P5.1'); + }); + + it('should handle large averages', () => { + expect(FinishFormatter.formatAverage(100.5)).toBe('P100.5'); + }); + }); +}); diff --git a/apps/website/lib/formatters/HealthAlertFormatter.test.ts b/apps/website/lib/formatters/HealthAlertFormatter.test.ts new file mode 100644 index 000000000..a38404c5c --- /dev/null +++ b/apps/website/lib/formatters/HealthAlertFormatter.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from 'vitest'; +import { HealthAlertFormatter } from './HealthAlertFormatter'; + +describe('HealthAlertFormatter', () => { + describe('formatSeverity', () => { + it('should format critical severity correctly', () => { + expect(HealthAlertFormatter.formatSeverity('critical')).toBe('Critical'); + }); + + it('should format warning severity correctly', () => { + expect(HealthAlertFormatter.formatSeverity('warning')).toBe('Warning'); + }); + + it('should format info severity correctly', () => { + expect(HealthAlertFormatter.formatSeverity('info')).toBe('Info'); + }); + + it('should default to Info for unknown severity', () => { + expect(HealthAlertFormatter.formatSeverity('unknown')).toBe('Info'); + }); + }); + + describe('formatSeverityColor', () => { + it('should return red for critical', () => { + expect(HealthAlertFormatter.formatSeverityColor('critical')).toBe('#ef4444'); + }); + + it('should return amber for warning', () => { + expect(HealthAlertFormatter.formatSeverityColor('warning')).toBe('#f59e0b'); + }); + + it('should return blue for info', () => { + expect(HealthAlertFormatter.formatSeverityColor('info')).toBe('#3b82f6'); + }); + + it('should default to blue for unknown severity', () => { + expect(HealthAlertFormatter.formatSeverityColor('unknown')).toBe('#3b82f6'); + }); + }); + + describe('formatTimestamp', () => { + it('should format timestamp correctly', () => { + const timestamp = '2026-01-15T14:30:45Z'; + const result = HealthAlertFormatter.formatTimestamp(timestamp); + expect(result).toBe('Jan 15, 2026, 14:30:45'); + }); + + it('should handle different timestamps', () => { + const timestamp = '2026-12-25T09:15:30Z'; + const result = HealthAlertFormatter.formatTimestamp(timestamp); + expect(result).toBe('Dec 25, 2026, 09:15:30'); + }); + }); + + describe('formatRelativeTime', () => { + it('should return "Just now" for less than 1 minute ago', () => { + const now = new Date(); + const oneSecondAgo = new Date(now.getTime() - 1000); + const result = HealthAlertFormatter.formatRelativeTime(oneSecondAgo.toISOString()); + expect(result).toBe('Just now'); + }); + + it('should return minutes ago for less than 1 hour', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = HealthAlertFormatter.formatRelativeTime(thirtyMinutesAgo.toISOString()); + expect(result).toBe('30m ago'); + }); + + it('should return hours ago for less than 24 hours', () => { + const now = new Date(); + const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000); + const result = HealthAlertFormatter.formatRelativeTime(fiveHoursAgo.toISOString()); + expect(result).toBe('5h ago'); + }); + + it('should return days ago for less than 7 days', () => { + const now = new Date(); + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); + const result = HealthAlertFormatter.formatRelativeTime(threeDaysAgo.toISOString()); + expect(result).toBe('3d ago'); + }); + + it('should return weeks ago for more than 7 days', () => { + const now = new Date(); + const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); + const result = HealthAlertFormatter.formatRelativeTime(tenDaysAgo.toISOString()); + expect(result).toBe('1w ago'); + }); + }); +}); diff --git a/apps/website/lib/formatters/HealthComponentFormatter.test.ts b/apps/website/lib/formatters/HealthComponentFormatter.test.ts new file mode 100644 index 000000000..c2bc58d17 --- /dev/null +++ b/apps/website/lib/formatters/HealthComponentFormatter.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from 'vitest'; +import { HealthComponentFormatter } from './HealthComponentFormatter'; + +describe('HealthComponentFormatter', () => { + describe('formatStatusLabel', () => { + it('should format ok status correctly', () => { + expect(HealthComponentFormatter.formatStatusLabel('ok')).toBe('Healthy'); + }); + + it('should format degraded status correctly', () => { + expect(HealthComponentFormatter.formatStatusLabel('degraded')).toBe('Degraded'); + }); + + it('should format error status correctly', () => { + expect(HealthComponentFormatter.formatStatusLabel('error')).toBe('Error'); + }); + + it('should format unknown status correctly', () => { + expect(HealthComponentFormatter.formatStatusLabel('unknown')).toBe('Unknown'); + }); + + it('should default to Unknown for unknown status', () => { + expect(HealthComponentFormatter.formatStatusLabel('invalid')).toBe('Unknown'); + }); + }); + + describe('formatStatusColor', () => { + it('should return green for ok', () => { + expect(HealthComponentFormatter.formatStatusColor('ok')).toBe('#10b981'); + }); + + it('should return amber for degraded', () => { + expect(HealthComponentFormatter.formatStatusColor('degraded')).toBe('#f59e0b'); + }); + + it('should return red for error', () => { + expect(HealthComponentFormatter.formatStatusColor('error')).toBe('#ef4444'); + }); + + it('should return gray for unknown', () => { + expect(HealthComponentFormatter.formatStatusColor('unknown')).toBe('#6b7280'); + }); + + it('should default to gray for invalid status', () => { + expect(HealthComponentFormatter.formatStatusColor('invalid')).toBe('#6b7280'); + }); + }); + + describe('formatStatusIcon', () => { + it('should return checkmark for ok', () => { + expect(HealthComponentFormatter.formatStatusIcon('ok')).toBe('✓'); + }); + + it('should return warning for degraded', () => { + expect(HealthComponentFormatter.formatStatusIcon('degraded')).toBe('⚠'); + }); + + it('should return X for error', () => { + expect(HealthComponentFormatter.formatStatusIcon('error')).toBe('✕'); + }); + + it('should return question mark for unknown', () => { + expect(HealthComponentFormatter.formatStatusIcon('unknown')).toBe('?'); + }); + + it('should default to question mark for invalid status', () => { + expect(HealthComponentFormatter.formatStatusIcon('invalid')).toBe('?'); + }); + }); + + describe('formatTimestamp', () => { + it('should format timestamp correctly', () => { + const timestamp = '2026-01-15T14:30:45Z'; + const result = HealthComponentFormatter.formatTimestamp(timestamp); + expect(result).toBe('Jan 15, 2026, 14:30:45'); + }); + + it('should handle different timestamps', () => { + const timestamp = '2026-12-25T09:15:30Z'; + const result = HealthComponentFormatter.formatTimestamp(timestamp); + expect(result).toBe('Dec 25, 2026, 09:15:30'); + }); + }); +}); diff --git a/apps/website/lib/formatters/HealthMetricFormatter.test.ts b/apps/website/lib/formatters/HealthMetricFormatter.test.ts new file mode 100644 index 000000000..bbc56ca74 --- /dev/null +++ b/apps/website/lib/formatters/HealthMetricFormatter.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from 'vitest'; +import { HealthMetricFormatter } from './HealthMetricFormatter'; + +describe('HealthMetricFormatter', () => { + describe('formatUptime', () => { + it('should format uptime as percentage with 2 decimal places', () => { + expect(HealthMetricFormatter.formatUptime(99.99)).toBe('99.99%'); + expect(HealthMetricFormatter.formatUptime(95.5)).toBe('95.50%'); + }); + + it('should handle undefined uptime', () => { + expect(HealthMetricFormatter.formatUptime(undefined)).toBe('N/A'); + }); + + it('should handle null uptime', () => { + expect(HealthMetricFormatter.formatUptime(null)).toBe('N/A'); + }); + + it('should handle negative uptime', () => { + expect(HealthMetricFormatter.formatUptime(-1)).toBe('N/A'); + }); + + it('should handle zero uptime', () => { + expect(HealthMetricFormatter.formatUptime(0)).toBe('0.00%'); + }); + }); + + describe('formatResponseTime', () => { + it('should format response time in milliseconds for values under 1000', () => { + expect(HealthMetricFormatter.formatResponseTime(123)).toBe('123ms'); + expect(HealthMetricFormatter.formatResponseTime(999)).toBe('999ms'); + }); + + it('should format response time in seconds for values between 1000 and 60000', () => { + expect(HealthMetricFormatter.formatResponseTime(1000)).toBe('1.00s'); + expect(HealthMetricFormatter.formatResponseTime(12345)).toBe('12.35s'); + expect(HealthMetricFormatter.formatResponseTime(59999)).toBe('60.00s'); + }); + + it('should format response time in minutes for values 60000 or above', () => { + expect(HealthMetricFormatter.formatResponseTime(60000)).toBe('1.00m'); + expect(HealthMetricFormatter.formatResponseTime(123456)).toBe('2.06m'); + }); + + it('should handle undefined response time', () => { + expect(HealthMetricFormatter.formatResponseTime(undefined)).toBe('N/A'); + }); + + it('should handle null response time', () => { + expect(HealthMetricFormatter.formatResponseTime(null)).toBe('N/A'); + }); + + it('should handle negative response time', () => { + expect(HealthMetricFormatter.formatResponseTime(-1)).toBe('N/A'); + }); + + it('should handle zero response time', () => { + expect(HealthMetricFormatter.formatResponseTime(0)).toBe('0ms'); + }); + }); + + describe('formatErrorRate', () => { + it('should format error rate as percentage with 2 decimal places', () => { + expect(HealthMetricFormatter.formatErrorRate(0.5)).toBe('0.50%'); + expect(HealthMetricFormatter.formatErrorRate(12.34)).toBe('12.34%'); + }); + + it('should handle undefined error rate', () => { + expect(HealthMetricFormatter.formatErrorRate(undefined)).toBe('N/A'); + }); + + it('should handle null error rate', () => { + expect(HealthMetricFormatter.formatErrorRate(null)).toBe('N/A'); + }); + + it('should handle negative error rate', () => { + expect(HealthMetricFormatter.formatErrorRate(-1)).toBe('N/A'); + }); + + it('should handle zero error rate', () => { + expect(HealthMetricFormatter.formatErrorRate(0)).toBe('0.00%'); + }); + }); + + describe('formatTimestamp', () => { + it('should format timestamp correctly', () => { + const timestamp = '2026-01-15T14:30:45Z'; + const result = HealthMetricFormatter.formatTimestamp(timestamp); + expect(result).toBe('Jan 15, 2026, 14:30:45'); + }); + + it('should handle different timestamps', () => { + const timestamp = '2026-12-25T09:15:30Z'; + const result = HealthMetricFormatter.formatTimestamp(timestamp); + expect(result).toBe('Dec 25, 2026, 09:15:30'); + }); + }); + + describe('formatSuccessRate', () => { + it('should format success rate correctly', () => { + expect(HealthMetricFormatter.formatSuccessRate(95, 5)).toBe('95.0%'); + expect(HealthMetricFormatter.formatSuccessRate(99, 1)).toBe('99.0%'); + }); + + it('should handle zero total checks', () => { + expect(HealthMetricFormatter.formatSuccessRate(0, 0)).toBe('N/A'); + }); + + it('should handle only passed checks', () => { + expect(HealthMetricFormatter.formatSuccessRate(100, 0)).toBe('100.0%'); + }); + + it('should handle only failed checks', () => { + expect(HealthMetricFormatter.formatSuccessRate(0, 100)).toBe('0.0%'); + }); + + it('should handle undefined checks', () => { + expect(HealthMetricFormatter.formatSuccessRate(undefined, undefined)).toBe('N/A'); + }); + + it('should handle null checks', () => { + expect(HealthMetricFormatter.formatSuccessRate(null, null)).toBe('N/A'); + }); + }); +}); diff --git a/apps/website/lib/formatters/HealthStatusFormatter.test.ts b/apps/website/lib/formatters/HealthStatusFormatter.test.ts new file mode 100644 index 000000000..9895ae959 --- /dev/null +++ b/apps/website/lib/formatters/HealthStatusFormatter.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from 'vitest'; +import { HealthStatusFormatter } from './HealthStatusFormatter'; + +describe('HealthStatusFormatter', () => { + describe('formatStatusLabel', () => { + it('should format ok status correctly', () => { + expect(HealthStatusFormatter.formatStatusLabel('ok')).toBe('Healthy'); + }); + + it('should format degraded status correctly', () => { + expect(HealthStatusFormatter.formatStatusLabel('degraded')).toBe('Degraded'); + }); + + it('should format error status correctly', () => { + expect(HealthStatusFormatter.formatStatusLabel('error')).toBe('Error'); + }); + + it('should format unknown status correctly', () => { + expect(HealthStatusFormatter.formatStatusLabel('unknown')).toBe('Unknown'); + }); + + it('should default to Unknown for unknown status', () => { + expect(HealthStatusFormatter.formatStatusLabel('invalid')).toBe('Unknown'); + }); + }); + + describe('formatStatusColor', () => { + it('should return green for ok', () => { + expect(HealthStatusFormatter.formatStatusColor('ok')).toBe('#10b981'); + }); + + it('should return amber for degraded', () => { + expect(HealthStatusFormatter.formatStatusColor('degraded')).toBe('#f59e0b'); + }); + + it('should return red for error', () => { + expect(HealthStatusFormatter.formatStatusColor('error')).toBe('#ef4444'); + }); + + it('should return gray for unknown', () => { + expect(HealthStatusFormatter.formatStatusColor('unknown')).toBe('#6b7280'); + }); + + it('should default to gray for invalid status', () => { + expect(HealthStatusFormatter.formatStatusColor('invalid')).toBe('#6b7280'); + }); + }); + + describe('formatStatusIcon', () => { + it('should return checkmark for ok', () => { + expect(HealthStatusFormatter.formatStatusIcon('ok')).toBe('✓'); + }); + + it('should return warning for degraded', () => { + expect(HealthStatusFormatter.formatStatusIcon('degraded')).toBe('⚠'); + }); + + it('should return X for error', () => { + expect(HealthStatusFormatter.formatStatusIcon('error')).toBe('✕'); + }); + + it('should return question mark for unknown', () => { + expect(HealthStatusFormatter.formatStatusIcon('unknown')).toBe('?'); + }); + + it('should default to question mark for invalid status', () => { + expect(HealthStatusFormatter.formatStatusIcon('invalid')).toBe('?'); + }); + }); + + describe('formatTimestamp', () => { + it('should format timestamp correctly', () => { + const timestamp = '2026-01-15T14:30:45Z'; + const result = HealthStatusFormatter.formatTimestamp(timestamp); + expect(result).toBe('Jan 15, 2026, 14:30:45'); + }); + + it('should handle different timestamps', () => { + const timestamp = '2026-12-25T09:15:30Z'; + const result = HealthStatusFormatter.formatTimestamp(timestamp); + expect(result).toBe('Dec 25, 2026, 09:15:30'); + }); + }); + + describe('formatRelativeTime', () => { + it('should return "Just now" for less than 1 minute ago', () => { + const now = new Date(); + const oneSecondAgo = new Date(now.getTime() - 1000); + const result = HealthStatusFormatter.formatRelativeTime(oneSecondAgo.toISOString()); + expect(result).toBe('Just now'); + }); + + it('should return minutes ago for less than 1 hour', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = HealthStatusFormatter.formatRelativeTime(thirtyMinutesAgo.toISOString()); + expect(result).toBe('30m ago'); + }); + + it('should return hours ago for less than 24 hours', () => { + const now = new Date(); + const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000); + const result = HealthStatusFormatter.formatRelativeTime(fiveHoursAgo.toISOString()); + expect(result).toBe('5h ago'); + }); + + it('should return days ago for less than 7 days', () => { + const now = new Date(); + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); + const result = HealthStatusFormatter.formatRelativeTime(threeDaysAgo.toISOString()); + expect(result).toBe('3d ago'); + }); + + it('should return weeks ago for more than 7 days', () => { + const now = new Date(); + const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); + const result = HealthStatusFormatter.formatRelativeTime(tenDaysAgo.toISOString()); + expect(result).toBe('1w ago'); + }); + }); +}); diff --git a/apps/website/lib/formatters/LeagueCreationStatusFormatter.test.ts b/apps/website/lib/formatters/LeagueCreationStatusFormatter.test.ts new file mode 100644 index 000000000..14358beb7 --- /dev/null +++ b/apps/website/lib/formatters/LeagueCreationStatusFormatter.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueCreationStatusFormatter } from './LeagueCreationStatusFormatter'; + +describe('LeagueCreationStatusFormatter', () => { + describe('statusMessage', () => { + it('should return success message when league created successfully', () => { + expect(LeagueCreationStatusFormatter.statusMessage(true)).toBe('League created successfully!'); + }); + + it('should return failure message when league creation failed', () => { + expect(LeagueCreationStatusFormatter.statusMessage(false)).toBe('Failed to create league.'); + }); + }); +}); diff --git a/apps/website/lib/formatters/LeagueFormatter.test.ts b/apps/website/lib/formatters/LeagueFormatter.test.ts new file mode 100644 index 000000000..b06d063dc --- /dev/null +++ b/apps/website/lib/formatters/LeagueFormatter.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueFormatter } from './LeagueFormatter'; + +describe('LeagueFormatter', () => { + describe('formatCount', () => { + it('should format 1 league correctly', () => { + expect(LeagueFormatter.formatCount(1)).toBe('1 league'); + }); + + it('should format 2 leagues correctly', () => { + expect(LeagueFormatter.formatCount(2)).toBe('2 leagues'); + }); + + it('should format 0 leagues correctly', () => { + expect(LeagueFormatter.formatCount(0)).toBe('0 leagues'); + }); + + it('should format large numbers correctly', () => { + expect(LeagueFormatter.formatCount(100)).toBe('100 leagues'); + }); + + it('should handle negative numbers', () => { + expect(LeagueFormatter.formatCount(-1)).toBe('-1 leagues'); + }); + }); +}); diff --git a/apps/website/lib/formatters/LeagueRoleFormatter.test.ts b/apps/website/lib/formatters/LeagueRoleFormatter.test.ts new file mode 100644 index 000000000..15ef90664 --- /dev/null +++ b/apps/website/lib/formatters/LeagueRoleFormatter.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueRoleFormatter, leagueRoleDisplay } from './LeagueRoleFormatter'; +import type { LeagueRole } from './LeagueRoleFormatter'; + +describe('LeagueRoleFormatter', () => { + describe('getLeagueRoleDisplay', () => { + it('should return correct display data for owner role', () => { + const result = LeagueRoleFormatter.getLeagueRoleDisplay('owner'); + expect(result).toEqual({ + text: 'Owner', + badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30', + }); + }); + + it('should return correct display data for admin role', () => { + const result = LeagueRoleFormatter.getLeagueRoleDisplay('admin'); + expect(result).toEqual({ + text: 'Admin', + badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30', + }); + }); + + it('should return correct display data for steward role', () => { + const result = LeagueRoleFormatter.getLeagueRoleDisplay('steward'); + expect(result).toEqual({ + text: 'Steward', + badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30', + }); + }); + + it('should return correct display data for member role', () => { + const result = LeagueRoleFormatter.getLeagueRoleDisplay('member'); + expect(result).toEqual({ + text: 'Member', + badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30', + }); + }); + }); + + describe('leagueRoleDisplay constant', () => { + it('should contain all role definitions', () => { + expect(leagueRoleDisplay.owner).toBeDefined(); + expect(leagueRoleDisplay.admin).toBeDefined(); + expect(leagueRoleDisplay.steward).toBeDefined(); + expect(leagueRoleDisplay.member).toBeDefined(); + }); + + it('should have correct structure for each role', () => { + const roles: LeagueRole[] = ['owner', 'admin', 'steward', 'member']; + + roles.forEach(role => { + const display = leagueRoleDisplay[role]; + expect(display).toHaveProperty('text'); + expect(display).toHaveProperty('badgeClasses'); + expect(typeof display.text).toBe('string'); + expect(typeof display.badgeClasses).toBe('string'); + }); + }); + }); +}); diff --git a/apps/website/lib/formatters/LeagueTierFormatter.test.ts b/apps/website/lib/formatters/LeagueTierFormatter.test.ts new file mode 100644 index 000000000..e7af9a432 --- /dev/null +++ b/apps/website/lib/formatters/LeagueTierFormatter.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueTierFormatter } from './LeagueTierFormatter'; + +describe('LeagueTierFormatter', () => { + describe('getDisplay', () => { + it('should return correct display data for premium tier', () => { + const result = LeagueTierFormatter.getDisplay('premium'); + expect(result).toEqual({ + color: 'text-yellow-400', + bgColor: 'bg-yellow-500/10', + border: 'border-yellow-500/30', + icon: '⭐', + }); + }); + + it('should return correct display data for standard tier', () => { + const result = LeagueTierFormatter.getDisplay('standard'); + expect(result).toEqual({ + color: 'text-primary-blue', + bgColor: 'bg-primary-blue/10', + border: 'border-primary-blue/30', + icon: '🏆', + }); + }); + + it('should return correct display data for starter tier', () => { + const result = LeagueTierFormatter.getDisplay('starter'); + expect(result).toEqual({ + color: 'text-gray-400', + bgColor: 'bg-gray-500/10', + border: 'border-gray-500/30', + icon: '🚀', + }); + }); + }); +}); diff --git a/apps/website/lib/formatters/LeagueWizardValidationMessages.test.ts b/apps/website/lib/formatters/LeagueWizardValidationMessages.test.ts new file mode 100644 index 000000000..267360757 --- /dev/null +++ b/apps/website/lib/formatters/LeagueWizardValidationMessages.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { LeagueWizardValidationMessages } from './LeagueWizardValidationMessages'; + +describe('LeagueWizardValidationMessages', () => { + it('should have LEAGUE_NAME_REQUIRED message', () => { + expect(LeagueWizardValidationMessages.LEAGUE_NAME_REQUIRED).toBe('League name is required'); + }); + + it('should have LEAGUE_NAME_TOO_SHORT message', () => { + expect(LeagueWizardValidationMessages.LEAGUE_NAME_TOO_SHORT).toBe('League name must be at least 3 characters'); + }); + + it('should have LEAGUE_NAME_TOO_LONG message', () => { + expect(LeagueWizardValidationMessages.LEAGUE_NAME_TOO_LONG).toBe('League name must be less than 100 characters'); + }); + + it('should have DESCRIPTION_TOO_LONG message', () => { + expect(LeagueWizardValidationMessages.DESCRIPTION_TOO_LONG).toBe('Description must be less than 500 characters'); + }); + + it('should have VISIBILITY_REQUIRED message', () => { + expect(LeagueWizardValidationMessages.VISIBILITY_REQUIRED).toBe('Visibility is required'); + }); + + it('should have MAX_DRIVERS_INVALID_SOLO message', () => { + expect(LeagueWizardValidationMessages.MAX_DRIVERS_INVALID_SOLO).toBe('Max drivers must be greater than 0 for solo leagues'); + }); + + it('should have MAX_DRIVERS_TOO_HIGH message', () => { + expect(LeagueWizardValidationMessages.MAX_DRIVERS_TOO_HIGH).toBe('Max drivers cannot exceed 100'); + }); + + it('should have MAX_TEAMS_INVALID_TEAM message', () => { + expect(LeagueWizardValidationMessages.MAX_TEAMS_INVALID_TEAM).toBe('Max teams must be greater than 0 for team leagues'); + }); + + it('should have DRIVERS_PER_TEAM_INVALID message', () => { + expect(LeagueWizardValidationMessages.DRIVERS_PER_TEAM_INVALID).toBe('Drivers per team must be greater than 0'); + }); + + it('should have QUALIFYING_DURATION_INVALID message', () => { + expect(LeagueWizardValidationMessages.QUALIFYING_DURATION_INVALID).toBe('Qualifying duration must be greater than 0 minutes'); + }); + + it('should have MAIN_RACE_DURATION_INVALID message', () => { + expect(LeagueWizardValidationMessages.MAIN_RACE_DURATION_INVALID).toBe('Main race duration must be greater than 0 minutes'); + }); + + it('should have SCORING_PRESET_OR_CUSTOM_REQUIRED message', () => { + expect(LeagueWizardValidationMessages.SCORING_PRESET_OR_CUSTOM_REQUIRED).toBe('Select a scoring preset or enable custom scoring'); + }); +}); diff --git a/apps/website/lib/formatters/MedalFormatter.test.ts b/apps/website/lib/formatters/MedalFormatter.test.ts new file mode 100644 index 000000000..93d3be341 --- /dev/null +++ b/apps/website/lib/formatters/MedalFormatter.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from 'vitest'; +import { MedalFormatter } from './MedalFormatter'; + +describe('MedalFormatter', () => { + describe('getVariant', () => { + it('should return "warning" for 1st place', () => { + expect(MedalFormatter.getVariant(1)).toBe('warning'); + }); + + it('should return "high" for 2nd place', () => { + expect(MedalFormatter.getVariant(2)).toBe('high'); + }); + + it('should return "warning" for 3rd place', () => { + expect(MedalFormatter.getVariant(3)).toBe('warning'); + }); + + it('should return "low" for 4th place', () => { + expect(MedalFormatter.getVariant(4)).toBe('low'); + }); + + it('should return "low" for any position after 3rd', () => { + expect(MedalFormatter.getVariant(5)).toBe('low'); + expect(MedalFormatter.getVariant(10)).toBe('low'); + expect(MedalFormatter.getVariant(100)).toBe('low'); + }); + }); + + describe('getMedalIcon', () => { + it('should return trophy for 1st place', () => { + expect(MedalFormatter.getMedalIcon(1)).toBe('🏆'); + }); + + it('should return trophy for 2nd place', () => { + expect(MedalFormatter.getMedalIcon(2)).toBe('🏆'); + }); + + it('should return trophy for 3rd place', () => { + expect(MedalFormatter.getMedalIcon(3)).toBe('🏆'); + }); + + it('should return null for 4th place', () => { + expect(MedalFormatter.getMedalIcon(4)).toBeNull(); + }); + + it('should return null for any position after 3rd', () => { + expect(MedalFormatter.getMedalIcon(5)).toBeNull(); + expect(MedalFormatter.getMedalIcon(10)).toBeNull(); + expect(MedalFormatter.getMedalIcon(100)).toBeNull(); + }); + }); + + describe('getBg', () => { + it('should return bg-warning-amber for 1st place', () => { + expect(MedalFormatter.getBg(1)).toBe('bg-warning-amber'); + }); + + it('should return bg-gray-300 for 2nd place', () => { + expect(MedalFormatter.getBg(2)).toBe('bg-gray-300'); + }); + + it('should return bg-orange-700 for 3rd place', () => { + expect(MedalFormatter.getBg(3)).toBe('bg-orange-700'); + }); + + it('should return bg-gray-800 for 4th place', () => { + expect(MedalFormatter.getBg(4)).toBe('bg-gray-800'); + }); + + it('should return bg-gray-800 for any position after 3rd', () => { + expect(MedalFormatter.getBg(5)).toBe('bg-gray-800'); + expect(MedalFormatter.getBg(10)).toBe('bg-gray-800'); + expect(MedalFormatter.getBg(100)).toBe('bg-gray-800'); + }); + }); + + describe('getColor', () => { + it('should return text-warning-amber for 1st place', () => { + expect(MedalFormatter.getColor(1)).toBe('text-warning-amber'); + }); + + it('should return text-gray-300 for 2nd place', () => { + expect(MedalFormatter.getColor(2)).toBe('text-gray-300'); + }); + + it('should return text-orange-700 for 3rd place', () => { + expect(MedalFormatter.getColor(3)).toBe('text-orange-700'); + }); + + it('should return text-gray-400 for 4th place', () => { + expect(MedalFormatter.getColor(4)).toBe('text-gray-400'); + }); + + it('should return text-gray-400 for any position after 3rd', () => { + expect(MedalFormatter.getColor(5)).toBe('text-gray-400'); + expect(MedalFormatter.getColor(10)).toBe('text-gray-400'); + expect(MedalFormatter.getColor(100)).toBe('text-gray-400'); + }); + }); +}); diff --git a/apps/website/lib/formatters/MemberFormatter.test.ts b/apps/website/lib/formatters/MemberFormatter.test.ts new file mode 100644 index 000000000..4fa779550 --- /dev/null +++ b/apps/website/lib/formatters/MemberFormatter.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { MemberFormatter } from './MemberFormatter'; + +describe('MemberFormatter', () => { + describe('formatCount', () => { + it('should format 1 member correctly', () => { + expect(MemberFormatter.formatCount(1)).toBe('1 member'); + }); + + it('should format 2 members correctly', () => { + expect(MemberFormatter.formatCount(2)).toBe('2 members'); + }); + + it('should format 0 members correctly', () => { + expect(MemberFormatter.formatCount(0)).toBe('0 members'); + }); + + it('should format large numbers correctly', () => { + expect(MemberFormatter.formatCount(100)).toBe('100 members'); + }); + + it('should handle negative numbers', () => { + expect(MemberFormatter.formatCount(-1)).toBe('-1 members'); + }); + }); + + describe('formatUnits', () => { + it('should format 1 unit correctly', () => { + expect(MemberFormatter.formatUnits(1)).toBe('1 Unit'); + }); + + it('should format 2 units correctly', () => { + expect(MemberFormatter.formatUnits(2)).toBe('2 Units'); + }); + + it('should format 0 units correctly', () => { + expect(MemberFormatter.formatUnits(0)).toBe('0 Units'); + }); + + it('should format large numbers correctly', () => { + expect(MemberFormatter.formatUnits(100)).toBe('100 Units'); + }); + + it('should handle negative numbers', () => { + expect(MemberFormatter.formatUnits(-1)).toBe('-1 Units'); + }); + }); +}); diff --git a/apps/website/lib/formatters/MembershipFeeTypeFormatter.test.ts b/apps/website/lib/formatters/MembershipFeeTypeFormatter.test.ts new file mode 100644 index 000000000..3dc403df4 --- /dev/null +++ b/apps/website/lib/formatters/MembershipFeeTypeFormatter.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { MembershipFeeTypeFormatter } from './MembershipFeeTypeFormatter'; + +describe('MembershipFeeTypeFormatter', () => { + describe('format', () => { + it('should format "monthly" correctly', () => { + expect(MembershipFeeTypeFormatter.format('monthly')).toBe('Monthly'); + }); + + it('should format "yearly" correctly', () => { + expect(MembershipFeeTypeFormatter.format('yearly')).toBe('Yearly'); + }); + + it('should format "one_time" correctly', () => { + expect(MembershipFeeTypeFormatter.format('one_time')).toBe('One time'); + }); + + it('should handle unknown types', () => { + expect(MembershipFeeTypeFormatter.format('unknown')).toBe('Unknown'); + }); + + it('should handle empty string', () => { + expect(MembershipFeeTypeFormatter.format('')).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/formatters/MemoryFormatter.test.ts b/apps/website/lib/formatters/MemoryFormatter.test.ts new file mode 100644 index 000000000..f26b19975 --- /dev/null +++ b/apps/website/lib/formatters/MemoryFormatter.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { MemoryFormatter } from './MemoryFormatter'; + +describe('MemoryFormatter', () => { + describe('formatMB', () => { + it('should format bytes as MB with 1 decimal place', () => { + expect(MemoryFormatter.formatMB(1048576)).toBe('1.0MB'); + expect(MemoryFormatter.formatMB(10485760)).toBe('10.0MB'); + expect(MemoryFormatter.formatMB(104857600)).toBe('100.0MB'); + }); + + it('should handle zero bytes', () => { + expect(MemoryFormatter.formatMB(0)).toBe('0.0MB'); + }); + + it('should handle small values', () => { + expect(MemoryFormatter.formatMB(1024)).toBe('0.0MB'); + expect(MemoryFormatter.formatMB(524288)).toBe('0.5MB'); + }); + + it('should handle large values', () => { + expect(MemoryFormatter.formatMB(1073741824)).toBe('1024.0MB'); + }); + }); + + describe('formatKB', () => { + it('should format bytes as KB with 1 decimal place', () => { + expect(MemoryFormatter.formatKB(1024)).toBe('1.0KB'); + expect(MemoryFormatter.formatKB(10240)).toBe('10.0KB'); + expect(MemoryFormatter.formatKB(102400)).toBe('100.0KB'); + }); + + it('should handle zero bytes', () => { + expect(MemoryFormatter.formatKB(0)).toBe('0.0KB'); + }); + + it('should handle small values', () => { + expect(MemoryFormatter.formatKB(1)).toBe('0.0KB'); + expect(MemoryFormatter.formatKB(512)).toBe('0.5KB'); + }); + + it('should handle large values', () => { + expect(MemoryFormatter.formatKB(1048576)).toBe('1024.0KB'); + }); + }); +}); diff --git a/apps/website/lib/formatters/NumberFormatter.test.ts b/apps/website/lib/formatters/NumberFormatter.test.ts new file mode 100644 index 000000000..ab56798e1 --- /dev/null +++ b/apps/website/lib/formatters/NumberFormatter.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import { NumberFormatter } from './NumberFormatter'; + +describe('NumberFormatter', () => { + describe('format', () => { + it('should format number with thousands separators', () => { + expect(NumberFormatter.format(1234567)).toBe('1,234,567'); + expect(NumberFormatter.format(1000000)).toBe('1,000,000'); + }); + + it('should handle numbers without thousands separators', () => { + expect(NumberFormatter.format(123)).toBe('123'); + expect(NumberFormatter.format(999)).toBe('999'); + }); + + it('should handle decimal numbers', () => { + expect(NumberFormatter.format(1234.56)).toBe('1,234.56'); + expect(NumberFormatter.format(1234567.89)).toBe('1,234,567.89'); + }); + + it('should handle zero', () => { + expect(NumberFormatter.format(0)).toBe('0'); + }); + + it('should handle negative numbers', () => { + expect(NumberFormatter.format(-1234567)).toBe('-1,234,567'); + expect(NumberFormatter.format(-1234.56)).toBe('-1,234.56'); + }); + + it('should handle large numbers', () => { + expect(NumberFormatter.format(1234567890)).toBe('1,234,567,890'); + }); + }); + + describe('formatCompact', () => { + it('should format numbers under 1000 as is', () => { + expect(NumberFormatter.formatCompact(123)).toBe('123'); + expect(NumberFormatter.formatCompact(999)).toBe('999'); + }); + + it('should format numbers 1000-999999 with k suffix', () => { + expect(NumberFormatter.formatCompact(1000)).toBe('1.0k'); + expect(NumberFormatter.formatCompact(1234)).toBe('1.2k'); + expect(NumberFormatter.formatCompact(999999)).toBe('1000.0k'); + }); + + it('should format numbers 1000000 and above with M suffix', () => { + expect(NumberFormatter.formatCompact(1000000)).toBe('1.0M'); + expect(NumberFormatter.formatCompact(1234567)).toBe('1.2M'); + expect(NumberFormatter.formatCompact(999999999)).toBe('1000.0M'); + }); + + it('should handle zero', () => { + expect(NumberFormatter.formatCompact(0)).toBe('0'); + }); + + it('should handle negative numbers', () => { + expect(NumberFormatter.formatCompact(-1234)).toBe('-1.2k'); + expect(NumberFormatter.formatCompact(-1234567)).toBe('-1.2M'); + }); + }); + + describe('formatCurrency', () => { + it('should format number with currency symbol', () => { + expect(NumberFormatter.formatCurrency(1234567, 'USD')).toBe('USD 1,234,567'); + expect(NumberFormatter.formatCurrency(1234.56, 'EUR')).toBe('EUR 1,234.56'); + }); + + it('should handle zero', () => { + expect(NumberFormatter.formatCurrency(0, 'USD')).toBe('USD 0'); + }); + + it('should handle negative numbers', () => { + expect(NumberFormatter.formatCurrency(-1234567, 'USD')).toBe('USD -1,234,567'); + }); + + it('should handle different currencies', () => { + expect(NumberFormatter.formatCurrency(1234567, 'GBP')).toBe('GBP 1,234,567'); + expect(NumberFormatter.formatCurrency(1234567, 'JPY')).toBe('JPY 1,234,567'); + }); + }); +}); diff --git a/apps/website/lib/formatters/OnboardingStatusFormatter.test.ts b/apps/website/lib/formatters/OnboardingStatusFormatter.test.ts new file mode 100644 index 000000000..d81700e9a --- /dev/null +++ b/apps/website/lib/formatters/OnboardingStatusFormatter.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { OnboardingStatusFormatter } from './OnboardingStatusFormatter'; + +describe('OnboardingStatusFormatter', () => { + describe('statusLabel', () => { + it('should return "Onboarding Complete" when success is true', () => { + expect(OnboardingStatusFormatter.statusLabel(true)).toBe('Onboarding Complete'); + }); + + it('should return "Onboarding Failed" when success is false', () => { + expect(OnboardingStatusFormatter.statusLabel(false)).toBe('Onboarding Failed'); + }); + }); + + describe('statusVariant', () => { + it('should return "performance-green" when success is true', () => { + expect(OnboardingStatusFormatter.statusVariant(true)).toBe('performance-green'); + }); + + it('should return "racing-red" when success is false', () => { + expect(OnboardingStatusFormatter.statusVariant(false)).toBe('racing-red'); + }); + }); + + describe('statusIcon', () => { + it('should return "✅" when success is true', () => { + expect(OnboardingStatusFormatter.statusIcon(true)).toBe('✅'); + }); + + it('should return "❌" when success is false', () => { + expect(OnboardingStatusFormatter.statusIcon(false)).toBe('❌'); + }); + }); + + describe('statusMessage', () => { + it('should return success message when success is true', () => { + expect(OnboardingStatusFormatter.statusMessage(true)).toBe('Your onboarding has been completed successfully.'); + }); + + it('should return default failure message when success is false and no error message', () => { + expect(OnboardingStatusFormatter.statusMessage(false)).toBe('Failed to complete onboarding. Please try again.'); + }); + + it('should return custom error message when success is false and error message provided', () => { + expect(OnboardingStatusFormatter.statusMessage(false, 'Custom error')).toBe('Custom error'); + }); + }); +}); diff --git a/apps/website/lib/formatters/PayerTypeFormatter.test.ts b/apps/website/lib/formatters/PayerTypeFormatter.test.ts new file mode 100644 index 000000000..328b7d6ef --- /dev/null +++ b/apps/website/lib/formatters/PayerTypeFormatter.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { PayerTypeFormatter } from './PayerTypeFormatter'; + +describe('PayerTypeFormatter', () => { + describe('format', () => { + it('should capitalize the first letter and lowercase the rest', () => { + expect(PayerTypeFormatter.format('individual')).toBe('Individual'); + expect(PayerTypeFormatter.format('organization')).toBe('Organization'); + expect(PayerTypeFormatter.format('company')).toBe('Company'); + }); + + it('should handle single character strings', () => { + expect(PayerTypeFormatter.format('a')).toBe('A'); + }); + + it('should handle already capitalized strings', () => { + expect(PayerTypeFormatter.format('Individual')).toBe('Individual'); + }); + + it('should handle all uppercase strings', () => { + expect(PayerTypeFormatter.format('INDIVIDUAL')).toBe('Individual'); + }); + + it('should handle empty string', () => { + expect(PayerTypeFormatter.format('')).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/formatters/PaymentTypeFormatter.test.ts b/apps/website/lib/formatters/PaymentTypeFormatter.test.ts new file mode 100644 index 000000000..ebd67f996 --- /dev/null +++ b/apps/website/lib/formatters/PaymentTypeFormatter.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { PaymentTypeFormatter } from './PaymentTypeFormatter'; + +describe('PaymentTypeFormatter', () => { + describe('format', () => { + it('should replace underscores with spaces and capitalize words', () => { + expect(PaymentTypeFormatter.format('credit_card')).toBe('Credit Card'); + expect(PaymentTypeFormatter.format('bank_transfer')).toBe('Bank Transfer'); + expect(PaymentTypeFormatter.format('paypal')).toBe('Paypal'); + }); + + it('should handle strings without underscores', () => { + expect(PaymentTypeFormatter.format('creditcard')).toBe('Creditcard'); + expect(PaymentTypeFormatter.format('banktransfer')).toBe('Banktransfer'); + }); + + it('should handle single word strings', () => { + expect(PaymentTypeFormatter.format('cash')).toBe('Cash'); + expect(PaymentTypeFormatter.format('check')).toBe('Check'); + }); + + it('should handle empty string', () => { + expect(PaymentTypeFormatter.format('')).toBe(''); + }); + + it('should handle multiple underscores', () => { + expect(PaymentTypeFormatter.format('credit_card_payment')).toBe('Credit Card Payment'); + }); + }); +}); diff --git a/apps/website/lib/formatters/PercentFormatter.test.ts b/apps/website/lib/formatters/PercentFormatter.test.ts new file mode 100644 index 000000000..61e04257d --- /dev/null +++ b/apps/website/lib/formatters/PercentFormatter.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { PercentFormatter } from './PercentFormatter'; + +describe('PercentFormatter', () => { + describe('format', () => { + it('should format decimal value as percentage with 1 decimal place', () => { + expect(PercentFormatter.format(0.1234)).toBe('12.3%'); + expect(PercentFormatter.format(0.5)).toBe('50.0%'); + expect(PercentFormatter.format(1.0)).toBe('100.0%'); + }); + + it('should handle zero', () => { + expect(PercentFormatter.format(0)).toBe('0.0%'); + }); + + it('should handle null', () => { + expect(PercentFormatter.format(null)).toBe('0.0%'); + }); + + it('should handle undefined', () => { + expect(PercentFormatter.format(undefined)).toBe('0.0%'); + }); + + it('should handle negative values', () => { + expect(PercentFormatter.format(-0.1234)).toBe('-12.3%'); + }); + + it('should handle values greater than 1', () => { + expect(PercentFormatter.format(1.5)).toBe('150.0%'); + expect(PercentFormatter.format(2.0)).toBe('200.0%'); + }); + }); + + describe('formatWhole', () => { + it('should format whole number as percentage', () => { + expect(PercentFormatter.formatWhole(12)).toBe('12%'); + expect(PercentFormatter.formatWhole(50)).toBe('50%'); + expect(PercentFormatter.formatWhole(100)).toBe('100%'); + }); + + it('should handle zero', () => { + expect(PercentFormatter.formatWhole(0)).toBe('0%'); + }); + + it('should handle null', () => { + expect(PercentFormatter.formatWhole(null)).toBe('0%'); + }); + + it('should handle undefined', () => { + expect(PercentFormatter.formatWhole(undefined)).toBe('0%'); + }); + + it('should round decimal values', () => { + expect(PercentFormatter.formatWhole(12.3)).toBe('12%'); + expect(PercentFormatter.formatWhole(12.7)).toBe('13%'); + }); + + it('should handle negative values', () => { + expect(PercentFormatter.formatWhole(-12)).toBe('-12%'); + }); + }); +}); diff --git a/apps/website/lib/formatters/PrizeTypeFormatter.test.ts b/apps/website/lib/formatters/PrizeTypeFormatter.test.ts new file mode 100644 index 000000000..c20e0105a --- /dev/null +++ b/apps/website/lib/formatters/PrizeTypeFormatter.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { PrizeTypeFormatter } from './PrizeTypeFormatter'; + +describe('PrizeTypeFormatter', () => { + describe('format', () => { + it('should format "cash" as "Cash Prize"', () => { + expect(PrizeTypeFormatter.format('cash')).toBe('Cash Prize'); + }); + + it('should format "merchandise" as "Merchandise"', () => { + expect(PrizeTypeFormatter.format('merchandise')).toBe('Merchandise'); + }); + + it('should format "other" as "Other"', () => { + expect(PrizeTypeFormatter.format('other')).toBe('Other'); + }); + + it('should handle unknown types', () => { + expect(PrizeTypeFormatter.format('unknown')).toBe('unknown'); + }); + + it('should handle empty string', () => { + expect(PrizeTypeFormatter.format('')).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/formatters/ProfileFormatter.test.ts b/apps/website/lib/formatters/ProfileFormatter.test.ts new file mode 100644 index 000000000..e5fe0888b --- /dev/null +++ b/apps/website/lib/formatters/ProfileFormatter.test.ts @@ -0,0 +1,359 @@ +import { describe, it, expect } from 'vitest'; +import { ProfileFormatter } from './ProfileFormatter'; + +describe('ProfileFormatter', () => { + describe('getCountryFlag', () => { + it('should return correct flag for US', () => { + const result = ProfileFormatter.getCountryFlag('US'); + expect(result).toEqual({ + flag: '🇺🇸', + label: 'United States', + }); + }); + + it('should return correct flag for GB', () => { + const result = ProfileFormatter.getCountryFlag('GB'); + expect(result).toEqual({ + flag: '🇬🇧', + label: 'United Kingdom', + }); + }); + + it('should return correct flag for DE', () => { + const result = ProfileFormatter.getCountryFlag('DE'); + expect(result).toEqual({ + flag: '🇩🇪', + label: 'Germany', + }); + }); + + it('should handle lowercase country codes', () => { + const result = ProfileFormatter.getCountryFlag('us'); + expect(result).toEqual({ + flag: '🇺🇸', + label: 'United States', + }); + }); + + it('should return default flag for unknown country code', () => { + const result = ProfileFormatter.getCountryFlag('XX'); + expect(result).toEqual({ + flag: '🏁', + label: 'Unknown', + }); + }); + }); + + describe('getAchievementRarity', () => { + it('should return correct display data for common rarity', () => { + const result = ProfileFormatter.getAchievementRarity('common'); + expect(result).toEqual({ + text: 'Common', + badgeClasses: 'bg-gray-400/10 text-gray-400', + borderClasses: 'border-gray-400/30', + }); + }); + + it('should return correct display data for rare rarity', () => { + const result = ProfileFormatter.getAchievementRarity('rare'); + expect(result).toEqual({ + text: 'Rare', + badgeClasses: 'bg-primary-blue/10 text-primary-blue', + borderClasses: 'border-primary-blue/30', + }); + }); + + it('should return correct display data for epic rarity', () => { + const result = ProfileFormatter.getAchievementRarity('epic'); + expect(result).toEqual({ + text: 'Epic', + badgeClasses: 'bg-purple-400/10 text-purple-400', + borderClasses: 'border-purple-400/30', + }); + }); + + it('should return correct display data for legendary rarity', () => { + const result = ProfileFormatter.getAchievementRarity('legendary'); + expect(result).toEqual({ + text: 'Legendary', + badgeClasses: 'bg-yellow-400/10 text-yellow-400', + borderClasses: 'border-yellow-400/30', + }); + }); + + it('should default to common for unknown rarity', () => { + const result = ProfileFormatter.getAchievementRarity('unknown'); + expect(result).toEqual({ + text: 'Common', + badgeClasses: 'bg-gray-400/10 text-gray-400', + borderClasses: 'border-gray-400/30', + }); + }); + }); + + describe('getAchievementIcon', () => { + it('should return correct display data for trophy icon', () => { + const result = ProfileFormatter.getAchievementIcon('trophy'); + expect(result).toEqual({ + name: 'Trophy', + }); + }); + + it('should return correct display data for medal icon', () => { + const result = ProfileFormatter.getAchievementIcon('medal'); + expect(result).toEqual({ + name: 'Medal', + }); + }); + + it('should return correct display data for star icon', () => { + const result = ProfileFormatter.getAchievementIcon('star'); + expect(result).toEqual({ + name: 'Star', + }); + }); + + it('should return correct display data for crown icon', () => { + const result = ProfileFormatter.getAchievementIcon('crown'); + expect(result).toEqual({ + name: 'Crown', + }); + }); + + it('should return correct display data for target icon', () => { + const result = ProfileFormatter.getAchievementIcon('target'); + expect(result).toEqual({ + name: 'Target', + }); + }); + + it('should return correct display data for zap icon', () => { + const result = ProfileFormatter.getAchievementIcon('zap'); + expect(result).toEqual({ + name: 'Zap', + }); + }); + + it('should default to trophy for unknown icon', () => { + const result = ProfileFormatter.getAchievementIcon('unknown'); + expect(result).toEqual({ + name: 'Trophy', + }); + }); + }); + + describe('getSocialPlatform', () => { + it('should return correct display data for twitter', () => { + const result = ProfileFormatter.getSocialPlatform('twitter'); + expect(result).toEqual({ + name: 'Twitter', + hoverClasses: 'hover:text-sky-400 hover:bg-sky-400/10', + }); + }); + + it('should return correct display data for youtube', () => { + const result = ProfileFormatter.getSocialPlatform('youtube'); + expect(result).toEqual({ + name: 'YouTube', + hoverClasses: 'hover:text-red-500 hover:bg-red-500/10', + }); + }); + + it('should return correct display data for twitch', () => { + const result = ProfileFormatter.getSocialPlatform('twitch'); + expect(result).toEqual({ + name: 'Twitch', + hoverClasses: 'hover:text-purple-400 hover:bg-purple-400/10', + }); + }); + + it('should return correct display data for discord', () => { + const result = ProfileFormatter.getSocialPlatform('discord'); + expect(result).toEqual({ + name: 'Discord', + hoverClasses: 'hover:text-indigo-400 hover:bg-indigo-400/10', + }); + }); + + it('should default to discord for unknown platform', () => { + const result = ProfileFormatter.getSocialPlatform('unknown'); + expect(result).toEqual({ + name: 'Discord', + hoverClasses: 'hover:text-indigo-400 hover:bg-indigo-400/10', + }); + }); + }); + + describe('formatMonthYear', () => { + it('should format date as "Jan 2026"', () => { + expect(ProfileFormatter.formatMonthYear('2026-01-15')).toBe('Jan 2026'); + }); + + it('should format different months correctly', () => { + expect(ProfileFormatter.formatMonthYear('2026-02-15')).toBe('Feb 2026'); + expect(ProfileFormatter.formatMonthYear('2026-12-25')).toBe('Dec 2026'); + }); + }); + + describe('formatMonthDayYear', () => { + it('should format date as "Jan 15, 2026"', () => { + expect(ProfileFormatter.formatMonthDayYear('2026-01-15')).toBe('Jan 15, 2026'); + }); + + it('should format different dates correctly', () => { + expect(ProfileFormatter.formatMonthDayYear('2026-02-15')).toBe('Feb 15, 2026'); + expect(ProfileFormatter.formatMonthDayYear('2026-12-25')).toBe('Dec 25, 2026'); + }); + }); + + describe('formatPercentage', () => { + it('should format decimal value as percentage with 1 decimal place', () => { + expect(ProfileFormatter.formatPercentage(0.1234)).toBe('12.3%'); + expect(ProfileFormatter.formatPercentage(0.5)).toBe('50.0%'); + expect(ProfileFormatter.formatPercentage(1.0)).toBe('100.0%'); + }); + + it('should handle zero', () => { + expect(ProfileFormatter.formatPercentage(0)).toBe('0.0%'); + }); + + it('should handle null', () => { + expect(ProfileFormatter.formatPercentage(null)).toBe('0.0%'); + }); + + it('should handle undefined', () => { + expect(ProfileFormatter.formatPercentage(undefined)).toBe('0.0%'); + }); + }); + + describe('formatFinishPosition', () => { + it('should format position as "P1"', () => { + expect(ProfileFormatter.formatFinishPosition(1)).toBe('P1'); + }); + + it('should format position as "P10"', () => { + expect(ProfileFormatter.formatFinishPosition(10)).toBe('P10'); + }); + + it('should handle null value', () => { + expect(ProfileFormatter.formatFinishPosition(null)).toBe('P-'); + }); + + it('should handle undefined value', () => { + expect(ProfileFormatter.formatFinishPosition(undefined)).toBe('P-'); + }); + }); + + describe('formatAvgFinish', () => { + it('should format average as "P5.4"', () => { + expect(ProfileFormatter.formatAvgFinish(5.4)).toBe('P5.4'); + }); + + it('should format average as "P10.0"', () => { + expect(ProfileFormatter.formatAvgFinish(10.0)).toBe('P10.0'); + }); + + it('should handle null value', () => { + expect(ProfileFormatter.formatAvgFinish(null)).toBe('P-'); + }); + + it('should handle undefined value', () => { + expect(ProfileFormatter.formatAvgFinish(undefined)).toBe('P-'); + }); + }); + + describe('formatRating', () => { + it('should format rating as rounded number', () => { + expect(ProfileFormatter.formatRating(1234.56)).toBe('1235'); + expect(ProfileFormatter.formatRating(1234.4)).toBe('1234'); + }); + + it('should handle null value', () => { + expect(ProfileFormatter.formatRating(null)).toBe('0'); + }); + + it('should handle undefined value', () => { + expect(ProfileFormatter.formatRating(undefined)).toBe('0'); + }); + }); + + describe('formatConsistency', () => { + it('should format consistency as percentage', () => { + expect(ProfileFormatter.formatConsistency(85)).toBe('85%'); + expect(ProfileFormatter.formatConsistency(99.5)).toBe('100%'); + }); + + it('should handle null value', () => { + expect(ProfileFormatter.formatConsistency(null)).toBe('0%'); + }); + + it('should handle undefined value', () => { + expect(ProfileFormatter.formatConsistency(undefined)).toBe('0%'); + }); + }); + + describe('formatPercentile', () => { + it('should format percentile as "Top X%"', () => { + expect(ProfileFormatter.formatPercentile(5)).toBe('Top 5%'); + expect(ProfileFormatter.formatPercentile(10)).toBe('Top 10%'); + }); + + it('should handle null value', () => { + expect(ProfileFormatter.formatPercentile(null)).toBe('Top -%'); + }); + + it('should handle undefined value', () => { + expect(ProfileFormatter.formatPercentile(undefined)).toBe('Top -%'); + }); + }); + + describe('getTeamRole', () => { + it('should return correct display data for owner role', () => { + const result = ProfileFormatter.getTeamRole('owner'); + expect(result).toEqual({ + text: 'Owner', + badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30', + }); + }); + + it('should return correct display data for manager role', () => { + const result = ProfileFormatter.getTeamRole('manager'); + expect(result).toEqual({ + text: 'Manager', + badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30', + }); + }); + + it('should return correct display data for admin role', () => { + const result = ProfileFormatter.getTeamRole('admin'); + expect(result).toEqual({ + text: 'Admin', + badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30', + }); + }); + + it('should return correct display data for steward role', () => { + const result = ProfileFormatter.getTeamRole('steward'); + expect(result).toEqual({ + text: 'Steward', + badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30', + }); + }); + + it('should return correct display data for member role', () => { + const result = ProfileFormatter.getTeamRole('member'); + expect(result).toEqual({ + text: 'Member', + badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30', + }); + }); + + it('should default to member for unknown role', () => { + const result = ProfileFormatter.getTeamRole('unknown'); + expect(result).toEqual({ + text: 'Member', + badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30', + }); + }); + }); +}); diff --git a/apps/website/lib/formatters/RaceStatusFormatter.test.ts b/apps/website/lib/formatters/RaceStatusFormatter.test.ts new file mode 100644 index 000000000..dcfc839b2 --- /dev/null +++ b/apps/website/lib/formatters/RaceStatusFormatter.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { RaceStatusFormatter } from './RaceStatusFormatter'; + +describe('RaceStatusFormatter', () => { + describe('getLabel', () => { + it('should return "Scheduled" for scheduled status', () => { + expect(RaceStatusFormatter.getLabel('scheduled')).toBe('Scheduled'); + }); + + it('should return "LIVE" for running status', () => { + expect(RaceStatusFormatter.getLabel('running')).toBe('LIVE'); + }); + + it('should return "Completed" for completed status', () => { + expect(RaceStatusFormatter.getLabel('completed')).toBe('Completed'); + }); + + it('should return "Cancelled" for cancelled status', () => { + expect(RaceStatusFormatter.getLabel('cancelled')).toBe('Cancelled'); + }); + + it('should return uppercase status for unknown status', () => { + expect(RaceStatusFormatter.getLabel('unknown')).toBe('UNKNOWN'); + }); + }); + + describe('getVariant', () => { + it('should return "primary" for scheduled status', () => { + expect(RaceStatusFormatter.getVariant('scheduled')).toBe('primary'); + }); + + it('should return "success" for running status', () => { + expect(RaceStatusFormatter.getVariant('running')).toBe('success'); + }); + + it('should return "default" for completed status', () => { + expect(RaceStatusFormatter.getVariant('completed')).toBe('default'); + }); + + it('should return "warning" for cancelled status', () => { + expect(RaceStatusFormatter.getVariant('cancelled')).toBe('warning'); + }); + + it('should return "neutral" for unknown status', () => { + expect(RaceStatusFormatter.getVariant('unknown')).toBe('neutral'); + }); + }); + + describe('getIconName', () => { + it('should return "Clock" for scheduled status', () => { + expect(RaceStatusFormatter.getIconName('scheduled')).toBe('Clock'); + }); + + it('should return "PlayCircle" for running status', () => { + expect(RaceStatusFormatter.getIconName('running')).toBe('PlayCircle'); + }); + + it('should return "CheckCircle2" for completed status', () => { + expect(RaceStatusFormatter.getIconName('completed')).toBe('CheckCircle2'); + }); + + it('should return "XCircle" for cancelled status', () => { + expect(RaceStatusFormatter.getIconName('cancelled')).toBe('XCircle'); + }); + + it('should return "HelpCircle" for unknown status', () => { + expect(RaceStatusFormatter.getIconName('unknown')).toBe('HelpCircle'); + }); + }); +}); diff --git a/apps/website/lib/formatters/RatingTrendFormatter.test.ts b/apps/website/lib/formatters/RatingTrendFormatter.test.ts new file mode 100644 index 000000000..c9189ea3f --- /dev/null +++ b/apps/website/lib/formatters/RatingTrendFormatter.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { RatingTrendFormatter } from './RatingTrendFormatter'; + +describe('RatingTrendFormatter', () => { + describe('getTrend', () => { + it('should return "up" when current rating is higher than previous', () => { + expect(RatingTrendFormatter.getTrend(1200, 1100)).toBe('up'); + expect(RatingTrendFormatter.getTrend(1500, 1400)).toBe('up'); + }); + + it('should return "down" when current rating is lower than previous', () => { + expect(RatingTrendFormatter.getTrend(1100, 1200)).toBe('down'); + expect(RatingTrendFormatter.getTrend(1400, 1500)).toBe('down'); + }); + + it('should return "same" when current rating equals previous', () => { + expect(RatingTrendFormatter.getTrend(1200, 1200)).toBe('same'); + }); + + it('should return "same" when previous rating is undefined', () => { + expect(RatingTrendFormatter.getTrend(1200, undefined)).toBe('same'); + }); + + it('should return "same" when previous rating is null', () => { + expect(RatingTrendFormatter.getTrend(1200, null)).toBe('same'); + }); + }); + + describe('getChangeIndicator', () => { + it('should return "+X" when current rating is higher than previous', () => { + expect(RatingTrendFormatter.getChangeIndicator(1200, 1100)).toBe('+100'); + expect(RatingTrendFormatter.getChangeIndicator(1500, 1400)).toBe('+100'); + }); + + it('should return "-X" when current rating is lower than previous', () => { + expect(RatingTrendFormatter.getChangeIndicator(1100, 1200)).toBe('-100'); + expect(RatingTrendFormatter.getChangeIndicator(1400, 1500)).toBe('-100'); + }); + + it('should return "0" when current rating equals previous', () => { + expect(RatingTrendFormatter.getChangeIndicator(1200, 1200)).toBe('0'); + }); + + it('should return "0" when previous rating is undefined', () => { + expect(RatingTrendFormatter.getChangeIndicator(1200, undefined)).toBe('0'); + }); + + it('should return "0" when previous rating is null', () => { + expect(RatingTrendFormatter.getChangeIndicator(1200, null)).toBe('0'); + }); + + it('should handle small changes', () => { + expect(RatingTrendFormatter.getChangeIndicator(1201, 1200)).toBe('+1'); + expect(RatingTrendFormatter.getChangeIndicator(1199, 1200)).toBe('-1'); + }); + + it('should handle large changes', () => { + expect(RatingTrendFormatter.getChangeIndicator(2000, 1000)).toBe('+1000'); + expect(RatingTrendFormatter.getChangeIndicator(1000, 2000)).toBe('-1000'); + }); + }); +}); diff --git a/apps/website/lib/formatters/RelativeTimeFormatter.test.ts b/apps/website/lib/formatters/RelativeTimeFormatter.test.ts new file mode 100644 index 000000000..63796ac17 --- /dev/null +++ b/apps/website/lib/formatters/RelativeTimeFormatter.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { RelativeTimeFormatter } from './RelativeTimeFormatter'; + +describe('RelativeTimeFormatter', () => { + describe('format', () => { + it('should return "Just now" for less than 1 hour ago', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = RelativeTimeFormatter.format(thirtyMinutesAgo, now); + expect(result).toBe('Just now'); + }); + + it('should return "Xh ago" for less than 24 hours ago', () => { + const now = new Date(); + const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(fiveHoursAgo, now); + expect(result).toBe('5h ago'); + }); + + it('should return "Yesterday" for exactly 1 day ago', () => { + const now = new Date(); + const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(oneDayAgo, now); + expect(result).toBe('Yesterday'); + }); + + it('should return "Xd ago" for less than 7 days ago', () => { + const now = new Date(); + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(threeDaysAgo, now); + expect(result).toBe('3d ago'); + }); + + it('should return absolute date for more than 7 days ago', () => { + const now = new Date('2026-01-24T23:49:00Z'); + const tenDaysAgo = new Date('2026-01-14T23:49:00Z'); + const result = RelativeTimeFormatter.format(tenDaysAgo, now); + expect(result).toBe('Jan 14'); + }); + + it('should return "Starting soon" for less than 1 hour in the future', () => { + const now = new Date(); + const thirtyMinutesFromNow = new Date(now.getTime() + 30 * 60 * 1000); + const result = RelativeTimeFormatter.format(thirtyMinutesFromNow, now); + expect(result).toBe('Starting soon'); + }); + + it('should return "In Xh" for less than 24 hours in the future', () => { + const now = new Date(); + const fiveHoursFromNow = new Date(now.getTime() + 5 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(fiveHoursFromNow, now); + expect(result).toBe('In 5h'); + }); + + it('should return "Tomorrow" for exactly 1 day in the future', () => { + const now = new Date(); + const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(oneDayFromNow, now); + expect(result).toBe('Tomorrow'); + }); + + it('should return "In X days" for less than 7 days in the future', () => { + const now = new Date(); + const threeDaysFromNow = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); + const result = RelativeTimeFormatter.format(threeDaysFromNow, now); + expect(result).toBe('In 3 days'); + }); + + it('should return absolute date for more than 7 days in the future', () => { + const now = new Date('2026-01-24T23:49:00Z'); + const tenDaysFromNow = new Date('2026-02-03T23:49:00Z'); + const result = RelativeTimeFormatter.format(tenDaysFromNow, now); + expect(result).toBe('Feb 3'); + }); + + it('should handle string input', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = RelativeTimeFormatter.format(thirtyMinutesAgo.toISOString(), now); + expect(result).toBe('Just now'); + }); + }); +}); diff --git a/apps/website/lib/formatters/SeasonStatusFormatter.test.ts b/apps/website/lib/formatters/SeasonStatusFormatter.test.ts new file mode 100644 index 000000000..684a1b8cd --- /dev/null +++ b/apps/website/lib/formatters/SeasonStatusFormatter.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { SeasonStatusFormatter } from './SeasonStatusFormatter'; + +describe('SeasonStatusFormatter', () => { + describe('getDisplay', () => { + it('should return correct display data for active status', () => { + const result = SeasonStatusFormatter.getDisplay('active'); + expect(result).toEqual({ + color: 'text-performance-green', + bg: 'bg-performance-green/10', + label: 'Active Season', + }); + }); + + it('should return correct display data for upcoming status', () => { + const result = SeasonStatusFormatter.getDisplay('upcoming'); + expect(result).toEqual({ + color: 'text-warning-amber', + bg: 'bg-warning-amber/10', + label: 'Starting Soon', + }); + }); + + it('should return correct display data for completed status', () => { + const result = SeasonStatusFormatter.getDisplay('completed'); + expect(result).toEqual({ + color: 'text-gray-400', + bg: 'bg-gray-400/10', + label: 'Season Ended', + }); + }); + }); +}); diff --git a/apps/website/lib/formatters/SkillLevelFormatter.test.ts b/apps/website/lib/formatters/SkillLevelFormatter.test.ts new file mode 100644 index 000000000..cd81fefbc --- /dev/null +++ b/apps/website/lib/formatters/SkillLevelFormatter.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest'; +import { SkillLevelFormatter } from './SkillLevelFormatter'; + +describe('SkillLevelFormatter', () => { + describe('getLabel', () => { + it('should return "Pro" for pro level', () => { + expect(SkillLevelFormatter.getLabel('pro')).toBe('Pro'); + }); + + it('should return "Advanced" for advanced level', () => { + expect(SkillLevelFormatter.getLabel('advanced')).toBe('Advanced'); + }); + + it('should return "Intermediate" for intermediate level', () => { + expect(SkillLevelFormatter.getLabel('intermediate')).toBe('Intermediate'); + }); + + it('should return "Beginner" for beginner level', () => { + expect(SkillLevelFormatter.getLabel('beginner')).toBe('Beginner'); + }); + + it('should return the input for unknown level', () => { + expect(SkillLevelFormatter.getLabel('unknown')).toBe('unknown'); + }); + }); + + describe('getColor', () => { + it('should return "text-yellow-400" for pro level', () => { + expect(SkillLevelFormatter.getColor('pro')).toBe('text-yellow-400'); + }); + + it('should return "text-purple-400" for advanced level', () => { + expect(SkillLevelFormatter.getColor('advanced')).toBe('text-purple-400'); + }); + + it('should return "text-primary-blue" for intermediate level', () => { + expect(SkillLevelFormatter.getColor('intermediate')).toBe('text-primary-blue'); + }); + + it('should return "text-green-400" for beginner level', () => { + expect(SkillLevelFormatter.getColor('beginner')).toBe('text-green-400'); + }); + + it('should return "text-gray-400" for unknown level', () => { + expect(SkillLevelFormatter.getColor('unknown')).toBe('text-gray-400'); + }); + }); + + describe('getBgColor', () => { + it('should return "bg-yellow-400/10" for pro level', () => { + expect(SkillLevelFormatter.getBgColor('pro')).toBe('bg-yellow-400/10'); + }); + + it('should return "bg-purple-400/10" for advanced level', () => { + expect(SkillLevelFormatter.getBgColor('advanced')).toBe('bg-purple-400/10'); + }); + + it('should return "bg-primary-blue/10" for intermediate level', () => { + expect(SkillLevelFormatter.getBgColor('intermediate')).toBe('bg-primary-blue/10'); + }); + + it('should return "bg-green-400/10" for beginner level', () => { + expect(SkillLevelFormatter.getBgColor('beginner')).toBe('bg-green-400/10'); + }); + + it('should return "bg-gray-400/10" for unknown level', () => { + expect(SkillLevelFormatter.getBgColor('unknown')).toBe('bg-gray-400/10'); + }); + }); + + describe('getBorderColor', () => { + it('should return "border-yellow-400/30" for pro level', () => { + expect(SkillLevelFormatter.getBorderColor('pro')).toBe('border-yellow-400/30'); + }); + + it('should return "border-purple-400/30" for advanced level', () => { + expect(SkillLevelFormatter.getBorderColor('advanced')).toBe('border-purple-400/30'); + }); + + it('should return "border-primary-blue/30" for intermediate level', () => { + expect(SkillLevelFormatter.getBorderColor('intermediate')).toBe('border-primary-blue/30'); + }); + + it('should return "border-green-400/30" for beginner level', () => { + expect(SkillLevelFormatter.getBorderColor('beginner')).toBe('border-green-400/30'); + }); + + it('should return "border-gray-400/30" for unknown level', () => { + expect(SkillLevelFormatter.getBorderColor('unknown')).toBe('border-gray-400/30'); + }); + }); +}); diff --git a/apps/website/lib/formatters/SkillLevelIconFormatter.test.ts b/apps/website/lib/formatters/SkillLevelIconFormatter.test.ts new file mode 100644 index 000000000..18520c79f --- /dev/null +++ b/apps/website/lib/formatters/SkillLevelIconFormatter.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { SkillLevelIconFormatter } from './SkillLevelIconFormatter'; + +describe('SkillLevelIconFormatter', () => { + describe('getIcon', () => { + it('should return "🥉" for beginner level', () => { + expect(SkillLevelIconFormatter.getIcon('beginner')).toBe('🥉'); + }); + + it('should return "🥈" for intermediate level', () => { + expect(SkillLevelIconFormatter.getIcon('intermediate')).toBe('🥈'); + }); + + it('should return "🥇" for advanced level', () => { + expect(SkillLevelIconFormatter.getIcon('advanced')).toBe('🥇'); + }); + + it('should return "👑" for expert level', () => { + expect(SkillLevelIconFormatter.getIcon('expert')).toBe('👑'); + }); + + it('should return "🏁" for unknown level', () => { + expect(SkillLevelIconFormatter.getIcon('unknown')).toBe('🏁'); + }); + + it('should return "🏁" for any other level', () => { + expect(SkillLevelIconFormatter.getIcon('pro')).toBe('🏁'); + expect(SkillLevelIconFormatter.getIcon('master')).toBe('🏁'); + }); + }); +}); diff --git a/apps/website/lib/formatters/StatusFormatter.test.ts b/apps/website/lib/formatters/StatusFormatter.test.ts new file mode 100644 index 000000000..23415fe8d --- /dev/null +++ b/apps/website/lib/formatters/StatusFormatter.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { StatusFormatter } from './StatusFormatter'; + +describe('StatusFormatter', () => { + describe('transactionStatus', () => { + it('should format "paid" correctly', () => { + expect(StatusFormatter.transactionStatus('paid')).toBe('Paid'); + }); + + it('should format "pending" correctly', () => { + expect(StatusFormatter.transactionStatus('pending')).toBe('Pending'); + }); + + it('should format "overdue" correctly', () => { + expect(StatusFormatter.transactionStatus('overdue')).toBe('Overdue'); + }); + + it('should format "failed" correctly', () => { + expect(StatusFormatter.transactionStatus('failed')).toBe('Failed'); + }); + + it('should handle unknown status', () => { + expect(StatusFormatter.transactionStatus('unknown')).toBe('unknown'); + }); + }); + + describe('raceStatus', () => { + it('should format "scheduled" correctly', () => { + expect(StatusFormatter.raceStatus('scheduled')).toBe('Scheduled'); + }); + + it('should format "running" correctly', () => { + expect(StatusFormatter.raceStatus('running')).toBe('Live'); + }); + + it('should format "completed" correctly', () => { + expect(StatusFormatter.raceStatus('completed')).toBe('Completed'); + }); + + it('should handle unknown status', () => { + expect(StatusFormatter.raceStatus('unknown')).toBe('unknown'); + }); + }); + + describe('protestStatus', () => { + it('should format "pending" correctly', () => { + expect(StatusFormatter.protestStatus('pending')).toBe('Pending'); + }); + + it('should format "under_review" correctly', () => { + expect(StatusFormatter.protestStatus('under_review')).toBe('Under Review'); + }); + + it('should format "resolved" correctly', () => { + expect(StatusFormatter.protestStatus('resolved')).toBe('Resolved'); + }); + + it('should handle unknown status', () => { + expect(StatusFormatter.protestStatus('unknown')).toBe('unknown'); + }); + }); +}); diff --git a/apps/website/lib/formatters/TeamCreationStatusFormatter.test.ts b/apps/website/lib/formatters/TeamCreationStatusFormatter.test.ts new file mode 100644 index 000000000..a7909ec39 --- /dev/null +++ b/apps/website/lib/formatters/TeamCreationStatusFormatter.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { TeamCreationStatusFormatter } from './TeamCreationStatusFormatter'; + +describe('TeamCreationStatusFormatter', () => { + describe('statusMessage', () => { + it('should return success message when team created successfully', () => { + expect(TeamCreationStatusFormatter.statusMessage(true)).toBe('Team created successfully!'); + }); + + it('should return failure message when team creation failed', () => { + expect(TeamCreationStatusFormatter.statusMessage(false)).toBe('Failed to create team.'); + }); + }); +}); diff --git a/apps/website/lib/formatters/TimeFormatter.test.ts b/apps/website/lib/formatters/TimeFormatter.test.ts new file mode 100644 index 000000000..ed6f6b231 --- /dev/null +++ b/apps/website/lib/formatters/TimeFormatter.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { TimeFormatter } from './TimeFormatter'; + +describe('TimeFormatter', () => { + describe('timeAgo', () => { + it('should return "Just now" for less than 1 minute ago', () => { + const now = new Date(); + const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000); + const result = TimeFormatter.timeAgo(thirtySecondsAgo); + expect(result).toBe('Just now'); + }); + + it('should return "X min ago" for less than 1 hour ago', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = TimeFormatter.timeAgo(thirtyMinutesAgo); + expect(result).toBe('30 min ago'); + }); + + it('should return "X h ago" for less than 24 hours ago', () => { + const now = new Date(); + const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000); + const result = TimeFormatter.timeAgo(fiveHoursAgo); + expect(result).toBe('5 h ago'); + }); + + it('should return "X d ago" for 24 hours or more ago', () => { + const now = new Date(); + const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000); + const result = TimeFormatter.timeAgo(twoDaysAgo); + expect(result).toBe('2 d ago'); + }); + + it('should handle string input', () => { + const now = new Date(); + const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); + const result = TimeFormatter.timeAgo(thirtyMinutesAgo.toISOString()); + expect(result).toBe('30 min ago'); + }); + }); +}); diff --git a/apps/website/lib/formatters/TransactionTypeFormatter.test.ts b/apps/website/lib/formatters/TransactionTypeFormatter.test.ts new file mode 100644 index 000000000..152cef835 --- /dev/null +++ b/apps/website/lib/formatters/TransactionTypeFormatter.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { TransactionTypeFormatter } from './TransactionTypeFormatter'; + +describe('TransactionTypeFormatter', () => { + describe('format', () => { + it('should capitalize the first letter and lowercase the rest', () => { + expect(TransactionTypeFormatter.format('deposit')).toBe('Deposit'); + expect(TransactionTypeFormatter.format('withdrawal')).toBe('Withdrawal'); + expect(TransactionTypeFormatter.format('refund')).toBe('Refund'); + }); + + it('should handle single character strings', () => { + expect(TransactionTypeFormatter.format('a')).toBe('A'); + }); + + it('should handle already capitalized strings', () => { + expect(TransactionTypeFormatter.format('Deposit')).toBe('Deposit'); + }); + + it('should handle all uppercase strings', () => { + expect(TransactionTypeFormatter.format('DEPOSIT')).toBe('Deposit'); + }); + + it('should handle empty string', () => { + expect(TransactionTypeFormatter.format('')).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/formatters/UserRoleFormatter.test.ts b/apps/website/lib/formatters/UserRoleFormatter.test.ts new file mode 100644 index 000000000..62dacc2f9 --- /dev/null +++ b/apps/website/lib/formatters/UserRoleFormatter.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { UserRoleFormatter } from './UserRoleFormatter'; + +describe('UserRoleFormatter', () => { + describe('roleLabel', () => { + it('should format "owner" correctly', () => { + expect(UserRoleFormatter.roleLabel('owner')).toBe('Owner'); + }); + + it('should format "admin" correctly', () => { + expect(UserRoleFormatter.roleLabel('admin')).toBe('Admin'); + }); + + it('should format "user" correctly', () => { + expect(UserRoleFormatter.roleLabel('user')).toBe('User'); + }); + + it('should handle unknown roles', () => { + expect(UserRoleFormatter.roleLabel('unknown')).toBe('unknown'); + }); + + it('should handle empty string', () => { + expect(UserRoleFormatter.roleLabel('')).toBe(''); + }); + }); +}); diff --git a/apps/website/lib/formatters/UserStatusFormatter.test.ts b/apps/website/lib/formatters/UserStatusFormatter.test.ts new file mode 100644 index 000000000..77d08e716 --- /dev/null +++ b/apps/website/lib/formatters/UserStatusFormatter.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { UserStatusFormatter } from './UserStatusFormatter'; + +describe('UserStatusFormatter', () => { + describe('statusLabel', () => { + it('should format "active" correctly', () => { + expect(UserStatusFormatter.statusLabel('active')).toBe('Active'); + }); + + it('should format "suspended" correctly', () => { + expect(UserStatusFormatter.statusLabel('suspended')).toBe('Suspended'); + }); + + it('should format "deleted" correctly', () => { + expect(UserStatusFormatter.statusLabel('deleted')).toBe('Deleted'); + }); + + it('should handle unknown status', () => { + expect(UserStatusFormatter.statusLabel('unknown')).toBe('unknown'); + }); + }); + + describe('statusVariant', () => { + it('should return "performance-green" for active status', () => { + expect(UserStatusFormatter.statusVariant('active')).toBe('performance-green'); + }); + + it('should return "yellow-500" for suspended status', () => { + expect(UserStatusFormatter.statusVariant('suspended')).toBe('yellow-500'); + }); + + it('should return "racing-red" for deleted status', () => { + expect(UserStatusFormatter.statusVariant('deleted')).toBe('racing-red'); + }); + + it('should return "gray-500" for unknown status', () => { + expect(UserStatusFormatter.statusVariant('unknown')).toBe('gray-500'); + }); + }); + + describe('canSuspend', () => { + it('should return true for active status', () => { + expect(UserStatusFormatter.canSuspend('active')).toBe(true); + }); + + it('should return false for suspended status', () => { + expect(UserStatusFormatter.canSuspend('suspended')).toBe(false); + }); + + it('should return false for deleted status', () => { + expect(UserStatusFormatter.canSuspend('deleted')).toBe(false); + }); + + it('should return false for unknown status', () => { + expect(UserStatusFormatter.canSuspend('unknown')).toBe(false); + }); + }); + + describe('canActivate', () => { + it('should return false for active status', () => { + expect(UserStatusFormatter.canActivate('active')).toBe(false); + }); + + it('should return true for suspended status', () => { + expect(UserStatusFormatter.canActivate('suspended')).toBe(true); + }); + + it('should return false for deleted status', () => { + expect(UserStatusFormatter.canActivate('deleted')).toBe(false); + }); + + it('should return false for unknown status', () => { + expect(UserStatusFormatter.canActivate('unknown')).toBe(false); + }); + }); + + describe('canDelete', () => { + it('should return true for active status', () => { + expect(UserStatusFormatter.canDelete('active')).toBe(true); + }); + + it('should return true for suspended status', () => { + expect(UserStatusFormatter.canDelete('suspended')).toBe(true); + }); + + it('should return false for deleted status', () => { + expect(UserStatusFormatter.canDelete('deleted')).toBe(false); + }); + + it('should return true for unknown status', () => { + expect(UserStatusFormatter.canDelete('unknown')).toBe(true); + }); + }); +}); diff --git a/apps/website/lib/formatters/WinRateFormatter.test.ts b/apps/website/lib/formatters/WinRateFormatter.test.ts new file mode 100644 index 000000000..d65eba0ea --- /dev/null +++ b/apps/website/lib/formatters/WinRateFormatter.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { WinRateFormatter } from './WinRateFormatter'; + +describe('WinRateFormatter', () => { + describe('calculate', () => { + it('should return "0.0" when no races completed', () => { + expect(WinRateFormatter.calculate(0, 0)).toBe('0.0'); + }); + + it('should calculate win rate correctly', () => { + expect(WinRateFormatter.calculate(10, 5)).toBe('50.0'); + expect(WinRateFormatter.calculate(100, 25)).toBe('25.0'); + expect(WinRateFormatter.calculate(100, 75)).toBe('75.0'); + }); + + it('should handle 100% win rate', () => { + expect(WinRateFormatter.calculate(10, 10)).toBe('100.0'); + }); + + it('should handle 0% win rate', () => { + expect(WinRateFormatter.calculate(10, 0)).toBe('0.0'); + }); + + it('should handle decimal win rates', () => { + expect(WinRateFormatter.calculate(10, 3)).toBe('30.0'); + expect(WinRateFormatter.calculate(10, 7)).toBe('70.0'); + }); + }); + + describe('format', () => { + it('should format rate with 1 decimal place and % sign', () => { + expect(WinRateFormatter.format(50.0)).toBe('50.0%'); + expect(WinRateFormatter.format(25.5)).toBe('25.5%'); + expect(WinRateFormatter.format(100.0)).toBe('100.0%'); + }); + + it('should handle null rate', () => { + expect(WinRateFormatter.format(null)).toBe('0.0%'); + }); + + it('should handle undefined rate', () => { + expect(WinRateFormatter.format(undefined)).toBe('0.0%'); + }); + + it('should handle zero rate', () => { + expect(WinRateFormatter.format(0)).toBe('0.0%'); + }); + + it('should handle decimal rates', () => { + expect(WinRateFormatter.format(12.34)).toBe('12.3%'); + expect(WinRateFormatter.format(99.99)).toBe('100.0%'); + }); + }); +});