@envive-ai/react-widgets-v3 0.3.21 → 0.3.23

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 (91) hide show
  1. package/dist/debug/chatEmbed.cjs +1 -1
  2. package/dist/debug/chatEmbed.js +1 -1
  3. package/dist/debug/reportIssue.cjs +1 -1
  4. package/dist/debug/reportIssue.js +1 -1
  5. package/dist/hocs/withBaseWidget/types.d.cts +5 -6
  6. package/dist/hocs/withBaseWidget/types.d.ts +5 -6
  7. package/dist/hocs/withBaseWidget/withBaseWidget.cjs +37 -43
  8. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  9. package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
  10. package/dist/hocs/withBaseWidget/withBaseWidget.js +39 -45
  11. package/dist/packages/widgets/dist/SearchResults/SearchResultsWidget.d.ts +2 -2
  12. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  13. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +2 -2
  14. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.cjs +34 -8
  15. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  16. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
  17. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.js +35 -9
  18. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.cjs +13 -5
  19. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  20. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
  21. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.js +13 -5
  22. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.cjs +32 -15
  23. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  24. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  25. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.js +35 -18
  26. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +36 -32
  27. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
  28. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +37 -33
  29. package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.cjs +2 -2
  30. package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.js +2 -2
  31. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.cjs +7 -1
  32. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.js +7 -1
  33. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +31 -15
  34. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  35. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  36. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +33 -17
  37. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +33 -16
  38. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  39. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  40. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +36 -19
  41. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +3 -12
  42. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  43. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
  44. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +5 -14
  45. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.cts +2 -2
  46. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  47. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +34 -17
  48. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  49. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  50. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +37 -20
  51. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +2 -11
  52. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  53. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  54. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +4 -13
  55. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
  56. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  57. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +31 -14
  58. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  59. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  60. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +34 -17
  61. package/dist/widgets/dist/SearchResults/SearchResultsWidget.d.cts +2 -2
  62. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  63. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +2 -2
  64. package/dist/widgets/hooks/useGetWidgetStatus.cjs +1 -1
  65. package/dist/widgets/hooks/useGetWidgetStatus.js +1 -1
  66. package/dist/widgets/utils/functions.cjs +1 -1
  67. package/dist/widgets/utils/functions.js +1 -1
  68. package/package.json +1 -1
  69. package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +68 -302
  70. package/src/hocs/withBaseWidget/types.ts +6 -6
  71. package/src/hocs/withBaseWidget/withBaseWidget.tsx +46 -61
  72. package/src/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.tsx +33 -0
  73. package/src/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.tsx +9 -1
  74. package/src/widgets/ChatPreviewWidget/ChatPreviewWidget.tsx +34 -13
  75. package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +11 -5
  76. package/src/widgets/FloatingChatWidget/FloatingChatWidget.tsx +56 -38
  77. package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +11 -5
  78. package/src/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.tsx +8 -0
  79. package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +37 -15
  80. package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +12 -8
  81. package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +35 -13
  82. package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +19 -8
  83. package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +1 -12
  84. package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +21 -8
  85. package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +34 -12
  86. package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +19 -8
  87. package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +1 -12
  88. package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +19 -8
  89. package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +37 -14
  90. package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +19 -8
  91. package/src/widgets/__tests__/trackEventCanary.test.ts +1 -1
@@ -1,16 +1,17 @@
1
1
  import { withBaseWidget } from "../../hocs/withBaseWidget/withBaseWidget.js";
2
2
  import "../../hocs/withBaseWidget/index.js";
3
3
  import { getStringIdForText } from "../utils/functions.js";
4
- import { useCallback, useEffect } from "react";
5
- import { EnviveMetricsEventName, SpiffyMetricsEventName, useAmplitude } from "@envive-ai/react-hooks/contexts/amplitudeContext";
4
+ import { useCallback } from "react";
5
+ import { EnviveMetricsEventName, useAmplitude } from "@envive-ai/react-hooks/contexts/amplitudeContext";
6
6
  import { jsx } from "react/jsx-runtime";
