@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
@@ -1,33 +1,154 @@
1
- import { ActionButtonComponent } from './../message/buttons/action-button/action-button.component';
2
- import { TextButtonComponent } from './../message/buttons/text-button/text-button.component';
3
- import { LinkButtonComponent } from './../message/buttons/link-button/link-button.component';
4
- import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { CommonModule } from '@angular/common';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
4
+
5
+ import { MessageModel } from 'src/chat21-core/models/message';
6
+ import { ActionButtonComponent } from '../message/buttons/action-button/action-button.component';
7
+ import { LinkButtonComponent } from '../message/buttons/link-button/link-button.component';
8
+ import { TextButtonComponent } from '../message/buttons/text-button/text-button.component';
5
9
 
6
10
  import { MessageAttachmentComponent } from './message-attachment.component';
7
11
 
12
+ function makeMessageWithAttachment(buttons: any[]): MessageModel {
13
+ return new MessageModel(
14
+ 'm1',
15
+ 'it',
16
+ 'r1',
17
+ 'Recipient',
18
+ 's1',
19
+ 'Sender',
20
+ 200,
21
+ {},
22
+ 'testo',
23
+ Date.now(),
24
+ 'text',
25
+ {
26
+ attachment: {
27
+ type: 'template',
28
+ buttons,
29
+ },
30
+ },
31
+ 'chat',
32
+ true,
33
+ false,
34
+ );
35
+ }
36
+
8
37
  describe('MessageAttachmentComponent', () => {
9
38
  let component: MessageAttachmentComponent;
10
39
  let fixture: ComponentFixture<MessageAttachmentComponent>;
11
40
 
12
41
  beforeEach(waitForAsync(() => {
13
42
  TestBed.configureTestingModule({
14
- declarations: [
15
- MessageAttachmentComponent,
16
- LinkButtonComponent,
17
- TextButtonComponent,
18
- ActionButtonComponent
19
- ]
20
- })
21
- .compileComponents();
43
+ imports: [CommonModule],
44
+ declarations: [
45
+ MessageAttachmentComponent,
46
+ LinkButtonComponent,
47
+ TextButtonComponent,
48
+ ActionButtonComponent,
49
+ ],
50
+ }).compileComponents();
22
51
  }));
23
52
 
24
53
  beforeEach(() => {
25
54
  fixture = TestBed.createComponent(MessageAttachmentComponent);
26
55
  component = fixture.componentInstance;
27
- fixture.detectChanges();
56
+ component.isConversationArchived = false;
57
+ component.isLastMessage = true;
58
+ component.fullscreenMode = false;
59
+ component.limit = 5;
60
+ component.stylesMap = new Map<string, string>([
61
+ ['buttonFontSize', '14px'],
62
+ ['buttonBackgroundColor', '#eee'],
63
+ ['buttonTextColor', '#111'],
64
+ ['buttonHoverBackgroundColor', '#ddd'],
65
+ ['buttonHoverTextColor', '#000'],
66
+ ]);
28
67
  });
29
68
 
30
69
  it('should create', () => {
70
+ component.message = makeMessageWithAttachment([]);
71
+ fixture.detectChanges();
31
72
  expect(component).toBeTruthy();
32
73
  });
