@envive-ai/react-widgets-v3 0.3.13 → 0.3.14

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 (80) hide show
  1. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.cjs +65 -0
  2. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.js +64 -0
  3. package/dist/CXIntegration/implementations/useTalkdeskUnifiedCXButton.cjs +64 -0
  4. package/dist/CXIntegration/implementations/useTalkdeskUnifiedCXButton.js +63 -0
  5. package/dist/CXIntegration/types.cjs +2 -0
  6. package/dist/CXIntegration/types.js +2 -0
  7. package/dist/CXIntegration/utils/functions.cjs +4 -0
  8. package/dist/CXIntegration/utils/functions.js +4 -0
  9. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  10. package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
  11. package/dist/packages/widgets/dist/SearchResults/SearchResults.d.ts +2 -2
  12. package/dist/packages/widgets/dist/SearchResults/SearchResultsWidget.d.ts +2 -2
  13. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  14. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +2 -2
  15. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  16. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
  17. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  18. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
  19. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  20. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  21. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
  22. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
  23. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
  24. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
  25. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +19 -3
  26. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  27. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  28. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +20 -4
  29. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +10 -0
  30. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  31. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  32. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +12 -2
  33. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +2 -9
  34. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  35. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
  36. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +1 -8
  37. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  38. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +10 -0
  39. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  40. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  41. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +12 -2
  42. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +11 -0
  43. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  44. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  45. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +12 -1
  46. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
  47. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  48. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +23 -3
  49. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  50. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  51. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +24 -4
  52. package/dist/widgets/dist/SearchResults/SearchResults.d.cts +2 -2
  53. package/dist/widgets/dist/SearchResults/SearchResultsWidget.d.cts +2 -2
  54. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  55. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +2 -2
  56. package/dist/widgets/utils/functions.cjs +9 -0
  57. package/dist/widgets/utils/functions.js +9 -1
  58. package/package.json +1 -1
  59. package/src/CXIntegration/implementations/useHelpScoutUnifiedCXButton.ts +108 -0
  60. package/src/CXIntegration/implementations/useTalkdeskUnifiedCXButton.ts +94 -0
  61. package/src/CXIntegration/types.ts +2 -0
  62. package/src/CXIntegration/utils/functions.ts +8 -0
  63. package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +15 -3
  64. package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +114 -0
  65. package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +119 -0
  66. package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +15 -3
  67. package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +144 -0
  68. package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +12 -1
  69. package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +179 -0
  70. package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +1 -14
  71. package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +150 -0
  72. package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +12 -1
  73. package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +184 -0
  74. package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +12 -0
  75. package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +150 -0
  76. package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +19 -2
  77. package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +163 -0
  78. package/src/widgets/__tests__/testUtils.tsx +63 -0
  79. package/src/widgets/__tests__/trackEventCanary.test.ts +45 -0
  80. package/src/widgets/utils/functions.ts +16 -0