7
- import { useSalesAgent } from "@envive-ai/react-hooks/contexts/salesAgentContext";
8
7
  import { WidgetTypeV3 } from "@envive-ai/react-hooks/contexts/typesV3";
9
8
  import { useSetAtom } from "jotai";
10
- import { ChatElementDisplayLocationV3 } from "@envive-ai/react-hooks/application/models";
11
- import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
9
+ import { useFeatureFlagService } from "@envive-ai/react-hooks/contexts/featureFlagServiceContext";
10
+ import { ChatElementDisplayLocationV3, FeatureGates } from "@envive-ai/react-hooks/application/models";
11
+ import { useSalesAgent } from "@envive-ai/react-hooks/contexts/salesAgentContext";
12
12
  import { WidgetInteractionComponent, WidgetInteractionType } from "@envive-ai/react-hooks/hooks/WidgetInteraction/types";
13
13
  import { PromptCarouselRows, usePromptCarouselAnalytics } from "@envive-ai/react-toolkit-v3/PromptCarousel";
14
+ import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
14
15
  import { chatOnToggleAtom } from "@envive-ai/react-hooks/atoms/chat/chatState";
15
16
  import { PromptButtonVariant } from "@envive-ai/react-toolkit-v3/PromptButton/types";
16
17
  import { TypingAnimation } from "@envive-ai/react-toolkit-v3/TypingAnimation";
@@ -30,7 +31,7 @@ const mockHintText = "Ask me anything";
30
31
  const TypingAnimationWidgetHandler = (props) => {
31
32
  const { onTypedMessageSubmitted } = useSalesAgent();
32
33
  const onToggle = useSetAtom(chatOnToggleAtom);
33
- const { hardcopyContent, widgetConfig, uiConfig, isLoading, widgetConfigId } = props;
34
+ const { hardcopyContent, widgetConfig, uiConfig, isLoading } = props;
34
35
  const titleLabel = hardcopyContent?.values?.titleLabel || "";
35
36
  const headlineText = hardcopyContent?.values?.headlineText || "";
36
37
  const animatedTextSequence = hardcopyContent?.values?.animatedTextSequence || [];
@@ -47,21 +48,15 @@ const TypingAnimationWidgetHandler = (props) => {
47
48
  const showTextField = typingAnimationWidgetConfig?.showTextField !== false;
48
49
  const logoSrc = uiConfig?.lookAndFeel?.widgetLogoSrc;
49
50
  const hideLogo = uiConfig?.lookAndFeel?.hideWidgetLogo;
51
+ const voiceInputEnabled = uiConfig?.lookAndFeel?.voiceInputEnabled;
52
+ const { featureFlagService } = useFeatureFlagService();
53
+ const isVoiceInputEnabled = featureFlagService?.isFeatureGateEnabled(FeatureGates.IsVoiceInputEnabled) || voiceInputEnabled;
50
54
  const { trackEvent } = useAmplitude();
51
55
  const { trackWidgetInteraction } = useWidgetInteraction();
52
56
  const { onClick: onPromptClicked, onDrag, onHover, onMouseDown, onMouseUp, onTouchStart, onTouchEnd } = usePromptCarouselAnalytics(WidgetInteractionComponent.EMBEDDED_WIDGET, (text) => {
53
57
  const rawValues = hardcopyContent?.rawValues;
54
58
  return getStringIdForText(rawValues, text);
55
59
  });
56
- useEffect(() => {
57
- trackEvent({
58
- eventName: SpiffyMetricsEventName.ChatComponentVisible,
59
- eventProps: {
60
- widget_config_id: widgetConfigId,
61
- widget_type: WidgetTypeV3.TypingAnimationV3
62
- }
63
- });
64
- }, [trackEvent, widgetConfigId]);
65
60
  const handleButtonClick = useCallback((text) => {
66
61
  const rawValues = hardcopyContent?.rawValues;
67
62
  const stringId = getStringIdForText(rawValues, text);
@@ -96,6 +91,25 @@ const TypingAnimationWidgetHandler = (props) => {
96
91
  }
97
92
  });
98
93
  }, [onToggle]);
