|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { |
| 3 | + FeedItemType, |
| 4 | + MentionSchema, |
| 5 | + FieldChangeEntrySchema, |
| 6 | + ReactionSchema, |
| 7 | + FeedActorSchema, |
| 8 | + FeedVisibility, |
| 9 | + FeedItemSchema, |
| 10 | + FeedFilterMode, |
| 11 | + type FeedItem, |
| 12 | + type Mention, |
| 13 | + type FieldChangeEntry, |
| 14 | + type Reaction, |
| 15 | + type FeedActor, |
| 16 | +} from './feed.zod'; |
| 17 | + |
| 18 | +describe('FeedItemType', () => { |
| 19 | + it('should accept all valid feed item types', () => { |
| 20 | + const types = [ |
| 21 | + 'comment', 'field_change', 'task', 'event', 'email', 'call', |
| 22 | + 'note', 'file', 'record_create', 'record_delete', 'approval', |
| 23 | + 'sharing', 'system', |
| 24 | + ]; |
| 25 | + types.forEach(type => { |
| 26 | + expect(() => FeedItemType.parse(type)).not.toThrow(); |
| 27 | + }); |
| 28 | + }); |
| 29 | + |
| 30 | + it('should reject invalid types', () => { |
| 31 | + expect(() => FeedItemType.parse('unknown')).toThrow(); |
| 32 | + expect(() => FeedItemType.parse('')).toThrow(); |
| 33 | + }); |
| 34 | +}); |
| 35 | + |
| 36 | +describe('MentionSchema', () => { |
| 37 | + it('should accept a valid user mention', () => { |
| 38 | + const mention: Mention = { |
| 39 | + type: 'user', |
| 40 | + id: 'user_123', |
| 41 | + name: 'Jane Doe', |
| 42 | + offset: 17, |
| 43 | + length: 9, |
| 44 | + }; |
| 45 | + const result = MentionSchema.parse(mention); |
| 46 | + expect(result.type).toBe('user'); |
| 47 | + expect(result.id).toBe('user_123'); |
| 48 | + expect(result.name).toBe('Jane Doe'); |
| 49 | + expect(result.offset).toBe(17); |
| 50 | + expect(result.length).toBe(9); |
| 51 | + }); |
| 52 | + |
| 53 | + it('should accept team and record mention types', () => { |
| 54 | + expect(() => MentionSchema.parse({ type: 'team', id: 'team_1', name: 'Engineering', offset: 0, length: 12 })).not.toThrow(); |
| 55 | + expect(() => MentionSchema.parse({ type: 'record', id: 'rec_1', name: 'Acme Corp', offset: 5, length: 9 })).not.toThrow(); |
| 56 | + }); |
| 57 | + |
| 58 | + it('should reject invalid mention type', () => { |
| 59 | + expect(() => MentionSchema.parse({ type: 'group', id: '1', name: 'X', offset: 0, length: 1 })).toThrow(); |
| 60 | + }); |
| 61 | + |
| 62 | + it('should reject negative offset', () => { |
| 63 | + expect(() => MentionSchema.parse({ type: 'user', id: '1', name: 'X', offset: -1, length: 1 })).toThrow(); |
| 64 | + }); |
| 65 | + |
| 66 | + it('should reject zero length', () => { |
| 67 | + expect(() => MentionSchema.parse({ type: 'user', id: '1', name: 'X', offset: 0, length: 0 })).toThrow(); |
| 68 | + }); |
| 69 | + |
| 70 | + it('should reject missing required fields', () => { |
| 71 | + expect(() => MentionSchema.parse({})).toThrow(); |
| 72 | + expect(() => MentionSchema.parse({ type: 'user' })).toThrow(); |
| 73 | + }); |
| 74 | +}); |
| 75 | + |
| 76 | +describe('FieldChangeEntrySchema', () => { |
| 77 | + it('should accept minimal field change', () => { |
| 78 | + const result = FieldChangeEntrySchema.parse({ field: 'status' }); |
| 79 | + expect(result.field).toBe('status'); |
| 80 | + expect(result.oldValue).toBeUndefined(); |
| 81 | + expect(result.newValue).toBeUndefined(); |
| 82 | + }); |
| 83 | + |
| 84 | + it('should accept full field change with display values', () => { |
| 85 | + const change: FieldChangeEntry = { |
| 86 | + field: 'region', |
| 87 | + fieldLabel: 'Region', |
| 88 | + oldValue: null, |
| 89 | + newValue: 'asia_pacific', |
| 90 | + oldDisplayValue: '', |
| 91 | + newDisplayValue: 'Asia-Pacific', |
| 92 | + }; |
| 93 | + const result = FieldChangeEntrySchema.parse(change); |
| 94 | + expect(result.fieldLabel).toBe('Region'); |
| 95 | + expect(result.newDisplayValue).toBe('Asia-Pacific'); |
| 96 | + }); |
| 97 | + |
| 98 | + it('should reject without field name', () => { |
| 99 | + expect(() => FieldChangeEntrySchema.parse({})).toThrow(); |
| 100 | + }); |
| 101 | +}); |
| 102 | + |
| 103 | +describe('ReactionSchema', () => { |
| 104 | + it('should accept valid reaction', () => { |
| 105 | + const reaction: Reaction = { |
| 106 | + emoji: '👍', |
| 107 | + userIds: ['user_1', 'user_2'], |
| 108 | + count: 2, |
| 109 | + }; |
| 110 | + const result = ReactionSchema.parse(reaction); |
| 111 | + expect(result.emoji).toBe('👍'); |
| 112 | + expect(result.userIds).toHaveLength(2); |
| 113 | + expect(result.count).toBe(2); |
| 114 | + }); |
| 115 | + |
| 116 | + it('should reject count less than 1', () => { |
| 117 | + expect(() => ReactionSchema.parse({ emoji: '👍', userIds: [], count: 0 })).toThrow(); |
| 118 | + }); |
| 119 | + |
| 120 | + it('should reject missing required fields', () => { |
| 121 | + expect(() => ReactionSchema.parse({})).toThrow(); |
| 122 | + expect(() => ReactionSchema.parse({ emoji: '👍' })).toThrow(); |
| 123 | + }); |
| 124 | +}); |
| 125 | + |
| 126 | +describe('FeedActorSchema', () => { |
| 127 | + it('should accept user actor', () => { |
| 128 | + const actor: FeedActor = { |
| 129 | + type: 'user', |
| 130 | + id: 'user_456', |
| 131 | + name: 'John Smith', |
| 132 | + }; |
| 133 | + const result = FeedActorSchema.parse(actor); |
| 134 | + expect(result.type).toBe('user'); |
| 135 | + expect(result.name).toBe('John Smith'); |
| 136 | + }); |
| 137 | + |
| 138 | + it('should accept system actor with source', () => { |
| 139 | + const result = FeedActorSchema.parse({ |
| 140 | + type: 'system', |
| 141 | + id: 'sys_001', |
| 142 | + source: 'Omni', |
| 143 | + }); |
| 144 | + expect(result.type).toBe('system'); |
| 145 | + expect(result.source).toBe('Omni'); |
| 146 | + }); |
| 147 | + |
| 148 | + it('should accept all actor types', () => { |
| 149 | + const types = ['user', 'system', 'service', 'automation']; |
| 150 | + types.forEach(type => { |
| 151 | + expect(() => FeedActorSchema.parse({ type, id: 'test_1' })).not.toThrow(); |
| 152 | + }); |
| 153 | + }); |
| 154 | + |
| 155 | + it('should accept actor with avatarUrl', () => { |
| 156 | + const result = FeedActorSchema.parse({ |
| 157 | + type: 'user', |
| 158 | + id: 'user_1', |
| 159 | + avatarUrl: 'https://example.com/avatar.png', |
| 160 | + }); |
| 161 | + expect(result.avatarUrl).toBe('https://example.com/avatar.png'); |
| 162 | + }); |
| 163 | + |
| 164 | + it('should reject invalid actor type', () => { |
| 165 | + expect(() => FeedActorSchema.parse({ type: 'bot', id: '1' })).toThrow(); |
| 166 | + }); |
| 167 | +}); |
| 168 | + |
| 169 | +describe('FeedVisibility', () => { |
| 170 | + it('should accept valid visibility levels', () => { |
| 171 | + ['public', 'internal', 'private'].forEach(v => { |
| 172 | + expect(() => FeedVisibility.parse(v)).not.toThrow(); |
| 173 | + }); |
| 174 | + }); |
| 175 | + |
| 176 | + it('should reject invalid visibility', () => { |
| 177 | + expect(() => FeedVisibility.parse('secret')).toThrow(); |
| 178 | + }); |
| 179 | +}); |
| 180 | + |
| 181 | +describe('FeedFilterMode', () => { |
| 182 | + it('should accept valid filter modes', () => { |
| 183 | + ['all', 'comments_only', 'changes_only', 'tasks_only'].forEach(mode => { |
| 184 | + expect(() => FeedFilterMode.parse(mode)).not.toThrow(); |
| 185 | + }); |
| 186 | + }); |
| 187 | + |
| 188 | + it('should reject invalid filter mode', () => { |
| 189 | + expect(() => FeedFilterMode.parse('custom')).toThrow(); |
| 190 | + }); |
| 191 | +}); |
| 192 | + |
| 193 | +describe('FeedItemSchema', () => { |
| 194 | + const minimalComment: FeedItem = { |
| 195 | + id: 'feed_001', |
| 196 | + type: 'comment', |
| 197 | + object: 'account', |
| 198 | + recordId: 'rec_123', |
| 199 | + actor: { type: 'user', id: 'user_456', name: 'John Smith' }, |
| 200 | + body: 'Great progress on this deal!', |
| 201 | + createdAt: '2026-01-15T10:30:00Z', |
| 202 | + }; |
| 203 | + |
| 204 | + it('should accept a minimal comment feed item', () => { |
| 205 | + const result = FeedItemSchema.parse(minimalComment); |
| 206 | + expect(result.id).toBe('feed_001'); |
| 207 | + expect(result.type).toBe('comment'); |
| 208 | + expect(result.object).toBe('account'); |
| 209 | + expect(result.recordId).toBe('rec_123'); |
| 210 | + expect(result.body).toBe('Great progress on this deal!'); |
| 211 | + expect(result.replyCount).toBe(0); |
| 212 | + expect(result.visibility).toBe('public'); |
| 213 | + expect(result.isEdited).toBe(false); |
| 214 | + }); |
| 215 | + |
| 216 | + it('should accept a comment with mentions', () => { |
| 217 | + const result = FeedItemSchema.parse({ |
| 218 | + ...minimalComment, |
| 219 | + mentions: [ |
| 220 | + { type: 'user', id: 'user_789', name: 'Jane Doe', offset: 17, length: 9 }, |
| 221 | + ], |
| 222 | + }); |
| 223 | + expect(result.mentions).toHaveLength(1); |
| 224 | + expect(result.mentions![0].name).toBe('Jane Doe'); |
| 225 | + }); |
| 226 | + |
| 227 | + it('should accept a field_change feed item with changes', () => { |
| 228 | + const fieldChange: FeedItem = { |
| 229 | + id: 'feed_002', |
| 230 | + type: 'field_change', |
| 231 | + object: 'account', |
| 232 | + recordId: 'rec_123', |
| 233 | + actor: { type: 'user', id: 'user_456', name: 'John Smith' }, |
| 234 | + changes: [ |
| 235 | + { field: 'status', oldDisplayValue: 'New', newDisplayValue: 'Active' }, |
| 236 | + { field: 'region', oldDisplayValue: '', newDisplayValue: 'Asia-Pacific' }, |
| 237 | + ], |
| 238 | + createdAt: '2026-01-15T10:25:00Z', |
| 239 | + }; |
| 240 | + const result = FeedItemSchema.parse(fieldChange); |
| 241 | + expect(result.type).toBe('field_change'); |
| 242 | + expect(result.changes).toHaveLength(2); |
| 243 | + expect(result.changes![0].field).toBe('status'); |
| 244 | + }); |
| 245 | + |
| 246 | + it('should accept a threaded reply', () => { |
| 247 | + const result = FeedItemSchema.parse({ |
| 248 | + ...minimalComment, |
| 249 | + id: 'feed_003', |
| 250 | + parentId: 'feed_001', |
| 251 | + }); |
| 252 | + expect(result.parentId).toBe('feed_001'); |
| 253 | + }); |
| 254 | + |
| 255 | + it('should accept edited comment', () => { |
| 256 | + const result = FeedItemSchema.parse({ |
| 257 | + ...minimalComment, |
| 258 | + isEdited: true, |
| 259 | + editedAt: '2026-01-15T11:00:00Z', |
| 260 | + }); |
| 261 | + expect(result.isEdited).toBe(true); |
| 262 | + expect(result.editedAt).toBe('2026-01-15T11:00:00Z'); |
| 263 | + }); |
| 264 | + |
| 265 | + it('should accept reactions on a feed item', () => { |
| 266 | + const result = FeedItemSchema.parse({ |
| 267 | + ...minimalComment, |
| 268 | + reactions: [ |
| 269 | + { emoji: '👍', userIds: ['user_789'], count: 1 }, |
| 270 | + { emoji: '❤️', userIds: ['user_101', 'user_102'], count: 2 }, |
| 271 | + ], |
| 272 | + }); |
| 273 | + expect(result.reactions).toHaveLength(2); |
| 274 | + }); |
| 275 | + |
| 276 | + it('should accept internal visibility', () => { |
| 277 | + const result = FeedItemSchema.parse({ |
| 278 | + ...minimalComment, |
| 279 | + visibility: 'internal', |
| 280 | + }); |
| 281 | + expect(result.visibility).toBe('internal'); |
| 282 | + }); |
| 283 | + |
| 284 | + it('should accept system actor with source', () => { |
| 285 | + const result = FeedItemSchema.parse({ |
| 286 | + ...minimalComment, |
| 287 | + actor: { type: 'system', id: 'sys_001', source: 'API' }, |
| 288 | + }); |
| 289 | + expect(result.actor.type).toBe('system'); |
| 290 | + expect(result.actor.source).toBe('API'); |
| 291 | + }); |
| 292 | + |
| 293 | + it('should accept all feed item types', () => { |
| 294 | + const types = [ |
| 295 | + 'comment', 'field_change', 'task', 'event', 'email', 'call', |
| 296 | + 'note', 'file', 'record_create', 'record_delete', 'approval', |
| 297 | + 'sharing', 'system', |
| 298 | + ]; |
| 299 | + types.forEach(type => { |
| 300 | + expect(() => FeedItemSchema.parse({ |
| 301 | + id: `feed_${type}`, |
| 302 | + type, |
| 303 | + object: 'account', |
| 304 | + recordId: 'rec_1', |
| 305 | + actor: { type: 'user', id: 'user_1' }, |
| 306 | + createdAt: '2026-01-15T10:00:00Z', |
| 307 | + })).not.toThrow(); |
| 308 | + }); |
| 309 | + }); |
| 310 | + |
| 311 | + it('should apply default values', () => { |
| 312 | + const result = FeedItemSchema.parse({ |
| 313 | + id: 'feed_def', |
| 314 | + type: 'note', |
| 315 | + object: 'lead', |
| 316 | + recordId: 'rec_1', |
| 317 | + actor: { type: 'user', id: 'user_1' }, |
| 318 | + createdAt: '2026-01-15T10:00:00Z', |
| 319 | + }); |
| 320 | + expect(result.replyCount).toBe(0); |
| 321 | + expect(result.visibility).toBe('public'); |
| 322 | + expect(result.isEdited).toBe(false); |
| 323 | + }); |
| 324 | + |
| 325 | + it('should reject without required fields', () => { |
| 326 | + expect(() => FeedItemSchema.parse({})).toThrow(); |
| 327 | + expect(() => FeedItemSchema.parse({ id: 'x' })).toThrow(); |
| 328 | + expect(() => FeedItemSchema.parse({ id: 'x', type: 'comment' })).toThrow(); |
| 329 | + }); |
| 330 | + |
| 331 | + it('should reject invalid datetime format', () => { |
| 332 | + expect(() => FeedItemSchema.parse({ |
| 333 | + ...minimalComment, |
| 334 | + createdAt: 'not-a-date', |
| 335 | + })).toThrow(); |
| 336 | + }); |
| 337 | +}); |
0 commit comments