@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc8

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 (246) hide show
  1. package/.angular-mcp-cache/package.json +1 -0
  2. package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
  3. package/.cursor/mcp.json +15 -0
  4. package/.github/workflows/playwright.yml +27 -0
  5. package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
  6. package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
  7. package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
  8. package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
  9. package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
  10. package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
  11. package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
  12. package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
  13. package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
  14. package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
  15. package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
  16. package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
  17. package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
  18. package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
  19. package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
  20. package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
  21. package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
  22. package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
  23. package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
  24. package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
  25. package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
  26. package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
  27. package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
  28. package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
  29. package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
  30. package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
  31. package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
  32. package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
  33. package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
  34. package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
  35. package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
  36. package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
  37. package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
  38. package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +86 -0
  39. package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
  40. package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
  41. package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
  42. package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +91 -0
  43. package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
  44. package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
  45. package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
  46. package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
  47. package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
  48. package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
  49. package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +91 -0
  50. package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
  51. package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
  52. package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
  53. package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
  54. package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
  55. package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
  56. package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
  57. package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
  58. package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
  59. package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
  60. package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
  61. package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
  62. package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
  63. package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
  64. package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
  65. package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
  66. package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
  67. package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
  68. package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
  69. package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
  70. package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
  71. package/CHANGELOG.md +54 -4
  72. package/angular.json +20 -3
  73. package/deploy_amazon_beta.sh +7 -17
  74. package/deploy_amazon_prod.sh +41 -0
  75. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
  76. package/env.sample +3 -2
  77. package/mocks/voice-websocket-mock/server.cjs +245 -0
  78. package/package.json +7 -3
  79. package/playwright-report/index.html +90 -0
  80. package/playwright.config.ts +41 -0
  81. package/src/app/app.component.html +2 -2
  82. package/src/app/app.component.scss +25 -14
  83. package/src/app/app.component.spec.ts +21 -6
  84. package/src/app/app.module.ts +4 -0
  85. package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
  86. package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
  87. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  88. package/src/app/component/conversation-detail/conversation/conversation.component.ts +61 -17
  89. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  90. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  91. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  92. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +17 -7
  93. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
  94. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +249 -149
  95. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +0 -1
  96. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  97. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
  98. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -62
  99. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -7
  100. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  101. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +192 -84
  102. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  103. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  104. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  105. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  106. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  107. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  108. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  109. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  110. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -18
  111. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +56 -2
  112. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
  113. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  114. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  115. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  116. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  117. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  118. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  119. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  120. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  121. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  122. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  123. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  124. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  125. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  126. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  127. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  128. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  129. package/src/app/component/form/inputs/form-text/form-text.component.ts +26 -0
  130. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  131. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  132. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  133. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  134. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  135. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  136. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  137. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  138. package/src/app/component/home/home.component.html +38 -31
  139. package/src/app/component/home/home.component.scss +4 -2
  140. package/src/app/component/home/home.component.spec.ts +226 -11
  141. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  142. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  143. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  144. package/src/app/component/last-message/last-message.component.html +15 -9
  145. package/src/app/component/last-message/last-message.component.scss +16 -2
  146. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  147. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  148. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  149. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  150. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  151. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  152. package/src/app/component/menu-options/menu-options.component.html +30 -20
  153. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  154. package/src/app/component/message/audio/audio.component.html +13 -15
  155. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  156. package/src/app/component/message/audio/audio.component.ts +1 -0
  157. package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -0
  158. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
  159. package/src/app/component/message/audio-sync/audio-sync.component.ts +133 -86
  160. package/src/app/component/message/avatar/avatar.component.html +2 -2
  161. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  162. package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
  163. package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
  164. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  165. package/src/app/component/message/bubble-message/bubble-message.component.ts +138 -110
  166. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  167. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  168. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  169. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  170. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  171. package/src/app/component/message/carousel/carousel.component.html +29 -16
  172. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  173. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  174. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  175. package/src/app/component/message/frame/frame.component.html +9 -4
  176. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  177. package/src/app/component/message/frame/frame.component.ts +7 -2
  178. package/src/app/component/message/html/html.component.html +1 -1
  179. package/src/app/component/message/html/html.component.scss +1 -1
  180. package/src/app/component/message/html/html.component.spec.ts +24 -7
  181. package/src/app/component/message/image/image.component.html +12 -10
  182. package/src/app/component/message/image/image.component.scss +16 -0
  183. package/src/app/component/message/image/image.component.spec.ts +101 -15
  184. package/src/app/component/message/image/image.component.ts +90 -51
  185. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  186. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  187. package/src/app/component/message/json-sources/json-sources.component.scss +197 -0
  188. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  189. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  190. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  191. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  192. package/src/app/component/message/text/text.component.html +3 -3
  193. package/src/app/component/message/text/text.component.scss +80 -86
  194. package/src/app/component/message/text/text.component.spec.ts +106 -13
  195. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  196. package/src/app/component/selection-department/selection-department.component.html +21 -23
  197. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  198. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  199. package/src/app/component/send-button/send-button.component.html +5 -13
  200. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  201. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  202. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  203. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  204. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  205. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  206. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  207. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  208. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  209. package/src/app/providers/app-config.service.ts +4 -2
  210. package/src/app/providers/brand.service.spec.ts +23 -2
  211. package/src/app/providers/brand.service.ts +1 -1
  212. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  213. package/src/app/providers/global-settings.service.ts +30 -2
  214. package/src/app/providers/json-sources-parser.service.ts +182 -0
  215. package/src/app/providers/translator.service.ts +24 -6
  216. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  217. package/src/app/providers/tts-audio-playback-coordinator.service.ts +39 -16
  218. package/src/app/providers/url-preview.service.ts +82 -0
  219. package/src/app/providers/voice/audio.types.ts +6 -0
  220. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  221. package/src/app/providers/voice/voice-streaming.service.ts +710 -0
  222. package/src/app/providers/voice/voice-streaming.types.ts +113 -0
  223. package/src/app/providers/voice/voice.service.spec.ts +203 -3
  224. package/src/app/providers/voice/voice.service.ts +517 -13
  225. package/src/app/sass/_variables.scss +1 -1
  226. package/src/app/sass/animations.scss +19 -1
  227. package/src/app/utils/globals.ts +4 -0
  228. package/src/app/utils/json-sources-utils.ts +27 -0
  229. package/src/app/utils/url-utils.ts +98 -0
  230. package/src/app/utils/utils-resources.ts +1 -1
  231. package/src/assets/i18n/en.json +26 -1
  232. package/src/assets/i18n/es.json +106 -101
  233. package/src/assets/i18n/fr.json +106 -101
  234. package/src/assets/i18n/it.json +106 -99
  235. package/src/assets/twp/index-dev.html +18 -0
  236. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  237. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  238. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  239. package/src/chat21-core/utils/constants.ts +4 -0
  240. package/src/chat21-core/utils/utils-message.ts +23 -1
  241. package/src/widget-config-template.json +3 -1
  242. package/src/widget-config.json +28 -27
  243. package/test-results/.last-run.json +4 -0
  244. package/tests/widget-form-rich.spec.ts +67 -0
  245. package/tests/widget-index-dev-settings.spec.ts +52 -0
  246. package/tests/widget-twp-iframe.spec.ts +39 -0
