/ src / utils / title-sanitization.test.ts
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  });