title-sanitization.test.ts
1 import { describe, it, expect } from 'vitest'; 2 import { 3 sanitizeTitleToPascalCase, 4 validateTitle, 5 isPascalCase, 6 pascalCaseToTitle 7 } from './title-sanitization'; 8 9 describe('sanitizeTitleToPascalCase', () => { 10 it('converts simple titles with spaces', () => { 11 expect(sanitizeTitleToPascalCase('Thunderstorm Generator')).toBe('ThunderstormGenerator'); 12 expect(sanitizeTitleToPascalCase('Dream Node')).toBe('DreamNode'); 13 expect(sanitizeTitleToPascalCase('My First Idea')).toBe('MyFirstIdea'); 14 }); 15 16 it('handles titles with hyphens', () => { 17 expect(sanitizeTitleToPascalCase('Mind-Body Connection')).toBe('MindBodyConnection'); 18 expect(sanitizeTitleToPascalCase('Self-Driving-Car')).toBe('SelfDrivingCar'); 19 }); 20 21 it('handles titles with underscores', () => { 22 expect(sanitizeTitleToPascalCase('machine_learning_model')).toBe('MachineLearningModel'); 23 expect(sanitizeTitleToPascalCase('deep_neural_net')).toBe('DeepNeuralNet'); 24 }); 25 26 it('handles titles with numbers', () => { 27 expect(sanitizeTitleToPascalCase('Dream 2.0')).toBe('Dream20'); 28 expect(sanitizeTitleToPascalCase('Version 3.14.159')).toBe('Version314159'); 29 expect(sanitizeTitleToPascalCase('AI Model v2')).toBe('AiModelV2'); 30 }); 31 32 it('handles titles with special characters', () => { 33 expect(sanitizeTitleToPascalCase('Café Philosophy')).toBe('CafePhilosophy'); 34 expect(sanitizeTitleToPascalCase('Niño Project')).toBe('NinoProject'); 35 expect(sanitizeTitleToPascalCase('Über Idea')).toBe('UberIdea'); 36 }); 37 38 it('handles mixed case input', () => { 39 expect(sanitizeTitleToPascalCase('thunderstorm GENERATOR')).toBe('ThunderstormGenerator'); 40 expect(sanitizeTitleToPascalCase('DREAM node')).toBe('DreamNode'); 41 expect(sanitizeTitleToPascalCase('MiXeD CaSe')).toBe('MixedCase'); 42 }); 43 44 it('removes multiple consecutive spaces', () => { 45 expect(sanitizeTitleToPascalCase('Multiple Spaces')).toBe('MultipleSpaces'); 46 expect(sanitizeTitleToPascalCase('Tab\t\tSeparated')).toBe('TabSeparated'); 47 }); 48 49 it('handles leading and trailing whitespace', () => { 50 expect(sanitizeTitleToPascalCase(' Leading Spaces')).toBe('LeadingSpaces'); 51 expect(sanitizeTitleToPascalCase('Trailing Spaces ')).toBe('TrailingSpaces'); 52 expect(sanitizeTitleToPascalCase(' Both ')).toBe('Both'); 53 }); 54 55 it('handles edge case: very long titles', () => { 56 const longTitle = 'This Is A Very Long Title That Exceeds The Maximum Length Limit Of One Hundred Characters And Should Be Truncated Properly Without Breaking'; 57 const result = sanitizeTitleToPascalCase(longTitle); 58 expect(result.length).toBeLessThanOrEqual(100); 59 // Should be truncated to exactly 100 characters 60 expect(result).toBe('ThisIsAVeryLongTitleThatExceedsTheMaximumLengthLimitOfOneHundredCharactersAndShouldBeTruncatedProper'); 61 expect(result.length).toBe(100); 62 }); 63 64 it('handles edge case: single word', () => { 65 expect(sanitizeTitleToPascalCase('Thunderstorm')).toBe('Thunderstorm'); 66 expect(sanitizeTitleToPascalCase('dream')).toBe('Dream'); 67 }); 68 69 it('handles edge case: only special characters', () => { 70 expect(sanitizeTitleToPascalCase('!@#$%^&*()')).toBe(''); 71 expect(sanitizeTitleToPascalCase('---')).toBe(''); 72 }); 73 74 it('handles edge case: empty string', () => { 75 expect(sanitizeTitleToPascalCase('')).toBe(''); 76 }); 77 78 it('handles real-world example from ThunderstormGenerator', () => { 79 expect(sanitizeTitleToPascalCase('Thunderstorm Generator UPDATED COMPLETE DIY BUILD')) 80 .toBe('ThunderstormGeneratorUpdatedCompleteDiyBuild'); 81 }); 82 83 it('handles alphanumeric combinations', () => { 84 expect(sanitizeTitleToPascalCase('Project 2024 Q1')).toBe('Project2024Q1'); 85 expect(sanitizeTitleToPascalCase('API v3.2')).toBe('ApiV32'); 86 }); 87 }); 88 89 describe('validateTitle', () => { 90 it('validates good titles', () => { 91 const result = validateTitle('Thunderstorm Generator'); 92 expect(result.valid).toBe(true); 93 expect(result.sanitized).toBe('ThunderstormGenerator'); 94 expect(result.error).toBeUndefined(); 95 }); 96 97 it('rejects empty titles', () => { 98 const result = validateTitle(''); 99 expect(result.valid).toBe(false); 100 expect(result.error).toBe('Title cannot be empty'); 101 }); 102 103 it('rejects whitespace-only titles', () => { 104 const result = validateTitle(' '); 105 expect(result.valid).toBe(false); 106 expect(result.error).toBe('Title cannot be empty'); 107 }); 108 109 it('rejects titles with only special characters', () => { 110 const result = validateTitle('!@#$%'); 111 expect(result.valid).toBe(false); 112 expect(result.error).toBe('Title must contain at least one alphanumeric character'); 113 }); 114 115 it('accepts titles with some special characters', () => { 116 const result = validateTitle('Mind-Body Connection!'); 117 expect(result.valid).toBe(true); 118 expect(result.sanitized).toBe('MindBodyConnection'); 119 }); 120 }); 121 122 describe('isPascalCase', () => { 123 it('recognizes valid PascalCase', () => { 124 expect(isPascalCase('ThunderstormGenerator')).toBe(true); 125 expect(isPascalCase('DreamNode')).toBe(true); 126 expect(isPascalCase('MyFirstIdea')).toBe(true); 127 expect(isPascalCase('Dream20')).toBe(true); 128 }); 129 130 it('rejects non-PascalCase strings', () => { 131 expect(isPascalCase('thunderstorm')).toBe(false); // lowercase 132 expect(isPascalCase('THUNDERSTORM')).toBe(true); // all uppercase is technically valid 133 expect(isPascalCase('thunderstorm-generator')).toBe(false); // kebab-case 134 expect(isPascalCase('Thunderstorm Generator')).toBe(false); // has space 135 expect(isPascalCase('Thunderstorm_Generator')).toBe(false); // has underscore 136 }); 137 138 it('rejects empty strings', () => { 139 expect(isPascalCase('')).toBe(false); 140 }); 141 142 it('handles edge cases', () => { 143 expect(isPascalCase('A')).toBe(true); // single uppercase letter 144 expect(isPascalCase('a')).toBe(false); // single lowercase letter 145 expect(isPascalCase('1')).toBe(false); // starts with number 146 }); 147 }); 148 149 describe('pascalCaseToTitle', () => { 150 it('converts PascalCase to human-readable', () => { 151 expect(pascalCaseToTitle('ThunderstormGenerator')).toBe('Thunderstorm Generator'); 152 expect(pascalCaseToTitle('DreamNode')).toBe('Dream Node'); 153 expect(pascalCaseToTitle('MyFirstIdea')).toBe('My First Idea'); 154 }); 155 156 it('handles single words', () => { 157 expect(pascalCaseToTitle('Thunderstorm')).toBe('Thunderstorm'); 158 expect(pascalCaseToTitle('Dream')).toBe('Dream'); 159 }); 160 161 it('handles numbers', () => { 162 expect(pascalCaseToTitle('Dream20')).toBe('Dream20'); 163 expect(pascalCaseToTitle('ApiV32')).toBe('Api V32'); 164 }); 165 166 it('handles empty strings', () => { 167 expect(pascalCaseToTitle('')).toBe(''); 168 }); 169 170 it('is lossy (cannot perfectly reverse)', () => { 171 // Note: This demonstrates the lossy nature of the conversion 172 const original = 'Mind-Body Connection'; 173 const pascalCase = sanitizeTitleToPascalCase(original); 174 const recovered = pascalCaseToTitle(pascalCase); 175 expect(pascalCase).toBe('MindBodyConnection'); 176 expect(recovered).toBe('Mind Body Connection'); // Lost the hyphen 177 expect(recovered).not.toBe(original); 178 }); 179 }); 180 181 describe('round-trip consistency', () => { 182 it('maintains consistency for simple titles', () => { 183 const original = 'Thunderstorm Generator'; 184 const pascalCase = sanitizeTitleToPascalCase(original); 185 const recovered = pascalCaseToTitle(pascalCase); 186 expect(pascalCase).toBe('ThunderstormGenerator'); 187 expect(recovered).toBe('Thunderstorm Generator'); 188 }); 189 190 it('shows lossy conversion for complex titles', () => { 191 const complexTitles = [ 192 'Mind-Body Connection', 193 'Self-Driving Car', 194 'Deep_Neural_Net', 195 'API v3.2' 196 ]; 197 198 complexTitles.forEach(title => { 199 const pascalCase = sanitizeTitleToPascalCase(title); 200 const recovered = pascalCaseToTitle(pascalCase); 201 // Recovered version may not match exactly, but should be readable 202 expect(recovered).toBeTruthy(); 203 expect(recovered.length).toBeGreaterThan(0); 204 }); 205 }); 206 });