94
+ const handleTranscriptionStarted = useCallback(() => {
95
+ trackWidgetInteraction({
96
+ eventName: EnviveMetricsEventName.WidgetInteraction,
97
+ trigger: {
98
+ widget: WidgetInteractionComponent.EMBEDDED_WIDGET,
99
+ widget_interaction: WidgetInteractionType.VOICE_TRANSCRIPTION_STARTED
100
+ }
101
+ });
102
+ }, []);
103
+ const handleTranscriptionCompleted = useCallback((transcript) => {
104
+ trackWidgetInteraction({
105
+ eventName: EnviveMetricsEventName.WidgetInteraction,
106
+ trigger: {
107
+ widget: WidgetInteractionComponent.EMBEDDED_WIDGET,
108
+ widget_interaction: WidgetInteractionType.VOICE_TRANSCRIPTION_COMPLETED,
109
+ widget_interaction_data: { transcription: transcript }
110
+ }
111
+ });
112
+ }, []);
99
113
  return /* @__PURE__ */ jsx(TypingAnimation, {
100
114
  baseProps: { id },
101
115
  widgetContentProps: {
@@ -106,7 +120,8 @@ const TypingAnimationWidgetHandler = (props) => {
106
120
  hintText: isLoading ? mockHintText : hintText,
107
121
  textFieldAriaLabel,
108
122
  logoSrc: logoSrc ?? void 0,
109
- hideLogo: hideLogo ?? false
123
+ hideLogo: hideLogo ?? false,
124
+ voiceInputEnabled: isVoiceInputEnabled
110
125
  },
111
126
  widgetStyleProps: {
112
127
  widgetVariant,
@@ -124,7 +139,9 @@ const TypingAnimationWidgetHandler = (props) => {
124
139
  handleButtonMouseUp: onMouseUp,
125
140
  handleButtonTouchStart: onTouchStart,
126
141
  handleButtonTouchEnd: onTouchEnd,
127
- handleTextFieldClick
142
+ handleTextFieldClick,
143
+ onTranscriptionStarted: handleTranscriptionStarted,
144
+ onTranscriptionCompleted: handleTranscriptionCompleted
128
145
  }
129
146
  });
130
147
  };
@@ -1,8 +1,8 @@
1
- import * as react_jsx_runtime0 from "react/jsx-runtime";
1
+ import * as react_jsx_runtime1 from "react/jsx-runtime";
2
2
 
3
3
  //#region ../widgets/dist/SearchResults/SearchResultsWidget.d.ts
4
4
  //#region src/SearchResults/SearchResultsWidget.d.ts
5
- declare const SearchResultsWidget: () => react_jsx_runtime0.JSX.Element;
5
+ declare const SearchResultsWidget: () => react_jsx_runtime1.JSX.Element;
6
6
  //#endregion
7
7
 
8
8
  //#endregion
@@ -1,4 +1,4 @@
1
- import * as react_jsx_runtime2 from "react/jsx-runtime";
1
+ import * as react_jsx_runtime3 from "react/jsx-runtime";
2
2
 
3
3
  //#region ../widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts
4
4
  //#region src/SearchZeroState/SearchZeroStateWidget.d.ts
@@ -11,7 +11,7 @@ declare const SearchZeroStateWidget: ({
11
11
  initialIsOpen,
12
12
  widgetConfigId,
13
13
  entryPointRef
14
- }: SearchZeroStateWidgetProps) => react_jsx_runtime2.JSX.Element;
14
+ }: SearchZeroStateWidgetProps) => react_jsx_runtime3.JSX.Element;
15
15
  //#endregion
16
16
 
