@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,55 +1,45 @@
1
1
  <div id='c21-menu-options'>
2
- <button type="button"
3
- class="button-menu-options c21-button-clean"
4
- [attr.aria-label]="g.OPTIONS"
5
- [attr.aria-expanded]="g.isOpenMenuOptions ? 'true' : 'false'"
6
- aria-haspopup="true"
7
- (click)="f21_toggle_options()"
8
- (mouseover)="isHover=true"
9
- (mouseleave)="isHover=false">
10
- <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" width="24px" viewBox="0 0 24 24" [ngStyle]="{'display': (isHover || g.isOpenMenuOptions)? 'block': 'none'}">
2
+ <div class="button-menu-options" (click)="f21_toggle_options()" (mouseover)="isHover=true" (mouseleave)="isHover=false">
3
+ <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" width="24px" viewBox="0 0 24 24" [ngStyle]="{'display': (isHover || g.isOpenMenuOptions)? 'block': 'none'}">
11
4
  <g>
12
5
  <path d="M0,0h24v24H0V0z" fill="none"/>
13
6
  <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
14
7
  </g>
15
8
  </svg>
16
- </button>
17
- <div *ngIf="g.isOpenMenuOptions"
18
- class="modal-menu-options"
19
- role="group"
20
- [attr.aria-label]="g.OPTIONS"
21
- (keydown.escape)="$event.preventDefault(); $event.stopPropagation(); f21_toggle_options()">
9
+ <!-- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Livello_1" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" [ngStyle]="{'display': (isHover)? 'block': 'none'}" xml:space="preserve">
10
+ <circle class="st1" cx="6" cy="12" r="2" [ngStyle]="{'fill': g.themeColor, 'opacity': 1}"/>
11
+ <circle class="st2" cx="18" cy="12" r="2"[ngStyle]="{'fill': g.themeColor, 'opacity': 0.6}"/>
12
+ <circle class="st3" cx="12" cy="12" r="2"[ngStyle]="{'fill': g.themeColor, 'opacity': 0.4}"/>
13
+ </svg> -->
14
+ </div>
15
+ <div *ngIf="g.isOpenMenuOptions" class="modal-menu-options">
22
16
  <div class="">
23
-
24
- <button type="button"
25
- class="c21-button c21-button-clean"
26
- [attr.aria-pressed]="g.soundEnabled ? 'true' : 'false'"
27
- [attr.aria-label]="g.soundEnabled ? g.SOUND_ON : g.SOUND_OFF"
28
- (click)="toggleSound()">
29
- <svg *ngIf="!g.soundEnabled" aria-hidden="true" focusable="false" class="icon-menu" xmlns="http://www.w3.org/2000/svg"
17
+
18
+ <div class="c21-button" (click)="toggleSound()">
19
+ <svg *ngIf="!g.soundEnabled" aria-labelledby="altIconTitle" class="icon-menu" xmlns="http://www.w3.org/2000/svg"
30
20
  width="20" height="20" viewBox="0 0 24 24">
31
21
  <path fill="none" d="M0 0h24v24H0V0z" />
32
22
  <path d="M4.34 2.93L2.93 4.34 7.29 8.7 7 9H3v6h4l5 5v-6.59l4.18 4.18c-.65.49-1.38.88-2.18 1.11v2.06c1.34-.3 2.57-.92 3.61-1.75l2.05 2.05 1.41-1.41L4.34 2.93zM10 15.17L7.83 13H5v-2h2.83l.88-.88L10 11.41v3.76zM19 12c0 .82-.15 1.61-.41 2.34l1.53 1.53c.56-1.17.88-2.48.88-3.87 0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zm-7-8l-1.88 1.88L12 7.76zm4.5 8c0-1.77-1.02-3.29-2.5-4.03v1.79l2.48 2.48c.01-.08.02-.16.02-.24z" />
23
+ <title id="altIconTitle">{{g.SOUND_OFF}}</title>
33
24
  </svg>
34
- <svg *ngIf="g.soundEnabled" aria-hidden="true" focusable="false" class="icon-menu" xmlns="http://www.w3.org/2000/svg"
25
+ <svg *ngIf="g.soundEnabled" aria-labelledby="altIconTitle" class="icon-menu" xmlns="http://www.w3.org/2000/svg"
35
26
  width="20" height="20" viewBox="0 0 24 24">