@@ -0,0 +1,184 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import {
3
+ EnviveMetricsEventName,
4
+ SpiffyMetricsEventName,
5
+ } from '@envive-ai/react-hooks/contexts/amplitudeContext';
6
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
7
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
8
+ import { SocialProofWidget } from '../SocialProofWidget';
9
+
10
+ const mockTrackEvent = vi.fn();
11
+ const mockGetHardcopy = vi.fn();
12
+
13
+ vi.mock('@envive-ai/react-toolkit-v3/SocialProof', async importOriginal => {
14
+ const actual = await importOriginal();
15
+ const MockSocialProof = ({
16
+ widgetContentProps,
17
+ widgetEventProps,
18
+ }: {
19
+ widgetContentProps: { primaryButtonText?: string };
20
+ widgetEventProps: { handlePrimaryButtonClick: (text: string) => void };
21
+ }) => (
22
+ <div data-testid="social-proof-mock">
23
+ <button
24
+ type="button"
25
+ onClick={() =>
26
+ widgetEventProps.handlePrimaryButtonClick(
27
+ widgetContentProps.primaryButtonText || 'Primary',
28
+ )
29
+ }
30
+ >
31
+ {widgetContentProps.primaryButtonText || 'Primary'}
32
+ </button>
33
+ </div>
34
+ );
35
+ return {
36
+ ...actual,
37
+ SocialProof: MockSocialProof,
38
+ };
39
+ });
40
+
41
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
42
+ useAmplitude: () => ({
43
+ trackEvent: mockTrackEvent,
44
+ isReady: true,
45
+ }),
46
+ SpiffyMetricsEventName: {
47
+ ChatComponentVisible: 'Chat Component Visible',
48
+ SearchComponentVisible: 'Search Component Visible',
49
+ },
50
+ EnviveMetricsEventName: {
51
+ WidgetTextClicked: 'Widget Text Clicked',
52
+ },
53
+ }));
54
+
55
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
56
+ useHardcopy: () => ({
57
+ getHardcopy: mockGetHardcopy,
58
+ }),
59
+ }));
60
+
61
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
62
+ usePage: () => ({
63
+ userEvent: {
64
+ id: 'test-user-event-id',
65
+ category: UserEventCategory.PageVisit,
66
+ created_at: '2025-01-01T00:00:00Z',
67
+ event_id: 'test-event-id',
68
+ } as UserEvent,
69
+ }),
70
+ }));
71
+
72
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
73
+ useWidgetConfig: () => ({
74
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
75
+ }),
76
+ }));
77
+
78
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
79
+ useUiConfig: () => ({
80
+ getUiConfig: vi.fn().mockResolvedValue({}),
81
+ }),
82
+ }));
83
+
84
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
85
+ useSalesAgent: () => ({
86
+ onTypedMessageSubmitted: vi.fn(),
87
+ }),
88
+ }));
89
+
90
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
91
+ useChatToggle: () => ({
92
+ openChat: vi.fn(),
93
+ }),
94
+ }));
95
+
96
+ vi.mock('@envive-ai/react-hooks/atoms/chat', () => ({
97
+ lastAssistantMessageAtom: {},
98
+ }));
99
+
100
+ vi.mock('@envive-ai/react-hooks/atoms/app', () => ({
101
+ variantInfoAtom: {},
102
+ }));
103
+
104
+ vi.mock('@envive-ai/react-hooks/atoms/widget', () => ({
105
+ chatPreviewLoadingDataAtom: {},
106
+ }));
107
+
108
+ vi.mock('jotai', async importOriginal => {
109
+ const actual = await importOriginal();
110
+ const variantInfo = {
111
+ variant: 'PageVisit',
112
+ productId: undefined,
113
+ plpId: undefined,
114
+ url: '',
115
+ pageVisitCategory: '',
116
+ };
117
+ const useAtomValueMock = vi.fn();
118
+ useAtomValueMock.mockReturnValueOnce([]).mockReturnValueOnce(variantInfo).mockReturnValue([]);
119
+ return {
120
+ ...actual,
121
+ useAtomValue: useAtomValueMock,
122
+ useSetAtom: vi.fn(() => vi.fn()),
123
+ };
124
+ });
125
+
126
+ describe('SocialProofWidget analytics', () => {
127
+ const defaultHardcopy = {
128
+ responseId: 'test-response-id',
129
+ language: 'en',
130
+ values: {
131
+ primaryButtonText: 'Primary Button',
132
+ titleLabel: 'Title',
133
+ },
134
+ rawValues: {
135
+ primaryButtonText: { id: 'id-1', value: 'Primary Button' },
136
+ },
137
+ };
138
+
139
+ beforeEach(() => {
140
+ vi.clearAllMocks();
141
+ mockGetHardcopy.mockResolvedValue(defaultHardcopy);
142
+ });
143
+
144
+ it('should track ChatComponentVisible when widget mounts', async () => {
145
+ render(<SocialProofWidget widgetConfigId="test-config-1" />);
146
+
147
+ await waitFor(
148
+ () => {
149
+ expect(mockTrackEvent).toHaveBeenCalledWith({
150
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
151
+ eventProps: {
152
+ widget_config_id: 'test-config-1',
153
+ widget_type: WidgetTypeV3.SocialProofV3,
154
+ },
155
+ });
156
+ },
157
+ { timeout: 3000 },
158
+ );
159
+ }, 5000);
160
+
161
+ it('should track WidgetTextClicked when primary button is clicked', async () => {
162
+ render(<SocialProofWidget widgetConfigId="test-config-2" />);
163
+
164
+ await waitFor(
165
+ () => {
166
+ expect(screen.getByText('Primary Button')).toBeInTheDocument();
167
+ },
168
+ { timeout: 3000 },
169
+ );
170
+
171
+ mockTrackEvent.mockClear();
172
+
173
+ screen.getByText('Primary Button').click();
174
+
175
+ expect(mockTrackEvent).toHaveBeenCalledWith({
176
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
177
+ eventProps: {
178
+ response_id: 'test-response-id',
179
+ string_id: 'id-1',
180
+ text: 'Primary Button',
181
+ },
182
+ });
183
+ }, 5000);
184
+ });
@@ -7,6 +7,7 @@ import { ChatElementDisplayLocationV3 } from '@envive-ai/react-hooks/application
7
7
  import { useSalesAgent } from '@envive-ai/react-hooks/contexts/salesAgentContext';