17
17
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { SuggestionBarLocationForMetrics } from "./types.cjs";
2
- import * as react_jsx_runtime3 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime2 from "react/jsx-runtime";
3
3
  import { SuggestionButtonVariant } from "@envive-ai/react-hooks/contexts/types";
4
4
  import { Message } from "postcss";
5
5
 
@@ -30,7 +30,7 @@ declare function SuggestionBar({
30
30
  buttonBorderRadius,
31
31
  handleReply,
32
32
  dataTestId
33
- }: Readonly<SuggestionBarProps>): react_jsx_runtime3.JSX.Element;
33
+ }: Readonly<SuggestionBarProps>): react_jsx_runtime2.JSX.Element;
34
34
  //#endregion
35
35
  //#endregion
36
36
  export { SuggestionBar };
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
2
2
  const require_functions = require('../utils/functions.cjs');
3
3
  let react = require("react");
4
- let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
5
4
  let jotai = require("jotai");
5
+ let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
6
6
  let __envive_ai_react_hooks_atoms_chat_chatState = require("@envive-ai/react-hooks/atoms/chat/chatState");
7
7
 
8
8
  //#region src/widgets/hooks/useGetWidgetStatus.tsx
@@ -1,7 +1,7 @@
1
1
  import { isLoading, isProductComparison, userHasInteracted, userHasNotInteracted } from "../utils/functions.js";
2
2
  import { useMemo } from "react";
3
- import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
4
3
  import { useAtomValue } from "jotai";
4
+ import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
5
5
  import { responseStreamingAtom } from "@envive-ai/react-hooks/atoms/chat/chatState";
6
6
 
7
7
  //#region src/widgets/hooks/useGetWidgetStatus.tsx
@@ -1,6 +1,6 @@
1
1
  const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
2
- let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
3
2
  let __envive_ai_react_hooks_application_models = require("@envive-ai/react-hooks/application/models");
3
+ let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
4
4
  let __envive_ai_react_hooks_atoms_atomStore = require("@envive-ai/react-hooks/atoms/atomStore");
5
5
 
6
6
  //#region src/widgets/utils/functions.ts
@@ -1,5 +1,5 @@
1
- import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
2
1
  import { MessageRole, MessageType } from "@envive-ai/react-hooks/application/models";
2
+ import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
3
3
  import { getAtomStore } from "@envive-ai/react-hooks/atoms/atomStore";
4
4
 
5
5
  //#region src/widgets/utils/functions.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envive-ai/react-widgets-v3",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "React widget library v3 for Envive services.",
5
5
  "keywords": [
6
6
  "react",
@@ -1,70 +1,15 @@
1
1
  import { render, screen, waitFor } from '@testing-library/react';
2
2
  import { RefObject } from 'react';
3
- import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
4
3
  import { HardcopyResponse } from '@envive-ai/react-hooks/contexts/hardcopyContext';
5
4
  import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
6
5
  import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
7
6
  import { BaseWidgetProps } from '../types';
8
7
  import { withBaseWidget } from '../withBaseWidget';
9
8
 
10
- // Mock IntersectionObserver
11
- class MockIntersectionObserver implements IntersectionObserver {
12
- observe = vi.fn();
13
-
14
- disconnect = vi.fn();
15
-
16
- unobserve = vi.fn();
17
-
18
- root: Element | null = null;
19
-
20
- rootMargin: string = '';
21
-
22
- thresholds: ReadonlyArray<number> = [];
23
-
24
- private callback: (entries: IntersectionObserverEntry[]) => void;
25
-
26
- private options?: IntersectionObserverInit;
27
-
28
- constructor(
29
- callback: (entries: IntersectionObserverEntry[]) => void,
30
- options?: IntersectionObserverInit,
31
- ) {
32
- this.callback = callback;
33
- this.options = options;
34
- if (options) {
35
- this.rootMargin = options.rootMargin || '';
36
- }
37
- }
38
-
39
- // Helper method to trigger intersection
40
- triggerIntersection(isIntersecting: boolean, target?: Element) {
41
- const entry = {
42
- isIntersecting,
43
- intersectionRatio: isIntersecting ? 1 : 0,
44
- boundingClientRect: {} as DOMRectReadOnly,
45
- rootBounds: {} as DOMRectReadOnly,
46
- target: target || ({} as Element),
47
- time: Date.now(),
48
- } as IntersectionObserverEntry;
49
-
50
- this.callback([entry]);
51
- }
52
-
53
- // Helper method to get options
54
- getOptions() {
55
- return this.options;
56
- }
57
-
58
- // Required by IntersectionObserver interface but not used in tests
59
- // eslint-disable-next-line class-methods-use-this
60
- takeRecords(): IntersectionObserverEntry[] {
61
- return [];
62
- }
63
- }
64
-
65
9
  // Mock the contexts
66
10
  const mockTrackEvent = vi.fn();
67
11
  const mockGetHardcopy = vi.fn();
12
+ const mockUseTrackComponentVisibleEvent = vi.fn();
68
13
 
69
14
  vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
70
15
  useAmplitude: () => ({
@@ -106,6 +51,10 @@ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
106
51
  }),
107
52
  }));
