@envive-ai/react-widgets-v3 0.3.13 → 0.3.15-beta.0

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 (143) 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/types.d.cts +3 -3
  10. package/dist/hocs/withBaseWidget/types.d.ts +5 -3
  11. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  12. package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
  13. package/dist/hooks/dist/application/models/api/widgetText.d.cts +8 -0
  14. package/dist/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.cts +12 -0
  15. package/dist/hooks/dist/contexts/types.d.cts +38 -0
  16. package/dist/hooks/dist/contexts/typesV3.d.cts +239 -0
  17. package/dist/hooks/dist/services/amplitudeService/eventNames.d.cts +43 -0
  18. package/dist/hooks/dist/types/customerService.d.cts +21 -0
  19. package/dist/packages/hooks/dist/application/models/api/orgConfigResults.d.ts +1 -0
  20. package/dist/packages/hooks/dist/application/models/api/widgetText.d.ts +8 -0
  21. package/dist/packages/hooks/dist/application/models/frontendConfig.d.ts +1 -0
  22. package/dist/packages/hooks/dist/contexts/amplitudeContext/amplitudeContext.d.ts +2 -0
  23. package/dist/packages/hooks/dist/contexts/amplitudeContext/index.d.ts +2 -0
  24. package/dist/packages/hooks/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.ts +2 -0
  25. package/dist/packages/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.ts +14 -0
  26. package/dist/packages/hooks/dist/contexts/hardcopyContext/index.d.ts +1 -0
  27. package/dist/packages/hooks/dist/contexts/types.d.ts +42 -0
  28. package/dist/packages/hooks/dist/contexts/typesV3.d.ts +239 -0
  29. package/dist/packages/hooks/dist/services/amplitudeService/amplitudeService.d.ts +1 -0
  30. package/dist/packages/hooks/dist/services/amplitudeService/eventNames.d.ts +43 -0
  31. package/dist/packages/hooks/dist/types/customerService.d.ts +21 -0
  32. package/dist/packages/widgets/dist/SearchResults/SearchResults.d.ts +3 -2
  33. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  34. package/dist/packages/widgets/dist/SearchZeroState/index.d.ts +2 -1
  35. package/dist/packages/widgets/dist/SearchZeroState/types.d.ts +2 -2
  36. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +3 -3
  37. package/dist/packages/widgets/dist/SuggestionButtonContainer/index.d.ts +2 -0
  38. package/dist/packages/widgets/dist/SuggestionButtonContainer/types.d.ts +3 -2
  39. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/api/response.d.ts +14 -0
  40. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/api/search.d.ts +15 -0
  41. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCase.d.ts +73 -0
  42. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCasedPropertiesDeep.d.ts +61 -0
  43. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/internal.d.ts +25 -0
  44. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/splitWords.d.ts +35 -0
  45. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/trim.d.ts +32 -0
  46. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/unknownArray.d.ts +32 -0
  47. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/variantInfo/variantInfo.d.ts +1 -0
  48. package/dist/packages/widgets/dist/packages/hooks/dist/atoms/search/searchAPI.d.ts +15 -0
  49. package/dist/packages/widgets/dist/packages/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.ts +1 -0
  50. package/dist/packages/widgets/dist/packages/hooks/dist/contexts/types.d.ts +63 -0
  51. package/dist/packages/widgets/dist/packages/hooks/dist/hooks/Search/useSearch.d.ts +60 -0
  52. package/dist/packages/widgets/dist/packages/hooks/dist/hooks/utils.d.ts +13 -0
  53. package/dist/packages/widgets/dist/packages/hooks/dist/types/OrgInfo.d.ts +1 -0
  54. package/dist/packages/widgets/dist/packages/hooks/dist/types/index.d.ts +1 -0
  55. package/dist/packages/widgets/dist/packages/hooks/dist/types/search-filter-types.d.ts +28 -0
  56. package/dist/packages/widgets/dist/packages/hooks/dist/types/test-types.d.ts +10 -0
  57. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  58. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  59. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
  60. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  61. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  62. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +29 -5
  63. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
  64. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
  65. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +30 -6
  66. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
  67. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
  68. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +19 -3
  69. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  70. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  71. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +20 -4
  72. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +10 -0
  73. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  74. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  75. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +12 -2
  76. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +15 -10
  77. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  78. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
  79. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +16 -11
  80. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.cts +2 -2
  81. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  82. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +10 -0
  83. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  84. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  85. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +12 -2
  86. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +11 -0
  87. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  88. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  89. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +12 -1
  90. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
  91. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  92. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +23 -3
  93. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  94. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  95. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +24 -4
  96. package/dist/widgets/dist/SearchResults/SearchResults.d.cts +3 -2
  97. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  98. package/dist/widgets/dist/SearchZeroState/types.d.cts +2 -2
  99. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +3 -3
  100. package/dist/widgets/dist/SuggestionButtonContainer/types.d.cts +3 -2
  101. package/dist/widgets/dist/packages/hooks/dist/application/models/api/response.d.cts +14 -0
  102. package/dist/widgets/dist/packages/hooks/dist/application/models/api/search.d.cts +15 -0
  103. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCase.d.cts +73 -0
  104. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCasedPropertiesDeep.d.cts +61 -0
  105. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/internal.d.cts +25 -0
  106. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/splitWords.d.cts +35 -0
  107. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/trim.d.cts +32 -0
  108. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/unknownArray.d.cts +32 -0
  109. package/dist/widgets/dist/packages/hooks/dist/atoms/search/searchAPI.d.cts +14 -0
  110. package/dist/widgets/dist/packages/hooks/dist/contexts/types.d.cts +61 -0
  111. package/dist/widgets/dist/packages/hooks/dist/hooks/Search/useSearch.d.cts +60 -0
  112. package/dist/widgets/dist/packages/hooks/dist/hooks/utils.d.cts +12 -0
  113. package/dist/widgets/dist/packages/hooks/dist/types/search-filter-types.d.cts +28 -0
  114. package/dist/widgets/dist/packages/hooks/dist/types/test-types.d.cts +10 -0
  115. package/dist/widgets/utils/functions.cjs +9 -0
  116. package/dist/widgets/utils/functions.js +9 -1
  117. package/dist/widgets-v2/SearchZeroState/index.d.cts +2 -1
  118. package/dist/widgets-v2/SearchZeroState/index.d.ts +2 -1
  119. package/dist/widgets-v2/SuggestionButtonContainer/index.d.ts +1 -0
  120. package/package.json +1 -1
  121. package/src/CXIntegration/implementations/useHelpScoutUnifiedCXButton.ts +108 -0
  122. package/src/CXIntegration/implementations/useTalkdeskUnifiedCXButton.ts +94 -0
  123. package/src/CXIntegration/types.ts +2 -0
  124. package/src/CXIntegration/utils/functions.ts +8 -0
  125. package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +15 -3
  126. package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +114 -0
  127. package/src/widgets/FloatingChatWidget/FloatingChatWidget.tsx +56 -15
  128. package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +119 -0
  129. package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +15 -3
  130. package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +144 -0
  131. package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +12 -1
  132. package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +179 -0
  133. package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +33 -23
  134. package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +150 -0
  135. package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +12 -1
  136. package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +184 -0
  137. package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +12 -0
  138. package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +150 -0
  139. package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +19 -2
  140. package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +163 -0
  141. package/src/widgets/__tests__/testUtils.tsx +63 -0
  142. package/src/widgets/__tests__/trackEventCanary.test.ts +45 -0
  143. package/src/widgets/utils/functions.ts +16 -0