36
27
  <path fill="none" d="M0 0h24v24H0V0z" />
37
28
  <path d="M3 9v6h4l5 5V4L7 9H3zm7-.17v6.34L7.83 13H5v-2h2.83L10 8.83zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77 0-4.28-2.99-7.86-7-8.77z" />
29
+ <title id="altIconTitle">{{g.SOUND_ON}}</title>
38
30
  </svg>
39
31
  <span *ngIf="!g.soundEnabled" class="label-menu-item"> {{g.SOUND_OFF}}</span>
40
32
  <span *ngIf="g.soundEnabled" class="label-menu-item"> {{g.SOUND_ON}}</span>
41
33
 
42
- </button>
43
- <button type="button"
44
- class="c21-button c21-button-clean" *ngIf="g.isLogged && g.showLogoutOption"
45
- [attr.aria-label]="g.LOGOUT"
46
- (click)="signOut()">
47
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false" class="icon-menu">
34
+ </div>
35
+ <div class="c21-button" *ngIf="g.isLogged && g.showLogoutOption" (click)="signOut()">
36
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-labelledby="altIconTitle" class="icon-menu">
48
37
  <path fill="none" d="M0 0h24v24H0V0z"/>
49
38
  <path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
39
+ <title id="altIconTitle">{{g.LOGOUT}}</title>
50
40
  </svg>
51
41
  <span *ngIf="g.isLogged && g.showLogoutOption" class="label-menu-item"> {{g.LOGOUT}}</span>
52
- </button>
42
+ </div>
53
43
  <div class="c21-button build_version_menu">
54
44
  <span class="label-menu-item"> {{g.BUILD_VERSION}}</span>
55
45
  </div>
@@ -1,143 +1,27 @@
1
- import { NO_ERRORS_SCHEMA } from '@angular/core';
2
- import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
- import { By } from '@angular/platform-browser';
4
- import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
1
+ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
5
2
  import { Globals } from '../../utils/globals';
3
+
6
4
  import { MenuOptionsComponent } from './menu-options.component';
7
5
 