@@ -1,38 +1,207 @@
1
- import { DomSanitizer } from '@angular/platform-browser';
2
- import { LoggerInstance } from './../../../../chat21-core/providers/logger/loggerInstance';
3
- import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
4
- import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2
+ import { FormsModule } from '@angular/forms';
3
+ import { By } from '@angular/platform-browser';
5
4
 
5
+ import { MIN_WIDTH_IMAGES } from 'src/app/utils/constants';
6
6
  import { ConversationPreviewComponent } from './conversation-preview.component';
7
- import { LogLevel } from 'src/chat21-core/utils/constants';
8
- import { NGXLogger } from 'ngx-logger';
9
7
  import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
10
- const mockService = jasmine.createSpyObj('LoggerService', ['setLoggerConfig', "debug", "log", "warn", "info", "error" ]);
8
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
9
+ import { NGXLogger } from 'ngx-logger';
11
10
 
12
11
  describe('ConversationPreviewComponent', () => {
13
12
  let component: ConversationPreviewComponent;
14
13
  let fixture: ComponentFixture<ConversationPreviewComponent>;
15
- let ngxlogger: NGXLogger;
16
- let customLogger = new CustomLogger(ngxlogger)
14
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
15
+ const customLogger = new CustomLogger(ngxlogger);
16
+
17
+ const imageAttachment = (name = 'a.png') => ({
18
+ file: new File(['x'], name, { type: 'image/png' }),
19
+ metadata: { width: 400, height: 200, type: 'image/png', uid: 'u1', src: 'blob:mock' },
20
+ });
17
21
 
18
22
  beforeEach(waitForAsync(() => {
23
+ LoggerInstance.setInstance(customLogger);
19
24
  TestBed.configureTestingModule({
20
- declarations: [ ConversationPreviewComponent ],
21
- providers: [LoggerService]
22
- }).compileComponents();
25
+ declarations: [ConversationPreviewComponent],
26
+ imports: [FormsModule],
27
+ })
28
+ .overrideComponent(ConversationPreviewComponent, {
29
+ set: {
30
+ template: `
31
+ <div #scrollMe id="c21-contentScroll-preview" style="height:39%">preview</div>
32
+ <textarea id="chat21-main-message-context-preview" [(ngModel)]="textInputTextArea"></textarea>
33
+ `,
34
+ },
35
+ })
36
+ .compileComponents();
23
37
  }));
