@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
@@ -56,7 +56,7 @@
56
56
  .sources-panel {
57
57
  display: flex;
58
58
  flex-direction: column;
59
- padding: 18px 12px 10px 12px;
59
+ padding: 10px 12px 10px 12px;
60
60
  background: var(--panel-bck);
61
61
  border-radius: 18px;
62
62
  font-size: 14px;
@@ -79,15 +79,14 @@
79
79
  width: 100%;
80
80
  box-sizing: border-box;
81
81
 
82
- border-bottom: 1px solid var(--row-sep);
83
- border-bottom-left-radius: 0;
84
- border-bottom-right-radius: 0;
82
+ &:not(:last-of-type) {
83
+ border-bottom: 1px solid var(--row-sep);
84
+ border-bottom-left-radius: 0;
85
+ border-bottom-right-radius: 0;
86
+ }
85
87
 
86
88
  &:hover {
87
89
  background: rgba(255, 255, 255, 0.55);
88
- .source-row__title {
89
- text-decoration: underline;
90
- }
91
90
  }
92
91
 
93
92
  // & + & {
@@ -105,31 +104,40 @@
105
104
  }
106
105
 
107
106
  .source-row__title {
107
+ font-size: 14px;
108
+ color: #0a0a0a;
108
109
  font-weight: 500;
109
- color: var(--text);
110
- font-size: var(--title-font-size);
110
+ font-family: Arial, sans-serif;
111
+ white-space: normal;
112
+ -webkit-font-smoothing: auto;
113
+ font-style: normal;
114
+ text-decoration: none;
111
115
  line-height: var(--title-line-height);
112
116
  margin: 0px;
113
117
  min-width: 0;
114
118
  overflow: hidden;
115
119
  text-overflow: ellipsis;
116
120
  display: -webkit-box;
117
- line-clamp: 2;
118
121
  -webkit-line-clamp: 2;
119
122
  -webkit-box-orient: vertical;
120
- white-space: normal;
121
123
  }
122
124
 
123
125
  .source-row__desc {
124
- font-size: var(--desc-font-size);
125
- line-height: var(--desc-line-height);
126
+ font-size: 12px;
127
+ color: #56595e;
128
+ /* font-weight: bold; */
129
+ font-family: Arial, sans-serif;
130
+ white-space: normal;
131
+ /* -webkit-font-smoothing: auto; */
132
+ font-style: normal;
133
+ text-decoration: none;
134
+ line-height: 1.3;
126
135
  overflow: hidden;
127
136
  text-overflow: ellipsis;
128
137
  display: -webkit-box;
129
138
  line-clamp: 2;
130
139
  -webkit-line-clamp: 2;
131
140
  -webkit-box-orient: vertical;
132
- color: #56595e;
133
141
  margin: 0;
134
142
  }
135
143
 
@@ -144,11 +152,11 @@
144
152
  .source-row__favicon {
145
153
  width: 16px;
146
154
  height: 16px;
147
- border-radius: 50%;
155
+ // border-radius: 50%;
148
156
  flex: 0 0 auto;
149
- border: 2px solid #fff;
150
- background: rgba(255, 255, 255, 0.7);
151
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
157
+ // border: 2px solid #fff;
158
+ // background: rgba(255, 255, 255, 0.7);
159
+ // box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
152
160
  }
153
161
 