74
+
75
+ it('getAttachmentButton should read type and buttons from message.attributes.attachment', () => {
76
+ component.message = makeMessageWithAttachment([
77
+ { type: 'text', value: 'OK' },
78
+ { type: 'url', value: 'Apri', link: 'https://example.com' },
79
+ ]);
80
+ component.ngOnInit();
81
+ expect(component.type).toBe('template');
82
+ expect((component.buttons as unknown as any[]).length).toBe(2);
83
+ });
84
+
85
+ it('should render text and url attachment buttons in DOM when isLastMessage is true for text', () => {
86
+ component.message = makeMessageWithAttachment([
87
+ { type: 'text', value: 'Conferma' },
88
+ { type: 'url', value: 'Vai', link: 'https://example.com', target: '_blank' },
89
+ ]);
90
+ fixture.detectChanges();
91
+ const root = fixture.nativeElement.querySelector('#buttons-in-message') as HTMLElement;
92
+ expect(root).toBeTruthy();
93
+ expect(fixture.nativeElement.querySelector('chat-text-button-attachment')).toBeTruthy();
94
+ expect(fixture.nativeElement.querySelector('chat-link-button-attachment')).toBeTruthy();
95
+ const textBtn = fixture.nativeElement.querySelector('chat-text-button-attachment .text') as HTMLElement;
96
+ expect(textBtn.textContent?.trim()).toContain('Conferma');
97
+ });
98
+
99
+ it('should respect limit via slice (only first button when limit is 1)', () => {
100
+ component.limit = 1;
101
+ component.message = makeMessageWithAttachment([
102
+ { type: 'text', value: 'A' },
103
+ { type: 'text', value: 'B' },
104
+ ]);
105
+ fixture.detectChanges();
106
+ const textButtons = fixture.nativeElement.querySelectorAll('chat-text-button-attachment');
107
+ expect(textButtons.length).toBe(1);
108
+ });
109
+
110
+ it('should hide text and action buttons when isLastMessage is false', () => {
111
+ component.isLastMessage = false;
112
+ component.message = makeMessageWithAttachment([
113
+ { type: 'text', value: 'Nascosto' },
114
+ { type: 'url', value: 'Visibile', link: 'https://x.com' },
115
+ ]);
116
+ fixture.detectChanges();
117
+ expect(fixture.nativeElement.querySelector('chat-text-button-attachment')).toBeNull();
118
+ expect(fixture.nativeElement.querySelector('chat-link-button-attachment')).toBeTruthy();
119
+ });
120
+
121
+ it('returnOnAttachmentButtonClicked should emit structured event when target exists', () => {
122
+ component.message = makeMessageWithAttachment([{ type: 'text', value: 'X' }]);
123
+ fixture.detectChanges();
124
+ spyOn(component.onAttachmentButtonClicked, 'emit');
125
+ const inner = fixture.debugElement.query(By.css('chat-text-button-attachment .text'));
126
+ inner.triggerEventHandler('click', {});
127
+ expect(component.onAttachmentButtonClicked.emit).toHaveBeenCalled();
128
+ const arg = (component.onAttachmentButtonClicked.emit as jasmine.Spy).calls.mostRecent().args[0];
129
+ expect(arg.message).toBe(component.message);
130
+ expect(arg.target).toBeTruthy();
131
+ });
132
+
133
+ it('returnOnAttachmentButtonClicked should not emit when event has no target', () => {
134
+ component.message = makeMessageWithAttachment([{ type: 'text', value: 'X' }]);
135
+ fixture.detectChanges();
136
+ spyOn(component.onAttachmentButtonClicked, 'emit');
137
+ component.returnOnAttachmentButtonClicked({} as any);
138
+ expect(component.onAttachmentButtonClicked.emit).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it('ngAfterViewInit should emit onElementRendered for attachment', () => {
142
+ spyOn(component.onElementRendered, 'emit');
143
+ component.message = makeMessageWithAttachment([{ type: 'url', value: 'L', link: 'https://a' }]);
144
+ fixture.detectChanges();
145
+ component.ngAfterViewInit();
146
+ expect(component.onElementRendered.emit).toHaveBeenCalledWith({ element: 'attachment', status: true });
147
+ });
148
+
149
+ it('should render action button when type is action and isLastMessage', () => {
150
+ component.message = makeMessageWithAttachment([{ type: 'action', value: 'Azione', action: 'reply' }]);
151
+ fixture.detectChanges();
152
+ expect(fixture.nativeElement.querySelector('chat-action-button-attachment')).toBeTruthy();
153
+ });
33
154
  });
@@ -1,24 +1,25 @@
1
- <!-- <div id="chat21-modal-content modal-body"></div> -->
2
- <!-- tabindex="1200"-->
3
-
4
- <div id="chat21-selection-department"
1
+ <div id="chat21-selection-department"
5
2
  #afSelectionDepartment
6
- tabindex="1200"
3
+ role="dialog"
7
4
  aria-modal="true"
8
- onFocus="document.querySelector('[start-focus-chat21-selection-department]').focus()">
5
+ cdkTrapFocus
6
+ [cdkTrapFocusAutoCapture]="true"
7
+ [attr.aria-label]="g.LABEL_SELECT_TOPIC">
9
8
 
