@cdevhub/ngx-chat 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,950 @@
1
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @fileoverview Test harness for ChatComponent.
5
+ * Provides ergonomic methods for testing chat interactions.
6
+ * @module ngx-chat/testing
7
+ */
8
+ /**
9
+ * Test harness for ChatComponent.
10
+ *
11
+ * Provides methods for:
12
+ * - Sending messages
13
+ * - Typing in the input
14
+ * - Querying message state
15
+ * - Checking component state
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const harness = await loader.getHarness(ChatHarness);
20
+ * await harness.typeMessage('Hello');
21
+ * await harness.sendMessage('Hello');
22
+ * const count = await harness.getMessageCount();
23
+ * ```
24
+ */
25
+ class ChatHarness extends ComponentHarness {
26
+ static hostSelector = 'ngx-chat';
27
+ // ===========================================================================
28
+ // Element Locators
29
+ // ===========================================================================
30
+ _textarea = this.locatorFor('.ngx-chat-sender__textarea');
31
+ _sendButton = this.locatorFor('.ngx-chat-sender__send-btn');
32
+ _messagesList = this.locatorForOptional('.ngx-chat__messages-list');
33
+ _messages = this.locatorForAll('.ngx-chat__message-placeholder');
34
+ _messageTexts = this.locatorForAll('.ngx-chat__message-text');
35
+ _typingIndicator = this.locatorForOptional('.ngx-chat__typing');
36
+ _loadingIndicator = this.locatorForOptional('.ngx-chat__loading');
37
+ _emptyState = this.locatorForOptional('.ngx-chat__empty');
38
+ // ===========================================================================
39
+ // Static Methods
40
+ // ===========================================================================
41
+ /**
42
+ * Gets a `HarnessPredicate` that can be used to search for a chat harness
43
+ * that meets certain criteria.
44
+ * @param options Options for filtering which chat instances are considered a match.
45
+ * @returns A predicate for finding matching chat harnesses.
46
+ */
47
+ static with(options = {}) {
48
+ return new HarnessPredicate(ChatHarness, options)
49
+ .addOption('disabled', options.disabled, async (harness, disabled) => {
50
+ return (await harness.isDisabled()) === disabled;
51
+ });
52
+ }
53
+ // ===========================================================================
54
+ // Action Methods
55
+ // ===========================================================================
56
+ /**
57
+ * Types a message and sends it.
58
+ * Combines typeMessage and clicking send button.
59
+ * @param content The message content to send.
60
+ */
61
+ async sendMessage(content) {
62
+ await this.typeMessage(content);
63
+ const sendButton = await this._sendButton();
64
+ await sendButton.click();
65
+ }
66
+ /**
67
+ * Types content into the message input.
68
+ * Clears existing content first.
69
+ * @param content The content to type.
70
+ */
71
+ async typeMessage(content) {
72
+ const textarea = await this._textarea();
73
+ await textarea.clear();
74
+ await textarea.sendKeys(content);
75
+ }
76
+ /**
77
+ * Clears the message input.
78
+ */
79
+ async clearInput() {
80
+ const textarea = await this._textarea();
81
+ await textarea.clear();
82
+ }
83
+ /**
84
+ * Focuses the message input.
85
+ */
86
+ async focusInput() {
87
+ const textarea = await this._textarea();
88
+ await textarea.focus();
89
+ }
90
+ /**
91
+ * Blurs the message input.
92
+ */
93
+ async blurInput() {
94
+ const textarea = await this._textarea();
95
+ await textarea.blur();
96
+ }
97
+ // ===========================================================================
98
+ // Query Methods
99
+ // ===========================================================================
100
+ /**
101
+ * Gets the number of messages displayed.
102
+ * @returns The message count.
103
+ */
104
+ async getMessageCount() {
105
+ const messages = await this._messages();
106
+ return messages.length;
107
+ }
108
+ /**
109
+ * Gets all messages with their content and sender info.
110
+ * @returns Array of message data.
111
+ */
112
+ async getMessages() {
113
+ const messages = await this._messages();
114
+ const messageTexts = await this._messageTexts();
115
+ const result = [];
116
+ for (let i = 0; i < messages.length; i++) {
117
+ const message = messages[i];
118
+ const classes = await message.getAttribute('class');
119
+ let sender = 'other';
120
+ if (classes?.includes('--self')) {
121
+ sender = 'self';
122
+ }
123
+ else if (classes?.includes('--system')) {
124
+ sender = 'system';
125
+ }
126
+ // Get content from corresponding text element
127
+ const content = i < messageTexts.length ? await messageTexts[i].text() : '';
128
+ // Get sender name from full text if present (appears before message content for 'other')
129
+ // The sender name element is separate, so we extract from full message text
130
+ const fullText = await message.text();
131
+ let senderName;
132
+ if (sender === 'other' && fullText !== content && fullText.includes(content)) {
133
+ // Extract sender name (text before content)
134
+ const idx = fullText.indexOf(content);
135
+ if (idx > 0) {
136
+ senderName = fullText.substring(0, idx).trim();
137
+ }
138
+ }
139
+ result.push({ content, sender, senderName });
140
+ }
141
+ return result;
142
+ }
143
+ /**
144
+ * Gets the content of the last message.
145
+ * @returns The last message content, or null if no messages.
146
+ */
147
+ async getLastMessageContent() {
148
+ const messageTexts = await this._messageTexts();
149
+ if (messageTexts.length === 0) {
150
+ return null;
151
+ }
152
+ const lastText = messageTexts[messageTexts.length - 1];
153
+ return await lastText.text();
154
+ }
155
+ /**
156
+ * Gets the current value of the message input.
157
+ * @returns The input value.
158
+ */
159
+ async getInputValue() {
160
+ const textarea = await this._textarea();
161
+ return await textarea.getProperty('value');
162
+ }
163
+ /**
164
+ * Checks if the chat is disabled.
165
+ * @returns True if disabled.
166
+ */
167
+ async isDisabled() {
168
+ const host = await this.host();
169
+ return (await host.hasClass('ngx-chat--disabled'));
170
+ }
171
+ /**
172
+ * Checks if the send button is enabled (can send a message).
173
+ * @returns True if can send.
174
+ */
175
+ async canSend() {
176
+ const sendButton = await this._sendButton();
177
+ const disabled = await sendButton.getProperty('disabled');
178
+ return !disabled;
179
+ }
180
+ /**
181
+ * Checks if the typing indicator is visible.
182
+ * @returns True if typing indicator is shown.
183
+ */
184
+ async isTypingIndicatorVisible() {
185
+ const typing = await this._typingIndicator();
186
+ return typing !== null;
187
+ }
188
+ /**
189
+ * Checks if the loading indicator is visible.
190
+ * @returns True if loading.
191
+ */
192
+ async isLoading() {
193
+ const loading = await this._loadingIndicator();
194
+ return loading !== null;
195
+ }
196
+ /**
197
+ * Checks if the empty state is visible.
198
+ * @returns True if showing empty state.
199
+ */
200
+ async isEmpty() {
201
+ const empty = await this._emptyState();
202
+ return empty !== null;
203
+ }
204
+ /**
205
+ * Checks if any messages are displayed.
206
+ * @returns True if messages are visible.
207
+ */
208
+ async hasMessages() {
209
+ const list = await this._messagesList();
210
+ return list !== null;
211
+ }
212
+ /**
213
+ * Gets the placeholder text of the input.
214
+ * @returns The placeholder text.
215
+ */
216
+ async getPlaceholder() {
217
+ const textarea = await this._textarea();
218
+ return await textarea.getProperty('placeholder');
219
+ }
220
+ /**
221
+ * Checks if the input has focus.
222
+ * @returns True if input is focused.
223
+ */
224
+ async isInputFocused() {
225
+ const textarea = await this._textarea();
226
+ return await textarea.isFocused();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * @fileoverview Mock data and test scenarios for ngx-chat testing.
232
+ * Provides pre-built scenarios and generator functions for comprehensive testing.
233
+ * @module ngx-chat/testing
234
+ */
235
+ // ============================================================================
236
+ // Helper Functions
237
+ // ============================================================================
238
+ /**
239
+ * Creates a unique ID with optional prefix.
240
+ */
241
+ function createId(prefix = 'msg') {
242
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
243
+ }
244
+ /**
245
+ * Creates a timestamp offset from now by the given milliseconds.
246
+ */
247
+ function createTimestamp(offsetMs = 0) {
248
+ return new Date(Date.now() - offsetMs);
249
+ }
250
+ // ============================================================================
251
+ // Sample Actions
252
+ // ============================================================================
253
+ /**
254
+ * Sample confirm action for testing.
255
+ */
256
+ const SAMPLE_CONFIRM_ACTION = {
257
+ type: 'confirm',
258
+ id: 'confirm-1',
259
+ confirmText: 'Yes',
260
+ cancelText: 'No',
261
+ confirmVariant: 'primary',
262
+ cancelVariant: 'ghost',
263
+ };
264
+ /**
265
+ * Sample select action for testing.
266
+ */
267
+ const SAMPLE_SELECT_ACTION = {
268
+ type: 'select',
269
+ id: 'select-1',
270
+ label: 'Choose an option:',
271
+ placeholder: 'Select...',
272
+ options: [
273
+ { id: 'opt-1', label: 'Option One' },
274
+ { id: 'opt-2', label: 'Option Two' },
275
+ { id: 'opt-3', label: 'Option Three' },
276
+ ],
277
+ searchable: true,
278
+ };
279
+ /**
280
+ * Sample multi-select action for testing.
281
+ */
282
+ const SAMPLE_MULTI_SELECT_ACTION = {
283
+ type: 'multi-select',
284
+ id: 'multi-select-1',
285
+ label: 'Select multiple:',
286
+ options: [
287
+ { id: 'feature-1', label: 'Feature A' },
288
+ { id: 'feature-2', label: 'Feature B' },
289
+ { id: 'feature-3', label: 'Feature C' },
290
+ { id: 'feature-4', label: 'Feature D' },
291
+ ],
292
+ minSelect: 1,
293
+ maxSelect: 3,
294
+ submitText: 'Apply',
295
+ };
296
+ /**
297
+ * Sample buttons action for testing.
298
+ */
299
+ const SAMPLE_BUTTONS_ACTION = {
300
+ type: 'buttons',
301
+ id: 'buttons-1',
302
+ layout: 'horizontal',
303
+ buttons: [
304
+ { id: 'btn-yes', label: 'Yes', variant: 'primary' },
305
+ { id: 'btn-no', label: 'No', variant: 'secondary' },
306
+ { id: 'btn-maybe', label: 'Maybe', variant: 'ghost' },
307
+ ],
308
+ };
309
+ // ============================================================================
310
+ // Sample Attachments
311
+ // ============================================================================
312
+ /**
313
+ * Sample image attachment.
314
+ */
315
+ const SAMPLE_IMAGE_ATTACHMENT = {
316
+ id: 'att-img-1',
317
+ type: 'image',
318
+ url: 'https://example.com/image.jpg',
319
+ name: 'photo.jpg',
320
+ size: 102400,
321
+ mimeType: 'image/jpeg',
322
+ thumbnail: 'https://example.com/thumb.jpg',
323
+ dimensions: { width: 1920, height: 1080 },
324
+ };
325
+ /**
326
+ * Sample file attachment.
327
+ */
328
+ const SAMPLE_FILE_ATTACHMENT = {
329
+ id: 'att-file-1',
330
+ type: 'file',
331
+ url: 'https://example.com/document.pdf',
332
+ name: 'document.pdf',
333
+ size: 2048000,
334
+ mimeType: 'application/pdf',
335
+ };
336
+ /**
337
+ * Sample video attachment.
338
+ */
339
+ const SAMPLE_VIDEO_ATTACHMENT = {
340
+ id: 'att-video-1',
341
+ type: 'video',
342
+ url: 'https://example.com/video.mp4',
343
+ name: 'recording.mp4',
344
+ size: 10485760,
345
+ mimeType: 'video/mp4',
346
+ thumbnail: 'https://example.com/video-thumb.jpg',
347
+ dimensions: { width: 1280, height: 720 },
348
+ duration: 120,
349
+ };
350
+ /**
351
+ * Sample audio attachment.
352
+ */
353
+ const SAMPLE_AUDIO_ATTACHMENT = {
354
+ id: 'att-audio-1',
355
+ type: 'audio',
356
+ url: 'https://example.com/audio.mp3',
357
+ name: 'voice-note.mp3',
358
+ size: 1024000,
359
+ mimeType: 'audio/mpeg',
360
+ duration: 45,
361
+ };
362
+ // ============================================================================
363
+ // Sample Errors
364
+ // ============================================================================
365
+ /**
366
+ * Sample network error.
367
+ */
368
+ const SAMPLE_NETWORK_ERROR = {
369
+ code: 'NETWORK_ERROR',
370
+ message: 'Network connection failed',
371
+ retryable: true,
372
+ retryCount: 0,
373
+ };
374
+ /**
375
+ * Sample timeout error.
376
+ */
377
+ const SAMPLE_TIMEOUT_ERROR = {
378
+ code: 'TIMEOUT',
379
+ message: 'Request timed out',
380
+ retryable: true,
381
+ retryCount: 1,
382
+ lastRetryAt: new Date(),
383
+ };
384
+ /**
385
+ * Sample rate limit error.
386
+ */
387
+ const SAMPLE_RATE_LIMIT_ERROR = {
388
+ code: 'RATE_LIMITED',
389
+ message: 'Too many requests. Please wait.',
390
+ retryable: true,
391
+ retryCount: 0,
392
+ };
393
+ // ============================================================================
394
+ // Mock Scenarios
395
+ // ============================================================================
396
+ /**
397
+ * Pre-built test scenarios for common testing needs.
398
+ * Each scenario provides a complete, valid ChatMessage[] array.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * // In a test
403
+ * const messages = MOCK_SCENARIOS.simpleConversation;
404
+ * fixture.componentRef.setInput('messages', messages);
405
+ * ```
406
+ */
407
+ const MOCK_SCENARIOS = {
408
+ /**
409
+ * Empty message list for testing empty states.
410
+ */
411
+ empty: [],
412
+ /**
413
+ * Single message from self.
414
+ */
415
+ singleMessage: [
416
+ {
417
+ id: 'msg-single-1',
418
+ sender: 'self',
419
+ content: 'Hello, world!',
420
+ timestamp: createTimestamp(0),
421
+ status: 'sent',
422
+ },
423
+ ],
424
+ /**
425
+ * Simple back-and-forth conversation.
426
+ */
427
+ simpleConversation: [
428
+ {
429
+ id: 'msg-conv-1',
430
+ sender: 'self',
431
+ content: 'Hi there!',
432
+ timestamp: createTimestamp(300000),
433
+ status: 'read',
434
+ },
435
+ {
436
+ id: 'msg-conv-2',
437
+ sender: 'other',
438
+ content: 'Hello! How can I help you today?',
439
+ timestamp: createTimestamp(240000),
440
+ senderName: 'Assistant',
441
+ avatar: 'https://example.com/bot-avatar.png',
442
+ },
443
+ {
444
+ id: 'msg-conv-3',
445
+ sender: 'self',
446
+ content: 'I have a question about my order.',
447
+ timestamp: createTimestamp(180000),
448
+ status: 'read',
449
+ },
450
+ {
451
+ id: 'msg-conv-4',
452
+ sender: 'other',
453
+ content: 'Of course! Please provide your order number and I\'ll look it up for you.',
454
+ timestamp: createTimestamp(120000),
455
+ senderName: 'Assistant',
456
+ },
457
+ {
458
+ id: 'msg-conv-5',
459
+ sender: 'self',
460
+ content: 'It\'s #12345',
461
+ timestamp: createTimestamp(60000),
462
+ status: 'delivered',
463
+ },
464
+ ],
465
+ /**
466
+ * Messages with various actions attached.
467
+ */
468
+ withActions: [
469
+ {
470
+ id: 'msg-action-1',
471
+ sender: 'other',
472
+ content: 'Would you like to proceed with the order?',
473
+ timestamp: createTimestamp(180000),
474
+ senderName: 'Assistant',
475
+ actions: [SAMPLE_CONFIRM_ACTION],
476
+ },
477
+ {
478
+ id: 'msg-action-2',
479
+ sender: 'other',
480
+ content: 'Please select your preferred shipping method:',
481
+ timestamp: createTimestamp(120000),
482
+ senderName: 'Assistant',
483
+ actions: [SAMPLE_SELECT_ACTION],
484
+ },
485
+ ],
486
+ /**
487
+ * Message with multi-select action.
488
+ */
489
+ withMultiSelect: [
490
+ {
491
+ id: 'msg-multi-1',
492
+ sender: 'other',
493
+ content: 'Which features would you like to enable?',
494
+ timestamp: createTimestamp(60000),
495
+ senderName: 'Assistant',
496
+ actions: [SAMPLE_MULTI_SELECT_ACTION],
497
+ },
498
+ ],
499
+ /**
500
+ * Message with buttons action.
501
+ */
502
+ withButtons: [
503
+ {
504
+ id: 'msg-btn-1',
505
+ sender: 'other',
506
+ content: 'How would you rate your experience?',
507
+ timestamp: createTimestamp(60000),
508
+ senderName: 'Assistant',
509
+ actions: [SAMPLE_BUTTONS_ACTION],
510
+ },
511
+ ],
512
+ /**
513
+ * Messages with error states.
514
+ */
515
+ withErrors: [
516
+ {
517
+ id: 'msg-err-1',
518
+ sender: 'self',
519
+ content: 'This message failed to send',
520
+ timestamp: createTimestamp(120000),
521
+ status: 'error',
522
+ error: SAMPLE_NETWORK_ERROR,
523
+ },
524
+ {
525
+ id: 'msg-err-2',
526
+ sender: 'self',
527
+ content: 'This one timed out',
528
+ timestamp: createTimestamp(60000),
529
+ status: 'error',
530
+ error: SAMPLE_TIMEOUT_ERROR,
531
+ },
532
+ {
533
+ id: 'msg-err-3',
534
+ sender: 'self',
535
+ content: 'Rate limited',
536
+ timestamp: createTimestamp(0),
537
+ status: 'error',
538
+ error: SAMPLE_RATE_LIMIT_ERROR,
539
+ },
540
+ ],
541
+ /**
542
+ * Messages with file attachments.
543
+ */
544
+ withAttachments: [
545
+ {
546
+ id: 'msg-att-1',
547
+ sender: 'self',
548
+ content: 'Here is the photo you requested',
549
+ timestamp: createTimestamp(180000),
550
+ status: 'sent',
551
+ attachments: [SAMPLE_IMAGE_ATTACHMENT],
552
+ },
553
+ {
554
+ id: 'msg-att-2',
555
+ sender: 'other',
556
+ content: 'Thanks! Here is the document.',
557
+ timestamp: createTimestamp(120000),
558
+ senderName: 'Assistant',
559
+ attachments: [SAMPLE_FILE_ATTACHMENT],
560
+ },
561
+ {
562
+ id: 'msg-att-3',
563
+ sender: 'self',
564
+ content: 'Check out this video and audio',
565
+ timestamp: createTimestamp(60000),
566
+ status: 'delivered',
567
+ attachments: [SAMPLE_VIDEO_ATTACHMENT, SAMPLE_AUDIO_ATTACHMENT],
568
+ },
569
+ ],
570
+ /**
571
+ * Long conversation with 100 messages for scroll testing.
572
+ */
573
+ longConversation: generateConversation(100),
574
+ /**
575
+ * Large dataset with 1000 messages for performance testing.
576
+ */
577
+ performanceTest: generateConversation(1000),
578
+ /**
579
+ * RTL content for internationalization testing.
580
+ */
581
+ rtlContent: [
582
+ {
583
+ id: 'msg-rtl-1',
584
+ sender: 'self',
585
+ content: 'مرحبا، كيف حالك؟',
586
+ timestamp: createTimestamp(120000),
587
+ status: 'sent',
588
+ },
589
+ {
590
+ id: 'msg-rtl-2',
591
+ sender: 'other',
592
+ content: 'أنا بخير، شكرا لك! كيف يمكنني مساعدتك اليوم؟',
593
+ timestamp: createTimestamp(60000),
594
+ senderName: 'المساعد',
595
+ },
596
+ {
597
+ id: 'msg-rtl-3',
598
+ sender: 'self',
599
+ content: 'שלום! מה נשמע?',
600
+ timestamp: createTimestamp(0),
601
+ status: 'delivered',
602
+ },
603
+ ],
604
+ /**
605
+ * Mixed content with markdown, code, and special characters.
606
+ */
607
+ mixedContent: [
608
+ {
609
+ id: 'msg-mix-1',
610
+ sender: 'self',
611
+ content: 'Can you show me a code example?',
612
+ timestamp: createTimestamp(180000),
613
+ status: 'read',
614
+ },
615
+ {
616
+ id: 'msg-mix-2',
617
+ sender: 'other',
618
+ content: `Here's a TypeScript example:
619
+
620
+ \`\`\`typescript
621
+ function greet(name: string): string {
622
+ return \`Hello, \${name}!\`;
623
+ }
624
+ \`\`\`
625
+
626
+ You can also use **bold** and *italic* text.`,
627
+ timestamp: createTimestamp(120000),
628
+ senderName: 'Assistant',
629
+ },
630
+ {
631
+ id: 'msg-mix-3',
632
+ sender: 'other',
633
+ content: 'Special characters: <>&"\' and emoji: 😀🎉',
634
+ timestamp: createTimestamp(60000),
635
+ senderName: 'Assistant',
636
+ },
637
+ ],
638
+ /**
639
+ * System messages for notifications and status updates.
640
+ */
641
+ systemMessages: [
642
+ {
643
+ id: 'msg-sys-1',
644
+ sender: 'system',
645
+ content: 'Chat session started',
646
+ timestamp: createTimestamp(300000),
647
+ },
648
+ {
649
+ id: 'msg-sys-2',
650
+ sender: 'self',
651
+ content: 'Hello!',
652
+ timestamp: createTimestamp(240000),
653
+ status: 'sent',
654
+ },
655
+ {
656
+ id: 'msg-sys-3',
657
+ sender: 'system',
658
+ content: 'Assistant joined the conversation',
659
+ timestamp: createTimestamp(180000),
660
+ },
661
+ {
662
+ id: 'msg-sys-4',
663
+ sender: 'other',
664
+ content: 'Hi there! How can I help?',
665
+ timestamp: createTimestamp(120000),
666
+ senderName: 'Assistant',
667
+ },
668
+ {
669
+ id: 'msg-sys-5',
670
+ sender: 'system',
671
+ content: 'This conversation will end in 5 minutes',
672
+ timestamp: createTimestamp(60000),
673
+ },
674
+ ],
675
+ };
676
+ // ============================================================================
677
+ // Generator Functions
678
+ // ============================================================================
679
+ /**
680
+ * Generates a conversation with alternating self/other messages.
681
+ *
682
+ * @param count - Number of messages to generate
683
+ * @returns Array of ChatMessage objects
684
+ *
685
+ * @example
686
+ * ```typescript
687
+ * const messages = generateConversation(50);
688
+ * expect(messages.length).toBe(50);
689
+ * ```
690
+ */
691
+ function generateConversation(count) {
692
+ if (count < 0) {
693
+ throw new Error('Count must be non-negative');
694
+ }
695
+ const messages = [];
696
+ const baseTime = Date.now();
697
+ for (let i = 0; i < count; i++) {
698
+ const isUser = i % 2 === 0;
699
+ const message = {
700
+ id: `msg-gen-${i}`,
701
+ sender: isUser ? 'self' : 'other',
702
+ content: isUser
703
+ ? `User message ${Math.floor(i / 2) + 1}`
704
+ : `Assistant response ${Math.floor(i / 2) + 1}`,
705
+ timestamp: new Date(baseTime - (count - i) * 60000),
706
+ ...(isUser
707
+ ? { status: 'read' }
708
+ : { senderName: 'Assistant' }),
709
+ };
710
+ messages.push(message);
711
+ }
712
+ return messages;
713
+ }
714
+ /**
715
+ * Generates messages with various action types attached.
716
+ *
717
+ * @param count - Number of messages to generate (each will have an action)
718
+ * @returns Array of ChatMessage objects with actions
719
+ *
720
+ * @example
721
+ * ```typescript
722
+ * const messages = generateWithActions(10);
723
+ * expect(messages.every(m => m.actions?.length)).toBe(true);
724
+ * ```
725
+ */
726
+ function generateWithActions(count) {
727
+ if (count < 0) {
728
+ throw new Error('Count must be non-negative');
729
+ }
730
+ const actions = [
731
+ SAMPLE_CONFIRM_ACTION,
732
+ SAMPLE_SELECT_ACTION,
733
+ SAMPLE_MULTI_SELECT_ACTION,
734
+ SAMPLE_BUTTONS_ACTION,
735
+ ];
736
+ const messages = [];
737
+ const baseTime = Date.now();
738
+ for (let i = 0; i < count; i++) {
739
+ const action = actions[i % actions.length];
740
+ const message = {
741
+ id: `msg-action-gen-${i}`,
742
+ sender: 'other',
743
+ content: `Message with ${action.type} action (#${i + 1})`,
744
+ timestamp: new Date(baseTime - (count - i) * 60000),
745
+ senderName: 'Assistant',
746
+ actions: [{ ...action, id: `${action.id}-${i}` }],
747
+ };
748
+ messages.push(message);
749
+ }
750
+ return messages;
751
+ }
752
+ /**
753
+ * Generates messages with various attachment types.
754
+ *
755
+ * @param count - Number of messages to generate
756
+ * @returns Array of ChatMessage objects with attachments
757
+ */
758
+ function generateWithAttachments(count) {
759
+ if (count < 0) {
760
+ throw new Error('Count must be non-negative');
761
+ }
762
+ const attachments = [
763
+ SAMPLE_IMAGE_ATTACHMENT,
764
+ SAMPLE_FILE_ATTACHMENT,
765
+ SAMPLE_VIDEO_ATTACHMENT,
766
+ SAMPLE_AUDIO_ATTACHMENT,
767
+ ];
768
+ const messages = [];
769
+ const baseTime = Date.now();
770
+ for (let i = 0; i < count; i++) {
771
+ const attachment = attachments[i % attachments.length];
772
+ const isUser = i % 2 === 0;
773
+ const message = {
774
+ id: `msg-att-gen-${i}`,
775
+ sender: isUser ? 'self' : 'other',
776
+ content: `Message with ${attachment.type} attachment`,
777
+ timestamp: new Date(baseTime - (count - i) * 60000),
778
+ ...(isUser
779
+ ? { status: 'sent' }
780
+ : { senderName: 'Assistant' }),
781
+ attachments: [{ ...attachment, id: `${attachment.id}-${i}` }],
782
+ };
783
+ messages.push(message);
784
+ }
785
+ return messages;
786
+ }
787
+ // ============================================================================
788
+ // Wait Utilities
789
+ // ============================================================================
790
+ /**
791
+ * Default timeout for wait operations in milliseconds.
792
+ */
793
+ const DEFAULT_WAIT_TIMEOUT = 5000;
794
+ /**
795
+ * Default polling interval for wait operations in milliseconds.
796
+ */
797
+ const DEFAULT_POLL_INTERVAL = 50;
798
+ /**
799
+ * Waits for the chat harness to display a specific number of messages.
800
+ *
801
+ * @param harness - The ChatHarness instance
802
+ * @param count - Expected number of messages
803
+ * @param timeout - Maximum wait time in milliseconds (default: 5000)
804
+ * @returns Promise that resolves when count is reached or rejects on timeout
805
+ *
806
+ * @example
807
+ * ```typescript
808
+ * const harness = await loader.getHarness(ChatHarness);
809
+ * fixture.componentRef.setInput('messages', generateConversation(5));
810
+ * await waitForMessages(harness, 5);
811
+ * ```
812
+ */
813
+ async function waitForMessages(harness, count, timeout = DEFAULT_WAIT_TIMEOUT) {
814
+ const startTime = Date.now();
815
+ while (Date.now() - startTime < timeout) {
816
+ const currentCount = await harness.getMessageCount();
817
+ if (currentCount >= count) {
818
+ return;
819
+ }
820
+ await sleep(DEFAULT_POLL_INTERVAL);
821
+ }
822
+ const finalCount = await harness.getMessageCount();
823
+ throw new Error(`Timeout waiting for ${count} messages. Current count: ${finalCount}`);
824
+ }
825
+ /**
826
+ * Waits for the typing indicator to reach a specific visibility state.
827
+ *
828
+ * @param harness - The ChatHarness instance
829
+ * @param visible - Expected visibility state (true = visible, false = hidden)
830
+ * @param timeout - Maximum wait time in milliseconds (default: 5000)
831
+ * @returns Promise that resolves when state is reached or rejects on timeout
832
+ *
833
+ * @example
834
+ * ```typescript
835
+ * const harness = await loader.getHarness(ChatHarness);
836
+ * fixture.componentRef.setInput('isTyping', true);
837
+ * await waitForTypingIndicator(harness, true);
838
+ * ```
839
+ */
840
+ async function waitForTypingIndicator(harness, visible, timeout = DEFAULT_WAIT_TIMEOUT) {
841
+ const startTime = Date.now();
842
+ while (Date.now() - startTime < timeout) {
843
+ const isVisible = await harness.isTypingIndicatorVisible();
844
+ if (isVisible === visible) {
845
+ return;
846
+ }
847
+ await sleep(DEFAULT_POLL_INTERVAL);
848
+ }
849
+ const finalState = await harness.isTypingIndicatorVisible();
850
+ throw new Error(`Timeout waiting for typing indicator to be ${visible ? 'visible' : 'hidden'}. Current: ${finalState ? 'visible' : 'hidden'}`);
851
+ }
852
+ /**
853
+ * Waits for the loading indicator to reach a specific visibility state.
854
+ *
855
+ * @param harness - The ChatHarness instance
856
+ * @param visible - Expected visibility state
857
+ * @param timeout - Maximum wait time in milliseconds (default: 5000)
858
+ */
859
+ async function waitForLoading(harness, visible, timeout = DEFAULT_WAIT_TIMEOUT) {
860
+ const startTime = Date.now();
861
+ while (Date.now() - startTime < timeout) {
862
+ const isLoading = await harness.isLoading();
863
+ if (isLoading === visible) {
864
+ return;
865
+ }
866
+ await sleep(DEFAULT_POLL_INTERVAL);
867
+ }
868
+ const finalState = await harness.isLoading();
869
+ throw new Error(`Timeout waiting for loading to be ${visible ? 'visible' : 'hidden'}. Current: ${finalState ? 'visible' : 'hidden'}`);
870
+ }
871
+ /**
872
+ * Waits for the empty state to reach a specific visibility state.
873
+ *
874
+ * @param harness - The ChatHarness instance
875
+ * @param visible - Expected visibility state
876
+ * @param timeout - Maximum wait time in milliseconds (default: 5000)
877
+ */
878
+ async function waitForEmpty(harness, visible, timeout = DEFAULT_WAIT_TIMEOUT) {
879
+ const startTime = Date.now();
880
+ while (Date.now() - startTime < timeout) {
881
+ const isEmpty = await harness.isEmpty();
882
+ if (isEmpty === visible) {
883
+ return;
884
+ }
885
+ await sleep(DEFAULT_POLL_INTERVAL);
886
+ }
887
+ const finalState = await harness.isEmpty();
888
+ throw new Error(`Timeout waiting for empty state to be ${visible ? 'visible' : 'hidden'}. Current: ${finalState ? 'visible' : 'hidden'}`);
889
+ }
890
+ /**
891
+ * Waits for the send button to become enabled or disabled.
892
+ *
893
+ * @param harness - The ChatHarness instance
894
+ * @param enabled - Expected enabled state
895
+ * @param timeout - Maximum wait time in milliseconds (default: 5000)
896
+ */
897
+ async function waitForSendEnabled(harness, enabled, timeout = DEFAULT_WAIT_TIMEOUT) {
898
+ const startTime = Date.now();
899
+ while (Date.now() - startTime < timeout) {
900
+ const canSend = await harness.canSend();
901
+ if (canSend === enabled) {
902
+ return;
903
+ }
904
+ await sleep(DEFAULT_POLL_INTERVAL);
905
+ }
906
+ const finalState = await harness.canSend();
907
+ throw new Error(`Timeout waiting for send button to be ${enabled ? 'enabled' : 'disabled'}. Current: ${finalState ? 'enabled' : 'disabled'}`);
908
+ }
909
+ // ============================================================================
910
+ // Internal Utilities
911
+ // ============================================================================
912
+ /**
913
+ * Simple sleep utility for polling.
914
+ */
915
+ function sleep(ms) {
916
+ return new Promise(resolve => setTimeout(resolve, ms));
917
+ }
918
+
919
+ /**
920
+ * @fileoverview Internal testing module exports.
921
+ * @module ngx-chat/testing
922
+ */
923
+ // Harnesses
924
+
925
+ /**
926
+ * @fileoverview Public API for ngx-chat/testing secondary entry point.
927
+ *
928
+ * This entry point provides testing utilities for consumers of the ngx-chat library.
929
+ * Import from 'ngx-chat/testing' to access harnesses, mock data, and test utilities.
930
+ *
931
+ * @example
932
+ * ```typescript
933
+ * import { ChatHarness, MOCK_SCENARIOS, waitForMessages } from 'ngx-chat/testing';
934
+ *
935
+ * // In your tests
936
+ * const harness = await loader.getHarness(ChatHarness);
937
+ * fixture.componentRef.setInput('messages', MOCK_SCENARIOS.simpleConversation);
938
+ * await waitForMessages(harness, 5);
939
+ * ```
940
+ *
941
+ * @module ngx-chat/testing
942
+ */
943
+ // Re-export all testing utilities
944
+
945
+ /**
946
+ * Generated bundle index. Do not edit.
947
+ */
948
+
949
+ export { ChatHarness, DEFAULT_POLL_INTERVAL, DEFAULT_WAIT_TIMEOUT, MOCK_SCENARIOS, SAMPLE_AUDIO_ATTACHMENT, SAMPLE_BUTTONS_ACTION, SAMPLE_CONFIRM_ACTION, SAMPLE_FILE_ATTACHMENT, SAMPLE_IMAGE_ATTACHMENT, SAMPLE_MULTI_SELECT_ACTION, SAMPLE_NETWORK_ERROR, SAMPLE_RATE_LIMIT_ERROR, SAMPLE_SELECT_ACTION, SAMPLE_TIMEOUT_ERROR, SAMPLE_VIDEO_ATTACHMENT, generateConversation, generateWithActions, generateWithAttachments, waitForEmpty, waitForLoading, waitForMessages, waitForSendEnabled, waitForTypingIndicator };
950
+ //# sourceMappingURL=cdevhub-ngx-chat-testing.mjs.map