154
162
  .source-row__host {
@@ -10,6 +10,12 @@ export type JsonSourceItem = {
10
10
  image?: string;
11
11
  };
12
12
 
13
+ export type JsonSourcesDisplayFields = {
14
+ title?: boolean;
15
+ description?: boolean;
16
+ image?: boolean;
17
+ };
18
+
13
19
  @Component({
14
20
  selector: 'chat-json-sources',
15
21
  templateUrl: './json-sources.component.html',
@@ -19,11 +25,30 @@ export class JsonSourcesComponent {
19
25
  @Input() items: JsonSourceItem[] = [];
20
26
  @Input() themeColor?: string;
21
27
  @Input() limit = 3;
28
+ // Optional: per-field visibility. Missing/undefined fields default to visible
29
+ // (only an explicit `false` hides the field).
30
+ @Input() displayFields?: JsonSourcesDisplayFields;
31
+ // Optional: background color override for the sources panel.
32
+ @Input() backgroundColor?: string;
22
33
 
23
34
  @Output() onElementRendered = new EventEmitter<{ element: string; status: boolean }>();
24
35
 
25
36
  showAll = false;
26
37
 
38
+ isFieldVisible(field: keyof JsonSourcesDisplayFields): boolean {
39
+ return this.displayFields?.[field] !== false;
40
+ }
41
+
42
+ // Title is always rendered: when its content is missing or the field is
43
+ // hidden via displayFields, we fall back to the item URL so the row is never
44
+ // left without a label.
45
+ getTitleText(item: JsonSourceItem): string {
46
+ const titleVisible = this.isFieldVisible('title');
47
+ const title = (item?.title || '').trim();
48
+ if (titleVisible && title) return title;
49
+ return (item?.link || '').trim();
50
+ }
51
+
27
52
  trackByLink = (_: number, item: JsonSourceItem) => item?.link || item?.title || _;
28
53
 
29
54
  ngAfterViewInit() {
@@ -51,6 +76,22 @@ export class JsonSourcesComponent {
51
76
  return hostname || '';
52
77
  }
53
78
 
79
+ // Route large source images through wsrv.nl which downsamples them server-side
80
+ // to a thumbnail-sized version. Rendering at ~3x the CSS size keeps the result
81
+ // sharp on retina displays. Falls back to the original URL on any error.
82
+ getThumbUrl(item: JsonSourceItem): string {
83
+ const raw = (item?.image || '').trim();
84
+ if (!raw) return '';
85
+ if (!/^https?:\/\//i.test(raw)) return raw;
86
+ try {
87
+ const stripped = raw.replace(/^https?:\/\//i, '');
88
+ const encoded = encodeURIComponent(stripped);
89
+ return `https://wsrv.nl/?url=${encoded}&w=120&h=120&fit=cover&output=webp&n=-1`;
90
+ } catch {
91
+ return raw;
92
+ }
93
+ }
94
+
54
95
  private safeHostname(url: string): string {
55
96
  try {
56
97
  return new URL(url).hostname.replace(/^www\./, '');
@@ -1,21 +1,19 @@
1
1
  <div class="like-container">
2
2
  <div class="menu">
3
3
  <!-- LIKE -->
4
- <button class="c21-button-clean c21-like" (click)="onClick('like')">
5
- <svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
6
- width="24" height="24" viewBox="0 0 24 24" xml:space="preserve">
4
+ <button type="button" class="c21-button-clean c21-like" (click)="onClick('like')" aria-label="Like message">
5
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
6
+ width="24" height="24" viewBox="0 0 24 24" xml:space="preserve" aria-hidden="true" focusable="false">
7
7
  <path fill="black" d="M4.5,9h-3C0.7,9,0,9.6,0,10.4v10.4c0,0.8,0.7,1.5,1.5,1.5h3c0.8,0,1.5-0.7,1.5-1.5V10.5C6,9.7,5.3,9,4.5,9z"/>
8
8
  <path fill="black" d="M24,10.5c-0.1-1.7-1.6-3-3.3-3h-4.5c0,0,0,0,0,0c0.4-1.2,0.7-2.2,0.7-2.7c0-1.6-1.2-3.2-3.3-3.2 c-2.2,0-2.8,1.5-3.3,2.7C8.8,8,7.5,7.4,7.5,8.6c0,0.6,0.5,1.1,1.1,1.1c0.2,0,0.5-0.1,0.7-0.2c3.6-2.9,2.7-5.8,4.3-5.8 c0.8,0,1,0.6,1,1c0,0.3-0.4,1.9-1.2,3.4c-0.1,0.2-0.1,0.4-0.1,0.5c0,0.7,0.5,1.1,1.1,1.1h6.5c0.5,0,0.9,0.4,0.9,0.9 c0,0.5-0.4,0.8-0.8,0.9c-0.6,0-1,0.5-1,1.1c0,0.7,0.5,0.7,0.5,1.4c0,1.2-1.6,0.6-1.6,2c0,0.5,0.3,0.6,0.3,1c0,1.1-1.4,0.6-1.4,1.9 c0,0.1,0,0.2,0,0.2c0.1,0.6-0.3,1.1-0.9,1.1l-2.4,0c-1.2,0-2.4-0.4-3.4-1.1l-1.7-1.3c-0.2-0.2-0.4-0.2-0.7-0.2 c-0.6,0-1.1,0.5-1.1,1.1c0,0.3,0.2,0.7,0.4,0.9l1.7,1.3c1.4,1,3,1.6,4.7,1.6h2.5c1.7,0,3-1.3,3.1-2.9c0,0,0,0,0,0 c0.8-0.6,1.3-1.5,1.3-2.6c0-0.1,0-0.3,0-0.4c0.8-0.6,1.4-1.5,1.4-2.6c0-0.2,0-0.5-0.1-0.7C23.5,12.6,24.1,11.6,24,10.5z"/>
9
- <title id="altIconTitle">LIKE</title>
10
- </svg>
9
+ </svg>
11
10
  </button>
12
11
  <!-- UNLIKE -->
13
- <button class="c21-button-clean" (click)="onClick('like')">
14
- <svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
15
- width="24" height="24" viewBox="0 0 24 24" xml:space="preserve">
12
+ <button type="button" class="c21-button-clean" (click)="onClick('unlike')" aria-label="Unlike message">
13
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
14
+ width="24" height="24" viewBox="0 0 24 24" xml:space="preserve" aria-hidden="true" focusable="false">
16
15
  <path d="M6,13.5V3c0-0.8-0.7-1.5-1.5-1.5h-3C0.7,1.5,0,2.2,0,3v10.5c0,0.8,0.7,1.5,1.5,1.5h3C5.3,15,6,14.3,6,13.5z"/>
17
16
  <path d="M22.6,10.7c0.1-0.2,0.1-0.5,0.1-0.7c0-1.1-0.5-2-1.4-2.6c0-0.1,0-0.3,0-0.4c0-1-0.5-2-1.4-2.6c-0.1-1.6-1.5-2.9-3.1-2.9 h-2.5c-1.7,0-3.4,0.6-4.7,1.6L8,4.4C7.7,4.6,7.5,4.9,7.5,5.3c0,0.6,0.5,1.1,1.1,1.1c0.2,0,0.5-0.1,0.7-0.2L11,4.9 c1-0.7,2.2-1.1,3.4-1.1h2.5c0.5,0,0.9,0.4,0.9,0.9c0,0.2-0.1,0.2-0.1,0.4c0,1.3,1.4,0.8,1.4,1.9c0,0.4-0.3,0.5-0.3,1 c0,0.7,0.5,1,0.9,1.1c0.4,0.1,0.7,0.4,0.7,0.9c0,0.6-0.5,0.6-0.5,1.4c0,0.6,0.5,1.1,1,1.1c0.5,0,0.8,0.4,0.8,0.8 c0,0.5-0.4,0.9-0.9,0.9h-6.5c-0.6,0-1.1,0.5-1.1,1.1c0,0.2,0,0.4,0.1,0.5c0.8,1.6,1.2,3.1,1.2,3.4c0,0.4-0.3,1-1,1 c-0.6,0-0.7,0-1.2-1.3c-1.2-2.9-2.9-4.7-3.8-4.7c-0.6,0-1.1,0.5-1.1,1.1c0,0.3,0.1,0.7,0.4,0.9c3.2,2.6,2,6.2,5.7,6.2 c2.1,0,3.3-1.6,3.3-3.2c0-0.6-0.2-1.7-0.7-2.8h4.7c1.7,0,3.1-1.4,3.1-3.2C24,12.3,23.4,11.3,22.6,10.7z"/>
18
- <title id="altIconTitle">UNLIKE</title>
19
17
  </svg>
20
18
  </button>
21
19
  </div>
@@ -1,16 +1,22 @@
1
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { By } from '@angular/platform-browser';
3
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
4
+ import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
5
+ import { NGXLogger } from 'ngx-logger';
2
6
 
3
7
  import { LikeUnlikeComponent } from './like-unlike.component';
4
8
 
5
9
  describe('LikeUnlikeComponent', () => {
6
10
  let component: LikeUnlikeComponent;
7
11
  let fixture: ComponentFixture<LikeUnlikeComponent>;
12
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
13
+ const customLogger = new CustomLogger(ngxlogger);
8
14
 
9
15
  beforeEach(async () => {
16
+ LoggerInstance.setInstance(customLogger);
10
17
  await TestBed.configureTestingModule({
11
- declarations: [ LikeUnlikeComponent ]
12
- })
13
- .compileComponents();
18
+ declarations: [LikeUnlikeComponent],
19
+ }).compileComponents();
14
20
  });
15
21
 
16
22
  beforeEach(() => {
@@ -22,4 +28,26 @@ describe('LikeUnlikeComponent', () => {
22
28
  it('should create', () => {
23
29
  expect(component).toBeTruthy();
24
30
  });
31
+
32
+ it('onClick should log icon id', () => {
33
+ spyOn((component as any).logger, 'debug');
34
+ component.onClick('like');
35
+ expect((component as any).logger.debug).toHaveBeenCalledWith('[LIKE-UNLIKE] onClick-->', 'like');
36
+ });
37
+
38
+ it('like button should have accessible name', () => {
39
+ const btn = fixture.debugElement.queryAll(By.css('button'))[0];
40
+ expect(btn.nativeElement.getAttribute('aria-label')).toBe('Like message');
41
+ });
42
+
43
+ it('unlike button should have accessible name', () => {
44
+ const btn = fixture.debugElement.queryAll(By.css('button'))[1];
45
+ expect(btn.nativeElement.getAttribute('aria-label')).toBe('Unlike message');
46
+ });
47
+
48
+ it('clicking like should invoke logger', () => {
49
+ spyOn((component as any).logger, 'debug');
50
+ fixture.debugElement.queryAll(By.css('button'))[0].triggerEventHandler('click', {});
51
+ expect((component as any).logger.debug).toHaveBeenCalledWith('[LIKE-UNLIKE] onClick-->', 'like');
52
+ });
25
53
  });
@@ -1,6 +1,10 @@
1
- import { async, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
1
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2
2
  import { By } from '@angular/platform-browser';
3
- import { MSG_STATUS_SENT } from 'src/chat21-core/utils/constants';
3
+ import {
4
+ MSG_STATUS_RETURN_RECEIPT,
5
+ MSG_STATUS_SENT,
6
+ MSG_STATUS_SENT_SERVER,
7
+ } from 'src/app/utils/constants';
4
8
 
5
9
  import { ReturnReceiptComponent } from './return-receipt.component';
6
10
 
@@ -10,31 +14,48 @@ describe('ReturnReceiptComponent', () => {
10
14
 
11
15
  beforeEach(waitForAsync(() => {
12
16
  TestBed.configureTestingModule({
13
- declarations: [ ReturnReceiptComponent ]
14
- })
15
- .compileComponents();
17
+ declarations: [ReturnReceiptComponent],
18
+ }).compileComponents();
16
19
  }));
17
20
 
18
21
  beforeEach(() => {
19
22
  fixture = TestBed.createComponent(ReturnReceiptComponent);
20
23
  component = fixture.componentInstance;
21
- fixture.detectChanges();
22
24
  });
23
25
 
24
26
  it('should create', () => {
27
+ fixture.detectChanges();
25
28
  expect(component).toBeTruthy();
26
29
  });
27
30
 
28
- it('shold render MSG_STATUS_SENT icon', ()=> {
29
- component.status= MSG_STATUS_SENT
31
+ it('should render schedule icon when status is falsy', () => {
32
+ component.status = 0 as any;
33
+ fixture.detectChanges();
34
+ const icons = fixture.debugElement.queryAll(By.css('.icon'));
35
+ expect(icons.length).toBe(1);
36
+ });
37
+
38
+ it('should render sent icon for MSG_STATUS_SENT', () => {
39
+ component.status = MSG_STATUS_SENT;
40
+ fixture.detectChanges();
41
+ expect(fixture.debugElement.queryAll(By.css('.icon')).length).toBe(1);
42
+ });
43
+
44
+ it('should render server-sent icon for MSG_STATUS_SENT_SERVER', () => {
45
+ component.status = MSG_STATUS_SENT_SERVER;
46
+ fixture.detectChanges();
47
+ expect(fixture.debugElement.queryAll(By.css('.icon')).length).toBe(1);
48
+ });
49
+
50
+ it('should render return receipt icon for MSG_STATUS_RETURN_RECEIPT', () => {
51
+ component.status = MSG_STATUS_RETURN_RECEIPT;
30
52
  fixture.detectChanges();
31
- expect(component.status).toBe(MSG_STATUS_SENT)
32
- })
33
-
34
- // it('shold render MSG_STATUS_SENT icon', ()=> {
35
- // component.status= MSG_STATUS_SENT
36
- // fixture.detectChanges();
37
- // let element = fixture.debugElement.query(By.css('icon'))
38
- // expect(element.classes).toBeE('icon')
39
- // })
53
+ expect(fixture.debugElement.queryAll(By.css('.icon')).length).toBe(1);
54
+ });
55
+
56
+ it('should expose status constants on component', () => {
57
+ expect(component.MSG_STATUS_SENT).toBe(MSG_STATUS_SENT);
58
+ expect(component.MSG_STATUS_SENT_SERVER).toBe(MSG_STATUS_SENT_SERVER);
59
+ expect(component.MSG_STATUS_RETURN_RECEIPT).toBe(MSG_STATUS_RETURN_RECEIPT);
60
+ });
40
61
  });
@@ -1,3 +1,3 @@
1
- <p #messageEl class="message_innerhtml marked"
2
- [innerHTML]="printMessage(text, messageEl, this) | marked"
3
- [style.color]="color"></p>
1
+ <div #messageEl class="message_innerhtml marked"
2
+ [innerHTML]="printMessage(text, messageEl, this) | marked"
3
+ [style.color]="color"></div>
@@ -1,113 +1,107 @@
1
- // Shadow DOM styles - isolati dal resto dell'applicazione
2
1
  :host {
3
2
  display: block;
4
- // Le variabili CSS custom properties possono attraversare il boundary del Shadow DOM
5
3
  font-size: var(--font-size-bubble-message, 14px);
6
4
  }
7
5
 
8
6
  .message_innerhtml {
9
7
  margin: 0px;
10
- // padding: 0px 14px;
11
- &.marked{
8
+
9
+ &.marked {
12
10
  padding: 12px 16px;
13
- margin-block-start: 0em!important;
14
- margin-block-end: 0em!important;
11
+ margin-block-start: 0em !important;
12
+ margin-block-end: 0em !important;
15
13
  }
16
-
14
+
17
15
  .text-message {
18
16
  padding-top: 14px;
19
17
  }
20
- }
21
18
 
22
- p {
23
- font-size: inherit;
24
- margin: 0;
25
- // padding: 14px;
26
- line-height: 1.4em;
27
- font-style: normal;
28
- letter-spacing: normal;
29
- font-stretch: normal;
30
- font-variant: normal;
31
- font-weight: 300;
32
- overflow: hidden;
33
- }
19
+ p {
20
+ font-size: inherit;
21
+ margin: 0;
22
+ line-height: 1.4em;
23
+ font-style: normal;
24
+ letter-spacing: normal;
25
+ font-stretch: normal;
26
+ font-variant: normal;
27
+ font-weight: 300;
28
+ overflow: hidden;
29
+ }
34
30
 
35
- p ::ng-deep a,
36
- p a {
37
- word-break: break-word;
38
- color: inherit; // Eredita il colore dal parent
39
- text-decoration: underline;
40
- }
31
+ a {
32
+ word-break: break-word;
33
+ color: inherit;
34
+ text-decoration: underline;
35
+ }
41
36
 
42
- p ::ng-deep p,
43
- p a:hover{
44
- margin-block-end: 0em;
45
- margin-block-start: 0em;
46
- }
37
+ a:hover {
38
+ margin-block-end: 0em;
39
+ margin-block-start: 0em;
40
+ }
47
41
 
48
- p ol {
49
- margin-block-end: 0em;
50
- margin-block-start: 0em;
51
- padding-inline-start: 15px;
52
-
53
- li::before {
54
- content: "";
55
- font-size: 1.4em;
56
- vertical-align: middle;
42
+ ol {
43
+ margin-block-end: 0em;
44
+ margin-block-start: 0em;
45
+ padding-inline-start: 15px;
46
+
47
+ li::before {
48
+ content: "";
49
+ font-size: 1.4em;
50
+ vertical-align: middle;
51
+ }
57
52
  }
58
- }
59
53
 
60
- // Stili aggiuntivi per elementi markdown comuni
61
- p h1, p h2, p h3, p h4, p h5, p h6 {
62
- margin-block-start: 0.5em;
63
- margin-block-end: 0.5em;
64
- font-weight: bold;
65
- color: inherit; // Eredita il colore dal parent
66
- }
54
+ h1, h2, h3, h4, h5, h6 {
55
+ margin-block-start: 0.5em;
56
+ margin-block-end: 0.5em;
57
+ font-weight: bold;
58
+ color: inherit;
59
+ }
67
60
 
68
- .message_innerhtml.marked h1 {
69
- line-height: normal;
70
- }
61
+ &.marked h1 {
62
+ line-height: normal;
63
+ }
71
64
 
72
- p ul {
73
- margin-block-end: 0em;
74
- margin-block-start: 0em;
75
- padding-inline-start: 15px;
76
- }
65
+ ul {
66
+ margin-block-end: 0em;
67
+ margin-block-start: 0em;
68
+ padding-inline-start: 15px;
69
+ }
77
70
 
78
- p code {
79
- background-color: rgba(0, 0, 0, 0.05);
80
- padding: 2px 4px;
81
- border-radius: 3px;
82
- font-family: monospace;
83
- }
71
+ code {
72
+ background-color: rgba(0, 0, 0, 0.05);
73
+ padding: 2px 4px;
74
+ border-radius: 3px;
75
+ font-family: monospace;
76
+ }
84
77
 
85
- p pre {
86
- background-color: rgba(0, 0, 0, 0.05);
87
- padding: 10px;
88
- border-radius: 3px;
89
- overflow-x: auto;
90
- }
78
+ pre {
79
+ background-color: rgba(0, 0, 0, 0.05);
80
+ padding: 10px;
81
+ border-radius: 3px;
82
+ overflow-x: auto;
83
+ }
91
84
 
92
- p blockquote {
93
- border-left: 3px solid rgba(0, 0, 0, 0.1);
94
- padding-left: 10px;
95
- margin-left: 0;
96
- font-style: italic;
97
- }
85
+ blockquote {
86
+ border-left: 3px solid rgba(0, 0, 0, 0.1);
87
+ padding-left: 10px;
88
+ margin-left: 0;
89
+ font-style: italic;
90
+ }
98
91
 
99
- p table {
100
- border-collapse: collapse;
101
- width: 100%;
102
- margin: 10px 0;
103
- }
92
+ table {
93
+ border-collapse: collapse;
94
+ width: 100%;
95
+ margin: 10px 0;
96
+ }
104
97
 
105
- p table td, p table th {
106
- border: 1px solid rgba(0, 0, 0, 0.1);
107
- padding: 8px;
108
- }
98
+ table td, table th {
99
+ border: 1px solid rgba(0, 0, 0, 0.1);
100
+ padding: 8px;
101
+ }
109
102
 
110
- p table th {
111
- background-color: rgba(0, 0, 0, 0.05);
112
- font-weight: bold;
103
+ table th {
104
+ background-color: rgba(0, 0, 0, 0.05);
105
+ font-weight: bold;
106
+ }
113
107
  }
@@ -1,33 +1,126 @@
1
- import { HtmlEntitiesEncodePipe } from './../../../pipe/html-entities-encode.pipe';
2
- import { MarkedPipe } from './../../../pipe/marked.pipe';
3
- import { async, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
1
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
4
2
 
3
+ import { MarkedPipe } from '../../../pipe/marked.pipe';
5
4
  import { TextComponent } from './text.component';
6
5
 
7
- describe('TextComponent', () => {
6
+ /**
7
+ * Il template usa solo `| marked` su ciò che ritorna `printMessage(...)`:
8
+ * `[innerHTML]="printMessage(text, messageEl, this) | marked"`
9
+ * I test qui riproducono gli stessi use case (XSS / HTML grezzo / markdown) sulla pipeline reale del componente.
10
+ */
11
+ describe('TextComponent (render via MarkedPipe)', () => {
8
12
  let component: TextComponent;
9
13
  let fixture: ComponentFixture<TextComponent>;
14
+ let markedPipe: MarkedPipe;
15
+
16
+ function shadowInnerHtml(): string {
17
+ const host = fixture.nativeElement as HTMLElement;
18
+ const root = host.shadowRoot;
19
+ if (!root) {
20
+ throw new Error('Expected ShadowDom root');
21
+ }
22
+ return (root.querySelector('.message_innerhtml') as HTMLElement).innerHTML;
23
+ }
10
24
 
11
25
  beforeEach(waitForAsync(() => {
12
26
  TestBed.configureTestingModule({
13
- declarations: [
14
- TextComponent,
15
- MarkedPipe,
16
- HtmlEntitiesEncodePipe
17
- ]
18
- })
19
- .compileComponents();
27
+ declarations: [TextComponent, MarkedPipe],
28
+ }).compileComponents();
20
29
  }));
21
30
 
22
31
  beforeEach(() => {
23
32
  fixture = TestBed.createComponent(TextComponent);
24
33
  component = fixture.componentInstance;
25
- component.text = 'Msg text'
26
- component.color= 'black'
34
+ markedPipe = new MarkedPipe();
35
+ component.text = 'Msg text';
36
+ component.color = 'black';
27
37
  fixture.detectChanges();
28
38
  });
29
39
 
30
40
  it('should create', () => {
31
41
  expect(component).toBeTruthy();
32
42
  });
43
+
44
+ it('printMessage should emit before and after and return text (input grezzo per la pipe)', () => {
45
+ spyOn(component.onBeforeMessageRender, 'emit');
46
+ spyOn(component.onAfterMessageRender, 'emit');
47
+ const out = component.printMessage('Hello', {} as any, component);
48
+ expect(out).toBe('Hello');
49
+ expect(component.onBeforeMessageRender.emit).toHaveBeenCalled();
50
+ expect(component.onAfterMessageRender.emit).toHaveBeenCalled();
51
+ });
52
+
53
+ it('should render message container with color style', () => {
54
+ const host = fixture.nativeElement as HTMLElement;
55
+ const el = host.shadowRoot!.querySelector('div.message_innerhtml') as HTMLElement;
56
+ expect(el).toBeTruthy();
57
+ expect(el.style.color).toBe('black');
58
+ });
59
+
60
+ describe('markdown (use case contenuto legittimo)', () => {
61
+ it('should render heading and bold from markdown', () => {
62
+ component.text = '# Title\n\n**Bold**';
63
+ fixture.detectChanges();
64
+ const html = shadowInnerHtml();
65
+ expect(html).toMatch(/<h1|<h2/i);
66
+ expect(html).toMatch(/<strong>|<b>/i);
67
+ });
68
+
69
+ it('should render unordered list from markdown', () => {
70
+ component.text = '- uno\n- due';
71
+ fixture.detectChanges();
72
+ const html = shadowInnerHtml();
73
+ expect(html).toMatch(/<ul/i);
74
+ expect(html).toMatch(/<li/i);
75
+ });
76
+
77
+ it('should render safe https link with rel=noopener (MarkedPipe renderer)', () => {
78
+ component.text = '[label](https://example.com/path)';
79
+ fixture.detectChanges();
80
+ const html = shadowInnerHtml();
81
+ expect(html).toContain('https://example.com/path');
82
+ expect(html).toContain('rel="noopener noreferrer"');
83
+ expect(html).toContain('target="_blank"');
84
+ });
85
+ });
86
+
87
+ describe('sicurezza / anti code-injection (stesso use case, solo marked)', () => {
88
+ it('should not leave raw script tags in DOM innerHTML', () => {
89
+ component.text = '<script>alert(1)</script>testo';
90
+ fixture.detectChanges();
91
+ const html = shadowInnerHtml();
92
+ expect(html.toLowerCase()).not.toContain('<script');
93
+ expect(html).toContain('&lt;');
94
+ });
95
+
96
+ it('should escape inline HTML img/onerror so it is not a live <img> node', () => {
97
+ component.text = 'Hi <img src=x onerror="alert(1)">';
98
+ fixture.detectChanges();
99
+ const html = shadowInnerHtml();
100
+ expect(html.toLowerCase()).not.toContain('<img');
101
+ expect(html).toContain('&lt;');
102
+ });
103
+
104
+ it('should not emit javascript: href for markdown link', () => {
105
+ component.text = '[bad](javascript:void(0))';
106
+ fixture.detectChanges();
107
+ const html = shadowInnerHtml();
108
+ expect(html).not.toMatch(/href=["']javascript:/i);
109
+ });
110
+
111
+ it('should not emit data: href for markdown link', () => {
112
+ component.text = '[bad](data:text/html,<script>alert(1)</script>)';
113
+ fixture.detectChanges();
114
+ const html = shadowInnerHtml();
115
+ expect(html).not.toMatch(/href=["']data:/i);
116
+ });
117
+
118
+ it('DOM innerHTML should match MarkedPipe applied to printMessage output (stessa pipeline del template)', () => {
119
+ component.text = 'Line1\n\n**x**';
120
+ fixture.detectChanges();
121
+ const raw = component.printMessage(component.text, {} as any, component);
122
+ const viaPipe = markedPipe.transform(raw) as string;
123
+ expect(shadowInnerHtml()).toBe(viaPipe);
124
+ });
125
+ });
33
126
  });