8
8
  import { useCallback, useEffect } from 'react';
9
9
  import {
10
+ EnviveMetricsEventName,
10
11
  SpiffyMetricsEventName,
11
12
  useAmplitude,
12
13
  } from '@envive-ai/react-hooks/contexts/amplitudeContext';
@@ -15,6 +16,7 @@ import { useChatToggle } from '@envive-ai/react-hooks/hooks/ChatToggle';
15
16
  import { TitledPromptCarousel } from '@envive-ai/react-toolkit-v3/TitledPromptCarousel';
16
17
  import { AnimationSpeed, PromptCarouselRows } from '@envive-ai/react-toolkit-v3/PromptCarousel';
17
18
  import { BaseWidgetProps, withBaseWidget } from '../../hocs/withBaseWidget';
19
+ import { RawValues, getStringIdForText } from '../utils/functions';
18
20
 
19
21
  const mockButtonTexts = [
20
22
  'Loading button 1',
@@ -67,6 +69,16 @@ const TitledPromptCarouselWidgetHandler = (props: BaseWidgetProps) => {
67
69
 
68
70
  const handleButtonClick = useCallback(
69
71
  (text: string) => {
72
+ const rawValues = (hardcopyContent as { rawValues?: RawValues } | undefined)?.rawValues;
73
+ const stringId = getStringIdForText(rawValues, text);
74
+ trackEvent({
75
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
76
+ eventProps: {
77
+ response_id: hardcopyContent?.responseId,
78
+ string_id: stringId,
79
+ text,
80
+ },
81
+ });
70
82
  onTypedMessageSubmitted({
71
83
  query: text,
72
84
  userTyped: false,
@@ -0,0 +1,150 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import {
3
+ EnviveMetricsEventName,
4
+ SpiffyMetricsEventName,
5
+ } from '@envive-ai/react-hooks/contexts/amplitudeContext';
6
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
7
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
8
+ import { TitledPromptCarouselWidget } from '../TitledPromptCarouselWidget';
9
+
10
+ const mockTrackEvent = vi.fn();
11
+ const mockGetHardcopy = vi.fn();
12
+
13
+ vi.mock('@envive-ai/react-toolkit-v3/TitledPromptCarousel', () => ({
14
+ TitledPromptCarousel: ({
15
+ promptButtonTexts,
16
+ handleButtonClick,
17
+ }: {
18
+ promptButtonTexts: string[];
19
+ handleButtonClick: (text: string) => void;
20
+ }) => (
21
+ <div data-testid="titled-prompt-carousel-mock">
22
+ {promptButtonTexts.map(text => (
23
+ <button
24
+ key={text}
25
+ type="button"
26
+ onClick={() => handleButtonClick(text)}
27
+ >
28
+ {text}
29
+ </button>
30
+ ))}
31
+ </div>
32
+ ),
33
+ }));
34
+
35
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
36
+ useAmplitude: () => ({
37
+ trackEvent: mockTrackEvent,
38
+ isReady: true,
39
+ }),
40
+ SpiffyMetricsEventName: {
41
+ ChatComponentVisible: 'Chat Component Visible',
42
+ SearchComponentVisible: 'Search Component Visible',
43
+ },
44
+ EnviveMetricsEventName: {
45
+ WidgetTextClicked: 'Widget Text Clicked',
46
+ },
47
+ }));
48
+
49
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
50
+ useHardcopy: () => ({
51
+ getHardcopy: mockGetHardcopy,
52
+ }),
53
+ }));
54
+
55
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
56
+ usePage: () => ({
57
+ userEvent: {
58
+ id: 'test-user-event-id',
59
+ category: UserEventCategory.PageVisit,
60
+ created_at: '2025-01-01T00:00:00Z',
61
+ event_id: 'test-event-id',
62
+ } as UserEvent,
63
+ }),
64
+ }));
65
+
66
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
67
+ useWidgetConfig: () => ({
68
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
69
+ }),
70
+ }));
71
+
72
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
73
+ useUiConfig: () => ({
74
+ getUiConfig: vi.fn().mockResolvedValue({}),
75
+ }),
76
+ }));
77
+
78
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
79
+ useSalesAgent: () => ({
80
+ onTypedMessageSubmitted: vi.fn(),
81
+ }),
82
+ }));
83
+
84
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
85
+ useChatToggle: () => ({
86
+ openChat: vi.fn(),
87
+ }),
88
+ }));
89
+
90
+ describe('TitledPromptCarouselWidget analytics', () => {
91
+ const defaultHardcopy = {
92
+ responseId: 'test-response-id',
93
+ language: 'en',
94
+ values: {
95
+ promptButtonTexts: ['Button 1', 'Button 2', 'Button 3'],
96
+ },
97
+ rawValues: {
98
+ promptButtonTexts: [
99
+ { id: 'id-1', value: 'Button 1' },
100
+ { id: 'id-2', value: 'Button 2' },
101
+ ],
102
+ },
103
+ };
104
+
105
+ beforeEach(() => {
106
+ vi.clearAllMocks();
107
+ mockGetHardcopy.mockResolvedValue(defaultHardcopy);
108
+ });
109
+
110
+ it('should track ChatComponentVisible when widget mounts', async () => {
111
+ render(<TitledPromptCarouselWidget widgetConfigId="test-config-1" />);
112
+
113
+ await waitFor(
114
+ () => {
115
+ expect(mockTrackEvent).toHaveBeenCalledWith({
116
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
117
+ eventProps: {
118
+ widget_config_id: 'test-config-1',
119
+ widget_type: WidgetTypeV3.TitledPromptCarouselV3,
120
+ },
121
+ });
122
+ },
123
+ { timeout: 3000 },
124
+ );
125
+ }, 5000);
126
+
127
+ it('should track WidgetTextClicked when button is clicked', async () => {
128
+ render(<TitledPromptCarouselWidget widgetConfigId="test-config-2" />);
129
+
130
+ await waitFor(
131
+ () => {
132
+ expect(screen.getByText('Button 1')).toBeInTheDocument();
133
+ },
134
+ { timeout: 3000 },
135
+ );
136
+
137
+ mockTrackEvent.mockClear();
138
+
139
+ screen.getByText('Button 1').click();
140
+
141
+ expect(mockTrackEvent).toHaveBeenCalledWith({
142
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
143
+ eventProps: {
144
+ response_id: 'test-response-id',
145
+ string_id: 'id-1',
146
+ text: 'Button 1',
147
+ },
148
+ });
149
+ }, 5000);
150
+ });
@@ -7,6 +7,7 @@ import { WidgetWrapperVariant } from '@envive-ai/react-toolkit-v3/WidgetWrapper'
7
7
  import { useCallback, useEffect } from 'react';