10
9
  <!-- HEADER -->
11
- <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }" >
10
+ <div class="c21-header" [ngStyle]="{'color': g.themeForegroundColor, 'background-image': g.colorGradient180 }">
12
11
  <div class="c21-header-container">
13
12
 
14
- <!-- ICON CLOSE CHAT -->
13
+ <!-- ICON CLOSE PANEL -->
15
14
  <div class="c21-header-button">
16
- <div class="c21-close-button c21-small" role="button">
17
- <button tabindex="1210" aria-label=" chiudi pannello" class="c21-close-button-body" (click)="closePage()">
18
- <svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
15
+ <div class="c21-close-button c21-small">
16
+ <button type="button"
17
+ [attr.aria-label]="g.BUTTON_CLOSE_TO_ICON || 'Close panel'"
18
+ class="c21-close-button-body"
19
+ (click)="closePage()">
20
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
19
21
  <path fill="none" d="M0 0h24v24H0V0z"/>
20
22
  <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"/>
21
- <!-- <title id="altIconTitle">{{BUTTON_CLOSE_CHAT}}</title> -->
22
23
  </svg>
23
24
  </button>
24
25
  </div>
@@ -26,9 +27,7 @@
26
27
 
27
28
  <!-- CONTENT HEADER -->
28
29
  <div class="c21-header-content">
29
- <!-- TITLE HEADER -->
30
30
  <div class="c21-title" [ngStyle]="{'color': g.themeForegroundColor}">
31
- <!-- <span>{{g.LABEL_SELECT_TOPIC}}</span> -->
32
31
  </div>
33
32
  </div>
34
33
 
@@ -40,15 +39,17 @@
40
39
  <div class="c21-body-container">
41
40
 
42
41
  <div class="c21-body-header">
43
- <div start-focus-chat21-selection-department tabindex="1201" class="c21-message-field">{{g.LABEL_SELECT_TOPIC}}</div>
42
+ <h2 start-focus-chat21-selection-department class="c21-message-field">{{g.LABEL_SELECT_TOPIC}}</h2>
44
43
  </div>
45
44
 
46
- <div class="c21-body-content" tabindex="211" onFocus="document.querySelector('[start-focus]').focus()">
45
+ <div class="c21-body-content">
47
46
  <ul class="chat21-content-modal-select">
48
47
  <li *ngFor="let department of departments; let i = index">
49
- <button tabindex="1202" class="c21-button-department c21-button-clean" (click)="onSelectDepartment(department)">
50
- <span class="chat21-badge">{{i+1}}</span>
51
- <span class="chat21-name-list" >{{department?.name}}</span>
48
+ <button type="button"
49
+ class="c21-button-department c21-button-clean"
50
+ (click)="onSelectDepartment(department)">
51
+ <span class="chat21-badge" aria-hidden="true">{{i+1}}</span>
52
+ <span class="chat21-name-list">{{department?.name}}</span>
52
53
  </button>
53
54
  </li>
54
55
  </ul>
@@ -56,7 +57,4 @@
56
57
 
57
58
  </div>
58
59
  </div>
59
-
60
- <span style="height:0px" tabindex="1299" onFocus="document.querySelector('[start-focus-chat21-selection-department]').focus()"></span>
61
- </div>
62
-
60
+ </div>
@@ -1,34 +1,179 @@
1
- import { ElementRef } from '@angular/core';
2
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { A11yModule } from '@angular/cdk/a11y';
2
+ import { Injectable } from '@angular/core';
3
+ import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
4
+ import { By } from '@angular/platform-browser';
5
+ import { NGXLogger } from 'ngx-logger';
6
+
3
7
  import { AppStorageService } from '../../../chat21-core/providers/abstract/app-storage.service';
8
+ import { CustomLogger } from '../../../chat21-core/providers/logger/customLogger';
9
+ import { LoggerInstance } from '../../../chat21-core/providers/logger/loggerInstance';
4
10
  import { Globals } from '../../utils/globals';
5
11
 
6
12
  import { SelectionDepartmentComponent } from './selection-department.component';
7
13
 
