@chat21/chat21-web-widget 5.1.34 → 5.2.1

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 (191) 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/CHANGELOG.md +25 -0
  6. package/Dockerfile +4 -5
  7. package/README.md +1 -1
  8. package/angular.json +21 -3
  9. package/docs/ACCESSIBILITY-STATEMENT.md +388 -0
  10. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_ALIGNMENT.md +60 -0
  11. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +386 -0
  12. package/env.sample +3 -2
  13. package/mocks/voice-websocket-mock/server.cjs +245 -0
  14. package/package.json +10 -3
  15. package/playwright.config.ts +41 -0
  16. package/src/app/app.component.html +2 -2
  17. package/src/app/app.component.scss +25 -14
  18. package/src/app/app.component.spec.ts +21 -6
  19. package/src/app/app.module.ts +13 -0
  20. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  21. package/src/app/component/conversation-detail/conversation/conversation.component.scss +38 -0
  22. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  23. package/src/app/component/conversation-detail/conversation/conversation.component.ts +70 -2
  24. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  27. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +241 -149
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -5
  31. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  32. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +203 -110
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +212 -1
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +458 -78
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +288 -76
  36. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  39. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  41. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  44. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  47. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  48. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  51. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  52. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  53. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  57. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  58. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  59. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  60. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  61. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  62. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  63. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  64. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  68. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  69. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  70. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  71. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  72. package/src/app/component/home/home.component.html +38 -31
  73. package/src/app/component/home/home.component.scss +4 -2
  74. package/src/app/component/home/home.component.spec.ts +226 -11
  75. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  76. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  77. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  78. package/src/app/component/last-message/last-message.component.html +15 -9
  79. package/src/app/component/last-message/last-message.component.scss +16 -2
  80. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  81. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  82. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  83. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  84. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  85. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  86. package/src/app/component/menu-options/menu-options.component.html +30 -20
  87. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  88. package/src/app/component/message/audio/audio.component.html +13 -15
  89. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  90. package/src/app/component/message/audio/audio.component.ts +1 -5
  91. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  92. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  93. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +112 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.ts +714 -0
  95. package/src/app/component/message/avatar/avatar.component.html +2 -2
  96. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  97. package/src/app/component/message/bubble-message/bubble-message.component.html +41 -51
  98. package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
  99. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +147 -57
  100. package/src/app/component/message/bubble-message/bubble-message.component.ts +95 -13
  101. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  102. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  103. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  104. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  105. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  106. package/src/app/component/message/carousel/carousel.component.html +29 -16
  107. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  108. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  109. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  110. package/src/app/component/message/frame/frame.component.html +9 -4
  111. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  112. package/src/app/component/message/frame/frame.component.ts +7 -2
  113. package/src/app/component/message/html/html.component.html +1 -1
  114. package/src/app/component/message/html/html.component.scss +1 -1
  115. package/src/app/component/message/html/html.component.spec.ts +24 -7
  116. package/src/app/component/message/image/image.component.html +12 -10
  117. package/src/app/component/message/image/image.component.scss +16 -0
  118. package/src/app/component/message/image/image.component.spec.ts +101 -15
  119. package/src/app/component/message/image/image.component.ts +90 -51
  120. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  121. package/src/app/component/message/json-sources/json-sources.component.html +6 -5
  122. package/src/app/component/message/json-sources/json-sources.component.scss +26 -18
  123. package/src/app/component/message/json-sources/json-sources.component.ts +41 -0
  124. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  125. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  126. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  127. package/src/app/component/message/text/text.component.html +3 -3
  128. package/src/app/component/message/text/text.component.scss +80 -86
  129. package/src/app/component/message/text/text.component.spec.ts +106 -13
  130. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  131. package/src/app/component/selection-department/selection-department.component.html +21 -23
  132. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  133. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  134. package/src/app/component/send-button/send-button.component.html +5 -13
  135. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  136. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  137. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  138. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  139. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  140. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  141. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  142. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  143. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  144. package/src/app/pipe/marked.pipe.ts +51 -41
  145. package/src/app/providers/app-config.service.ts +4 -2
  146. package/src/app/providers/brand.service.spec.ts +23 -2
  147. package/src/app/providers/brand.service.ts +1 -1
  148. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  149. package/src/app/providers/global-settings.service.ts +40 -2
  150. package/src/app/providers/json-sources-parser.service.ts +13 -1
  151. package/src/app/providers/translator.service.ts +24 -7
  152. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +116 -0
  153. package/src/app/providers/tts-audio-playback-coordinator.service.ts +122 -0
  154. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  155. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +156 -0
  156. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  157. package/src/app/providers/voice/audio.types.ts +40 -0
  158. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  159. package/src/app/providers/voice/vad.service.ts +70 -0
  160. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  161. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  162. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  163. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  164. package/src/app/providers/voice/voice.service.ts +969 -0
  165. package/src/app/sass/_variables.scss +2 -0
  166. package/src/app/sass/animations.scss +19 -1
  167. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  168. package/src/app/utils/globals.ts +14 -0
  169. package/src/app/utils/utils-resources.ts +1 -1
  170. package/src/assets/i18n/en.json +128 -100
  171. package/src/assets/i18n/es.json +128 -100
  172. package/src/assets/i18n/fr.json +128 -100
  173. package/src/assets/i18n/it.json +128 -98
  174. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  175. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  176. package/src/assets/sounds/keyboard.mp3 +0 -0
  177. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  178. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  179. package/src/chat21-core/models/message.ts +2 -1
  180. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  181. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  182. package/src/chat21-core/providers/firebase/firebase-init-service.ts +5 -5
  183. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  184. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  185. package/src/chat21-core/utils/utils-message.ts +7 -0
  186. package/src/widget-config-template.json +3 -1
  187. package/src/widget-config.json +28 -27
  188. package/tests/widget-form-rich.spec.ts +67 -0
  189. package/tests/widget-index-dev-settings.spec.ts +52 -0
  190. package/tests/widget-twp-iframe.spec.ts +39 -0
  191. package/tsconfig.json +5 -0
