@chat21/chat21-web-widget 5.1.34-rc1 → 5.1.34

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.
Files changed (193) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +13 -23
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
  3. package/CHANGELOG.md +8 -129
  4. package/Dockerfile +5 -4
  5. package/angular.json +3 -21
  6. package/docs/changelog/this-branch.md +0 -36
  7. package/env.sample +2 -3
  8. package/nginx.conf +2 -22
  9. package/package.json +3 -10
  10. package/src/app/app.component.html +2 -2
  11. package/src/app/app.component.scss +14 -25
  12. package/src/app/app.component.spec.ts +6 -21
  13. package/src/app/app.component.ts +9 -10
  14. package/src/app/app.module.ts +0 -13
  15. package/src/app/component/conversation-detail/conversation/conversation.component.html +11 -25
  16. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -40
  17. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +75 -644
  18. package/src/app/component/conversation-detail/conversation/conversation.component.ts +14 -100
  19. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +13 -25
  20. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +5 -123
  21. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +0 -1
  22. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +10 -23
  23. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -19
  24. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +149 -242
  25. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -8
  26. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +3 -53
  27. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +96 -200
  28. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +6 -211
  29. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +78 -452
  30. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +76 -291
  31. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +53 -113
  32. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +4 -12
  33. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +29 -274
  34. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +9 -23
  35. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +8 -80
  36. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +23 -29
  37. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +16 -185
  38. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +14 -34
  39. package/src/app/component/error-alert/error-alert.component.spec.ts +5 -65
  40. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +7 -16
  41. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +0 -21
  42. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +7 -89
  43. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  44. package/src/app/component/form/form-builder/form-builder.component.spec.ts +21 -163
  45. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +4 -8
  46. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +5 -10
  47. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +16 -90
  48. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +0 -26
  49. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +11 -45
  50. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +6 -24
  51. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +5 -14
  52. package/src/app/component/form/inputs/form-text/form-text.component.html +12 -14
  53. package/src/app/component/form/inputs/form-text/form-text.component.scss +1 -11
  54. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +17 -113
  55. package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -35
  56. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +11 -13
  57. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +5 -6
  58. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +13 -149
  59. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +0 -26
  60. package/src/app/component/form/prechat-form/prechat-form.component.html +11 -14
  61. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +10 -102
  62. package/src/app/component/form/prechat-form/prechat-form.component.ts +1 -8
  63. package/src/app/component/home/home.component.html +31 -38
  64. package/src/app/component/home/home.component.scss +2 -4
  65. package/src/app/component/home/home.component.spec.ts +11 -226
  66. package/src/app/component/home-conversations/home-conversations.component.html +26 -30
  67. package/src/app/component/home-conversations/home-conversations.component.scss +0 -3
  68. package/src/app/component/home-conversations/home-conversations.component.spec.ts +36 -212
  69. package/src/app/component/last-message/last-message.component.html +9 -15
  70. package/src/app/component/last-message/last-message.component.scss +2 -16
  71. package/src/app/component/last-message/last-message.component.spec.ts +23 -204
  72. package/src/app/component/last-message/last-message.component.ts +1 -4
  73. package/src/app/component/launcher-button/launcher-button.component.html +13 -8
  74. package/src/app/component/launcher-button/launcher-button.component.spec.ts +8 -104
  75. package/src/app/component/list-all-conversations/list-all-conversations.component.html +17 -12
  76. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +0 -2
  77. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  78. package/src/app/component/menu-options/menu-options.component.html +20 -30
  79. package/src/app/component/menu-options/menu-options.component.spec.ts +9 -125
  80. package/src/app/component/message/audio/audio.component.html +15 -13
  81. package/src/app/component/message/audio/audio.component.spec.ts +5 -140
  82. package/src/app/component/message/audio/audio.component.ts +5 -1
  83. package/src/app/component/message/avatar/avatar.component.html +2 -2
  84. package/src/app/component/message/avatar/avatar.component.spec.ts +7 -99
  85. package/src/app/component/message/bubble-message/bubble-message.component.html +51 -38
  86. package/src/app/component/message/bubble-message/bubble-message.component.scss +1 -54
  87. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +57 -154
  88. package/src/app/component/message/bubble-message/bubble-message.component.ts +11 -89
  89. package/src/app/component/message/buttons/action-button/action-button.component.html +4 -3
  90. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +5 -49
  91. package/src/app/component/message/buttons/link-button/link-button.component.scss +8 -5
  92. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +5 -50
  93. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +5 -44
  94. package/src/app/component/message/carousel/carousel.component.html +16 -29
  95. package/src/app/component/message/carousel/carousel.component.scss +8 -20
  96. package/src/app/component/message/carousel/carousel.component.spec.ts +3 -80
  97. package/src/app/component/message/carousel/carousel.component.ts +0 -16
  98. package/src/app/component/message/frame/frame.component.html +4 -9
  99. package/src/app/component/message/frame/frame.component.spec.ts +15 -34
  100. package/src/app/component/message/frame/frame.component.ts +2 -7
  101. package/src/app/component/message/html/html.component.html +1 -1
  102. package/src/app/component/message/html/html.component.scss +1 -1
  103. package/src/app/component/message/html/html.component.spec.ts +7 -24
  104. package/src/app/component/message/image/image.component.html +10 -12
  105. package/src/app/component/message/image/image.component.scss +0 -16
  106. package/src/app/component/message/image/image.component.spec.ts +15 -101
  107. package/src/app/component/message/image/image.component.ts +51 -90
  108. package/src/app/component/message/info-message/info-message.component.spec.ts +14 -26
  109. package/src/app/component/message/like-unlike/like-unlike.component.html +9 -7
  110. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +3 -31
  111. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +17 -38
  112. package/src/app/component/message/text/text.component.html +3 -3
  113. package/src/app/component/message/text/text.component.scss +86 -80
  114. package/src/app/component/message/text/text.component.spec.ts +13 -106
  115. package/src/app/component/message-attachment/message-attachment.component.spec.ts +13 -134
  116. package/src/app/component/selection-department/selection-department.component.html +23 -21
  117. package/src/app/component/selection-department/selection-department.component.spec.ts +14 -159
  118. package/src/app/component/selection-department/selection-department.component.ts +1 -8
  119. package/src/app/component/send-button/send-button.component.html +13 -5
  120. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  121. package/src/app/component/star-rating-widget/star-rating-widget.component.html +81 -51
  122. package/src/app/directives/tooltip.directive.spec.ts +4 -8
  123. package/src/app/modals/confirm-close/confirm-close.component.html +8 -20
  124. package/src/app/modals/confirm-close/confirm-close.component.scss +0 -3
  125. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +4 -13
  126. package/src/app/modals/confirm-close/confirm-close.component.ts +1 -8
  127. package/src/app/pipe/html-entites-encode.pipe.spec.ts +2 -35
  128. package/src/app/pipe/marked.pipe.spec.ts +2 -38
  129. package/src/app/pipe/marked.pipe.ts +41 -51
  130. package/src/app/providers/app-config.service.ts +2 -4
  131. package/src/app/providers/brand.service.spec.ts +2 -23
  132. package/src/app/providers/brand.service.ts +1 -1
  133. package/src/app/providers/global-settings.service.spec.ts +14 -1009
  134. package/src/app/providers/global-settings.service.ts +2 -82
  135. package/src/app/providers/translator.service.ts +6 -26
  136. package/src/app/sass/_variables.scss +0 -3
  137. package/src/app/sass/animations.scss +1 -19
  138. package/src/app/utils/globals.ts +1 -21
  139. package/src/app/utils/utils-resources.ts +1 -1
  140. package/src/assets/i18n/en.json +99 -106
  141. package/src/assets/i18n/es.json +100 -107
  142. package/src/assets/i18n/fr.json +100 -107
  143. package/src/assets/i18n/it.json +98 -107
  144. package/src/assets/twp/index-dev.html +0 -18
  145. package/src/chat21-core/models/message.ts +1 -2
  146. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +2 -3
  147. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +0 -12
  148. package/src/chat21-core/providers/scripts/script.service.spec.ts +2 -12
  149. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  150. package/src/chat21-core/utils/utils-message.ts +0 -7
  151. package/src/chat21-core/utils/utils.ts +2 -5
  152. package/src/widget-config-template.json +1 -4
  153. package/src/widget-config.json +1 -4
  154. package/tsconfig.json +0 -5
  155. package/.angular-mcp-cache/package.json +0 -1
  156. package/.cursor/angular18-accessibility-auditor-skill.md +0 -442
  157. package/.cursor/mcp.json +0 -15
  158. package/.github/workflows/build.yml +0 -22
  159. package/.github/workflows/playwright.yml +0 -27
  160. package/mocks/voice-websocket-mock/server.cjs +0 -245
  161. package/playwright.config.ts +0 -41
  162. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +0 -46
  163. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +0 -83
  164. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +0 -192
  165. package/src/app/component/form/prechat-form-test-mock.ts +0 -35
  166. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -18
  167. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -65
  168. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +0 -103
  169. package/src/app/component/message/audio-sync/audio-sync.component.ts +0 -643
  170. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +0 -117
  171. package/src/app/providers/tts-audio-playback-coordinator.service.ts +0 -109
  172. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +0 -12
  173. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +0 -171
  174. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +0 -39
  175. package/src/app/providers/voice/audio.types.ts +0 -40
  176. package/src/app/providers/voice/vad.service.spec.ts +0 -28
  177. package/src/app/providers/voice/vad.service.ts +0 -70
  178. package/src/app/providers/voice/voice-streaming.service.spec.ts +0 -23
  179. package/src/app/providers/voice/voice-streaming.service.ts +0 -702
  180. package/src/app/providers/voice/voice-streaming.types.ts +0 -112
  181. package/src/app/providers/voice/voice.service.spec.ts +0 -227
  182. package/src/app/providers/voice/voice.service.ts +0 -973
  183. package/src/app/shims/onnxruntime-web-wasm.ts +0 -4
  184. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +0 -59
  185. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  186. package/src/assets/sounds/keyboard.mp3 +0 -0
  187. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +0 -14
  188. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  189. package/src/assets/vad/vad.worklet.bundle.min.js +0 -1
  190. package/src/chat21-core/providers/chat-manager.spec.ts +0 -72
  191. package/tests/widget-form-rich.spec.ts +0 -67
  192. package/tests/widget-index-dev-settings.spec.ts +0 -52
  193. package/tests/widget-twp-iframe.spec.ts +0 -39