14
+ @Injectable()
15
+ class AppStorageServiceStub extends AppStorageService {
16
+ lastAttributesJson: string | null = null;
17
+ setItem = jasmine.createSpy('setItem').and.callFake((key: string, value: any) => {
18
+ if (key === 'attributes') {
19
+ this.lastAttributesJson = value as string;
20
+ }
21
+ });
22
+ getItem(): any {
23
+ return null;
24
+ }
25
+ getItemWithoutProjectID(): any {
26
+ return null;
27
+ }
28
+ setItemWithoutProjectID(): void {}
29
+ removeItem(): void {}
30
+ clear(): void {}
31
+ initialize(): void {}
32
+ }
33
+
8
34
  describe('SelectionDepartmentComponent', () => {
9
35
  let component: SelectionDepartmentComponent;
10
36
  let fixture: ComponentFixture<SelectionDepartmentComponent>;
11
- class MockElementRef {}
12
-
13
- beforeEach(async(() => {
37
+ let globals: Globals;
38
+ let storage: AppStorageServiceStub;
39
+
40
+ beforeEach(waitForAsync(() => {
41
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
42
+ LoggerInstance.setInstance(new CustomLogger(ngxlogger));
14
43
  TestBed.configureTestingModule({
15
- declarations: [ SelectionDepartmentComponent ],
16
- providers: [
17
- Globals,
18
- { provide: ElementRef, useClass: MockElementRef },
19
- AppStorageService
20
- ]
21
- })
22
- .compileComponents();
44
+ imports: [A11yModule],
45
+ declarations: [SelectionDepartmentComponent],
46
+ providers: [
47
+ Globals,
48
+ { provide: AppStorageService, useClass: AppStorageServiceStub },
49
+ { provide: NGXLogger, useValue: ngxlogger },
50
+ ],
51
+ }).compileComponents();
23
52
  }));
24
53
 
25
54
  beforeEach(() => {
26
55
  fixture = TestBed.createComponent(SelectionDepartmentComponent);
27
56
  component = fixture.componentInstance;
28
- fixture.detectChanges();
57
+ globals = TestBed.inject(Globals);
58
+ storage = TestBed.inject(AppStorageService) as unknown as AppStorageServiceStub;
59
+ globals.initDefafultParameters();
60
+ globals.themeColor = '#112233';
61
+ globals.themeForegroundColor = '#ddeeff';
62
+ globals.colorGradient180 = 'linear-gradient(#000,#fff)';
63
+ globals.LABEL_SELECT_TOPIC = 'Scegli un argomento';
64
+ globals.BUTTON_CLOSE_TO_ICON = 'Chiudi';
65
+ globals.departments = [
66
+ { _id: 'dep-a', name: 'Vendite' } as any,
67
+ { _id: 'dep-b', name: 'Supporto' } as any,
68
+ ];
69
+ globals.attributes = { userFullname: 'Ada', userEmail: 'ada@example.com' } as any;
70
+ globals.windowContext = {} as any;
29
71
  });
30
72
 
31
73
  it('should create', () => {
74
+ fixture.detectChanges();
32
75
  expect(component).toBeTruthy();
33
76
  });
77
+
78
+ it('should render dialog with topic label and one list row per department', () => {
79
+ fixture.detectChanges();
80
+ const dialog = fixture.nativeElement.querySelector('#chat21-selection-department') as HTMLElement;
81
+ expect(dialog.getAttribute('role')).toBe('dialog');
82
+ expect(dialog.getAttribute('aria-modal')).toBe('true');
83
+ expect(dialog.getAttribute('aria-label')).toBe('Scegli un argomento');
84
+ const heading = fixture.nativeElement.querySelector('h2.c21-message-field') as HTMLElement;
85
+ expect(heading.textContent?.trim()).toBe('Scegli un argomento');
86
+ const buttons = fixture.nativeElement.querySelectorAll('.c21-button-department');
87
+ expect(buttons.length).toBe(2);
88
+ expect(buttons[0].textContent).toContain('Vendite');
89
+ expect(buttons[1].textContent).toContain('Supporto');
90
+ });
91
+
92
+ it('onSelectDepartment should set globals, attributes and persist JSON in storage', () => {
93
+ fixture.detectChanges();
94
+ spyOn(component.onDepartmentSelected, 'emit');
95
+ const dept = { _id: 'dep-b', name: 'Supporto' };
96
+ component.onSelectDepartment(dept);
97
+ expect(globals.departmentSelected).toEqual(dept as any);
98
+ expect(globals.attributes.departmentId).toBe('dep-b');
99
+ expect(globals.attributes.departmentName).toBe('Supporto');
100
+ expect(storage.setItem).toHaveBeenCalledWith('attributes', jasmine.any(String));
101
+ const parsed = JSON.parse(storage.lastAttributesJson!);
102
+ expect(parsed.departmentId).toBe('dep-b');
103
+ expect(parsed.departmentName).toBe('Supporto');
104
+ expect(parsed.userEmail).toBe('ada@example.com');
105
+ expect(component.onDepartmentSelected.emit).toHaveBeenCalledWith(dept);
106
+ });
107
+
108
+ it('click on department button should trigger selection flow', () => {
109
+ fixture.detectChanges();
110
+ spyOn(component, 'onSelectDepartment').and.callThrough();
111
+ const second = fixture.debugElement.queryAll(By.css('.c21-button-department'))[1];
112
+ second.nativeElement.click();
113
+ expect(component.onSelectDepartment).toHaveBeenCalledWith(jasmine.objectContaining({ _id: 'dep-b' }));
114
+ });
115
+
116
+ it('closePage should emit onClose', () => {
117
+ fixture.detectChanges();
118
+ spyOn(component.onClose, 'emit');
119
+ component.closePage();
120
+ expect(component.onClose.emit).toHaveBeenCalled();
121
+ });
122
+
123
+ it('openPage should emit onOpen', () => {
124
+ fixture.detectChanges();
125
+ spyOn(component.onOpen, 'emit');
126
+ component.openPage();
127
+ expect(component.onOpen.emit).toHaveBeenCalled();
128
+ });
129
+
130
+ it('cancelPage should reset newConversationStart and emit onClose', () => {
131
+ fixture.detectChanges();
132
+ globals.newConversationStart = true;
133
+ spyOn(component.onClose, 'emit');
134
+ component.cancelPage();
135
+ expect(globals.newConversationStart).toBe(false);
136
+ expect(component.onClose.emit).toHaveBeenCalled();
137
+ });
138
+
139
+ it('should invoke beforeDepartmentsFormRender hook when present on windowContext.tiledesk', () => {
140
+ const hook = jasmine.createSpy('beforeDepartmentsFormRender').and.returnValue([{ _id: 'x', name: 'Filtered' }]);
141
+ globals.windowContext = { tiledesk: { beforeDepartmentsFormRender: hook } } as any;
142
+ const f2 = TestBed.createComponent(SelectionDepartmentComponent);
143
+ const c2 = f2.componentInstance;
144
+ f2.detectChanges();
145
+ expect(hook).toHaveBeenCalledWith(globals.departments);
146
+ expect(c2.departments.length).toBe(1);
147
+ expect(c2.departments[0].name).toBe('Filtered');
148
+ f2.destroy();
149
+ });
150
+
151
+ it('should emit onBeforeDepartmentsFormRender when hook is absent', () => {
152
+ spyOn(component.onBeforeDepartmentsFormRender, 'emit');
153
+ fixture.detectChanges();
154
+ expect(component.onBeforeDepartmentsFormRender.emit).toHaveBeenCalledWith(
155
+ jasmine.arrayContaining([jasmine.objectContaining({ name: 'Vendite' })]),
156
+ );
157
+ });
158
+
159
+ it('Escape host listener should call closePage', () => {
160
+ fixture.detectChanges();
161
+ spyOn(component, 'closePage');
162
+ const ev = new KeyboardEvent('keydown', { key: 'Escape' });
163
+ spyOn(ev, 'preventDefault');
164
+ spyOn(ev, 'stopPropagation');
165
+ component.onEscape(ev);
166
+ expect(ev.preventDefault).toHaveBeenCalled();
167
+ expect(ev.stopPropagation).toHaveBeenCalled();
168
+ expect(component.closePage).toHaveBeenCalled();
169
+ });
170
+
171
+ it('should focus afSelectionDepartment after delay in ngAfterViewInit', fakeAsync(() => {
172
+ fixture.detectChanges();
173
+ const dialog = fixture.nativeElement.querySelector('#chat21-selection-department') as HTMLElement;
174
+ spyOn(dialog, 'focus');
175
+ component.ngAfterViewInit();
176
+ tick(1000);
177
+ expect(dialog.focus).toHaveBeenCalled();
178
+ }));
34
179
  });
@@ -1,4 +1,4 @@
1
- import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
1
+ import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnInit, Output, ViewChild } from '@angular/core';
2
2
  import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
