Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 295 lines 10 kB view raw
1import { describe, it, expect, beforeEach } from 'vitest'; 2 3/** 4 * Tests for the Version Panel pure functions. 5 * 6 * We test only the exported pure functions (formatRelativeTime, 7 * parseMetadata, computeDiffStats) — not the DOM-dependent 8 * createVersionPanel which requires a full browser environment. 9 */ 10 11import { 12 formatRelativeTime, 13 parseMetadata, 14 computeDiffStats, 15} from '../src/version-panel.js'; 16 17describe('formatRelativeTime', () => { 18 it('returns "Just now" for a time less than 60s ago', () => { 19 const now = new Date(); 20 const result = formatRelativeTime(now.toISOString()); 21 expect(result).toBe('Just now'); 22 }); 23 24 it('returns "N min ago" for times within the hour', () => { 25 const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000); 26 const result = formatRelativeTime(fiveMinAgo.toISOString()); 27 expect(result).toBe('5 min ago'); 28 }); 29 30 it('returns "1 min ago" for exactly 1 minute', () => { 31 const oneMinAgo = new Date(Date.now() - 60 * 1000); 32 const result = formatRelativeTime(oneMinAgo.toISOString()); 33 expect(result).toBe('1 min ago'); 34 }); 35 36 it('returns "N hours ago" for times within 24h', () => { 37 const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); 38 const result = formatRelativeTime(twoHoursAgo.toISOString()); 39 expect(result).toBe('2 hours ago'); 40 }); 41 42 it('returns "1 hour ago" for exactly 1 hour (no plural)', () => { 43 const oneHourAgo = new Date(Date.now() - 1 * 60 * 60 * 1000); 44 const result = formatRelativeTime(oneHourAgo.toISOString()); 45 expect(result).toBe('1 hour ago'); 46 }); 47 48 it('returns "Yesterday" for times 24-48h ago', () => { 49 const yesterday = new Date(Date.now() - 30 * 60 * 60 * 1000); 50 const result = formatRelativeTime(yesterday.toISOString()); 51 expect(result).toBe('Yesterday'); 52 }); 53 54 it('returns "Mon DD" for older dates', () => { 55 const oldDate = new Date('2025-03-15T10:00:00Z'); 56 const result = formatRelativeTime(oldDate.toISOString()); 57 expect(result).toBe('Mar 15'); 58 }); 59 60 it('handles dates without Z suffix (server format)', () => { 61 const now = new Date(); 62 // Server returns UTC without Z — e.g. "2025-03-15 10:00:00" 63 const serverFormat = now.toISOString().replace('T', ' ').replace('Z', ''); 64 const result = formatRelativeTime(serverFormat); 65 expect(result).toBe('Just now'); 66 }); 67 68 it('returns "Unknown" for empty string', () => { 69 expect(formatRelativeTime('')).toBe('Unknown'); 70 }); 71 72 it('returns "Unknown" for invalid date string', () => { 73 expect(formatRelativeTime('not-a-date')).toBe('Unknown'); 74 }); 75 76 it('returns "Unknown" for null-like empty input', () => { 77 expect(formatRelativeTime('')).toBe('Unknown'); 78 }); 79 80 it('handles January dates correctly', () => { 81 const jan = new Date('2025-01-05T10:00:00Z'); 82 const result = formatRelativeTime(jan.toISOString()); 83 expect(result).toBe('Jan 5'); 84 }); 85 86 it('handles December dates correctly', () => { 87 const dec = new Date('2025-12-25T10:00:00Z'); 88 const result = formatRelativeTime(dec.toISOString()); 89 expect(result).toBe('Dec 25'); 90 }); 91}); 92 93describe('parseMetadata', () => { 94 it('parses valid JSON string', () => { 95 const result = parseMetadata('{"author":"Alice","wordCount":42}'); 96 expect(result).toEqual({ author: 'Alice', wordCount: 42 }); 97 }); 98 99 it('returns empty object for null input', () => { 100 expect(parseMetadata(null)).toEqual({}); 101 }); 102 103 it('returns empty object for empty string', () => { 104 expect(parseMetadata('')).toEqual({}); 105 }); 106 107 it('returns empty object for invalid JSON', () => { 108 expect(parseMetadata('not json')).toEqual({}); 109 }); 110 111 it('returns empty object for JSON array', () => { 112 expect(parseMetadata('[1, 2, 3]')).toEqual({}); 113 }); 114 115 it('returns empty object for JSON primitive', () => { 116 expect(parseMetadata('"just a string"')).toEqual({}); 117 }); 118 119 it('returns empty object for JSON number', () => { 120 expect(parseMetadata('42')).toEqual({}); 121 }); 122 123 it('handles nested objects', () => { 124 const result = parseMetadata('{"a":{"b":1}}'); 125 expect(result).toEqual({ a: { b: 1 } }); 126 }); 127 128 it('handles empty JSON object', () => { 129 expect(parseMetadata('{}')).toEqual({}); 130 }); 131 132 it('preserves all properties in metadata', () => { 133 const result = parseMetadata('{"author":"Bob","wordCount":100,"timestamp":12345,"custom":"value"}'); 134 expect(result['author']).toBe('Bob'); 135 expect(result['wordCount']).toBe(100); 136 expect(result['timestamp']).toBe(12345); 137 expect(result['custom']).toBe('value'); 138 }); 139}); 140 141describe('computeDiffStats', () => { 142 it('returns positive delta when words added', () => { 143 const result = computeDiffStats({ wordCount: 100 }, { wordCount: 80 }); 144 expect(result.delta).toBe(20); 145 expect(result.label).toBe('+20'); 146 }); 147 148 it('returns negative delta when words removed', () => { 149 const result = computeDiffStats({ wordCount: 50 }, { wordCount: 80 }); 150 expect(result.delta).toBe(-30); 151 expect(result.label).toBe('-30'); 152 }); 153 154 it('returns zero delta when no change', () => { 155 const result = computeDiffStats({ wordCount: 42 }, { wordCount: 42 }); 156 expect(result.delta).toBe(0); 157 expect(result.label).toBe('0'); 158 }); 159 160 it('handles first version (no previous)', () => { 161 const result = computeDiffStats({ wordCount: 50 }, undefined); 162 expect(result.delta).toBe(50); 163 expect(result.label).toBe('+50'); 164 }); 165 166 it('handles missing wordCount in current', () => { 167 const result = computeDiffStats({}, { wordCount: 10 }); 168 expect(result.delta).toBe(-10); 169 expect(result.label).toBe('-10'); 170 }); 171 172 it('handles both undefined', () => { 173 const result = computeDiffStats(undefined, undefined); 174 expect(result.delta).toBe(0); 175 expect(result.label).toBe('+0'); 176 }); 177 178 it('handles previous without wordCount property', () => { 179 const result = computeDiffStats({ wordCount: 10 }, {}); 180 expect(result.delta).toBe(10); 181 expect(result.label).toBe('+10'); 182 }); 183 184 it('handles zero word count', () => { 185 const result = computeDiffStats({ wordCount: 0 }, { wordCount: 0 }); 186 expect(result.delta).toBe(0); 187 expect(result.label).toBe('0'); 188 }); 189 190 it('handles transition from zero to non-zero', () => { 191 const result = computeDiffStats({ wordCount: 5 }, { wordCount: 0 }); 192 expect(result.delta).toBe(5); 193 expect(result.label).toBe('+5'); 194 }); 195 196 it('handles large numbers', () => { 197 const result = computeDiffStats({ wordCount: 100000 }, { wordCount: 50000 }); 198 expect(result.delta).toBe(50000); 199 expect(result.label).toBe('+50000'); 200 }); 201}); 202 203// ===================================================================== 204// Edge cases 205// ===================================================================== 206 207describe('formatRelativeTime — edge cases', () => { 208 it('returns "Unknown" for undefined-like input', () => { 209 expect(formatRelativeTime(null as unknown as string)).toBe('Unknown'); 210 }); 211 212 it('returns Unknown for timezone offset format (only Z-suffix supported)', () => { 213 // formatRelativeTime appends Z to non-Z strings, making +05:00Z invalid 214 const result = formatRelativeTime('2025-06-15T10:00:00+05:00'); 215 expect(result).toBe('Unknown'); 216 }); 217 218 it('returns correct format for exactly 59 seconds ago', () => { 219 const almost1Min = new Date(Date.now() - 59 * 1000); 220 expect(formatRelativeTime(almost1Min.toISOString())).toBe('Just now'); 221 }); 222 223 it('returns "N min ago" at exactly 60 seconds', () => { 224 const exactly1Min = new Date(Date.now() - 60 * 1000); 225 expect(formatRelativeTime(exactly1Min.toISOString())).toBe('1 min ago'); 226 }); 227 228 it('returns "N hours ago" at exactly 60 minutes', () => { 229 const exactly1Hr = new Date(Date.now() - 60 * 60 * 1000); 230 expect(formatRelativeTime(exactly1Hr.toISOString())).toBe('1 hour ago'); 231 }); 232 233 it('returns "Yesterday" at exactly 24 hours', () => { 234 const exactly24h = new Date(Date.now() - 24 * 60 * 60 * 1000); 235 expect(formatRelativeTime(exactly24h.toISOString())).toBe('Yesterday'); 236 }); 237 238 it('returns "Mon DD" at exactly 48 hours', () => { 239 const exactly48h = new Date(Date.now() - 48 * 60 * 60 * 1000); 240 const result = formatRelativeTime(exactly48h.toISOString()); 241 // Should be "Mon DD" format, not "Yesterday" 242 expect(result).toMatch(/^[A-Z][a-z]{2} \d{1,2}$/); 243 }); 244 245 it('handles very old date (year 2000)', () => { 246 const result = formatRelativeTime('2000-07-04T12:00:00Z'); 247 expect(result).toBe('Jul 4'); 248 }); 249}); 250 251describe('parseMetadata — edge cases', () => { 252 it('returns empty object for JSON boolean', () => { 253 expect(parseMetadata('true')).toEqual({}); 254 expect(parseMetadata('false')).toEqual({}); 255 }); 256 257 it('returns empty object for JSON null', () => { 258 expect(parseMetadata('null')).toEqual({}); 259 }); 260 261 it('handles object with array values', () => { 262 const result = parseMetadata('{"tags":["a","b"]}'); 263 expect(result).toEqual({ tags: ['a', 'b'] }); 264 }); 265 266 it('handles deeply nested objects', () => { 267 const result = parseMetadata('{"a":{"b":{"c":{"d":1}}}}'); 268 expect((result['a'] as Record<string, unknown>)['b']).toBeDefined(); 269 }); 270}); 271 272describe('computeDiffStats — edge cases', () => { 273 it('handles wordCount as null (treated as 0 via ??)', () => { 274 const result = computeDiffStats({ wordCount: null as unknown as number }, { wordCount: 10 }); 275 expect(result.delta).toBe(-10); 276 }); 277 278 it('handles negative word counts gracefully', () => { 279 const result = computeDiffStats({ wordCount: -5 }, { wordCount: 10 }); 280 expect(result.delta).toBe(-15); 281 expect(result.label).toBe('-15'); 282 }); 283 284 it('handles first version with zero words', () => { 285 const result = computeDiffStats({ wordCount: 0 }, undefined); 286 expect(result.delta).toBe(0); 287 expect(result.label).toBe('+0'); 288 }); 289 290 it('handles transition from non-zero to zero', () => { 291 const result = computeDiffStats({ wordCount: 0 }, { wordCount: 50 }); 292 expect(result.delta).toBe(-50); 293 expect(result.label).toBe('-50'); 294 }); 295});