8
8
  import { ChatElementDisplayLocationV3 } from '@envive-ai/react-hooks/application/models';
9
9
  import {
10
+ EnviveMetricsEventName,
10
11
  SpiffyMetricsEventName,
11
12
  useAmplitude,
12
13
  } from '@envive-ai/react-hooks/contexts/amplitudeContext';
@@ -16,6 +17,7 @@ import { chatOnToggleAtom } from '@envive-ai/react-hooks/atoms/chat/chatState';
16
17
  import { PromptCarouselRows } from '@envive-ai/react-toolkit-v3/PromptCarousel';
17
18
  import { TypingAnimation } from '@envive-ai/react-toolkit-v3/TypingAnimation';
18
19
  import { BaseWidgetProps, withBaseWidget } from '../../hocs/withBaseWidget';
20
+ import { RawValues, getStringIdForText } from '../utils/functions';
19
21
 
20
22
  const mockButtonTexts = [
21
23
  'Loading button 1',
@@ -33,7 +35,7 @@ const TypingAnimationWidgetHandler = (props: BaseWidgetProps) => {
33
35
  const { onTypedMessageSubmitted } = useSalesAgent();
34
36
  const onToggle = useSetAtom(chatOnToggleAtom);
35
37
 
36
- const { hardcopyContent, widgetConfig, isLoading, widgetConfigId } = props;
38
+ const { hardcopyContent, widgetConfig, uiConfig, isLoading, widgetConfigId } = props;
37
39
 
38
40
  const titleLabel = (hardcopyContent?.values?.titleLabel as string | undefined) || '';
39
41
  const headlineText = (hardcopyContent?.values?.headlineText as string | undefined) || '';
@@ -58,6 +60,9 @@ const TypingAnimationWidgetHandler = (props: BaseWidgetProps) => {
58
60
  typingAnimationWidgetConfig?.promptCarouselRows || PromptCarouselRows.ALWAYS_ONE;
59
61
  const showTextField = typingAnimationWidgetConfig?.showTextField !== false; // Default: true
60
62
 
63
+ const logoSrc = uiConfig?.lookAndFeel?.widgetLogoSrc;
64
+ const hideLogo = uiConfig?.lookAndFeel?.hideWidgetLogo;
65
+
61
66
  const { trackEvent } = useAmplitude();
62
67
 
63
68
  useEffect(() => {
@@ -72,6 +77,16 @@ const TypingAnimationWidgetHandler = (props: BaseWidgetProps) => {
72
77
 
73
78
  const handleButtonClick = useCallback(
74
79
  (text: string) => {
80
+ const rawValues = (hardcopyContent as { rawValues?: RawValues } | undefined)?.rawValues;
81
+ const stringId = getStringIdForText(rawValues, text);
82
+ trackEvent({
83
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
84
+ eventProps: {
85
+ response_id: hardcopyContent?.responseId,
86
+ string_id: stringId,
87
+ text,
88
+ },
89
+ });
75
90
  onTypedMessageSubmitted({
76
91
  query: text,
77
92
  userTyped: false,
@@ -79,7 +94,7 @@ const TypingAnimationWidgetHandler = (props: BaseWidgetProps) => {
79
94
  });
80
95
  onToggle(ChatElementDisplayLocationV3.TYPING_ANIMATION);
81
96
  },
82
- [onTypedMessageSubmitted, onToggle],
97
+ [hardcopyContent, trackEvent, onTypedMessageSubmitted, onToggle],
83
98
  );
84
99
 
85
100
  const handleTextFieldClick = useCallback(() => {
@@ -98,6 +113,8 @@ const TypingAnimationWidgetHandler = (props: BaseWidgetProps) => {
98
113
  promptButtonTexts: buttonTexts,
99
114
  hintText: isLoading ? mockHintText : hintText,
100
115
  textFieldAriaLabel,
116
+ logoSrc: logoSrc ?? undefined,
117
+ hideLogo: hideLogo ?? false,
101
118
  }}
102
119
  widgetStyleProps={{
103
120
  widgetVariant,
@@ -0,0 +1,163 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import { atom } from 'jotai';
3
+ import {
4
+ EnviveMetricsEventName,
5
+ SpiffyMetricsEventName,
6
+ } from '@envive-ai/react-hooks/contexts/amplitudeContext';
7
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
8
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
9
+ import { TypingAnimationWidget } from '../TypingAnimationWidget';
10
+
11
+ const mockTrackEvent = vi.fn();
12
+
13
+ vi.mock('@envive-ai/react-toolkit-v3/TypingAnimation', () => ({
14
+ TypingAnimation: ({
15
+ widgetContentProps,
16
+ widgetEventProps,
17
+ }: {
18
+ widgetContentProps: { promptButtonTexts: string[] };
19
+ widgetEventProps: { handleButtonClick: (text: string) => void };
20
+ }) => (
21
+ <div data-testid="typing-animation-mock">
22
+ {widgetContentProps.promptButtonTexts.map(text => (
23
+ <button
24
+ key={text}
25
+ type="button"
26
+ onClick={() => widgetEventProps.handleButtonClick(text)}
27
+ >
28
+ {text}
29
+ </button>
30
+ ))}
31
+ </div>
32
+ ),
33
+ }));
34
+ const mockGetHardcopy = vi.fn();
35
+ const mockOnTypedMessageSubmitted = vi.fn();
36
+ const mockOpenChat = vi.fn();
37
+ const mockSetAtom = vi.fn();
38
+
39
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
40
+ useAmplitude: () => ({
41
+ trackEvent: mockTrackEvent,
42
+ isReady: true,
43
+ }),
44
+ SpiffyMetricsEventName: {
45
+ ChatComponentVisible: 'Chat Component Visible',
46
+ SearchComponentVisible: 'Search Component Visible',
47
+ },
48
+ EnviveMetricsEventName: {
49
+ WidgetTextClicked: 'Widget Text Clicked',
50
+ },
51
+ }));
52
+
53
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
54
+ useHardcopy: () => ({
55
+ getHardcopy: mockGetHardcopy,
56
+ }),
57
+ }));
58
+
59
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
60
+ usePage: () => ({
61
+ userEvent: {
62
+ id: 'test-user-event-id',
63
+ category: UserEventCategory.PageVisit,
64
+ created_at: '2025-01-01T00:00:00Z',
65
+ event_id: 'test-event-id',
66
+ } as UserEvent,
67
+ }),
68
+ }));
69
+
70
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
71
+ useWidgetConfig: () => ({
72
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
73
+ }),
74
+ }));
75
+
76
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
77
+ useUiConfig: () => ({
78
+ getUiConfig: vi.fn().mockResolvedValue({}),
79
+ }),
80
+ }));
81
+
82
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
83
+ useSalesAgent: () => ({
84
+ onTypedMessageSubmitted: mockOnTypedMessageSubmitted,
85
+ }),
86
+ }));
87
+
88
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
89
+ useChatToggle: () => ({
90
+ openChat: mockOpenChat,
91
+ }),
92
+ }));
93
+
94
+ vi.mock('@envive-ai/react-hooks/atoms/chat/chatState', () => ({
95
+ chatOnToggleAtom: atom(null),
96
+ }));
97
+
98
+ describe('TypingAnimationWidget analytics', () => {
99
+ const defaultHardcopy = {
100
+ responseId: 'test-response-id',
101
+ language: 'en',
102
+ values: {
103
+ promptButtonTexts: ['Button 1', 'Button 2', 'Button 3'],
104
+ titleLabel: 'Test Title',
105
+ headlineText: 'Headline',
106
+ animatedTextSequence: ['...'],
107
+ hintText: 'Ask me anything',
108
+ },
109
+ rawValues: {
110
+ promptButtonTexts: [
111
+ { id: 'id-1', value: 'Button 1' },
112
+ { id: 'id-2', value: 'Button 2' },
113
+ ],
114
+ },
115
+ };
116
+
117
+ beforeEach(() => {
118
+ vi.clearAllMocks();
119
+ mockGetHardcopy.mockResolvedValue(defaultHardcopy);
120
+ });
121
+
122
+ it('should track ChatComponentVisible when widget mounts', async () => {
123
+ render(<TypingAnimationWidget widgetConfigId="test-config-1" />);
124
+
125
+ await waitFor(
126
+ () => {
127
+ expect(mockTrackEvent).toHaveBeenCalledWith({
128
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
129
+ eventProps: {
130
+ widget_config_id: 'test-config-1',
131
+ widget_type: WidgetTypeV3.TypingAnimationV3,
132
+ },
133
+ });
134
+ },
135
+ { timeout: 3000 },
136
+ );
137
+ }, 5000);
138
+
139
+ it('should track WidgetTextClicked when button is clicked', async () => {
140
+ render(<TypingAnimationWidget widgetConfigId="test-config-2" />);
141
+
142
+ await waitFor(
143
+ () => {
144
+ expect(screen.getByText('Button 1')).toBeInTheDocument();
145
+ },
146
+ { timeout: 3000 },
147
+ );
148
+
149
+ mockTrackEvent.mockClear();
150
+
151
+ const button = screen.getByText('Button 1');
152
+ button.click();
153
+
154
+ expect(mockTrackEvent).toHaveBeenCalledWith({
155
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
156
+ eventProps: {
157
+ response_id: 'test-response-id',
158
+ string_id: 'id-1',
159
+ text: 'Button 1',
160
+ },
161
+ });
162
+ }, 5000);
163
+ });
@@ -0,0 +1,63 @@
1
+ import { RenderOptions, render } from '@testing-library/react';
2
+ import { ReactElement } from 'react';
3
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
4
+ import {
5
+ EnviveMetricsEventName,
6
+ SpiffyMetricsEventName,
7
+ } from '@envive-ai/react-hooks/contexts/amplitudeContext';
8
+
9
+ export const mockTrackEvent = vi.fn();
10
+
11
+ export const defaultHardcopyResponse = {
12
+ responseId: 'test-response-id',
13
+ language: 'en',
14
+ values: {
15
+ promptButtonTexts: ['Button 1', 'Button 2', 'Button 3'],
16
+ titleLabel: 'Test Title',
17
+ headlineText: 'Headline',
18
+ headline: 'Product Headline',
19
+ animatedTextSequence: ['...'],
20
+ hintText: 'Ask me anything',
21
+ prompts: ['Prompt 1', 'Prompt 2'],
22
+ primaryButtonText: 'Primary',
23
+ secondaryButtonTexts: ['Secondary 1'],
24
+ promptButtonsTexts: ['Prompt A', 'Prompt B'],
25
+ },
26
+ rawValues: {
27
+ promptButtonTexts: [
28
+ { id: 'id-1', value: 'Button 1' },
29
+ { id: 'id-2', value: 'Button 2' },
30
+ ],
31
+ },
32
+ };
33
+
34
+ export const defaultUserEvent = {
35
+ id: 'test-user-event-id',
36
+ category: UserEventCategory.PageVisit,
37
+ created_at: '2025-01-01T00:00:00Z',
38
+ event_id: 'test-event-id',
39
+ } as UserEvent;
40
+
41
+ export function createWidgetTestMocks(overrides?: {
42
+ trackEvent?: ReturnType<typeof vi.fn>;
43
+ hardcopy?: Partial<typeof defaultHardcopyResponse>;
44
+ }) {
45
+ const trackEvent = overrides?.trackEvent ?? mockTrackEvent;
46
+ const hardcopy = { ...defaultHardcopyResponse, ...overrides?.hardcopy };
47
+
48
+ return {
49
+ trackEvent,
50
+ hardcopy,
51
+ getHardcopy: vi.fn().mockResolvedValue(hardcopy),
52
+ userEvent: defaultUserEvent,
53
+ };
54
+ }
55
+
56
+ export function renderWithWidgetProviders(
57
+ ui: ReactElement,
58
+ options?: Omit<RenderOptions, 'wrapper'>,
59
+ ) {
60
+ return render(ui, options);
61
+ }
62
+
63
+ export { EnviveMetricsEventName, SpiffyMetricsEventName };