@@ -9,33 +9,22 @@ svg.star:hover{
9
9
  fill: #FFD700;
10
10
  }
11
11
  </style>
12
- <div id="chat21-star-rating-widget">
12
+ <div id="chat21-star-rating-widget" role="dialog" aria-modal="true" [attr.aria-label]="g.CUSTOMER_SATISFACTION">
13
13
  <!-- HEADER -->
14
14
  <div class="c21-header" [ngStyle]="{ 'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }">
15
15
  <div class="c21-header-container">
16
- <!-- ICON CLOSE CHAT -->
17
- <!-- <div class="c21-header-button">
18
- <div class="c21-close-button c21-small" aria-label="Close" aria-hidden="true" role="button">
19
- <div class="c21-close-button-body" (click)="returnClosePage()">
20
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': g.themeForegroundColor }"
21
- xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
22
- <path fill="none" d="M0 0h24v24H0V0z" />
23
- <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
24
- </svg>
25
- </div>
26
- </div>
27
- </div> -->
28
-
29
16
  <!-- CONTENT HEADER -->
30
17
  <div class="c21-header-content">
31
18
  <!-- ICON BACK -->
32
- <button *ngIf="(step > 0 && step != 2)" tabindex="1417" aria-label=" indietro " class="c21-header-button c21-left c21-button-clean" (click)="prevStep()">
33
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }" xmlns="http://www.w3.org/2000/svg"
19
+ <button *ngIf="(step > 0 && step != 2)"
20
+ type="button"
21
+ [attr.aria-label]="g['BACK']"
22
+ class="c21-header-button c21-left c21-button-clean"
23
+ (click)="prevStep()">
24
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }" xmlns="http://www.w3.org/2000/svg"
34
25
  width="24px" height="24px" viewBox="0 0 24 24">
35
- <!-- <path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM7.07 18.28c.43-.9 3.05-1.78 4.93-1.78s4.51.88 4.93 1.78C15.57 19.36 13.86 20 12 20s-3.57-.64-4.93-1.72zm11.29-1.45c-1.43-1.74-4.9-2.33-6.36-2.33s-4.93.59-6.36 2.33C4.62 15.49 4 13.82 4 12c0-4.41 3.59-8 8-8s8 3.59 8 8c0 1.82-.62 3.49-1.64 4.83zM12 6c-1.94 0-3.5 1.56-3.5 3.5S10.06 13 12 13s3.5-1.56 3.5-3.5S13.94 6 12 6zm0 5c-.83 0-1.5-.67-1.5-1.5S11.17 8 12 8s1.5.67 1.5 1.5S12.83 11 12 11z"/> -->
36
26
  <path fill="none" d="M0 0h24v24H0V0z" />