8
6
  describe('MenuOptionsComponent', () => {
9
- let fixture: ComponentFixture<MenuOptionsComponent>;
10
7
  let component: MenuOptionsComponent;
11
- let globals: Globals;
8
+ let fixture: ComponentFixture<MenuOptionsComponent>;
12
9
 
13
- beforeEach(waitForAsync(() => {
10
+ beforeEach(async(() => {
14
11
  TestBed.configureTestingModule({
15
- declarations: [MenuOptionsComponent],
16
- providers: [Globals],
17
- schemas: [NO_ERRORS_SCHEMA],
18
- }).compileComponents();
12
+ declarations: [ MenuOptionsComponent ],
13
+ providers: [ Globals ]
14
+ })
15
+ .compileComponents();
19
16
  }));
20
17
 
21
18
  beforeEach(() => {
22
19
  fixture = TestBed.createComponent(MenuOptionsComponent);
23
20
  component = fixture.componentInstance;
24
- globals = TestBed.inject(Globals);
25
- globals.initDefafultParameters();
26
- globals.OPTIONS = 'Opzioni';
27
- globals.SOUND_ON = 'Suono on';
28
- globals.SOUND_OFF = 'Suono off';
29
- globals.LOGOUT = 'Esci';
30
- globals.BUILD_VERSION = 'v9.9.9-test';
31
- globals.themeColor = '#2a6ac1';
32
- globals.soundEnabled = true;
33
- globals.isOpenMenuOptions = false;
34
- globals.isLogged = false;
35
- globals.showLogoutOption = true;
21
+ fixture.detectChanges();
36
22
  });
37
23
 
38
24
  it('should create', () => {
39
25
  expect(component).toBeTruthy();
40
26
  });
41
-
42
- it('ngOnInit calcola themeColor50 da themeColor', () => {
43
- fixture.detectChanges();
44
- expect(component.themeColor50).toBe(convertColorToRGBA(globals.themeColor, 50));
45
- });
46
-
47
- it('f21_toggle_options apre e chiude il menu', () => {
48
- fixture.detectChanges();
49
- const toggle = fixture.debugElement.query(By.css('.button-menu-options'));
50
- toggle.triggerEventHandler('click', null);
51
- expect(globals.isOpenMenuOptions).toBe(true);
52
- fixture.detectChanges();
53
- expect(fixture.debugElement.query(By.css('.modal-menu-options'))).toBeTruthy();
54
- component.f21_toggle_options();
55
- expect(globals.isOpenMenuOptions).toBe(false);
56
- });
57
-
58
- it('toggleSound inverte soundEnabled e chiude il menu', () => {
59
- globals.soundEnabled = false;
60
- globals.isOpenMenuOptions = true;
61
- fixture.detectChanges();
62
- component.toggleSound();
63
- expect(globals.soundEnabled).toBe(true);
64
- expect(globals.isOpenMenuOptions).toBe(false);
65
- });
66
-
67
- it('signOut emette onSignOut e chiude il menu', () => {
68
- spyOn(component.onSignOut, 'emit');
69
- globals.isOpenMenuOptions = true;
70
- component.signOut();
71
- expect(globals.isOpenMenuOptions).toBe(false);
72
- expect(component.onSignOut.emit).toHaveBeenCalled();
73
- });
74
-
75
- it('menu aperto: etichette suono e versione visibili', () => {
76
- globals.isOpenMenuOptions = true;
77
- globals.soundEnabled = true;
78
- fixture.detectChanges();
79
- const root = fixture.nativeElement as HTMLElement;
80
- expect(root.textContent).toContain('Suono on');
81
- expect(root.textContent).toContain('v9.9.9-test');
82
- });
83
-
84
- it('logout visibile solo con isLogged e showLogoutOption', () => {
85
- globals.isOpenMenuOptions = true;
86
- globals.isLogged = false;
87
- globals.showLogoutOption = true;
88
- fixture.detectChanges();
89
- let logoutBtn = fixture.debugElement.queryAll(By.css('.modal-menu-options button')).find((d) =>
90
- (d.nativeElement as HTMLElement).getAttribute('aria-label')?.includes('Esci'),
91
- );
92
- expect(logoutBtn).toBeFalsy();
93
-
94
- globals.isLogged = true;
95
- fixture.detectChanges();
96
- logoutBtn = fixture.debugElement.queryAll(By.css('.modal-menu-options button')).find((d) =>
97
- (d.nativeElement as HTMLElement).getAttribute('aria-label') === 'Esci',
98
- );
99
- expect(logoutBtn).toBeTruthy();
100
- });
101
-
102
- it('isHover espone l’icona ingranaggio (mouseover / mouseleave)', () => {
103
- fixture.detectChanges();
104
- const toggle = fixture.debugElement.query(By.css('.button-menu-options'));
105
- expect(component.isHover).toBe(false);
106
- toggle.triggerEventHandler('mouseover', null);
107
- expect(component.isHover).toBe(true);
108
- toggle.triggerEventHandler('mouseleave', null);
109
- expect(component.isHover).toBe(false);
110
- });
111
-
112
- it('aria-expanded del toggle riflette isOpenMenuOptions', () => {
113
- globals.isOpenMenuOptions = true;
114
- fixture.detectChanges();
115
- const btn = fixture.debugElement.query(By.css('.button-menu-options')).nativeElement as HTMLButtonElement;
116
- expect(btn.getAttribute('aria-expanded')).toBe('true');
117
- });
118
-
119
- it('keydown escape sul pannello opzioni chiude il menu', () => {
120
- globals.isOpenMenuOptions = true;
121
- fixture.detectChanges();
122
- const modal = fixture.debugElement.query(By.css('.modal-menu-options'));
123
- const ev = new KeyboardEvent('keydown', { key: 'Escape' });
124
- spyOn(ev, 'preventDefault');
125
- spyOn(ev, 'stopPropagation');
126
- modal.triggerEventHandler('keydown.escape', ev);
127
- expect(globals.isOpenMenuOptions).toBe(false);
128
- });
129
-
130
- it('pulsante suono espone aria-pressed e aria-label in base a soundEnabled', () => {
131
- globals.isOpenMenuOptions = true;
132
- globals.soundEnabled = false;
133
- fixture.detectChanges();
134
- const soundBtn = fixture.debugElement.query(By.css('button[aria-label="Suono off"]'))
135
- .nativeElement as HTMLButtonElement;
136
- expect(soundBtn.getAttribute('aria-pressed')).toBe('false');
137
- globals.soundEnabled = true;
138
- fixture.detectChanges();
139
- const soundOn = fixture.debugElement.query(By.css('button[aria-label="Suono on"]'))
140
- .nativeElement as HTMLButtonElement;
141
- expect(soundOn.getAttribute('aria-pressed')).toBe('true');
142
- });
143
27
  });
@@ -1,21 +1,23 @@
1
- <div class="audio-container">
2
1
 
2
+ <!-- <audio *ngIf="metadata" controls>
3
+ <source [src]="metadata.src" type="audio/mpeg">
4
+ </audio> *ngIf="!metadata"-->
5
+ <div class="audio-container">
6
+
3
7
  <div class="audio-track">
4
- <button type="button"
5
- class="play-pause"
6
- [attr.aria-label]="isPlaying
7
- ? (translationMap?.get('BUTTON_PAUSE_AUDIO') || 'Pause audio')
8
- : (translationMap?.get('BUTTON_PLAY_AUDIO') || 'Play audio')"
9
- [attr.aria-pressed]="isPlaying ? 'true' : 'false'"
10
- (click)="playPauseAudio()">
11
- <svg *ngIf="!isPlaying" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" aria-hidden="true" focusable="false">
8
+ <button *ngIf="!isPlaying" class="play-pause" (click)="playPauseAudio()">
9
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
12
10
  <path d="M320-200v-560l440 280-440 280Z"/>
13
11
  </svg>
14
- <svg *ngIf="isPlaying" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" aria-hidden="true" focusable="false">
12
+ <!-- <i class="material-icons">play_arrow</i> -->
13
+ </button>
14
+ <button *ngIf="isPlaying" class="play-pause" (click)="playPauseAudio()">
15
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" >
15
16
  <path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/>
16
17
  </svg>
18
+ <!-- <i class="material-icons">pause</i> -->
17
19
  </button>
18
- <div class="duration" [style.color]="color" aria-live="off">
20
+ <div class="duration" [style.color]="color">
19
21
  <span *ngIf="!isPlaying">{{ audioDuration ? formatTime(audioDuration) : '00:00' }}</span>
20
22
  <span *ngIf="isPlaying">{{ formatTime(currentTime) }}</span>
21
23
  </div>
@@ -24,7 +26,7 @@
24
26
 
25
27
  <div class="audio-player-custom">
26
28
  <audio #audioElement [src]="audioUrl"></audio>
27
- <canvas #canvasElement class="waveformCanvas" aria-hidden="true"></canvas>
29
+ <canvas #canvasElement class="waveformCanvas"></canvas>
28
30
  </div>
29
31
 
30
- </div>
32
+ </div>
@@ -1,158 +1,23 @@
1
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
3
- import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
4
- import { NGXLogger } from 'ngx-logger';
5
2
 
6
3
  import { AudioComponent } from './audio.component';
7
4
 
8
- describe('AudioComponent', () => {
5
+ describe('AudioTrackComponent', () => {
9
6
  let component: AudioComponent;
10
7
  let fixture: ComponentFixture<AudioComponent>;
11
- const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
12
- const customLogger = new CustomLogger(ngxlogger);
13
- const arrayBuf = new ArrayBuffer(64);
14
-
15
- const fakeBuffer = {
16
- duration: 90,
17
- getChannelData: () => new Float32Array(4000),
18
- } as unknown as AudioBuffer;
19
8
 
20
9
  beforeEach(async () => {
21
- LoggerInstance.setInstance(customLogger);
22
- spyOn(window, 'fetch').and.returnValue(
23
- Promise.resolve({
24
- arrayBuffer: () => Promise.resolve(arrayBuf),
25
- } as Response),
26
- );
27
- spyOn(AudioContext.prototype, 'decodeAudioData').and.returnValue(Promise.resolve(fakeBuffer));
28
-
29
10
  await TestBed.configureTestingModule({
30
- declarations: [AudioComponent],
11
+ declarations: [ AudioComponent ]
31
12
  })
32
- .overrideComponent(AudioComponent, {
33
- set: {
34
- template: `
35
- <div class="audio-container">
36
- <div class="audio-track"></div>
37
- <div class="audio-player-custom">
38
- <audio #audioElement></audio>
39
- <canvas #canvasElement width="120" height="32"></canvas>
40
- </div>
41
- </div>`,
42
- },
43
- })
44
- .compileComponents();
13
+ .compileComponents();
45
14
 
46
15
  fixture = TestBed.createComponent(AudioComponent);
47
16
  component = fixture.componentInstance;
48
- component.stylesMap = new Map<string, string>([
49
- ['bubbleSentBackground', 'rgba(10, 20, 30, 1)'],
50
- ['bubbleSentTextColor', '#112233'],
51
- ]);
52
- component.color = '#000000';
53
- });
54
-
55
- it('should create', async () => {
56
- const blob = new Blob([new Uint8Array(arrayBuf.byteLength)], { type: 'audio/wav' });
57
- component.audioBlob = blob;
58
17
  fixture.detectChanges();
59
- await fixture.whenStable();
60
- fixture.detectChanges();
61
- expect(component).toBeTruthy();
62
- });
63
-
64
- it('formatTime should pad seconds under 10', () => {
65
- expect(component.formatTime(0)).toBe('0:00');
66
- expect(component.formatTime(9)).toBe('0:09');
67
- expect(component.formatTime(70)).toBe('1:10');
68
- });
69
-
70
- it('extractFirstColor should parse first rgba from gradient string', () => {
71
- expect(component.extractFirstColor('linear-gradient(rgba(1, 2, 3, 0.5), red)')).toBe('rgba(1, 2, 3, 0.5)');
72
- expect(component.extractFirstColor('no-color')).toBeNull();
73
- });
74
-
75
- it('drawWaveform should return early when canvas context missing', () => {
76
- const canvas = document.createElement('canvas');
77
- spyOn(canvas, 'getContext').and.returnValue(null);
78
- (component as any).waveformCanvas = { nativeElement: canvas };
79
- (component as any).audioBuffer = fakeBuffer;
80
- (component as any).audioDuration = 10;
81
- (component as any).audioElement = {
82
- nativeElement: { currentTime: 0, paused: true },
83
- };
84
- expect(() => component.drawWaveform(fakeBuffer)).not.toThrow();
85
- });
86
-
87
- it('drawWaveform should render bars when context exists', () => {
88
- const fillRect = jasmine.createSpy('fillRect');
89
- const clearRect = jasmine.createSpy('clearRect');
90
- const canvas = document.createElement('canvas');
91
- canvas.width = 200;
92
- canvas.height = 40;
93
- spyOn(canvas, 'getContext').and.returnValue({ fillRect, clearRect } as any);
94
- (component as any).waveformCanvas = { nativeElement: canvas };
95
- (component as any).audioElement = {
96
- nativeElement: { currentTime: 0, paused: true },
97
- };
98
- (component as any).audioDuration = 10;
99
- component.drawWaveform(fakeBuffer);
100
- expect(clearRect).toHaveBeenCalled();
101
- expect(fillRect).toHaveBeenCalled();
102
18
  });
103
19
 
104
- it('ngAfterViewInit with blob should wire object URL and CSS vars', async () => {
105
- const blob = new Blob([new Uint8Array(128)], { type: 'audio/wav' });
106
- component.audioBlob = blob;
107
- spyOn(URL, 'createObjectURL').and.returnValue('blob:mock-audio');
108
- fixture.detectChanges();
109
- await fixture.whenStable();
110
- fixture.detectChanges();
111
- expect(component.rawAudioUrl).toBe('blob:mock-audio');
112
- expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
113
- });
114
-
115
- it('ngAfterViewInit with metadata.src should fetch and decode', async () => {
116
- component.audioBlob = null;
117
- component.metadata = { src: 'blob:from-meta' };
118
- fixture.detectChanges();
119
- await fixture.whenStable();
120
- fixture.detectChanges();
121
- expect(window.fetch).toHaveBeenCalled();
122
- expect(component.audioDuration).toBe(90);
123
- });
124
-
125
- it('playPauseAudio should toggle play state when buffer ready', () => {
126
- spyOn(window, 'requestAnimationFrame').and.stub();
127
- (component as any).audioBuffer = fakeBuffer;
128
- (component as any).audioDuration = 10;
129
- const play = jasmine.createSpy('play').and.returnValue(Promise.resolve());
130
- const pause = jasmine.createSpy('pause');
131
- const canvas = document.createElement('canvas');
132
- canvas.width = 120;
133
- canvas.height = 32;
134
- spyOn(canvas, 'getContext').and.returnValue({
135
- fillRect: jasmine.createSpy(),
136
- clearRect: jasmine.createSpy(),
137
- } as any);
138
- (component as any).waveformCanvas = { nativeElement: canvas };
139
- (component as any).audioElement = {
140
- nativeElement: { paused: true, currentTime: 0, play, pause, ontimeupdate: null as any, onended: null as any },
141
- };
142
- (component as any).audioContext = { resume: jasmine.createSpy().and.returnValue(Promise.resolve()) };
143
-
144
- component.playPauseAudio();
145
- expect(play).toHaveBeenCalled();
146
- expect(component.isPlaying).toBe(true);
147
-
148
- (component as any).audioElement.nativeElement.paused = false;
149
- component.playPauseAudio();
150
- expect(pause).toHaveBeenCalled();
151
- });
152
-
153
- it('getAudioDuration should set audioDuration from decoded buffer', async () => {
154
- component.metadata = { src: 'blob:x' };
155
- await component.getAudioDuration();
156
- expect(component.audioDuration).toBe(90);
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
157
22
  });
158
23
  });
@@ -18,7 +18,6 @@ export class AudioComponent implements AfterViewInit {
18
18
  @Input() metadata: any | null = null;
19
19
  @Input() color: string;
20
20
  @Input() stylesMap: Map<string, string>;
21
- @Input() translationMap: Map<string, string>;
22
21
 
23
22
  audioUrl: SafeUrl | null = null;
24
23
  rawAudioUrl: string | null = null;
@@ -155,10 +154,15 @@ export class AudioComponent implements AfterViewInit {
155
154
  // });
156
155
 
157
156
  const response = await fetch(this.rawAudioUrl!);
157
+ this.logger.debug('getAudioDuration: response ---> ', response)
158
158
  const arrayBuffer = await response.arrayBuffer();
159
+ this.logger.debug('getAudioDuration: arrayBuffer ---> ', arrayBuffer)
159
160
  const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
161
+ this.logger.debug('getAudioDuration: audioContext ---> ', audioContext)
160
162
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
163
+ this.logger.debug('getAudioDuration: audioBuffer ---> ', audioBuffer)
161
164
  this.audioDuration = audioBuffer.duration;
165
+ this.logger.debug('getAudioDuration: audioDuration ---> ', this.audioDuration)
162
166
 
163
167
  }
164
168
 
@@ -1,9 +1,9 @@
1
1
  <div class="c21-icon-avatar">
2
2
  <div class="c21-avatar-image profile_image">
3
3
  <!-- is a BOT -->
4
- <img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'Bot'" role="img" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
4
+ <img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
5
5
  <!-- is a HUMAN -->
6
- <img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'User'" role="img" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
6
+ <img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
7
7
  </div>
8
8
  </div>
9
9
 
@@ -1,119 +1,27 @@
1
- import { Injectable } from '@angular/core';
2
- import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
1
+ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3
2
  import { ImageRepoService } from '../../../../chat21-core/providers/abstract/image-repo.service';
4
3
 
5
4
  import { AvatarComponent } from './avatar.component';
6
5
 
7
- @Injectable()
8
- class ImageRepoStub extends ImageRepoService {
9
- getImagePhotoUrl(uid: string): string {
10
- return `https://cdn.test/photo/${uid}`;
11
- }
12
- checkImageExists(_uid: string, _cb: (exist: boolean) => void): void {}
13
- }
14
-
15
6
  describe('AvatarComponent', () => {
16
7
  let component: AvatarComponent;
17
8
  let fixture: ComponentFixture<AvatarComponent>;
18
9
 
19
- beforeEach(waitForAsync(() => {
10
+ beforeEach(async(() => {
20
11
  TestBed.configureTestingModule({
21
- declarations: [AvatarComponent],
22
- providers: [{ provide: ImageRepoService, useClass: ImageRepoStub }],
23
- }).compileComponents();
12
+ declarations: [ AvatarComponent ],
13
+ providers: [ ImageRepoService ]
14
+ })
15
+ .compileComponents();
24
16
  }));
25
17
 
26
18
  beforeEach(() => {
27
19
  fixture = TestBed.createComponent(AvatarComponent);
28
20
  component = fixture.componentInstance;
29
- component.baseLocation = 'https://app.test';
21
+ fixture.detectChanges();
30
22
  });
31
23
 
32
24
  it('should create', () => {
33
- component.senderID = 'user_1';
34
- component.senderFullname = 'Alice';
35
- fixture.detectChanges();
36
25
  expect(component).toBeTruthy();
37
26
  });
38
-
39
- it('ngOnInit should prefer remote photo when checkImageExists returns true', () => {
40
- const remote = 'https://cdn.test/photo/bot_1';
41
- spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
42
- cb(true);
43
- });
44
- component.senderID = 'bot_1';
45
- component.senderFullname = 'Support Bot';
46
- component.ngOnInit();
47
- expect(component.url).toBe(remote);
48
- });
49
-
50
- it('ngOnInit should keep default bot asset when remote image missing', () => {
51
- spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
52
- cb(false);
53
- });
54
- component.senderID = 'bot_1';
55
- component.senderFullname = 'Bot';
56
- component.ngOnInit();
57
- expect(component.url).toBe(component.baseLocation + '/assets/images/tommy_bot_tiledesk.svg');
58
- });
59
-
60
- it('ngOnInit should use human default when not bot and photo missing', () => {
61
- spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
62
- cb(false);
63
- });
64
- component.senderID = 'user_99';
65
- component.senderFullname = 'Bob';
66
- component.ngOnInit();
67
- expect(component.url).toBe(component.baseLocation + '/assets/images/chat_human_avatar.svg');
68
- });
69
-
70
- it('checkImageExists should invoke callback on image load', (done) => {
71
- const imgCtor = window.Image;
72
- (window as any).Image = function MockImage(this: any) {
73
- setTimeout(() => this.onload && this.onload(), 0);
74
- return this;
75
- } as any;
76
- component.checkImageExists('https://x', (ok) => {
77
- (window as any).Image = imgCtor;
78
- expect(ok).toBe(true);
79
- done();
80
- });
81
- });
82
-
83
- it('checkImageExists should invoke callback false on error', (done) => {
84
- const imgCtor = window.Image;
85
- (window as any).Image = function MockImage(this: any) {
86
- setTimeout(() => this.onerror && this.onerror(), 0);
87
- return this;
88
- } as any;
89
- component.checkImageExists('https://bad', (ok) => {
90
- (window as any).Image = imgCtor;
91
- expect(ok).toBe(false);
92
- done();
93
- });
94
- });
95
-
96
- it('onBotImgError should swap to local bot svg', () => {
97
- const target = { src: 'bad' } as any;
98
- component.baseLocation = 'https://host';
99
- component.onBotImgError({ target });
100
- expect(target.src).toContain('tommy_bot_tiledesk.svg');
101
- });
102
-
103
- it('onHumanImgError should swap to local human svg', () => {
104
- const target = { src: 'bad' } as any;
105
- component.baseLocation = 'https://host';
106
- component.onHumanImgError({ target });
107
- expect(target.src).toContain('chat_human_avatar.svg');
108
- });
109
-
110
- it('template should expose accessible name on avatar image (bot)', () => {
111
- spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.stub();
112
- component.senderID = 'bot_x';
113
- component.senderFullname = 'Helper Bot';
114
- fixture.detectChanges();
115
- const img = (fixture.nativeElement as HTMLElement).querySelector('img');
116
- expect(img?.getAttribute('alt')).toBeTruthy();
117
- expect(img?.getAttribute('role')).toBe('img');
118
- });
119
27
  });