managarten/docs/test-examples/shared/format.test.ts
2025-11-27 17:26:18 +01:00

352 lines
9.2 KiB
TypeScript

/**
* Example Shared Package Utility Test
*
* This demonstrates best practices for testing utility functions:
* - Test pure functions
* - Test edge cases
* - Test error handling
* - Parameterized tests
* - Property-based testing (optional)
*/
import { describe, it, expect } from 'vitest';
import { formatDate, truncate, slugify, capitalize, debounce } from '../format';
describe('formatDate', () => {
it('should format date with default format', () => {
const date = new Date('2024-01-15T12:00:00Z');
const result = formatDate(date);
expect(result).toBe('2024-01-15');
});
it('should format date with custom format', () => {
const date = new Date('2024-01-15T12:00:00Z');
const result = formatDate(date, 'MM/dd/yyyy');
expect(result).toBe('01/15/2024');
});
it('should handle different locales', () => {
const date = new Date('2024-01-15T12:00:00Z');
const result = formatDate(date, 'PPP', { locale: 'de' });
expect(result).toContain('Januar');
});
it('should handle invalid dates', () => {
expect(() => formatDate(new Date('invalid'))).toThrow('Invalid date');
});
it('should handle null or undefined', () => {
expect(() => formatDate(null as any)).toThrow('Invalid date');
expect(() => formatDate(undefined as any)).toThrow('Invalid date');
});
it('should handle dates at boundaries', () => {
// Min safe date
const minDate = new Date(-8640000000000000);
expect(() => formatDate(minDate)).not.toThrow();
// Max safe date
const maxDate = new Date(8640000000000000);
expect(() => formatDate(maxDate)).not.toThrow();
});
it('should handle timezone differences', () => {
const date = new Date('2024-01-15T00:00:00Z');
const resultUTC = formatDate(date, 'yyyy-MM-dd HH:mm', { timeZone: 'UTC' });
const resultEST = formatDate(date, 'yyyy-MM-dd HH:mm', { timeZone: 'America/New_York' });
expect(resultUTC).not.toBe(resultEST);
});
});
describe('truncate', () => {
it('should truncate long strings', () => {
const text = 'This is a very long string that should be truncated';
const result = truncate(text, 20);
expect(result).toBe('This is a very long…');
expect(result.length).toBeLessThanOrEqual(21); // 20 chars + ellipsis
});
it('should not truncate short strings', () => {
const text = 'Short';
const result = truncate(text, 20);
expect(result).toBe('Short');
});
it('should use custom ellipsis', () => {
const text = 'This is a very long string';
const result = truncate(text, 10, '...');
expect(result).toBe('This is...');
});
it('should handle exact length match', () => {
const text = 'Exactly20Characters!';
const result = truncate(text, 20);
expect(result).toBe('Exactly20Characters!');
});
it('should handle empty strings', () => {
const result = truncate('', 10);
expect(result).toBe('');
});
it('should handle length of 0', () => {
const text = 'Some text';
const result = truncate(text, 0);
expect(result).toBe('…');
});
it('should handle negative length', () => {
expect(() => truncate('text', -1)).toThrow('Length must be non-negative');
});
it('should preserve word boundaries (optional feature)', () => {
const text = 'This is a very long string';
const result = truncate(text, 15, '…', { preserveWords: true });
expect(result).toBe('This is a very…');
expect(result).not.toContain('very l'); // Should not break mid-word
});
});
describe('slugify', () => {
it('should convert to lowercase', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('should replace spaces with hyphens', () => {
expect(slugify('multiple spaces')).toBe('multiple-spaces');
});
it('should remove special characters', () => {
expect(slugify('Hello & World!')).toBe('hello-world');
expect(slugify('React@TypeScript#2024')).toBe('react-typescript-2024');
});
it('should handle unicode characters', () => {
expect(slugify('Café résumé')).toBe('cafe-resume');
expect(slugify('Zürich naïve')).toBe('zurich-naive');
});
it('should remove leading and trailing hyphens', () => {
expect(slugify(' hello world ')).toBe('hello-world');
expect(slugify('!!!hello world!!!')).toBe('hello-world');
});
it('should handle already slugified strings', () => {
expect(slugify('already-a-slug')).toBe('already-a-slug');
});
it('should handle empty strings', () => {
expect(slugify('')).toBe('');
});
it('should handle strings with only special characters', () => {
expect(slugify('!@#$%^&*()')).toBe('');
});
it('should handle very long strings', () => {
const longString = 'a'.repeat(1000);
const result = slugify(longString);
expect(result.length).toBeLessThanOrEqual(200); // Max slug length
});
// Parameterized tests
it.each([
['Hello World', 'hello-world'],
['React & TypeScript', 'react-typescript'],
['2024 年', '2024'],
[' Multiple Spaces ', 'multiple-spaces'],
['CamelCaseText', 'camelcasetext'],
])('slugify("%s") should return "%s"', (input, expected) => {
expect(slugify(input)).toBe(expected);
});
});
describe('capitalize', () => {
it('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
it('should handle already capitalized strings', () => {
expect(capitalize('Hello')).toBe('Hello');
});
it('should handle single characters', () => {
expect(capitalize('a')).toBe('A');
});
it('should handle empty strings', () => {
expect(capitalize('')).toBe('');
});
it('should not affect rest of string', () => {
expect(capitalize('hELLO wORLD')).toBe('HELLO wORLD');
});
it('should handle strings starting with numbers', () => {
expect(capitalize('123abc')).toBe('123abc');
});
it('should handle strings with leading whitespace', () => {
expect(capitalize(' hello')).toBe(' Hello');
});
});
describe('debounce', () => {
it('should delay function execution', async () => {
vi.useFakeTimers();
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 500);
debouncedFn();
expect(mockFn).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(mockFn).toHaveBeenCalledOnce();
vi.useRealTimers();
});
it('should cancel previous calls', async () => {
vi.useFakeTimers();
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 500);
debouncedFn('call1');
vi.advanceTimersByTime(200);
debouncedFn('call2');
vi.advanceTimersByTime(200);
debouncedFn('call3');
vi.advanceTimersByTime(500);
// Should only call once with last argument
expect(mockFn).toHaveBeenCalledOnce();
expect(mockFn).toHaveBeenCalledWith('call3');
vi.useRealTimers();
});
it('should preserve this context', async () => {
vi.useFakeTimers();
const obj = {
value: 42,
method: function () {
return this.value;
},
};
const debouncedMethod = debounce(obj.method, 100);
const result = debouncedMethod.call(obj);
vi.advanceTimersByTime(100);
expect(result).toBe(42);
vi.useRealTimers();
});
it('should handle immediate option', () => {
vi.useFakeTimers();
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 500, { immediate: true });
debouncedFn();
expect(mockFn).toHaveBeenCalledOnce(); // Called immediately
debouncedFn();
expect(mockFn).toHaveBeenCalledOnce(); // Still once (debounced)
vi.advanceTimersByTime(500);
debouncedFn();
expect(mockFn).toHaveBeenCalledTimes(2); // Called again after wait
vi.useRealTimers();
});
});
// Property-based testing example (requires fast-check)
describe('Property-based tests', () => {
it('slugify should always return lowercase', () => {
// Using property-based testing to generate random inputs
for (let i = 0; i < 100; i++) {
const randomString = Math.random().toString(36) + Math.random().toString(36);
const result = slugify(randomString);
expect(result).toBe(result.toLowerCase());
}
});
it('truncate should never exceed max length', () => {
const testCases = [
'short',
'exactly twenty chars',
'this is a very long string that needs truncation',
'a'.repeat(1000),
];
testCases.forEach((text) => {
const maxLength = 20;
const result = truncate(text, maxLength);
// Result should be <= maxLength + ellipsis length
expect(result.length).toBeLessThanOrEqual(maxLength + 1);
});
});
});
// Edge cases and boundary testing
describe('Edge Cases', () => {
describe('Unicode and Emoji handling', () => {
it('should handle emoji in truncate', () => {
const text = 'Hello 👋 World 🌍';
const result = truncate(text, 10);
expect(result.length).toBeLessThanOrEqual(11);
});
it('should handle emoji in slugify', () => {
const result = slugify('Hello 👋 World');
expect(result).toBe('hello-world');
expect(result).not.toContain('👋');
});
});
describe('Security considerations', () => {
it('should sanitize XSS in slugify', () => {
const malicious = '<script>alert("xss")</script>';
const result = slugify(malicious);
expect(result).not.toContain('<');
expect(result).not.toContain('>');
expect(result).not.toContain('script');
});
it('should handle SQL injection patterns', () => {
const sqlInjection = "'; DROP TABLE users; --";
const result = slugify(sqlInjection);
expect(result).not.toContain("'");
expect(result).not.toContain(';');
expect(result).not.toContain('--');
});
});
});