37
27
  <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z" />
38
- <title id="altIconTitle">{{ g['BACK'] }}</title>
39
28
  </svg>
40
29
  </button>
41
30
  <!-- TITLE HEADER -->
@@ -58,61 +47,56 @@ svg.star:hover{
58
47
  <!-- *************** STEP 0 : BEGIN ***************** -->
59
48
  <div *ngIf="step==0" class="step-rate">
60
49
  <div *ngIf="g.allowTranscriptDownload" class="c21-modal-content" style="text-align: right; margin: 0px 20px 30px 10px;">
61
- <div class="c21-link" (click)="dowloadTranscript()" >
50
+ <button type="button" class="c21-link c21-button-clean" (click)="dowloadTranscript()">
62
51
  <span [ngStyle]="{'color': stylesMap?.get('themeColor')}">{{ g.DOWNLOAD_TRANSCRIPT }}</span>
63
- </div>
52
+ </button>
64
53
  </div>
65
54
  <div class="clear"></div>
66
-
55
+
67
56
  <!-- CONTENT -->
68
57
  <div class="c21-modal-content">
69
58
  <div class="default-text">{{ g.YOUR_OPINION_ON_OUR_CUSTOMER_SERVICE }}</div>
70
59
  <fieldset class="chat21-rating" [attr.disabled]="!g.isOpenStartRating? '' : null">
60
+ <legend class="visually-hidden">{{ g.YOUR_RATING }}</legend>
71
61
  <input class="c21-input-star" type="radio" id="star1" name="rating" value="1" />
72
62
  <label (mouseover)="mouseOverRate(1)" (mouseleave)="mouseOverRate(0)" (click)="openRate(1)" class="full c21-button" for="star1" title="scarso - 1 star">
73
- <svg class="star" [class.active]="mouseRate>=1" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
63
+ <svg aria-hidden="true" focusable="false" class="star" [class.active]="mouseRate>=1" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
74
64
  </label>
75
65
  <input class="c21-input-star" type="radio" id="star2" name="rating" value="2" />
76
66
  <label (mouseover)="mouseOverRate(2)" (mouseleave)="mouseOverRate(0)" (click)="openRate(2)" class="full c21-button" for="star2" title="sufficiente - 2 stars">
77
- <svg class="star" [class.active]="mouseRate>=2" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
67
+ <svg aria-hidden="true" focusable="false" class="star" [class.active]="mouseRate>=2" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
78
68
  </label>
79
69
  <input class="c21-input-star" type="radio" id="star3" name="rating" value="3" />
80
70
  <label (mouseover)="mouseOverRate(3)" (mouseleave)="mouseOverRate(0)" (click)="openRate(3)" class="full c21-button" for="star3" title="buono - 3 stars">
81
- <svg class="star" [class.active]="mouseRate>=3" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
71
+ <svg aria-hidden="true" focusable="false" class="star" [class.active]="mouseRate>=3" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
82
72
  </label>
83
73
  <input class="c21-input-star" type="radio" id="star4" name="rating" value="4" />
84
74
  <label (mouseover)="mouseOverRate(4)" (mouseleave)="mouseOverRate(0)" (click)="openRate(4)" class="full c21-button" for="star4" title="ottimo - 4 stars">
85
- <svg class="star" [class.active]="mouseRate>=4" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
75
+ <svg aria-hidden="true" focusable="false" class="star" [class.active]="mouseRate>=4" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
86
76
  </label>
87
77
  <input class="c21-input-star" type="radio" id="star5" name="rating" value="5" />
88
78
  <label (mouseover)="mouseOverRate(5)" (mouseleave)="mouseOverRate(0)" (click)="openRate(5)" class="full c21-button" for="star5" title="eccellente - 5 stars">
89
- <svg class="star" [class.active]="mouseRate>=5" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
79
+ <svg aria-hidden="true" focusable="false" class="star" [class.active]="mouseRate>=5" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>
90
80
  </label>
91
81
  </fieldset>
92
82
  </div>
93
83
  <div class="clear"></div>
94
- <!-- <button [attr.disabled]="!g.isOpenStartRating? '' : null" *ngIf="g.allowTranscriptDownload !== true" class="c21-button-primary" [ngStyle]="{'background-color': g.themeColor, 'border-color': g.themeColor}"
95
- (click)="dowloadTranscript()">
96
- <span [ngStyle]="{'color': g.themeForegroundColor}">{{ g.DOWNLOAD_TRANSCRIPT }}</span>
97
- </button> -->
98
- <!-- <button class="c21-button-link" (click)="returnClosePage()">
99
- <span>{{ g.CLOSE }}</span>
100
- </button> -->
101
-
102
- <!-- ICON CLOSE -->
103
- <button tabindex="1414" class="c21-button-primary" (click)="returnClosePage()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
84
+
85
+ <!-- BUTTON CLOSE -->
86
+ <button type="button" class="c21-button-primary"
87
+ [attr.aria-label]="g.CLOSE"
88
+ (click)="returnClosePage()"
89
+ [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
104
90
  <span class="v-align-center">
105
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
91
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
106
92
  width="18" height="18" viewBox="0 0 24 24">
107
93
  <path fill="none" d="M0 0h24v24H0V0z" />
108
94
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
109
- <title id="altIconTitle">{{ g.CLOSE }}</title>
110
95
  </svg>
111
96
  </span>
112
97
  <span class="v-align-center c21-label-button">
113
98
  {{ g.CLOSE }}
114
- <!-- {{translationMap?.get('LABEL_START_NW_CONV')}} -->
115
- </span>
99
+ </span>
116
100
  <div class="clear"></div>
117
101
  </button>
118
102
  </div>
@@ -120,62 +104,47 @@ svg.star:hover{
120
104
 
121
105
  <!-- *************** STEP 1 : BEGIN ***************** -->
122
106
  <div *ngIf="step==1" class="step-rate">
123
- <!-- CONTENT -->
124
- <!-- <div *ngIf="g.allowTranscriptDownload" class="c21-modal-content" style="text-align: right; margin: 0px 20px 30px 10px;">
125
- <div class="c21-link" (click)="dowloadTranscript()" >
126
- <span [ngStyle]="{'color': g.themeColor}">{{ g.DOWNLOAD_TRANSCRIPT }}</span>
127
- </div>
128
- </div> -->
129
107
  <div class="clear"></div>
130
108
  <div class="c21-step-content">
131
- <!-- il tuo voto: -->
132
-
133
- <!-- <div class="chat21-stars">
134
- <span *ngIf="rate>0" class="fa fa-star checked"></span>
135
- <span *ngIf="rate>1" class="fa fa-star checked"></span>
136
- <span *ngIf="rate>2" class="fa fa-star checked"></span>
137
- <span *ngIf="rate>3" class="fa fa-star checked"></span>
138
- <span *ngIf="rate>4" class="fa fa-star checked"></span>
139
- </div> -->
140
- <!-- <div class="default-text">{{ g.YOUR_RATING }}</div> -->
141
-
142
-
143
- <!-- Scrivi la tua opinione...(opzionale) -->
109
+
144
110
  <textarea #textbox rows="1" id="chat21-message-rate-context" class='textarea-rate'
111
+ [attr.aria-label]="g.WRITE_YOUR_OPINION"
145
112
  placeholder="{{ g.WRITE_YOUR_OPINION }}">{{message}}</textarea>
146
113
  <div class="clear"></div>
147
114
 
148
- <!-- ICON CLOSE -->
149
- <button tabindex="1415" class="c21-button-primary" (click)="returnClosePage()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
115
+ <!-- BUTTON CLOSE -->
116
+ <button type="button" class="c21-button-primary"
117
+ [attr.aria-label]="g.CLOSE"
118
+ (click)="returnClosePage()"
119
+ [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
150
120
  <span class="v-align-center">
151
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
121
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
152
122
  width="18" height="18" viewBox="0 0 24 24">
153
123
  <path fill="none" d="M0 0h24v24H0V0z" />
154
124
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
155
- <title id="altIconTitle">{{ g.CLOSE }}</title>
156
125
  </svg>
157
126
  </span>
158
127
  <span class="v-align-center c21-label-button">
159
128
  {{ g.CLOSE }}
160
- <!-- {{translationMap?.get('LABEL_START_NW_CONV')}} -->
161
- </span>
129
+ </span>
162
130
  <div class="clear"></div>
163
131
  </button>
164
132
 
165
- <!-- ICON SUBMIT -->
166
- <button tabindex="1416" class="c21-button-primary" (click)="sendRate()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
133
+ <!-- BUTTON SUBMIT -->
134
+ <button type="button" class="c21-button-primary"
135
+ [attr.aria-label]="g.SUBMIT"
136
+ (click)="sendRate()"
137
+ [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
167
138
  <span class="v-align-center">
168
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
139
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
169
140
  width="18" height="18" viewBox="0 0 24 24">
170
141
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
171
142
  <path d="M0 0h24v24H0z" fill="none"/>
172
- <title id="altIconTitle">{{ g.SUBMIT }}</title>
173
143
  </svg>
174
144
  </span>
175
145
  <span class="v-align-center c21-label-button">
176
146
  {{ g.SUBMIT }}
177
- <!-- {{translationMap?.get('LABEL_START_NW_CONV')}} -->
178
- </span>
147
+ </span>
179
148
  <div class="clear"></div>
180
149
  </button>
181
150
 
@@ -187,32 +156,33 @@ svg.star:hover{
187
156
  <!-- *************** STEP 2 : BEGIN ***************** -->
188
157
  <div *ngIf="step==2" class="step-rate">
189
158
  <div *ngIf="g.allowTranscriptDownload" class="c21-modal-content" style="text-align: right; margin: 0px 20px 30px 10px;">
190
- <div class="c21-link" (click)="dowloadTranscript()" >
159
+ <button type="button" class="c21-link c21-button-clean" (click)="dowloadTranscript()">
191
160
  <span [ngStyle]="{'color': stylesMap?.get('themeColor')}">{{ g.DOWNLOAD_TRANSCRIPT }}</span>
192
- </div>
161
+ </button>
193
162
  </div>
194
163
  <div class="clear"></div>
195
-
164
+
196
165
  <!-- CONTENT -->
197
166
  <div class="c21-step-content">
198
167
  <div class="default-title">{{ g.THANK_YOU_FOR_YOUR_EVALUATION }}</div>
199
168
  <div class="default-text">{{ g.YOUR_RATING_HAS_BEEN_RECEIVED }}</div>
200
169
  </div>
201
170
 
202
- <!-- ICON CLOSE -->
203
- <button tabindex="1415" class="c21-button-primary" (click)="returnClosePage()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
171
+ <!-- BUTTON CLOSE -->
172
+ <button type="button" class="c21-button-primary"
173
+ [attr.aria-label]="g.CLOSE"
174
+ (click)="returnClosePage()"
175
+ [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
204
176
  <span class="v-align-center">
205
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
177
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}" xmlns="http://www.w3.org/2000/svg"
206
178
  width="18" height="18" viewBox="0 0 24 24">
207
179
  <path fill="none" d="M0 0h24v24H0V0z" />
208
180
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
209
- <title id="altIconTitle">{{ g.CLOSE }}</title>
210
181
  </svg>
211
182
  </span>
212
183
  <span class="v-align-center c21-label-button">
213
184
  {{ g.CLOSE }}
214
- <!-- {{translationMap?.get('LABEL_START_NW_CONV')}} -->
215
- </span>
185
+ </span>
216
186
  <div class="clear"></div>
217
187
  </button>
218
188
  </div>
@@ -225,4 +195,4 @@ svg.star:hover{
225
195
 
226
196
  </div>
227
197
 
228
- </div>
198
+ </div>
@@ -1,8 +1,12 @@
1
+ import { ElementRef, Renderer2 } from '@angular/core';
2
+
1
3
  import { TooltipDirective } from './tooltip.directive';
2
4
 
3
5
  describe('TooltipDirective', () => {
4
- // it('should create an instance', () => {
5
- // const directive = new TooltipDirective();
6
- // expect(directive).toBeTruthy();
7
- // });
6
+ it('should create', () => {
7
+ const el = document.createElement('span');
8
+ const renderer = jasmine.createSpyObj('Renderer2', ['createElement', 'appendChild', 'addClass', 'removeClass', 'removeChild', 'setStyle']);
9
+ const directive = new TooltipDirective(new ElementRef(el), renderer as unknown as Renderer2);
10
+ expect(directive).toBeTruthy();
11
+ });
8
12
  });
@@ -1,25 +1,37 @@
1
- <div class="modal-container">
1
+ <div class="modal-container"
2
+ role="dialog"
3
+ aria-modal="true"
4
+ cdkTrapFocus
5
+ [cdkTrapFocusAutoCapture]="true"
6
+ aria-labelledby="confirm-close-title"
7
+ [attr.aria-label]="translationMap?.get('CONFIRM_CLOSE_CHAT')">
2
8
  <div class="header">
3
9
  <!-- ICON CLOSE CHAT -->
4
- <button tabindex="-1" class="c21-header-button c21-right c21-close c21-button-clean" (click)="onBack()">
5
- <svg role="img" aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('themeColor') }" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24">
10
+ <button type="button"
11
+ class="c21-header-button c21-right c21-close c21-button-clean"
12
+ [attr.aria-label]="translationMap?.get('CLOSE')"
13
+ (click)="onBack()">
14
+ <svg aria-hidden="true" focusable="false" [ngStyle]="{'fill': stylesMap?.get('themeColor') }" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24">
6
15
  <path fill="none" d="M0 0h24v24H0V0z" />
7
16
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
8
- <title id="altIconTitle">{{ translationMap?.get('CLOSE') }}</title>
9
17
  </svg>
10
18
  </button>
11
19
  </div>
12
20
  <div class="content">
13
- <div class="text">{{ translationMap?.get('CONFIRM_CLOSE_CHAT') }}</div>
21
+ <h2 id="confirm-close-title" class="text">{{ translationMap?.get('CONFIRM_CLOSE_CHAT') }}</h2>
14
22
  <div class="options">
15
23
 
16
24
  <!-- BUTTON CANCEL-->
17
- <span class="v-align-center c21-label-button back-button" [class.disabled]="isLoadingActive" (click)="onBack()">
25
+ <button type="button"
26
+ class="c21-button-clean v-align-center c21-label-button back-button"
27
+ [class.disabled]="isLoadingActive"
28
+ [attr.disabled]="isLoadingActive ? true : null"
29
+ (click)="onBack()">
18
30
  {{translationMap?.get('BACK')}}
19
- </span>
31
+ </button>
20
32
 
21
33
  <!-- BUTTON CLOSE-->
22
- <button tabindex="1040" aflistconv #aflistconv class="c21-button-primary" (click)="onConfirm()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
34
+ <button type="button" aflistconv #aflistconv class="c21-button-primary" (click)="onConfirm()" [ngStyle]="{'background-color': stylesMap?.get('themeColor'), 'border-color': stylesMap?.get('themeColor'), 'color': stylesMap?.get('foregroundColor') }">
23
35
  <span *ngIf="isLoadingActive" class="spinner-container">
24
36
  <svg xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="25" height="25" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }"
25
37
  viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve">
@@ -16,6 +16,9 @@
16
16
  gap: 15px;
17
17
  .text{
18
18
  max-width: calc(100% - 30px);
19
+ margin: 0;
20
+ font-size: 1em;
21
+ font-weight: 500;
19
22
  }
20
23
  .options{
21
24
  display: flex;
@@ -1,4 +1,8 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { A11yModule } from '@angular/cdk/a11y';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+
4
+ import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
5
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
2
6
 
3
7
  import { ConfirmCloseComponent } from './confirm-close.component';
4
8
 
@@ -6,12 +10,17 @@ describe('ConfirmCloseComponent', () => {
6
10
  let component: ConfirmCloseComponent;
7
11
  let fixture: ComponentFixture<ConfirmCloseComponent>;
8
12
 
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
13
+ beforeEach(waitForAsync(() => {
14
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
15
+ LoggerInstance.setInstance(new CustomLogger(ngxlogger));
16
+ TestBed.configureTestingModule({
17
+ imports: [A11yModule],
11
18
  declarations: [ ConfirmCloseComponent ]
12
19
  })
13
- .compileComponents();
20
+ .compileComponents();
21
+ }));
14
22
 
23
+ beforeEach(() => {
15
24
  fixture = TestBed.createComponent(ConfirmCloseComponent);
16
25
  component = fixture.componentInstance;
17
26
  fixture.detectChanges();
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, EventEmitter, Input, OnInit, Output, SimpleChange, SimpleChanges, ViewChild, OnDestroy } from '@angular/core';
1
+ import { Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, SimpleChange, SimpleChanges, ViewChild, OnDestroy } from '@angular/core';
2
2
  import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
3
3
  import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
4
4
 
@@ -25,6 +25,13 @@ export class ConfirmCloseComponent implements OnInit{
25
25
  // this.dialog.showModal();
26
26
  }
27
27
 
28
+ @HostListener('keydown.escape', ['$event'])
29
+ onEscape(event: KeyboardEvent){
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ this.onBack();
33
+ }
34
+
28
35
  ngOnChanges(changes: SimpleChanges){
29
36
  if(changes &&
30
37
  changes['conversationId'] &&
@@ -1,8 +1,41 @@
1
1
  import { HtmlEntitiesEncodePipe } from './html-entities-encode.pipe';
2
2
 
3
3
  describe('HtmlEntitiesEncodePipe', () => {
4
- it('create an instance', () => {
5
- const pipe = new HtmlEntitiesEncodePipe();
4
+ let pipe: HtmlEntitiesEncodePipe;
5
+
6
+ beforeEach(() => {
7
+ pipe = new HtmlEntitiesEncodePipe();
8
+ });
9
+
10
+ it('should create', () => {
6
11
  expect(pipe).toBeTruthy();
7
12
  });
13
+
14
+ it('should encode angle brackets so raw HTML tags are not preserved (XSS mitigation)', () => {
15
+ const malicious = '<script>alert(1)</script><img src=x onerror=alert(1)>';
16
+ const out = pipe.transform(malicious);
17
+ expect(out).not.toContain('<script');
18
+ expect(out).not.toContain('<img');
19
+ expect(out).toContain('&lt;');
20
+ expect(out).toContain('&gt;');
21
+ });
22
+
23
+ it('should encode double quotes for attribute injection contexts', () => {
24
+ expect(pipe.transform('say "hello"')).toContain('&quot;');
25
+ });
26
+
27
+ it('should replace newlines with <br> via replaceEndOfLine', () => {
28
+ const out = pipe.transform('line1\nline2');
29
+ expect(out).toContain('<br>');
30
+ expect(out).toContain('line1');
31
+ expect(out).toContain('line2');
32
+ });
33
+
34
+ it('should trim leading and trailing whitespace', () => {
35
+ expect(pipe.transform(' abc ')).toBe('abc');
36
+ });
37
+
38
+ it('should coerce non-string input to string before encoding', () => {
39
+ expect(pipe.transform(123 as any)).toBe('123');
40
+ });
8
41
  });
@@ -1,8 +1,44 @@
1
1
  import { MarkedPipe } from './marked.pipe';
2
2
 
3
3
  describe('MarkedPipe', () => {
4
- it('create an instance', () => {
5
- const pipe = new MarkedPipe();
4
+ let pipe: MarkedPipe;
5
+
6
+ beforeEach(() => {
7
+ pipe = new MarkedPipe();
8
+ });
9
+
10
+ it('should create', () => {
6
11
  expect(pipe).toBeTruthy();
7
12
  });
13
+
14
+ it('should render markdown bold as HTML strong', () => {
15
+ const html = pipe.transform('Hello **world**') as string;
16
+ expect(html).toMatch(/<strong>world<\/strong>|<b>world<\/b>/);
17
+ });
18
+
19
+ it('should not leave raw script tags in output (HTML tokens escaped)', () => {
20
+ const html = pipe.transform('<script>alert(1)</script>') as string;
21
+ expect(html.toLowerCase()).not.toContain('<script>');
22
+ expect(html).toContain('&lt;');
23
+ });
24
+
25
+ it('should not emit javascript: links as clickable href', () => {
26
+ const html = pipe.transform('[clickme](javascript:alert(1))') as string;
27
+ expect(html).not.toMatch(/href=["']javascript:/i);
28
+ });
29
+
30
+ it('should add rel noopener on http links', () => {
31
+ const html = pipe.transform('[x](https://example.com)') as string;
32
+ expect(html).toContain('rel="noopener noreferrer"');
33
+ expect(html).toContain('target="_blank"');
34
+ });
35
+
36
+ it('should treat escaped newlines as real newlines (GFM breaks)', () => {
37
+ const html = pipe.transform('a\\nb') as string;
38
+ expect(html).toMatch(/<br\s*\/?>/i);
39
+ });
40
+
41
+ it('should coerce null to empty string', () => {
42
+ expect(pipe.transform(null)).toBe('');
43
+ });
8
44
  });
@@ -5,61 +5,71 @@ import { marked, Tokens } from 'marked';
5
5
  name: 'marked'
6
6
  })
7
7
  export class MarkedPipe implements PipeTransform {
8
+ private static renderer: any = null;
8
9
 
9
- transform(value: any): string {
10
+ private static getRenderer(): any {
11
+ if (!MarkedPipe.renderer) {
12
+ const renderer = new marked.Renderer();
10
13
 
11
- const input =
12
- typeof value === 'string'
13
- ? value
14
- : (value === null || value === undefined) ? '' : String(value);
14
+ /* --------------------------------------------------
15
+ 🔐 1. NON renderizzare HTML raw
16
+ -------------------------------------------------- */
17
+ renderer.html = function(token: Tokens.HTML | Tokens.Tag): string {
18
+ const html = 'text' in token ? token.text : '';
15
19
 
16
- const inputWithNewlines = input.replace(/\\n/g, '\n');
20
+ return html
21
+ .replace(/&/g, '&amp;')
22
+ .replace(/</g, '&lt;')
23
+ .replace(/>/g, '&gt;');
24
+ };
25
+
26
+ /* --------------------------------------------------
27
+ 🔐 2. Link sicuri
28
+ -------------------------------------------------- */
29
+ const originalLinkRenderer = renderer.link.bind(renderer);
30
+
31
+ const dangerousProtocols = [
32
+ /^javascript:/i,
33
+ /^data:/i,
34
+ /^vbscript:/i
35
+ ];
17
36
 
18
- const renderer = new marked.Renderer();
37
+ renderer.link = function({ href, title, tokens }: any) {
19
38
 
20
- /* --------------------------------------------------
21
- 🔐 1. NON renderizzare HTML raw
22
- -------------------------------------------------- */
23
- renderer.html = function(token: Tokens.HTML | Tokens.Tag): string {
24
- const html = 'text' in token ? token.text : '';
39
+ const normalized = (href || '').trim();
25
40
 
26
- return html
27
- .replace(/&/g, '&amp;')
28
- .replace(/</g, '&lt;')
29
- .replace(/>/g, '&gt;');
30
- };
41
+ const isDangerous = dangerousProtocols.some(pattern =>
42
+ pattern.test(normalized)
43
+ );
31
44
 
32
- /* --------------------------------------------------
33
- 🔐 2. Link sicuri
34
- -------------------------------------------------- */
35
- const originalLinkRenderer = renderer.link.bind(renderer);
45
+ if (isDangerous) {
46
+ return tokens ? tokens.map((t: any) => t.raw).join('') : href || '';
47
+ }
36
48
 
37
- const dangerousProtocols = [
38
- /^javascript:/i,
39
- /^data:/i,
40
- /^vbscript:/i
41
- ];
49
+ const html = originalLinkRenderer({ href, title, tokens });
42
50
 
43
- renderer.link = function({ href, title, tokens }) {
51
+ // aggiunge sicurezza ai link
52
+ return html.replace(
53
+ '<a ',
54
+ '<a target="_blank" rel="noopener noreferrer" '
55
+ );
56
+ };
44
57
 
45
- const normalized = (href || '').trim();
58
+ MarkedPipe.renderer = renderer;
59
+ }
60
+ return MarkedPipe.renderer;
61
+ }
46
62
 
47
- const isDangerous = dangerousProtocols.some(pattern =>
48
- pattern.test(normalized)
49
- );
63
+ transform(value: any): string {
50
64
 
51
- if (isDangerous) {
52
- return tokens ? tokens.map(t => t.raw).join('') : href || '';
53
- }
65
+ const input =
66
+ typeof value === 'string'
67
+ ? value
68
+ : (value === null || value === undefined) ? '' : String(value);
54
69
 
55
- const html = originalLinkRenderer({ href, title, tokens });
70
+ const inputWithNewlines = input.replace(/\\n/g, '\n');
56
71
 
57
- // aggiunge sicurezza ai link
58
- return html.replace(
59
- '<a ',
60
- '<a target="_blank" rel="noopener noreferrer" '
61
- );
62
- };
72
+ const renderer = MarkedPipe.getRenderer();
63
73
 
64
74
  marked.setOptions({
65
75
  renderer,
@@ -40,8 +40,10 @@ export class AppConfigService {
40
40
  }
41
41
  // END GET BASE URL and create absolute url of remoteConfigUrl //
42
42
  const that = this;
43
- return this.http.get(urlConfigFile).toPromise().then(data => {
44
- that.appConfig = data;
43
+ return this.http.get(urlConfigFile).toPromise().then((data: any) => {
44
+ if (data) {
45
+ that.appConfig = data;
46
+ }
45
47
  }).catch(err => {
46
48
  console.error('error loadAppConfig', err);
47
49
  });