@@ -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 };
@@ -0,0 +1,45 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const WIDGETS_SRC_DIR = join(process.cwd(), 'src');
5
+
6
+ /**
7
+ * Known count of trackEvent({ invocations in widget source files (excluding tests and stories).
8
+ * Update this when adding new trackEvent calls - and add a test for the new event!
9
+ */
10
+ const EXPECTED_TRACK_EVENT_CALL_COUNT = 15;
11
+
12
+ function getAllSourceFiles(dir: string, files: string[] = []): string[] {
13
+ const entries = readdirSync(dir, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const fullPath = join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
18
+ getAllSourceFiles(fullPath, files);
19
+ }
20
+ } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
21
+ files.push(fullPath);
22
+ }
23
+ }
24
+ return files;
25
+ }
26
+
27
+ describe('trackEvent canary', () => {
28
+ it('should not add new trackEvent calls without updating this test', () => {
29
+ const files = getAllSourceFiles(WIDGETS_SRC_DIR).filter(
30
+ file =>
31
+ !file.includes('__tests__') && !file.includes('.test.') && !file.includes('.stories.'),
32
+ );
33
+
34
+ let count = 0;
35
+ for (const file of files) {
36
+ const content = readFileSync(file, 'utf-8');
37
+ const matches = content.match(/trackEvent\s*\(\s*\{/g);
38
+ if (matches) {
39
+ count += matches.length;
40
+ }
41
+ }
42
+
43
+ expect(count).toBe(EXPECTED_TRACK_EVENT_CALL_COUNT);
44
+ });
45
+ });
@@ -102,3 +102,19 @@ export const isProductComparison = (lastMsg?: Message[] | null | undefined): boo
102
102
  const lastAssistantMessage = atomStore.get(lastAssistantMessageAtom);
103
103
  return lastAssistantMessage?.filter(msg => msg.type === MessageType.Product).length === 2;
104
104
  };
105
+
106
+ type RawWidgetString = { id: string; value: string };
107
+ export type RawValues = Record<string, RawWidgetString | RawWidgetString[]>;
108
+ /** Finds the string_id (id) from raw widget text values for the given display text */
109
+ export const getStringIdForText = (
110
+ rawValues: RawValues | undefined,
111
+ text: string,
112
+ ): string | undefined => {
113
+ if (!rawValues) return undefined;
114
+ for (const raw of Object.values(rawValues)) {
115
+ const list = Array.isArray(raw) ? raw : [raw];
116
+ const found = list.find((s: RawWidgetString) => s.value === text);
117
+ if (found) return found.id;
118
+ }
119
+ return undefined;
120
+ };