@envive-ai/react-widgets-v3 0.3.20 → 0.3.22
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.
- package/dist/hocs/withBaseWidget/types.d.cts +5 -6
- package/dist/hocs/withBaseWidget/types.d.ts +5 -6
- package/dist/hocs/withBaseWidget/withBaseWidget.cjs +37 -43
- package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
- package/dist/hocs/withBaseWidget/withBaseWidget.js +39 -45
- package/dist/packages/widgets/dist/SearchResults/SearchResultsWidget.d.ts +2 -2
- package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
- package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
- package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.cjs +0 -9
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.js +2 -11
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +24 -28
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +25 -29
- package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +0 -10
- package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.js +2 -12
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +0 -9
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +2 -11
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +0 -9
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +2 -11
- package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +0 -9
- package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
- package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
- package/dist/widgets/SocialProofWidget/SocialProofWidget.js +2 -11
- package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +0 -9
- package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
- package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
- package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +2 -11
- package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +0 -9
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +2 -11
- package/dist/widgets/dist/SearchResults/SearchResultsWidget.d.cts +2 -2
- package/package.json +1 -1
- package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +68 -302
- package/src/hocs/withBaseWidget/types.ts +6 -6
- package/src/hocs/withBaseWidget/withBaseWidget.tsx +46 -61
- package/src/widgets/ChatPreviewWidget/ChatPreviewWidget.tsx +1 -13
- package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +11 -5
- package/src/widgets/FloatingChatWidget/FloatingChatWidget.tsx +36 -36
- package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +11 -5
- package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +1 -13
- package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +12 -8
- package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +1 -12
- package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +19 -8
- package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +1 -12
- package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +21 -8
- package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +1 -12
- package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +19 -8
- package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +1 -12
- package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +19 -8
- package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +1 -12
- package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +19 -8
- package/src/widgets/__tests__/trackEventCanary.test.ts +1 -1
|
@@ -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
|
-
|
|
180
|
-
|
|
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(
|
|
195
|
-
|
|
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
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
155
|
+
render(
|
|
287
156
|
<WrappedWidget
|
|
288
|
-
widgetConfigId="test-config-
|
|
157
|
+
widgetConfigId="test-config-4"
|
|
289
158
|
widgetType={WidgetTypeV3.SocialProofV3}
|
|
290
159
|
/>,
|
|
291
160
|
);
|
|
292
161
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
|
-
import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
3
2
|
import { HardcopyResponse } from '@envive-ai/react-hooks/contexts/hardcopyContext';
|
|
4
3
|
import {
|
|
5
4
|
OrgUIConfigV3,
|
|
@@ -22,11 +21,6 @@ export interface BaseWidgetProps {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export interface WithBaseWidgetOptions {
|
|
25
|
-
/**
|
|
26
|
-
* The Amplitude event name to track when the widget becomes visible.
|
|
27
|
-
* Defaults to a generic component visible event if not provided.
|
|
28
|
-
*/
|
|
29
|
-
visibilityEventName?: SpiffyMetricsEventName;
|
|
30
24
|
/**
|
|
31
25
|
* Additional properties to include with the visibility event.
|
|
32
26
|
*/
|
|
@@ -36,4 +30,10 @@ export interface WithBaseWidgetOptions {
|
|
|
36
30
|
* Defaults to '0px'.
|
|
37
31
|
*/
|
|
38
32
|
rootMargin?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether to defer loading of the widget until it is visible.
|
|
36
|
+
* Defaults to true
|
|
37
|
+
*/
|
|
38
|
+
deferLoading?: boolean;
|
|
39
39
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useAmplitude,
|
|
6
|
-
} from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
3
|
+
import { useAmplitude } from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
4
|
+
import { useTrackComponentVisibleEvent } from '@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent';
|
|
7
5
|
import { HardcopyResponse, useHardcopy } from '@envive-ai/react-hooks/contexts/hardcopyContext';
|
|
8
6
|
import { usePage } from '@envive-ai/react-hooks/contexts/pageContext';
|
|
9
7
|
import { useWidgetConfig } from '@envive-ai/react-hooks/contexts/widgetConfigContext';
|
|
10
8
|
import { OrgUIConfigV3, WidgetConfigV3 } from '@envive-ai/react-hooks/contexts/typesV3';
|
|
11
9
|
import { useUiConfig } from '@envive-ai/react-hooks/contexts/uiConfigContext';
|
|
12
|
-
|
|
13
10
|
import { BaseWidgetProps, WithBaseWidgetOptions } from './types';
|
|
14
11
|
|
|
15
12
|
export function withBaseWidget<P extends BaseWidgetProps>(
|
|
@@ -19,10 +16,10 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
19
16
|
const WrappedWidget = (props: P) => {
|
|
20
17
|
const { widgetConfigId, widgetType } = props;
|
|
21
18
|
const widgetRef = useRef<HTMLDivElement>(null);
|
|
22
|
-
const
|
|
23
|
-
const { trackEvent, isReady } = useAmplitude();
|
|
19
|
+
const { isReady } = useAmplitude();
|
|
24
20
|
|
|
25
21
|
const [hardcopyContent, setHardcopyContent] = useState<HardcopyResponse | undefined>(undefined);
|
|
22
|
+
const hardcopyFetchedRef = useRef(false);
|
|
26
23
|
const { getHardcopy, isLoading: isHardcopyLoading } = useHardcopy();
|
|
27
24
|
|
|
28
25
|
const { userEvent, isLoading: isPageLoading } = usePage();
|
|
@@ -36,87 +33,75 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
36
33
|
const isLoading =
|
|
37
34
|
isHardcopyLoading || isPageLoading || isWidgetConfigLoading || isUiConfigLoading;
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const visibilityEventProps = {
|
|
37
|
+
widget_config_id: widgetConfigId,
|
|
38
|
+
widget_type: widgetType,
|
|
39
|
+
...options?.visibilityEventProps,
|
|
40
|
+
};
|
|
41
|
+
const { isVisible } = useTrackComponentVisibleEvent(
|
|
42
|
+
widgetRef,
|
|
43
|
+
visibilityEventProps,
|
|
44
|
+
options?.rootMargin || '0px',
|
|
45
|
+
isReady,
|
|
46
|
+
);
|
|
47
|
+
const startLoading: boolean = useMemo(() => {
|
|
48
|
+
if (options?.deferLoading === false) {
|
|
49
|
+
return true;
|
|
42
50
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (entry.isIntersecting && !hasTrackedEvent.current) {
|
|
47
|
-
const eventName =
|
|
48
|
-
options?.visibilityEventName || SpiffyMetricsEventName.ChatComponentVisible;
|
|
49
|
-
|
|
50
|
-
trackEvent({
|
|
51
|
-
eventName,
|
|
52
|
-
eventProps: {
|
|
53
|
-
widget_config_id: widgetConfigId,
|
|
54
|
-
widget_type: widgetType,
|
|
55
|
-
...options?.visibilityEventProps,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
hasTrackedEvent.current = true;
|
|
60
|
-
observer.disconnect();
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
rootMargin: options?.rootMargin || '0px',
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
observer.observe(widgetRef.current);
|
|
69
|
-
|
|
70
|
-
return () => {
|
|
71
|
-
observer.disconnect();
|
|
72
|
-
};
|
|
73
|
-
}, [isReady, widgetConfigId, widgetType, trackEvent]);
|
|
51
|
+
// Defer loading by default until the widget is visible
|
|
52
|
+
return isVisible && !!widgetConfigId && !!widgetType;
|
|
53
|
+
}, [isVisible, widgetConfigId, widgetType]);
|
|
74
54
|
|
|
75
55
|
useEffect(() => {
|
|
76
|
-
if (!
|
|
56
|
+
if (!startLoading || hardcopyFetchedRef.current) {
|
|
77
57
|
return;
|
|
78
58
|
}
|
|
59
|
+
hardcopyFetchedRef.current = true;
|
|
79
60
|
getHardcopy({ widgetType, userEvent })
|
|
80
61
|
.then(setHardcopyContent)
|
|
81
62
|
.catch(() => {
|
|
82
63
|
// Silently handle errors - widget will render without hardcopy content
|
|
83
64
|
});
|
|
84
|
-
}, [
|
|
65
|
+
}, [startLoading, widgetType, getHardcopy, userEvent, widgetConfigId]);
|
|
85
66
|
|
|
86
67
|
useEffect(() => {
|
|
68
|
+
if (!startLoading) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
87
71
|
getUiConfig()
|
|
88
72
|
.then(setUiConfig)
|
|
89
73
|
.catch(() => {
|
|
90
74
|
// Silently handle errors - widget will render without ui config
|
|
91
75
|
});
|
|
92
|
-
}, [getUiConfig, widgetConfigId, widgetType]);
|
|
76
|
+
}, [startLoading, getUiConfig, widgetConfigId, widgetType]);
|
|
93
77
|
|
|
94
78
|
useEffect(() => {
|
|
95
|
-
if (!
|
|
79
|
+
if (!startLoading) {
|
|
96
80
|
return;
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
getWidgetConfig({ widgetConfigId, widgetType })
|
|
100
84
|
.then(setWidgetConfig)
|
|
101
|
-
.catch(
|
|
102
|
-
console.error('Error getting widget config', widgetConfigId, widgetType, e);
|
|
85
|
+
.catch(() => {
|
|
103
86
|
// Silently handle errors - widget will render without widget config
|
|
104
87
|
});
|
|
105
|
-
}, [widgetConfigId, widgetType, getWidgetConfig]);
|
|
88
|
+
}, [startLoading, widgetConfigId, widgetType, getWidgetConfig]);
|
|
106
89
|
|
|
107
90
|
return (
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
91
|
+
<span ref={widgetRef}>
|
|
92
|
+
<Widget
|
|
93
|
+
{...props}
|
|
94
|
+
isLoading={isLoading}
|
|
95
|
+
isHardcopyLoading={isHardcopyLoading}
|
|
96
|
+
isPageLoading={isPageLoading}
|
|
97
|
+
isWidgetConfigLoading={isWidgetConfigLoading}
|
|
98
|
+
isUiConfigLoading={isUiConfigLoading}
|
|
99
|
+
uiConfig={uiConfig}
|
|
100
|
+
widgetConfig={widgetConfig}
|
|
101
|
+
hardcopyContent={hardcopyContent}
|
|
102
|
+
observedWidget={widgetRef}
|
|
103
|
+
/>
|
|
104
|
+
</span>
|
|
120
105
|
);
|
|
121
106
|
};
|
|
122
107
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from '@envive-ai/react-hooks/application/models';
|
|
13
13
|
import {
|
|
14
14
|
EnviveMetricsEventName,
|
|
15
|
-
SpiffyMetricsEventName,
|
|
16
15
|
useAmplitude,
|
|
17
16
|
} from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
18
17
|
import { useWidgetInteraction } from '@envive-ai/react-hooks/hooks/WidgetInteraction';
|
|
@@ -23,7 +22,7 @@ import {
|
|
|
23
22
|
import { ChatPreview } from '@envive-ai/react-toolkit-v3/ChatPreview';
|
|
24
23
|
import { ChatPreviewProps } from '@envive-ai/react-toolkit-v3/ChatPreview/types/types';
|
|
25
24
|
import { usePromptCarouselAnalytics } from '@envive-ai/react-toolkit-v3/PromptCarousel';
|
|
26
|
-
import { useCallback,
|
|
25
|
+
import { useCallback, useMemo } from 'react';
|
|
27
26
|
import { BaseWidgetProps } from '../../hocs/withBaseWidget/types';
|
|
28
27
|
import { withBaseWidget } from '../../hocs/withBaseWidget/withBaseWidget';
|
|
29
28
|
import { ChatPreviewLoadingWidgetWithBaseWidget } from '../ChatPreviewLoadingWidget/ChatPreviewLoadingWidget';
|
|
@@ -67,17 +66,6 @@ const ChatPreviewWidgetHandler = (props: BaseWidgetProps) => {
|
|
|
67
66
|
onTouchEnd,
|
|
68
67
|
} = usePromptCarouselAnalytics(WidgetInteractionComponent.CHAT_PREVIEW, text => text);
|
|
69
68
|
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
// TODO: -Add Widget Interaction here
|
|
72
|
-
trackEvent({
|
|
73
|
-
eventName: SpiffyMetricsEventName.ChatComponentVisible,
|
|
74
|
-
eventProps: {
|
|
75
|
-
widget_config_id: widgetConfigId,
|
|
76
|
-
widget_type: WidgetTypeV3.ChatPreviewV3,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
}, [trackEvent, widgetConfigId]);
|
|
80
|
-
|
|
81
69
|
const logoSrc = uiConfig?.lookAndFeel?.widgetLogoSrc;
|
|
82
70
|
|
|
83
71
|
const hideLogo = uiConfig?.lookAndFeel?.hideWidgetLogo;
|