24
38
 
25
39
  beforeEach(() => {
26
40
  fixture = TestBed.createComponent(ConversationPreviewComponent);
27
41
  component = fixture.componentInstance;
28
- LoggerInstance.setInstance(customLogger)
29
- let logger = LoggerInstance.getInstance()
30
- component['logger']= logger
42
+ component.attachments = [imageAttachment()] as any;
43
+ component.baseLocation = '';
44
+ component.translationMap = new Map([['LABEL_PLACEHOLDER', 'Placeholder']]);
45
+ component.stylesMap = new Map([['themeColor', '#000']]);
46
+ component.textInputTextArea = '';
31
47
  fixture.detectChanges();
32
48
  });
33
49
 
34
50
  it('should create', () => {
35
- console.log('ConversationPreviewComponent --->', component)
36
51
  expect(component).toBeTruthy();
37
52
  });
53
+
54
+ describe('getMetadataSize', () => {
55
+ it('should scale wide images to preview max width', () => {
56
+ const s = component.getMetadataSize({ width: 500, height: 100 });
57
+ expect(s.width).toBe(230);
58
+ expect(s.height).toBeCloseTo(46, 5);
59
+ });
60
+
61
+ it('should bump very narrow images to MIN_WIDTH_IMAGES', () => {
62
+ const s = component.getMetadataSize({ width: 40, height: 80 });
63
+ expect(s.width).toBe(MIN_WIDTH_IMAGES);
64
+ });
65
+
66
+ it('should cap tall images by max preview height', () => {
67
+ const s = component.getMetadataSize({ width: 100, height: 400 });
68
+ expect(s.height).toBe(150);
69
+ });
70
+
71
+ it('should default undefined metadata then cap tall square to preview max height', () => {
72
+ const s = component.getMetadataSize({});
73
+ // Both sides start at 230; height branch caps at 150 and scales width to match aspect ratio.
74
+ expect(s.height).toBe(150);
75
+ expect(s.width).toBe(150);
76
+ });
77
+ });
78
+
79
+ describe('readAsDataURL', () => {
80
+ it('should set fileSelected for raster images', () => {
81
+ component.fileSelected = undefined as any;
82
+ component.readAsDataURL(imageAttachment());
83
+ expect(component.fileSelected).toBeDefined();
84
+ expect(component.sizeImage).toBeDefined();
85
+ });
86
+
87
+ it('should sanitize SVG src', () => {
88
+ const svg = {
89
+ file: new File(['<svg/>'], 'a.svg', { type: 'image/svg+xml' }),
90
+ metadata: { width: 10, height: 10, type: 'image/svg+xml', uid: 's1', src: 'data:image/svg+xml;base64,PHN2Zy8+' },
91
+ };
92
+ component.fileSelected = undefined as any;
93
+ component.readAsDataURL(svg);
94
+ expect(component.fileSelected).toBeDefined();
95
+ });
96
+
97
+ it('should route non-image files to createFile', () => {
98
+ spyOn(component, 'createFile').and.returnValue(Promise.resolve());
99
+ const pdf = {
100
+ file: new File(['%PDF'], 'doc.pdf', { type: 'application/pdf' }),
101
+ metadata: { uid: 'p1', type: 'application/pdf' },
102
+ };
103
+ component.readAsDataURL(pdf);
104
+ expect(component.createFile).toHaveBeenCalledWith(pdf);
105
+ });
106
+ });
107
+
108
+ describe('createFile', () => {
109
+ it('should request placeholder asset from baseLocation', async () => {
110
+ const blob = new Blob(['x'], { type: 'image/png' });
111
+ spyOn(window, 'fetch').and.returnValue(Promise.resolve({ blob: () => Promise.resolve(blob) } as Response));
112
+ component.baseLocation = 'https://app.test';
113
+ const att = {
114
+ file: new File(['z'], 'note.txt', { type: 'text/plain' }),
115
+ metadata: { uid: 'f1', type: 'text/plain' },
116
+ };
117
+ await component.createFile(att);
118
+ expect(window.fetch).toHaveBeenCalledWith('https://app.test/assets/images/file-alt-solid.png');
119
+ });
120
+ });
121
+
122
+ describe('keyboard and actions', () => {
123
+ it('onkeydown Escape should close modal', () => {
124
+ spyOn(component.onCloseModalPreview, 'emit');
125
+ component.onkeydown({ which: 27, keyCode: 27 } as KeyboardEvent);
126
+ expect(component.onCloseModalPreview.emit).toHaveBeenCalled();
127
+ });
128
+
129
+ it('onkeydown Enter with text should emit send and restore', () => {
130
+ spyOn(component.onSendAttachment, 'emit');
131
+ spyOn(component as any, 'restoreTextArea');
132
+ const ta = fixture.debugElement.query(By.css('#chat21-main-message-context-preview')).nativeElement as HTMLTextAreaElement;
133
+ ta.value = ' caption ';
134
+ const ev = new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13 });
135
+ spyOn(ev, 'preventDefault');
136
+ component.onkeydown(ev);
137
+ expect(ev.preventDefault).toHaveBeenCalled();
138
+ expect(component.onSendAttachment.emit).toHaveBeenCalledWith(' caption ');
139
+ expect((component as any).restoreTextArea).toHaveBeenCalled();
140
+ });
141
+
142
+ it('onkeydown Enter with only whitespace should not emit', () => {
143
+ spyOn(component.onSendAttachment, 'emit');
144
+ const ta = fixture.debugElement.query(By.css('#chat21-main-message-context-preview')).nativeElement as HTMLTextAreaElement;
145
+ ta.value = ' ';
146
+ component.onkeydown({ which: 13, keyCode: 13, preventDefault: () => {} } as any);
147
+ expect(component.onSendAttachment.emit).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('onClickClose should emit', () => {
151
+ spyOn(component.onCloseModalPreview, 'emit');
152
+ component.onClickClose();
153
+ expect(component.onCloseModalPreview.emit).toHaveBeenCalled();
154
+ });
155
+
156
+ it('onSendPressed should emit current model text', () => {
157
+ spyOn(component.onSendAttachment, 'emit');
158
+ component.textInputTextArea = 'hi';
159
+ component.onSendPressed(new Event('click'));
160
+ expect(component.onSendAttachment.emit).toHaveBeenCalledWith('hi');
161
+ });
162
+ });
163
+
164
+ describe('textarea sizing', () => {
165
+ it('onTextAreaChange should call resize helpers', () => {
166
+ spyOn(component, 'resizeInputField');
167
+ spyOn(component, 'resizeModalHeight');
168
+ component.onTextAreaChange();
169
+ expect(component.resizeInputField).toHaveBeenCalled();
170
+ expect(component.resizeModalHeight).toHaveBeenCalled();
171
+ });
172
+
173
+ it('resizeInputField should grow with multiline content', () => {
174
+ const ta = fixture.debugElement.query(By.css('#chat21-main-message-context-preview')).nativeElement as HTMLTextAreaElement;
175
+ ta.value = 'a\nb\nc';
176
+ ta.style.height = '20px';
177
+ Object.defineProperty(ta, 'scrollHeight', { configurable: true, value: 80 });
178
+ Object.defineProperty(ta, 'offsetHeight', { configurable: true, value: 20 });
179
+ component.resizeInputField();
180
+ expect(parseInt(ta.style.height, 10)).toBeGreaterThan(20);
181
+ });
182
+
183
+ it('resizeModalHeight should adjust host when textarea grows', () => {
184
+ const ta = fixture.debugElement.query(By.css('#chat21-main-message-context-preview')).nativeElement as HTMLTextAreaElement;
185
+ ta.style.height = '40px';
186
+ component.scrollMe = { nativeElement: { style: { height: '' } } } as any;
187
+ component.resizeModalHeight();
188
+ expect((component.scrollMe.nativeElement.style.height as string).length).toBeGreaterThan(0);
189
+ });
190
+ });
191
+
192
+ describe('onImageRenderedFN', () => {
193
+ it('should clear pending load flag', () => {
194
+ component.isFilePendingToLoad = true;
195
+ component.onImageRenderedFN({});
196
+ expect(component.isFilePendingToLoad).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('onPaste', () => {
201
+ it('should call resizeInputField', () => {
202
+ spyOn(component, 'resizeInputField');
203
+ component.onPaste(new ClipboardEvent('paste'));
204
+ expect(component.resizeInputField).toHaveBeenCalled();
205
+ });
206
+ });
38
207
  });
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
1
+ import { Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild } from '@angular/core';
2
2
  import { DomSanitizer } from '@angular/platform-browser';
3
3
  import { MIN_WIDTH_IMAGES } from 'src/app/utils/constants';
4
4
  import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
@@ -244,27 +244,47 @@ export class ConversationPreviewComponent implements OnInit {
244
244
  this.setFocusOnId('chat21-main-message-context-preview');
245
245
  }
246
246
 
247
- /*
248
- * @param event
249
- */
250
- onkeypress(event) {
247
+ /**
248
+ * Single keyboard handler for the preview textarea:
249
+ * - Enter (no modifier) -> send the attachment with the typed message
250
+ * - Shift / Alt / Ctrl / Meta + Enter -> insert a newline (default browser behavior)
251
+ * - Escape -> close the preview modal
252
+ * Tab navigation is handled by CDK cdkTrapFocus on the dialog wrapper.
253
+ * @param event
254
+ */
255
+ onkeydown(event: KeyboardEvent) {
251
256
  const keyCode = event.which || event.keyCode;
252
- this.textInputTextArea = ((document.getElementById('chat21-main-message-context-preview') as HTMLInputElement).value);
253
- if (keyCode === 13) {
257
+
258
+ if (keyCode === 27) { // Esc
259
+ this.onClickClose();
260
+ return;
261
+ }
262
+
263
+ if (keyCode === 13) { // ENTER
264
+ const hasModifier = event.metaKey || event.shiftKey || event.altKey || event.ctrlKey;
265
+ if (hasModifier) {
266
+ return;
267
+ }
268
+
269
+ event.preventDefault();
270
+ const target = document.getElementById('chat21-main-message-context-preview') as HTMLInputElement;
271
+ if (target) {
272
+ this.textInputTextArea = target.value;
273
+ }
254
274
  if (this.textInputTextArea && this.textInputTextArea.trim() !== '') {
255
275
  this.onSendAttachment.emit(this.textInputTextArea);
256
276
  this.restoreTextArea();
257
277
  }
258
- } else if (keyCode === 9) {
259
- event.preventDefault();
278
+ return;
260
279
  }
261
280
  }
262
281
 
263
- onkeydown(event){
264
- const keyCode = event.which || event.keyCode;
265
- if (keyCode === 27) { // Esc keyboard code
266
- this.onClickClose()
267
- }
282
+ /** Component-level Esc handler to close the modal even when focus is not on the textarea. */
283
+ @HostListener('keydown.escape', ['$event'])
284
+ onHostEscape(event: KeyboardEvent){
285
+ event.preventDefault();
286
+ event.stopPropagation();
287
+ this.onClickClose();
268
288
  }
269
289
 
270
290
  onPaste(event){
@@ -1,18 +1,43 @@
1
- <div class="stream-audio-spectrum" [ngStyle]="accentColor ? { color: accentColor } : null">
2
- <svg class="stream-audio-spectrum__svg" viewBox="0 0 100 32" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
3
- <defs>
4
- <linearGradient [attr.id]="gradientId" x1="0" y1="16" x2="100" y2="16" gradientUnits="userSpaceOnUse">
5
- <stop offset="0%" stop-color="currentColor" stop-opacity="0.45"/>
6
- <stop offset="50%" stop-color="currentColor" stop-opacity="1"/>
7
- <stop offset="100%" stop-color="currentColor" stop-opacity="0.45"/>
8
- </linearGradient>
9
- </defs>
10
- <path class="stream-audio-spectrum__line"
11
- [attr.d]="spectrumLinePath"
12
- fill="none"
13
- [attr.stroke]="'url(#' + gradientId + ')'"
14
- stroke-width="2.4"
15
- stroke-linecap="round"
16
- stroke-linejoin="round"/>
17
- </svg>
18
- </div>
1
+ <ng-container [ngSwitch]="mode">
2
+ <!-- ALERT: spectrum line (fills streamAudioAlert width) -->
3
+ <div *ngSwitchCase="'alert'" class="stream-audio-spectrum" [ngStyle]="accentColor ? { color: accentColor } : null">
4
+ <svg class="stream-audio-spectrum__svg" viewBox="0 0 100 32" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
5
+ <defs>
6
+ <linearGradient [attr.id]="gradientId" x1="0" y1="16" x2="100" y2="16" gradientUnits="userSpaceOnUse">
7
+ <stop offset="0%" stop-color="currentColor" stop-opacity="0.45"/>
8
+ <stop offset="50%" stop-color="currentColor" stop-opacity="1"/>
9
+ <stop offset="100%" stop-color="currentColor" stop-opacity="0.45"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <path class="stream-audio-spectrum__line"
13
+ [attr.d]="spectrumLinePath"
14
+ fill="none"
15
+ [attr.stroke]="'url(#' + gradientId + ')'"
16
+ stroke-width="2.4"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"/>
19
+ </svg>
20
+ </div>
21
+
22
+ <!-- BUTTON: inactive icon / expanded pill content -->
23
+ <ng-container *ngSwitchCase="'button'">
24
+ <span class="stream-audio-button__icon" *ngIf="!active" aria-hidden="true">
25
+ <svg role="img" xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 30 30" fill="currentColor" preserveAspectRatio="xMidYMid meet">
26
+ <path class="s0" d="m5.21 7.41c-1.21 0-2.21 0.99-2.21 2.21v8.14c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.14c0-1.21-0.99-2.21-2.21-2.21z"/>
27
+ <path class="s0" d="m11.64 3.01c-1.22 0-2.21 0.99-2.21 2.2v16.94c0 1.21 0.99 2.2 2.21 2.2 1.22 0 2.21-0.98 2.21-2.2v-16.94c0-1.21-0.99-2.21-2.21-2.21z"/>
28
+ <path class="s0" d="m15.86 9.25v8.88c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.88c0-1.22-0.99-2.21-2.21-2.21-1.22 0-2.21 0.99-2.21 2.21z"/>
29
+ <path class="s0" d="m24.5 8.97c-1.22 0-2.21 0.99-2.21 2.21v5.02c0 1.22 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-5.02c0-1.21-0.99-2.21-2.21-2.21z"/>
30
+ </svg>
31
+ </span>
32
+
33
+ <span class="stream-audio-button__expanded" *ngIf="active">
34
+ <span class="stream-audio-button__bars" aria-hidden="true">
35
+ <span class="bar" [style.transform]="'scaleY(' + barScales[0] + ')'"></span>
36
+ <span class="bar" [style.transform]="'scaleY(' + barScales[1] + ')'"></span>
37
+ <span class="bar" [style.transform]="'scaleY(' + barScales[2] + ')'"></span>
38
+ <span class="bar" [style.transform]="'scaleY(' + barScales[3] + ')'"></span>
39
+ </span>
40
+ <span class="stream-audio-button__label">{{ translationMap.get('CLOSE') }}</span>
41
+ </span>
42
+ </ng-container>
43
+ </ng-container>
@@ -1,7 +1,8 @@
1
1
  :host {
2
- display: block;
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
3
5
  width: 100%;
4
- flex: 1 1 auto;
5
6
  }
6
7
 
7
8
  .stream-audio-spectrum {
@@ -23,3 +24,56 @@
23
24
  pointer-events: none;
24
25
  filter: drop-shadow(0 0 1px color-mix(in srgb, currentColor 35%, transparent));
25
26
  }
27
+
28
+ /* ===========================
29
+ * BUTTON (pill content)
30
+ * =========================== */
31
+ .stream-audio-button__icon {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ width: 100%;
36
+ }
37
+
38
+ .stream-audio-button__icon svg {
39
+ width: 20px;
40
+ height: 20px;
41
+ display: block;
42
+ }
43
+
44
+ .stream-audio-button__expanded {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ gap: 12px;
49
+ width: 100%;
50
+ user-select: none;
51
+ }
52
+
53
+ .stream-audio-button__label {
54
+ font-size: 14px;
55
+ line-height: 1;
56
+ font-weight: 500;
57
+ letter-spacing: 0.2px;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .stream-audio-button__bars {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ gap: 3px;
66
+ width: 26px;
67
+ height: 18px;
68
+ transform-origin: center;
69
+ margin: 0;
70
+ line-height: 0;
71
+ }
72
+
73
+ .stream-audio-button__bars .bar {
74
+ width: 3px;
75
+ height: 100%;
76
+ border-radius: 2px;
77
+ background: rgba(255, 255, 255, 0.92);
78
+ transform-origin: center;
79
+ }
@@ -1,4 +1,8 @@
1
- import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
1
+ import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core';
2
+ import { Subscription } from 'rxjs';
3
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
4
+
5
+ export type StreamAudioSpectrumMode = 'alert' | 'button';
2
6
 
3
7
  /**
4
8
  * Icona stream: cerchio con linea orizzontale tipo spettro, reattiva al volume del microfono.
@@ -18,19 +22,63 @@ export class StreamAudioSpectrumComponent implements OnInit, OnChanges {
18
22
  /** Colore tema (stroke / gradient); opzionale. */
19
23
  @Input() accentColor?: string;
20
24
 
25
+ /** UI variant. `alert` = spectrum line (in #streamAudioAlert). `button` = icon / pill with bars + label. */
26
+ @Input() mode: StreamAudioSpectrumMode = 'alert';
27
+ /** For `mode="button"`: whether the stream is active (expanded pill). */
28
+ @Input() active = false;
29
+ /** For `mode="button"`: VAD speech flag; if omitted, we fall back to a volume threshold heuristic. */
30
+ @Input() isUserSpeaking?: boolean;
31
+ /** For `mode="button"`: label on the pill. */
32
+ @Input() translationMap: Map< string, string>;
33
+
34
+ // ALERT (spectrum line)
21
35
  spectrumLinePath = 'M0,16 L100,16';
22
36
 
37
+ // BUTTON (bars)
38
+ barScales: [number, number, number, number] = [0.65, 0.65, 0.65, 0.65];
39
+ private rafId: number | null = null;
40
+ private lastSpeaking = false;
41
+ private voiceSpeechStartSub?: Subscription;
42
+ private voiceSpeechEndSub?: Subscription;
43
+ private internalIsUserSpeaking = false;
44
+
45
+ constructor(@Optional() private readonly voiceService: VoiceService | null) {}
46
+
23
47
  ngOnInit(): void {
24
- this.refreshPath();
48
+ // Optional: use VAD speech events to improve idle/speaking detection.
49
+ if (this.voiceService) {
50
+ this.voiceSpeechStartSub = this.voiceService.speechStart$?.subscribe(() => {
51
+ this.internalIsUserSpeaking = true;
52
+ });
53
+ this.voiceSpeechEndSub = this.voiceService.speechEnd$?.subscribe(() => {
54
+ this.internalIsUserSpeaking = false;
55
+ });
56
+ }
57
+ this.refreshAll();
25
58
  }
26
59
 
27
60
  ngOnChanges(changes: SimpleChanges): void {
28
- if (changes['volume']) {
29
- this.refreshPath();
61
+ if (changes['volume'] || changes['mode'] || changes['active'] || changes['isUserSpeaking']) {
62
+ this.refreshAll();
63
+ }
64
+ }
65
+
66
+ ngOnDestroy(): void {
67
+ this.stopRaf();
68
+ this.voiceSpeechStartSub?.unsubscribe();
69
+ this.voiceSpeechEndSub?.unsubscribe();
70
+ }
71
+
72
+ private refreshAll(): void {
73
+ if (this.mode === 'alert') {
74
+ this.refreshSpectrumPath();
75
+ this.stopRaf();
76
+ return;
30
77
  }
78
+ this.refreshBars();
31
79
  }
32
80
 
33
- private refreshPath(): void {
81
+ private refreshSpectrumPath(): void {
34
82
  const intensity = Math.min(this.volume / 80, 1);
35
83
  const t = Date.now() / 175;
36
84
  this.spectrumLinePath = this.buildSpectrumLinePath(intensity, t);
@@ -59,4 +107,86 @@ export class StreamAudioSpectrumComponent implements OnInit, OnChanges {
59
107
  }
60
108
  return parts.join('');
61
109
  }
110
+
111
+ private refreshBars(): void {
112
+ if (!this.active) {
113
+ this.stopRaf();
114
+ return;
115
+ }
116
+
117
+ const speaking = this.computeSpeaking();
118
+ if (!speaking) {
119
+ this.stopRaf();
120
+ this.barScales = [0.65, 0.65, 0.65, 0.65];
121
+ this.lastSpeaking = false;
122
+ return;
123
+ }
124
+
125
+ // speaking: animate bars with volume-driven intensity
126
+ if (!this.lastSpeaking) {
127
+ this.lastSpeaking = true;
128
+ }
129
+ this.startRaf();
130
+ }
131
+
132
+ private computeSpeaking(): boolean {
133
+ if (typeof this.isUserSpeaking === 'boolean') {
134
+ return this.isUserSpeaking;
135
+ }
136
+ if (this.voiceService) {
137
+ return this.internalIsUserSpeaking;
138
+ }
139
+ // Fallback heuristic: treat as speaking when volume crosses a low threshold.
140
+ return (this.volume || 0) >= 4;
141
+ }
142
+
143
+ private startRaf(): void {
144
+ if (this.rafId !== null) {
145
+ return;
146
+ }
147
+ const tick = () => {
148
+ if (!this.active) {
149
+ this.stopRaf();
150
+ return;
151
+ }
152
+ const speaking = this.computeSpeaking();
153
+ if (!speaking) {
154
+ this.stopRaf();
155
+ this.barScales = [0.65, 0.65, 0.65, 0.65];
156
+ return;
157
+ }
158
+
159
+ const intensity = Math.min((this.volume || 0) / 80, 1);
160
+ const t = performance.now() / 220;
161
+ const targets: [number, number, number, number] = [0.35, 0.35, 0.35, 0.35];
162
+
163
+ for (let i = 0; i < 4; i++) {
164
+ const phase = i * 0.9;
165
+ const w1 = (Math.sin(t * 1.35 + phase) + 1) / 2;
166
+ const w2 = (Math.sin(t * 2.05 + phase * 1.7) + 1) / 2;
167
+ const mix = w1 * 0.62 + w2 * 0.38;
168
+ const s = 0.25 + intensity * (0.25 + 0.95 * mix);
169
+ targets[i as 0 | 1 | 2 | 3] = Math.max(0.35, Math.min(1.2, s));
170
+ }
171
+
172
+ // Smooth toward targets to avoid jitter on rapid volume changes.
173
+ const lerp = (a: number, b: number, k: number) => a + (b - a) * k;
174
+ this.barScales = [
175
+ lerp(this.barScales[0], targets[0], 0.35),
176
+ lerp(this.barScales[1], targets[1], 0.35),
177
+ lerp(this.barScales[2], targets[2], 0.35),
178
+ lerp(this.barScales[3], targets[3], 0.35),
179
+ ];
180
+
181
+ this.rafId = requestAnimationFrame(tick);
182
+ };
183
+ this.rafId = requestAnimationFrame(tick);
184
+ }
185
+
186
+ private stopRaf(): void {
187
+ if (this.rafId !== null) {
188
+ cancelAnimationFrame(this.rafId);
189
+ this.rafId = null;
190
+ }
191
+ }
62
192
  }
@@ -1,25 +1,85 @@
1
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { By } from '@angular/platform-browser';
2
3
 
4
+ import { CustomTranslateService } from 'src/chat21-core/providers/custom-translate.service';
5
+ import { FILE_SIZE_LIMIT } from 'src/app/utils/constants';
3
6
  import { ErrorAlertComponent } from './error-alert.component';
4
7
 
5
8
  describe('ErrorAlertComponent', () => {
6
9
  let component: ErrorAlertComponent;
7
10
  let fixture: ComponentFixture<ErrorAlertComponent>;
11
+ let translateStub: { translateLanguage: jasmine.Spy };
8
12
 
9
13
  beforeEach(async () => {
14
+ translateStub = {
15
+ translateLanguage: jasmine.createSpy('translateLanguage').and.callFake((keys: string[]) => {
16
+ const m = new Map<string, string>();
17
+ keys.forEach((k) => m.set(k, `Errore: {{FILE_SIZE_LIMIT}} MB (chiave ${k})`));
18
+ return m;
19
+ }),
20
+ };
21
+
10
22
  await TestBed.configureTestingModule({
11
- declarations: [ ErrorAlertComponent ]
12
- })
13
- .compileComponents();
23
+ declarations: [ErrorAlertComponent],
24
+ providers: [{ provide: CustomTranslateService, useValue: translateStub }],
25
+ }).compileComponents();
14
26
 
15
27
  fixture = TestBed.createComponent(ErrorAlertComponent);
16
28
  component = fixture.componentInstance;
17
- fixture.detectChanges();
18
29
  });
19
30
 
20
31
  it('should create', () => {
32
+ fixture.detectChanges();
21
33
  expect(component).toBeTruthy();
22
34
  });
23
- });
24
35
 
36
+ describe('inputs and translation key path', () => {
37
+ it('should resolve errorKeyMessage via CustomTranslateService and interpolate constants + errorParams', () => {
38
+ component.errorKeyMessage = 'MY_KEY';
39
+ component.errorParams = { FILE_SIZE_LIMIT: 99 };
40
+ component.ngOnInit();
41
+ expect(translateStub.translateLanguage).toHaveBeenCalledWith(['MY_KEY']);
42
+ expect(component.errorMessage).toContain('99');
43
+ expect(component.errorMessage).toContain('(chiave MY_KEY)');
44
+ });
45
+
46
+ it('should render translated message in the alert DOM', () => {
47
+ component.errorKeyMessage = 'NET_ERR';
48
+ component.ngOnInit();
49
+ fixture.detectChanges();
50
+ const content = fixture.debugElement.query(By.css('.alert-content')).nativeElement as HTMLElement;
51
+ expect(content.textContent?.trim().length).toBeGreaterThan(0);
52
+ expect(fixture.nativeElement.querySelector('#alert-container')).toBeTruthy();
53
+ });
54
+ });
55
+
56
+ describe('plain errorMessage and interpolation', () => {
57
+ it('should use raw errorMessage when errorKeyMessage is empty', () => {
58
+ component.errorKeyMessage = '';
59
+ component.errorMessage = 'Connessione persa';
60
+ component.ngOnInit();
61
+ expect(translateStub.translateLanguage).not.toHaveBeenCalled();
62
+ expect(component.errorMessage).toBe('Connessione persa');
63
+ });
64
+
65
+ it('should replace {{placeholders}} from merged CONSTANTS and errorParams', () => {
66
+ component.errorKeyMessage = '';
67
+ component.errorMessage = 'Limite {{FILE_SIZE_LIMIT}} MB, extra {{custom}} e {{missing}}';
68
+ component.errorParams = { custom: 'X' };
69
+ component.ngOnInit();
70
+ expect(component.errorMessage).toContain(String(FILE_SIZE_LIMIT));
71
+ expect(component.errorMessage).toContain('X');
72
+ expect(component.errorMessage).toContain('{{missing}}');
73
+ });
25
74
 
75
+ it('should leave errorMessage empty when neither key nor plain message is set (widget happy path)', () => {
76
+ component.errorKeyMessage = '';
77
+ component.errorMessage = '';
78
+ component.ngOnInit();
79
+ expect(component.errorMessage).toBe('');
80
+ fixture.detectChanges();
81
+ const content = fixture.nativeElement.querySelector('.alert-content') as HTMLElement;
82
+ expect(content.textContent?.trim()).toBe('');
83
+ });
84
+ });
85
+ });