@@ -1,101 +1,49 @@
1
- import { SimpleChange } from '@angular/core';
2
- import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
3
- import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
4
- import { provideHttpClientTesting } from '@angular/common/http/testing';
5
- import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
1
+ import { style } from '@angular/animations';
2
+ import { ChatManager } from './../../../../chat21-core/providers/chat-manager';
3
+ import { Globals } from './../../../utils/globals';
4
+ import { async, ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
5
+ import { FormsModule } from '@angular/forms';
6
6
 
7
7
  import { ConversationFooterComponent } from './conversation-footer.component';
8
- import { ChatManager } from 'src/chat21-core/providers/chat-manager';
9
- import { TypingService } from 'src/chat21-core/providers/abstract/typing.service';
10
- import { UploadService } from 'src/chat21-core/providers/abstract/upload.service';
11
- import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
12
- import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
8
+ import { UploadService } from '../../../../chat21-core/providers/abstract/upload.service';
9
+ import { TypingService } from '../../../../chat21-core/providers/abstract/typing.service';
10
+ import { ConversationsHandlerService } from '../../../../chat21-core/providers/abstract/conversations-handler.service';
11
+ import { ArchivedConversationsHandlerService } from '../../../../chat21-core/providers/abstract/archivedconversations-handler.service';
12
+ import { By } from '@angular/platform-browser';
13
13
  import { NGXLogger } from 'ngx-logger';
14
- import { TYPE_MSG_TEXT } from 'src/chat21-core/utils/constants';
15
- import { ConversationHandlerService } from 'src/chat21-core/providers/abstract/conversation-handler.service';
16
- import { VoiceService } from 'src/app/providers/voice/voice.service';
17
- import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
18
- import { TiledeskAuthService } from 'src/chat21-core/providers/tiledesk/tiledesk-auth.service';
14
+ import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
15
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
19
16
 
20
17
  describe('ConversationFooterComponent', () => {
21
18
  let component: ConversationFooterComponent;
22
19
  let fixture: ComponentFixture<ConversationFooterComponent>;
23
-
24
- const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
25
- const customLogger = new CustomLogger(ngxlogger);
26
-
27
- const voiceServiceMock = {
28
- startSession: () => Promise.resolve(),
29
- stopSession: () => Promise.resolve({ voiceIngressResultUrl: null as string | null }),
30
- audioSegment$: { subscribe: () => ({ unsubscribe: () => undefined }) },
31
- voiceTranscript$: { subscribe: () => ({ unsubscribe: () => undefined }) },
32
- volume$: { subscribe: () => ({ unsubscribe: () => undefined }) },
33
- isAcquisitionBlocked$: { subscribe: () => ({ unsubscribe: () => undefined }) },
34
- };
35
- const ttsMock = { stopAll: () => undefined, isTTSPlaying$: { subscribe: () => ({ unsubscribe: () => undefined }) } };
36
-
37
- const conversationHandlerStub = {
38
- sendMessage: jasmine.createSpy('sendMessage').and.returnValue({ uid: 'm1' }),
39
- };
40
-
41
- const chatManagerStub = {
42
- getConversationHandlerByConversationId: jasmine
43
- .createSpy('getConversationHandlerByConversationId')
44
- .and.returnValue(conversationHandlerStub),
45
- };
46
-
47
- const typingStub = {
48
- setTyping: jasmine.createSpy('setTyping'),
49
- };
50
-
51
- const uploadServiceStub = {
52
- uploadFile: jasmine.createSpy('uploadFile'),
53
- };
54
-
55
- beforeEach(async () => {
56
- LoggerInstance.setInstance(customLogger);
57
-
58
- await TestBed.configureTestingModule({
59
- declarations: [ConversationFooterComponent],
60
- imports: [FormsModule, ReactiveFormsModule],
61
- providers: [
62
- FormBuilder,
63
- { provide: ChatManager, useValue: chatManagerStub },
64
- { provide: TypingService, useValue: typingStub },
65
- { provide: UploadService, useValue: uploadServiceStub as unknown as UploadService },
66
- { provide: VoiceService, useValue: voiceServiceMock },
67
- { provide: TtsAudioPlaybackCoordinator, useValue: ttsMock },
68
- { provide: TiledeskAuthService, useValue: { getTiledeskToken: () => '' } },
69
- provideHttpClient(withInterceptorsFromDi()),
70
- provideHttpClientTesting(),
20
+ let ngxlogger: NGXLogger;
21
+ let customLogger = new CustomLogger(ngxlogger)
22
+
23
+ beforeEach(waitForAsync(() => {
24
+ TestBed.configureTestingModule({
25
+ declarations: [
26
+ ConversationFooterComponent,
71
27
  ],
28
+ imports: [ FormsModule ],
29
+ providers: [
30
+ Globals,
31
+ ChatManager,
32
+ TypingService,
33
+ UploadService,
34
+ ConversationsHandlerService,
35
+ ArchivedConversationsHandlerService
36
+ ]
72
37
  })
73
- .overrideComponent(ConversationFooterComponent, {
74
- set: {
75
- template: `
76
- <textarea id="chat21-main-message-context" [(ngModel)]="textInputTextArea"></textarea>
77
- <div id="chat21-button-send"></div>
78
- <input id="chat21-file" type="file" />
79
- `,
80
- },
81
- })
82
- .compileComponents();
38
+ .compileComponents();
39
+ }));
83
40
 
41
+ beforeEach(() => {
84
42
  fixture = TestBed.createComponent(ConversationFooterComponent);
85
43
  component = fixture.componentInstance;
86
- component.conversationWith = 'support-group-x';
87
- component.senderId = 'user-1';
88
- component.project = { id: 'proj-1' } as any;
89
- component.channelType = 'group';
90
- component.translationMap = new Map([
91
- ['GUEST_LABEL', 'Guest'],
92
- ['LABEL_PLACEHOLDER', 'Write…'],
93
- ]);
94
- component.attributes = {} as any;
95
- component.fileUploadAccept = '';
96
- component.chat21_file = {
97
- nativeElement: { value: '' },
98
- } as any;
44
+ LoggerInstance.setInstance(customLogger)
45
+ let logger = LoggerInstance.getInstance()
46
+ component['logger']= logger
99
47
  fixture.detectChanges();
100
48
  });
101
49
 
@@ -103,376 +51,54 @@ describe('ConversationFooterComponent', () => {
103
51
  expect(component).toBeTruthy();
104
52
  });
105
53
 
106
- describe('ngOnChanges', () => {
107
- it('should load conversation handler when conversationWith changes', () => {
108
- component.ngOnChanges({
109
- conversationWith: new SimpleChange(undefined, 'support-group-x', true),
110
- });
111
- expect(chatManagerStub.getConversationHandlerByConversationId).toHaveBeenCalledWith('support-group-x');
112
- expect(component.conversationHandlerService).toBe(conversationHandlerStub as unknown as ConversationHandlerService);
113
- });
114
-
115
- it('should call restoreTextArea when hideTextReply changes', () => {
116
- const restore = spyOn(component as any, 'restoreTextArea');
117
- component.ngOnChanges({
118
- hideTextReply: new SimpleChange(false, true, false),
119
- });
120
- expect(restore).toHaveBeenCalled();
121
- });
122
- });
123
-
124
- describe('ngAfterViewInit', () => {
125
- it('should enable emoji picker flag', () => {
126
- component.showEmojiPicker = false;
127
- component.ngAfterViewInit();
128
- expect(component.showEmojiPicker).toBe(true);
129
- });
130
- });
131
-
132
- describe('sendMessage', () => {
133
- beforeEach(() => {
134
- component.textInputTextArea = ' hello ';
135
- component.conversationHandlerService = conversationHandlerStub as any;
136
- });
137
-
138
- it('should emit before/after and call handler when text is valid', () => {
139
- spyOn(component.onBeforeMessageSent, 'emit');
140
- spyOn(component.onAfterSendMessage, 'emit');
141
- spyOn(component.onEmojiiPickerShow, 'emit');
142
- component.sendMessage('Hi', TYPE_MSG_TEXT);
143
- expect(component.onBeforeMessageSent.emit).toHaveBeenCalled();
144
- expect(conversationHandlerStub.sendMessage).toHaveBeenCalled();
145
- expect(component.onAfterSendMessage.emit).toHaveBeenCalled();
146
- expect(component.onEmojiiPickerShow.emit).toHaveBeenCalledWith(false);
147
- });
148
-
149
- it('should return early when emoji not allowed and text has emoji', () => {
150
- component.showEmojiFooterButton = false;
151
- component.textInputTextArea = '🙂';
152
- const emitted = jasmine.createSpy();
153
- component.onBeforeMessageSent.emit = emitted;
154
- component.sendMessage('🙂', TYPE_MSG_TEXT);
155
- expect(emitted).not.toHaveBeenCalled();
156
- });
157
- });
158
-
159
- describe('uploadSingle observable success / error', () => {
160
- beforeEach(() => {
161
- component.conversationHandlerService = conversationHandlerStub as any;
162
- component.chat21_file = { nativeElement: { value: '' } } as any;
163
- });
164
-
165
- it('should resolve upload and call sendMessage for image', fakeAsync(() => {
166
- uploadServiceStub.uploadFile.and.resolveTo({
167
- src: 'https://file',
168
- downloadURL: 'https://dl',
169
- });
170
- const meta: any = { name: 'a.png', type: 'image/png', uid: 'u1' };
171
- const file = new File(['x'], 'a.png', { type: 'image/png' });
172
- spyOn(component, 'sendMessage').and.stub();
173
- component.uploadSingle(meta, file, '');
174
- tick();
175
- expect(uploadServiceStub.uploadFile).toHaveBeenCalled();
176
- expect(component.sendMessage).toHaveBeenCalled();
177
- expect(component.isFilePendingToUpload).toBe(false);
178
- }));
179
-
180
- it('should handle upload rejection', fakeAsync(() => {
181
- uploadServiceStub.uploadFile.and.rejectWith(new Error('net'));
182
- const meta: any = { name: 'a.png', type: 'image/png', uid: 'u1' };
183
- const file = new File(['x'], 'a.png', { type: 'image/png' });
184
- component.uploadSingle(meta, file, '');
185
- tick();
186
- expect(component.isFilePendingToUpload).toBe(false);
187
- }));
188
- });
189
-
190
- describe('onSendPressed', () => {
191
- beforeEach(() => {
192
- component.conversationHandlerService = conversationHandlerStub as any;
193
- });
194
-
195
- it('should ignore when emoji alert visible', () => {
196
- component.showAlertEmoji = true;
197
- spyOn(component, 'sendMessage');
198
- component.onSendPressed({ preventDefault: jasmine.createSpy() } as any);
199
- expect(component.sendMessage).not.toHaveBeenCalled();
200
- });
201
-
202
- it('should send trimmed text message', () => {
203
- component.showAlertEmoji = false;
204
- component.isFilePendingToUpload = false;
205
- component.textInputTextArea = 'abc';
206
- spyOn(component, 'sendMessage').and.stub();
207
- component.onSendPressed({ preventDefault: jasmine.createSpy() } as any);
208
- expect(component.sendMessage).toHaveBeenCalledWith('abc', TYPE_MSG_TEXT);
209
- });
210
- });
211
-
212
- describe('checkForEmojii', () => {
213
- it('should block when emoji disabled and text is emoji-only', () => {
214
- component.showEmojiFooterButton = false;
215
- expect(component.checkForEmojii('😀')).toBe(false);
216
- expect(component.showAlertEmoji).toBe(true);
217
- });
218
-
219
- it('should allow when emoji footer enabled', () => {
220
- component.showEmojiFooterButton = true;
221
- expect(component.checkForEmojii('😀')).toBe(true);
222
- });
223
- });
224
-
225
- describe('recording helpers', () => {
226
- it('onStartRecording / onDeleteRecording / onEndRecording / onSendRecording should update flags', () => {
227
- component.onStartRecording();
228
- expect(component.isStartRec).toBe(true);
229
- component.onDeleteRecording();
230
- expect(component.isStartRec).toBe(false);
231
- component.onEndRecording(new Blob());
232
- expect(component.isStopRec).toBe(true);
233
- uploadServiceStub.uploadFile.and.resolveTo({ src: '', downloadURL: '' });
234
- spyOn(component, 'uploadSingle').and.stub();
235
- component.onSendRecording(new Blob(['a'], { type: 'audio/webm' }));
236
- expect(component.uploadSingle).toHaveBeenCalled();
237
- });
238
- });
239
-
240
- describe('onEmojiiPickerClicked', () => {
241
- it('should toggle emoji panel output', () => {
242
- spyOn(component.onEmojiiPickerShow, 'emit');
243
- component.isEmojiiPickerShow = false;
244
- component.onEmojiiPickerClicked();
245
- expect(component.onEmojiiPickerShow.emit).toHaveBeenCalledWith(true);
246
- });
247
- });
248
-
249
- describe('openNewConversation', () => {
250
- it('should emit', () => {
251
- spyOn(component.onNewConversationButtonClicked, 'emit');
252
- component.openNewConversation();
253
- expect(component.onNewConversationButtonClicked.emit).toHaveBeenCalled();
254
- });
255
- });
256
-
257
- describe('reactive forms (pattern)', () => {
258
- it('FormBuilder group validates required email-like field', () => {
259
- const fb = TestBed.inject(FormBuilder);
260
- const form = fb.group({
261
- email: ['bad', []],
262
- });
263
- form.setValidators(() => null);
264
- expect(form.value.email).toBe('bad');
265
- form.patchValue({ email: 'user@test.com' });
266
- expect(form.valid || form.value.email.includes('@')).toBe(true);
267
- });
268
- });
269
-
270
- describe('setWritingMessages', () => {
271
- it('should forward to TypingService', () => {
272
- component.setWritingMessages('hi');
273
- expect(typingStub.setTyping).toHaveBeenCalledWith(
274
- 'support-group-x',
275
- 'hi',
276
- 'user-1',
277
- undefined,
278
- );
279
- });
280
- });
281
-
282
- describe('keyboard and paste handlers', () => {
283
- it('onkeydown Enter should send non-empty text', () => {
284
- const ta = document.getElementById('chat21-main-message-context') as HTMLTextAreaElement;
285
- ta.value = 'hello';
286
- spyOn(component, 'sendMessage');
287
- const ev: any = { which: 13, keyCode: 13, preventDefault: jasmine.createSpy() };
288
- component.onkeydown(ev);
289
- expect(ev.preventDefault).toHaveBeenCalled();
290
- expect(component.sendMessage).toHaveBeenCalledWith('hello', TYPE_MSG_TEXT);
291
- });
292
-
293
- it('onkeydown Tab should prevent default', () => {
294
- const ev: any = { which: 9, keyCode: 9, preventDefault: jasmine.createSpy() };
295
- component.onkeydown(ev);
296
- expect(ev.preventDefault).toHaveBeenCalled();
297
- });
298
-
299
- it('onkeydown modifier+Enter should not send (browser newline)', () => {
300
- spyOn(component, 'sendMessage');
301
- const ev: any = {
302
- which: 13,
303
- keyCode: 13,
304
- shiftKey: true,
305
- preventDefault: jasmine.createSpy(),
306
- };
307
- component.onkeydown(ev);
308
- expect(ev.preventDefault).not.toHaveBeenCalled();
309
- expect(component.sendMessage).not.toHaveBeenCalled();
310
- });
311
-
312
- it('onkeydown Enter should not send when emoji alert is visible', () => {
313
- const ta = document.getElementById('chat21-main-message-context') as HTMLTextAreaElement;
314
- ta.value = 'x';
315
- component.showAlertEmoji = true;
316
- spyOn(component, 'sendMessage');
317
- const ev: any = { which: 13, keyCode: 13, preventDefault: jasmine.createSpy() };
318
- component.onkeydown(ev);
319
- expect(ev.preventDefault).toHaveBeenCalled();
320
- expect(component.sendMessage).not.toHaveBeenCalled();
321
- });
322
- });
323
-
324
- describe('paste / drop', () => {
325
- it('onPaste routes image clipboard items through detectFiles', () => {
326
- const file = new File(['x'], 'paste.png', { type: 'image/png' });
327
- const item = { type: 'image/png', getAsFile: () => file };
328
- spyOn(component, 'detectFiles');
329
- spyOn(component as any, 'restoreTextArea');
330
- component.onPaste({
331
- clipboardData: { items: [item] },
332
- } as any);
333
- expect(component.detectFiles).toHaveBeenCalled();
334
- });
335
-
336
- it('onDrop forwards dataTransfer.files', () => {
337
- spyOn(component, 'detectFiles');
338
- const f = new File(['z'], 'drop.bin', { type: 'application/octet-stream' });
339
- const dt = new DataTransfer();
340
- dt.items.add(f);
341
- component.onDrop({ dataTransfer: { files: dt.files } } as any);
342
- expect(component.detectFiles).toHaveBeenCalled();
343
- });
344
- });
345
-
346
- describe('detectFiles edge cases', () => {
347
- it('should abort when MIME not accepted', () => {
348
- component.fileUploadAccept = 'application/pdf';
349
- const f = new File(['x'], 'a.png', { type: 'image/png' });
350
- const dt = new DataTransfer();
351
- dt.items.add(f);
352
- const input = document.createElement('input');
353
- Object.defineProperty(input, 'files', { value: dt.files });
354
- component.detectFiles({ target: input });
355
- expect(component.isFilePendingToUpload).toBe(false as any);
356
- });
357
- });
358
-
359
- describe('loadFile size limit', () => {
360
- it('should dispatch tooltip when file exceeds limit', () => {
361
- spyOn(window, 'dispatchEvent');
362
- component.arrayFilesLoad[0] = {
363
- uid: 'u1',
364
- file: { title: 'big.png', src: 'x', width: 1, height: 1 },
365
- type: 'image/png',
366
- size: 50 * 1024 * 1024,
367
- } as any;
368
- component.selectedFiles = { item: (i: number) => new File([], 'big.png') } as any;
369
- component.loadFile();
370
- expect(window.dispatchEvent).toHaveBeenCalled();
371
- });
372
- });
373
-
374
- describe('Powered-by / analytics', () => {
375
- it('managePoweredBy opens parent anchor href', () => {
376
- const a = document.createElement('a');
377
- a.setAttribute('href', 'https://example.test/logo');
378
- const span = document.createElement('span');
379
- a.appendChild(span);
380
- spyOn(window, 'open');
381
- window['analytics'] = {
382
- page: jasmine.createSpy('page'),
383
- identify: jasmine.createSpy('identify'),
384
- track: jasmine.createSpy('track'),
385
- };
386
- const ev: any = {
387
- stopPropagation: jasmine.createSpy(),
388
- target: span,
389
- };
390
- component.managePoweredBy(ev);
391
- expect(ev.stopPropagation).toHaveBeenCalled();
392
- expect(window.open).toHaveBeenCalledWith('https://example.test/logo', '_blank');
393
- });
394
- });
395
-
396
- describe('onSendPressed file branch', () => {
397
- it('should call loadFile when upload pending', () => {
398
- component.isFilePendingToUpload = true;
399
- spyOn(component, 'loadFile');
400
- component.onSendPressed({ preventDefault: jasmine.createSpy() } as any);
401
- expect(component.loadFile).toHaveBeenCalled();
402
- expect(component.isFilePendingToUpload).toBe(false as any);
403
- });
404
- });
405
-
406
- describe('onTextAreaChange', () => {
407
- it('should resize and set typing', () => {
408
- component.textInputTextArea = 't';
409
- spyOn(component, 'resizeInputField');
410
- spyOn(component, 'setWritingMessages');
411
- component.onTextAreaChange();
412
- expect(component.resizeInputField).toHaveBeenCalled();
413
- expect(component.setWritingMessages).toHaveBeenCalled();
414
- });
415
- });
416
-
417
- describe('addEmoji', () => {
418
- it('should append emoji when allowed', () => {
419
- component.showEmojiFooterButton = true;
420
- component.textInputTextArea = 'hi ';
421
- spyOn(component.onEmojiiPickerShow, 'emit');
422
- component.addEmoji({ emoji: { native: '⭐' } } as any);
423
- expect(component.textInputTextArea).toContain('⭐');
424
- expect(component.onEmojiiPickerShow.emit).toHaveBeenCalledWith(false);
425
- });
426
- });
427
-
428
- describe('removeFocusOnId', () => {
429
- it('should blur existing textarea', () => {
430
- const ta = document.getElementById('chat21-main-message-context')!;
431
- spyOn(ta, 'blur');
432
- component.removeFocusOnId('chat21-main-message-context');
433
- expect(ta.blur).toHaveBeenCalled();
434
- });
54
+ it('render basics component: attachment button, text-area, send-button', () => {
55
+ component.isConversationArchived = false
56
+ component.showAttachmentButton = true
57
+ component.hideTextReply = false
58
+ fixture.detectChanges();
59
+ const nativeEl: HTMLElement = fixture.nativeElement;
60
+ const attachmentButtonEL = nativeEl.querySelector('input#chat21-file');
61
+ const textAreaEl = nativeEl.querySelector('textarea#chat21-main-message-context')
62
+ const sendButtonEl = nativeEl.querySelector('div#chat21-button-send')
63
+ expect(attachmentButtonEL).toBeTruthy()
64
+ expect(textAreaEl).toBeTruthy();
65
+ expect(sendButtonEl).toBeTruthy();
435
66
  });
436
67
 
437
- describe('resizeInputField error handling', () => {
438
- it('should swallow DOM errors from getElementById', () => {
439
- const orig = document.getElementById.bind(document);
440
- let throwOnce = true;
441
- spyOn(document, 'getElementById').and.callFake((id: string) => {
442
- if (id === 'chat21-main-message-context' && throwOnce) {
443
- throwOnce = false;
444
- throw new Error('dom');
445
- }
446
- return orig(id);
447
- });
448
- expect(() => component.resizeInputField()).not.toThrow();
449
- });
68
+ it('not render attachment button if showAttachmentButton is false', () => {
69
+ component.showAttachmentButton = false
70
+ fixture.detectChanges();
71
+ const nativeEl: HTMLElement = fixture.nativeElement;
72
+ const attachmentButtonEL = nativeEl.querySelector('input#chat21-file');
73
+ expect(attachmentButtonEL).not.toBeTruthy();
450
74
  });
451
75
 
452
- describe('ngOnChanges dropEvent', () => {
453
- it('should forward component.dropEvent to onDrop', () => {
454
- const drop = new Event('drop');
455
- component.dropEvent = drop as any;
456
- spyOn(component, 'onDrop');
457
- component.ngOnChanges({
458
- dropEvent: new SimpleChange(undefined, drop, false),
459
- });
460
- expect(component.onDrop).toHaveBeenCalledWith(drop);
461
- });
76
+ it('not render .active class in send button if textInputTextArea is null', () => {
77
+ component.textInputTextArea = null
78
+ component.isConversationArchived = false
79
+ component.hideTextReply = false
80
+ fixture.detectChanges();
81
+ const nativeEl = fixture.debugElement;
82
+ const sendButtonEl = nativeEl.query(By.css('div#chat21-button-send'));
83
+ expect(sendButtonEl.classes['active']).toBe(false);
462
84
  });
463
85
 
464
- describe('uploadSingle audio branch', () => {
465
- it('should send empty body for audio metadata', fakeAsync(() => {
466
- uploadServiceStub.uploadFile.and.resolveTo({ src: 'https://a', downloadURL: 'https://a' });
467
- component.conversationHandlerService = conversationHandlerStub as any;
468
- const meta: any = { name: 'a.mp3', type: 'audio/mp3', uid: 'u1' };
469
- const file = new File(['x'], 'a.mp3', { type: 'audio/mp3' });
470
- spyOn(component, 'sendMessage');
471
- component.uploadSingle(meta, file, '');
472
- tick();
473
- expect(component.sendMessage).toHaveBeenCalled();
474
- const typeArg = (component.sendMessage as jasmine.Spy).calls.mostRecent().args[1];
475
- expect(typeArg).toBe('file');
476
- }));
477
- });
86
+ it('render .active class in send button if textInputTextArea is different from null', () => {
87
+ component.textInputTextArea = 'test input'
88
+ component.isConversationArchived = false
89
+ component.hideTextReply = false
90
+ fixture.detectChanges();
91
+ const nativeEl = fixture.debugElement;
92
+ const sendButtonEl = nativeEl.query(By.css('div#chat21-button-send'));
93
+ expect(sendButtonEl.classes['active']).toBe(true);
94
+ });
95
+
96
+ it('click event on sendButton is fired', fakeAsync(() => {
97
+ spyOn(component, 'onSendPressed')
98
+ const nativeEl = fixture.nativeElement;
99
+ const sendButtonEl = nativeEl.querySelector('div#chat21-button-send');
100
+ sendButtonEl.click();
101
+ tick();
102
+ expect(component.onSendPressed).toHaveBeenCalled();
103
+ }));
478
104
  });