3
3
  import { Globals } from '../../utils/globals';
4
4
 
@@ -172,6 +172,13 @@ export class SelectionDepartmentComponent implements OnInit, AfterViewInit {
172
172
  this.onClose.emit();
173
173
  }
174
174
 
175
+ @HostListener('keydown.escape', ['$event'])
176
+ onEscape(event: KeyboardEvent){
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ this.closePage();
180
+ }
181
+
175
182
  cancelPage() {
176
183
  this.logger.debug('[SELECT-DEP] cancelPage');
177
184
  this.g.newConversationStart = false;
@@ -1,21 +1,13 @@
1
- <!-- tabindex="000"-->
2
-
3
- <button aflauncherbutton #aflauncherbutton id="c21-launcher-button" class="c21-button-clean scale-in-center c21-align-right"
4
- *ngIf="g.isLogged == true"
1
+ <button aflauncherbutton #aflauncherbutton type="button" id="c21-launcher-button" class="c21-button-clean scale-in-center c21-align-right"
2
+ *ngIf="g.isLogged == true"
5
3
  [style]="{ 'background-color': g.themeColor, 'bottom': g.marginY+'px!important', 'left':(g.align==='left')?g.marginX+'px!important':'', 'right':(g.align==='right')?g.marginX+'px!important':'', 'width': g.launcherWidth, 'height': g.launcherHeight, 'border-radius': g.baloonShape}"
6
4
  (click)="onSendPressed($event)"
7
- tabindex="1000"
8
- aria-label=" apri chat"
9
- >
5
+ [attr.aria-label]="g.LABEL_SEND_NEW_MESSAGE || 'Send new message'">
10
6
 
11
7
  <!-- SEND ICON -->
12
- <!-- [@enterBubbleAnimation] -->
13
- <div class="launcher-button rotate-center" >
14
- <!-- <img *ngIf="g.baloonImage !== '' " [src]="g.baloonImage" [ngStyle]="{'width': '100%', 'height': '100%', 'margin': 'auto'}"> -->
15
- <svg role="img" [style]="{'fill': g.themeForegroundColor, 'display': (g.baloonImage==='')?'':'none' }" xmlns="http://www.w3.org/2000/svg" width="100%"
8
+ <div class="launcher-button rotate-center">
9
+ <svg aria-hidden="true" focusable="false" [style]="{'fill': g.themeForegroundColor, 'display': (g.baloonImage==='')?'':'none' }" xmlns="http://www.w3.org/2000/svg" width="100%"
16
10
  height="100%" viewBox="0 0 90 90">
17
- <!-- <path fill="none" d="M0 0h24v24H0V0z" />
18
- <path d="M33.57,31A4.58,4.58,0,0,0,29,35.57V58.3a1.94,1.94,0,0,0,3.32,1.38l3-3a.29.29,0,0,1,.21-.09H56.43A4.58,4.58,0,0,0,61,52V35.57A4.58,4.58,0,0,0,56.43,31Z" /> -->
19
11
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
20
12
  </svg>
21
13
  </div>
@@ -1,4 +1,4 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2
2
 
3
3
  import { SendButtonComponent } from './send-button.component';
4
4
 
@@ -6,7 +6,7 @@ describe('SendButtonComponent', () => {
6
6
  let component: SendButtonComponent;
7
7
  let fixture: ComponentFixture<SendButtonComponent>;
8
8
 
9
- beforeEach(async(() => {
9
+ beforeEach(waitForAsync(() => {
10
10
  TestBed.configureTestingModule({
11
11
  declarations: [ SendButtonComponent ]
12
12
  })