108
53
 
54
+ vi.mock('@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent', () => ({
55
+ useTrackComponentVisibleEvent: (...args: unknown[]) => mockUseTrackComponentVisibleEvent(...args),
56
+ }));
57
+
109
58
  // Test widget component
110
59
  interface TestWidgetProps extends BaseWidgetProps {
111
60
  testId?: string;
@@ -145,44 +94,18 @@ const TestWidget: React.FC<TestWidgetProps> = ({
145
94
  };
146
95
 
147
96
  describe('withBaseWidget', () => {
148
- let mockObserver: MockIntersectionObserver;
149
- let mockObservers: MockIntersectionObserver[];
150
- let originalIntersectionObserver: typeof IntersectionObserver;
151
-
152
97
  beforeEach(() => {
153
98
  vi.clearAllMocks();
154
- mockObservers = [];
155
99
 
156
100
  // Set default mock for getHardcopy to return a resolved promise
157
101
  mockGetHardcopy.mockResolvedValue({
158
102
  language: 'en',
159
103
  values: {},
160
104
  });
161
-
162
- // Store original IntersectionObserver
163
- originalIntersectionObserver = global.IntersectionObserver;
164
-
165
- // Replace IntersectionObserver with mock class
166
- global.IntersectionObserver = class extends MockIntersectionObserver {
167
- constructor(
168
- callback: (entries: IntersectionObserverEntry[]) => void,
169
- options?: IntersectionObserverInit,
170
- ) {
171
- super(callback, options);
172
- // eslint-disable-next-line @typescript-eslint/no-this-alias
173
- mockObserver = this;
174
- mockObservers.push(this);
175
- }
176
- } as unknown as typeof IntersectionObserver;
177
105
  });
178
106
 
179
- afterEach(() => {
180
- // Restore original IntersectionObserver
181
- global.IntersectionObserver = originalIntersectionObserver;
182
- });
183
-
184
- describe('IntersectionObserver setup', () => {
185
- it('should create an IntersectionObserver with default rootMargin', () => {
107
+ describe('Visibility tracking', () => {
108
+ it('should call useTrackComponentVisibleEvent with default event', () => {
186
109
  const WrappedWidget = withBaseWidget(TestWidget);
187
110
  render(
188
111
  <WrappedWidget
@@ -191,14 +114,19 @@ describe('withBaseWidget', () => {
191
114
  />,
192
115
  );
193
116
 
194
- expect(mockObserver).toBeDefined();
195
- expect(mockObserver.rootMargin).toBe('0px');
117
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
118
+ expect.any(Object),
119
+ {
120
+ widget_config_id: 'test-config-1',
121
+ widget_type: WidgetTypeV3.PromptCarouselV3,
122
+ },
123
+ '0px',
124
+ true,
125
+ );
196
126
  });
197
127
 
198
- it('should create an IntersectionObserver with custom rootMargin', () => {
199
- const WrappedWidget = withBaseWidget(TestWidget, {
200
- rootMargin: '100px',
201
- });
128
+ it('should pass custom rootMargin to useTrackComponentVisibleEvent', () => {
129
+ const WrappedWidget = withBaseWidget(TestWidget, { rootMargin: '100px' });
202
130
  render(
203
131
  <WrappedWidget
204
132
  widgetConfigId="test-config-2"
@@ -206,168 +134,42 @@ describe('withBaseWidget', () => {
206
134
  />,
207
135
  );
208
136
 
209
- expect(mockObserver).toBeDefined();
210
- expect(mockObserver.rootMargin).toBe('100px');
211
- });
212
-
213
- it('should observe the widget element', () => {
214
- const WrappedWidget = withBaseWidget(TestWidget);
215
- render(
216
- <WrappedWidget
217
- widgetConfigId="test-config-3"
218
- widgetType={WidgetTypeV3.PromptCarouselV3}
219
- />,
220
- );
221
-
222
- expect(mockObserver.observe).toHaveBeenCalled();
223
- });
224
- });
225
-
226
- describe('Amplitude event tracking', () => {
227
- it('should track default event when widget becomes visible', async () => {
228
- const WrappedWidget = withBaseWidget(TestWidget);
229
- const { container } = render(
230
- <WrappedWidget
231
- widgetConfigId="test-config-4"
232
- widgetType={WidgetTypeV3.PromptCarouselV3}
233
- />,
234
- );
235
-
236
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
237
-
238
- // Trigger intersection with the actual element
239
- mockObserver.triggerIntersection(true, widgetElement as Element);
240
-
241
- await waitFor(() => {
242
- expect(mockTrackEvent).toHaveBeenCalledWith({
243
- eventName: SpiffyMetricsEventName.ChatComponentVisible,
244
- eventProps: {
245
- widget_config_id: 'test-config-4',
246
- widget_type: WidgetTypeV3.PromptCarouselV3,
247
- },
248
- });
249
- });
250
- });
251
-
252
- it('should track custom event when widget becomes visible', async () => {
253
- const WrappedWidget = withBaseWidget(TestWidget, {
254
- visibilityEventName: SpiffyMetricsEventName.SearchComponentVisible,
255
- });
256
- const { container } = render(
257
- <WrappedWidget
258
- widgetConfigId="test-config-5"
259
- widgetType={WidgetTypeV3.ChatPreviewV3}
260
- />,
137
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
138
+ expect.any(Object),
139
+ {
140
+ widget_config_id: 'test-config-2',
141
+ widget_type: WidgetTypeV3.ChatPreviewV3,
142
+ },
143
+ '100px',
144
+ true,
261
145
  );
262
-
263
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
264
-
265
- // Trigger intersection
266
- mockObserver.triggerIntersection(true, widgetElement as Element);
267
-
268
- await waitFor(() => {
269
- expect(mockTrackEvent).toHaveBeenCalledWith({
270
- eventName: SpiffyMetricsEventName.SearchComponentVisible,
271
- eventProps: {
272
- widget_config_id: 'test-config-5',
273
- widget_type: WidgetTypeV3.ChatPreviewV3,
274
- },
275
- });
276
- });
277
146
  });
278
147
 
279
- it('should include custom event props when tracking', async () => {
148
+ it('should include custom event props', () => {
280
149
  const WrappedWidget = withBaseWidget(TestWidget, {
281
150
  visibilityEventProps: {
282
151
  custom_prop: 'custom_value',
283
152
  another_prop: 123,
284
153
  },
285
154
  });
286
- const { container } = render(
155
+ render(
287
156
  <WrappedWidget
288
- widgetConfigId="test-config-6"
157
+ widgetConfigId="test-config-4"
289
158
  widgetType={WidgetTypeV3.SocialProofV3}
290
159
  />,
291
160
  );
292
161
 
293
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
294
-
295
- // Trigger intersection
296
- mockObserver.triggerIntersection(true, widgetElement as Element);
297
-
298
- await waitFor(() => {
299
- expect(mockTrackEvent).toHaveBeenCalledWith({
300
- eventName: SpiffyMetricsEventName.ChatComponentVisible,
301
- eventProps: {
302
- widget_config_id: 'test-config-6',
303
- widget_type: WidgetTypeV3.SocialProofV3,
304
- custom_prop: 'custom_value',
305
- another_prop: 123,
306
- },
307
- });
308
- });
309
- });
310
-
311
- it('should only track event once even if intersection happens multiple times', async () => {
312
- const WrappedWidget = withBaseWidget(TestWidget);
313
- const { container } = render(
314
- <WrappedWidget
315
- widgetConfigId="test-config-7"
316
- widgetType={WidgetTypeV3.PromptCarouselV3}
317
- />,
318
- );
319
-
320
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
321
-
322
- // Trigger intersection multiple times
323
- mockObserver.triggerIntersection(true, widgetElement as Element);
324
- mockObserver.triggerIntersection(false, widgetElement as Element);
325
- mockObserver.triggerIntersection(true, widgetElement as Element);
326
-
327
- await waitFor(() => {
328
- expect(mockTrackEvent).toHaveBeenCalledTimes(1);
329
- });
330
- });
331
-
332
- it('should disconnect observer after tracking event', async () => {
333
- const WrappedWidget = withBaseWidget(TestWidget);
334
- const { container } = render(
335
- <WrappedWidget
336
- widgetConfigId="test-config-8"
337
- widgetType={WidgetTypeV3.PromptCarouselV3}
338
- />,
339
- );
340
-
341
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
342
-
343
- // Trigger intersection
344
- mockObserver.triggerIntersection(true, widgetElement as Element);
345
-
346
- await waitFor(() => {
347
- expect(mockObserver.disconnect).toHaveBeenCalled();
348
- });
349
- });
350
-
351
- it('should not track event if widget is not intersecting', async () => {
352
- const WrappedWidget = withBaseWidget(TestWidget);
353
- const { container } = render(
354
- <WrappedWidget
355
- widgetConfigId="test-config-9"
356
- widgetType={WidgetTypeV3.PromptCarouselV3}
357
- />,
162
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
163
+ expect.any(Object),
164
+ {
165
+ widget_config_id: 'test-config-4',
166
+ widget_type: WidgetTypeV3.SocialProofV3,
167
+ custom_prop: 'custom_value',
168
+ another_prop: 123,
169
+ },
170
+ '0px',
171
+ true,
358
172
  );
359
-
360
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
361
-
362
- // Trigger non-intersection
363
- mockObserver.triggerIntersection(false, widgetElement as Element);
364
-
365
- // Wait a bit to ensure no event is tracked
366
- await new Promise(resolve => {
367
- setTimeout(resolve, 100);
368
- });
369
-
370
- expect(mockTrackEvent).not.toHaveBeenCalled();
371
173
  });
372
174
  });
373
175
 
@@ -553,7 +355,6 @@ describe('withBaseWidget', () => {
553
355
  mockGetHardcopy.mockResolvedValue(mockHardcopy);
554
356
 
555
357
  const WrappedWidget = withBaseWidget(TestWidget, {
556
- visibilityEventName: SpiffyMetricsEventName.SearchComponentVisible,
557
358
  visibilityEventProps: {
558
359
  test_integration: true,
559
360
  },
@@ -573,20 +374,17 @@ describe('withBaseWidget', () => {
573
374
 
574
375
  const widgetElement = screen.getByTestId('test-widget');
575
376
 
576
- // Trigger intersection to track event
577
- mockObserver.triggerIntersection(true, widgetElement);
578
-
579
- // Verify both hardcopy and event tracking worked
580
- await waitFor(() => {
581
- expect(mockTrackEvent).toHaveBeenCalledWith({
582
- eventName: SpiffyMetricsEventName.SearchComponentVisible,
583
- eventProps: {
584
- widget_config_id: 'test-config-integration',
585
- widget_type: WidgetTypeV3.PromptCarouselV3,
586
- test_integration: true,
587
- },
588
- });
589
- });
377
+ expect(widgetElement).toBeInTheDocument();
378
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
379
+ expect.any(Object),
380
+ {
381
+ widget_config_id: 'test-config-integration',
382
+ widget_type: WidgetTypeV3.PromptCarouselV3,
383
+ test_integration: true,
384
+ },
385
+ '0px',
386
+ true,
387
+ );
590
388
 
591
389
  expect(screen.getByTestId('hardcopy-language')).toHaveTextContent('en');
592
390
  expect(screen.getByTestId('hardcopy-values')).toHaveTextContent(
@@ -652,56 +450,24 @@ describe('withBaseWidget', () => {
652
450
  userEvent: expect.any(Object),
653
451
  });
654
452
 
655
- // Verify first widget can trigger intersection and track event
656
- // When multiple widgets are rendered, each creates its own IntersectionObserver
657
- // We need to find the observer that's observing widget-1's element
658
- const widget1Element = screen.getByTestId('widget-1');
659
- const widget2Element = screen.getByTestId('widget-2');
660
-
661
- // Clear any previous calls to isolate this test
662
- mockTrackEvent.mockClear();
663
-
664
- // Find which observer is observing widget-1 by checking which one was called with widget1Element
665
- // In our mock setup, the first observer should be for widget-1, second for widget-2
666
- const widget1Observer = mockObservers[0];
667
- const widget2Observer = mockObservers[1];
668
-
669
- // Verify we have both observers
670
- expect(widget1Observer).toBeDefined();
671
- expect(widget2Observer).toBeDefined();
672
-
673
- // Trigger intersection for widget-1 using its observer
674
- widget1Observer.triggerIntersection(true, widget1Element);
675
-
676
- await waitFor(() => {
677
- // Verify that an event was tracked for widget-1
678
- expect(mockTrackEvent).toHaveBeenCalled();
679
- const { calls } = mockTrackEvent.mock;
680
- // Find any call that matches widget-1's properties
681
- const widget1Call = calls.find(
682
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
683
- (call: any) =>
684
- call[0]?.eventProps?.widget_config_id === 'widget-1' &&
685
- call[0]?.eventProps?.widget_type === WidgetTypeV3.PromptCarouselV3,
686
- );
687
- expect(widget1Call).toBeDefined();
688
- });
689
-
690
- // Verify widget-2 can also track independently
691
- mockTrackEvent.mockClear();
692
- widget2Observer.triggerIntersection(true, widget2Element);
693
-
694
- await waitFor(() => {
695
- expect(mockTrackEvent).toHaveBeenCalled();
696
- const { calls } = mockTrackEvent.mock;
697
- const widget2Call = calls.find(
698
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
699
- (call: any) =>
700
- call[0]?.eventProps?.widget_config_id === 'widget-2' &&
701
- call[0]?.eventProps?.widget_type === WidgetTypeV3.ChatPreviewV3,
702
- );
703
- expect(widget2Call).toBeDefined();
704
- });
453
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
454
+ expect.any(Object),
455
+ {
456
+ widget_config_id: 'widget-1',
457
+ widget_type: WidgetTypeV3.PromptCarouselV3,
458
+ },
459
+ '0px',
460
+ true,
461
+ );
462
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
463
+ expect.any(Object),
464
+ {
465
+ widget_config_id: 'widget-2',
466
+ widget_type: WidgetTypeV3.ChatPreviewV3,
467
+ },
468
+ '0px',
469
+ true,
470
+ );
705
471
  });
706
472
  });
707
473
  });