@envive-ai/react-widgets-v3 0.3.56 → 0.3.57
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/debug/GenericSelect.cjs +4 -1
- package/dist/debug/GenericSelect.js +3 -1
- package/dist/debug/chatEmbed.cjs +1 -1
- package/dist/debug/chatEmbed.js +1 -1
- package/dist/debug/reportIssue.cjs +7 -4
- package/dist/debug/reportIssue.js +6 -4
- package/dist/hocs/withBaseWidget/withBaseWidget.cjs +13 -4
- package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
- package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
- package/dist/hocs/withBaseWidget/withBaseWidget.js +13 -4
- package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.cjs +6 -6
- package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.js +6 -6
- package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
- package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.cjs +4 -4
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
- package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.js +4 -4
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +5 -27
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
- package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +6 -28
- package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.cjs +1 -1
- package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.js +1 -1
- package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
- package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +3 -3
- package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
- package/dist/widgets/ProductCardWidget/ProductCardWidget.js +3 -3
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +4 -4
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
- package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +4 -4
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +4 -4
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
- package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +4 -4
- package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.cts +2 -2
- package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +6 -6
- package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
- package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
- package/dist/widgets/SocialProofWidget/SocialProofWidget.js +6 -6
- package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +2 -2
- 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 -2
- package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
- package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +3 -3
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
- package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +3 -3
- package/dist/widgets/hooks/useGetWidgetStatus.cjs +1 -1
- package/dist/widgets/hooks/useGetWidgetStatus.js +1 -1
- package/dist/widgets/utils/functions.cjs +1 -1
- package/dist/widgets/utils/functions.js +1 -1
- package/package.json +1 -1
- package/src/debug/GenericSelect.tsx +4 -1
- package/src/debug/reportIssue.tsx +6 -3
- package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +159 -2
- package/src/hocs/withBaseWidget/withBaseWidget.tsx +29 -1
- package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +5 -1
- package/src/widgets/FloatingChatWidget/FloatingChatWidget.tsx +19 -42
- package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +1 -1
- package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +6 -1
- package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +5 -1
- package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +3 -1
- package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +1 -1
- package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +5 -1
- package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +1 -1
- package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +5 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BaseWidgetProps } from "../../hocs/withBaseWidget/types.cjs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime6 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts
|
|
5
5
|
declare const TypingAnimationWithBaseWidget: {
|
|
6
|
-
(props: BaseWidgetProps):
|
|
6
|
+
(props: BaseWidgetProps): react_jsx_runtime6.JSX.Element;
|
|
7
7
|
displayName: string;
|
|
8
8
|
};
|
|
9
9
|
interface TypingAnimationWidgetProps {
|
|
@@ -12,7 +12,7 @@ interface TypingAnimationWidgetProps {
|
|
|
12
12
|
declare const TypingAnimationWidget: {
|
|
13
13
|
({
|
|
14
14
|
widgetConfigId
|
|
15
|
-
}: TypingAnimationWidgetProps):
|
|
15
|
+
}: TypingAnimationWidgetProps): react_jsx_runtime6.JSX.Element;
|
|
16
16
|
displayName: string;
|
|
17
17
|
};
|
|
18
18
|
//#endregion
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BaseWidgetProps } from "../../hocs/withBaseWidget/types.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime18 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts
|
|
5
5
|
declare const TypingAnimationWithBaseWidget: {
|
|
6
|
-
(props: BaseWidgetProps):
|
|
6
|
+
(props: BaseWidgetProps): react_jsx_runtime18.JSX.Element;
|
|
7
7
|
displayName: string;
|
|
8
8
|
};
|
|
9
9
|
interface TypingAnimationWidgetProps {
|
|
@@ -12,7 +12,7 @@ interface TypingAnimationWidgetProps {
|
|
|
12
12
|
declare const TypingAnimationWidget: {
|
|
13
13
|
({
|
|
14
14
|
widgetConfigId
|
|
15
|
-
}: TypingAnimationWidgetProps):
|
|
15
|
+
}: TypingAnimationWidgetProps): react_jsx_runtime18.JSX.Element;
|
|
16
16
|
displayName: string;
|
|
17
17
|
};
|
|
18
18
|
//#endregion
|
|
@@ -4,14 +4,14 @@ import { getStringIdForText } from "../utils/functions.js";
|
|
|
4
4
|
import { useCallback } from "react";
|
|
5
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";
|
|
7
8
|
import { WidgetTypeV3 } from "@envive-ai/react-hooks/contexts/typesV3";
|
|
9
|
+
import { useChatToggle } from "@envive-ai/react-hooks/hooks/ChatToggle";
|
|
8
10
|
import { useFeatureFlagService } from "@envive-ai/react-hooks/contexts/featureFlagServiceContext";
|
|
9
11
|
import { ChatElementDisplayLocationV3, FeatureGates } from "@envive-ai/react-hooks/application/models";
|
|
10
|
-
import {
|
|
11
|
-
import { useChatToggle } from "@envive-ai/react-hooks/hooks/ChatToggle";
|
|
12
|
+
import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
|
|
12
13
|
import { WidgetInteractionComponent, WidgetInteractionType } from "@envive-ai/react-hooks/hooks/WidgetInteraction/types";
|
|
13
14
|
import { PromptCarouselRows, usePromptCarouselAnalytics } from "@envive-ai/react-toolkit-v3/PromptCarousel";
|
|
14
|
-
import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
|
|
15
15
|
import { PromptButtonVariant } from "@envive-ai/react-toolkit-v3/PromptButton/types";
|
|
16
16
|
import { TypingAnimation } from "@envive-ai/react-toolkit-v3/TypingAnimation";
|
|
17
17
|
import { WidgetWrapperVariant } from "@envive-ai/react-toolkit-v3/WidgetWrapper";
|
|
@@ -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 jotai = require("jotai");
|
|
5
4
|
let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
|
|
5
|
+
let jotai = require("jotai");
|
|
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 { useAtomValue } from "jotai";
|
|
4
3
|
import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
|
|
4
|
+
import { useAtomValue } from "jotai";
|
|
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_application_models = require("@envive-ai/react-hooks/application/models");
|
|
3
2
|
let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
|
|
3
|
+
let __envive_ai_react_hooks_application_models = require("@envive-ai/react-hooks/application/models");
|
|
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 { MessageRole, MessageType } from "@envive-ai/react-hooks/application/models";
|
|
2
1
|
import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
|
|
2
|
+
import { MessageRole, MessageType } from "@envive-ai/react-hooks/application/models";
|
|
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,4 +1,7 @@
|
|
|
1
1
|
import React, { CSSProperties, useRef, useState } from 'react';
|
|
2
|
+
import Logger from '@envive-ai/react-hooks/application/logging';
|
|
3
|
+
|
|
4
|
+
const logger = new Logger('GenericSelect');
|
|
2
5
|
|
|
3
6
|
type SelectType = 'priority';
|
|
4
7
|
|
|
@@ -164,7 +167,7 @@ export function GenericSelect({
|
|
|
164
167
|
}, [isOpen]);
|
|
165
168
|
|
|
166
169
|
if (!dictionary) {
|
|
167
|
-
|
|
170
|
+
logger.logError(`Invalid type "${type}".`, undefined);
|
|
168
171
|
return null;
|
|
169
172
|
}
|
|
170
173
|
|
|
@@ -16,9 +16,12 @@ import { userIdAtom } from '@envive-ai/react-hooks/atoms/app';
|
|
|
16
16
|
import { orgShortNameAtom } from '@envive-ai/react-hooks/atoms/envive/enviveConfig';
|
|
17
17
|
import { chatAtom, lastAssistantMessageAtom } from '@envive-ai/react-hooks/atoms/chat';
|
|
18
18
|
import { StringUtils, validateEmail } from '@envive-ai/react-hooks/application/utils';
|
|
19
|
+
import Logger from '@envive-ai/react-hooks/application/logging';
|
|
19
20
|
import { GenericSelect } from './GenericSelect';
|
|
20
21
|
import ChatEmbed from './chatEmbed';
|
|
21
22
|
|
|
23
|
+
const logger = new Logger('reportIssue');
|
|
24
|
+
|
|
22
25
|
const messageContent = (m: Message) => {
|
|
23
26
|
if (m.type === MessageType.Text) {
|
|
24
27
|
return m.metadata.content;
|
|
@@ -65,7 +68,7 @@ const ReportHeader = ({ onClose }: { onClose: () => void }) => {
|
|
|
65
68
|
try {
|
|
66
69
|
await navigator.clipboard.writeText(userId);
|
|
67
70
|
} catch (err) {
|
|
68
|
-
|
|
71
|
+
logger.logError('Failed to copy to clipboard', err);
|
|
69
72
|
}
|
|
70
73
|
}, [userId]);
|
|
71
74
|
|
|
@@ -549,11 +552,11 @@ export const ReportIssue = () => {
|
|
|
549
552
|
turn_info: turnInfo,
|
|
550
553
|
report_type: reportType,
|
|
551
554
|
};
|
|
552
|
-
|
|
555
|
+
logger.logInfo('Submitting report', request);
|
|
553
556
|
await CommerceApiClient.reportSession(request);
|
|
554
557
|
onClose();
|
|
555
558
|
} catch (error) {
|
|
556
|
-
|
|
559
|
+
logger.logError('Error sending report', error);
|
|
557
560
|
setErrorMessage(
|
|
558
561
|
'There was an error sending the report. Please try again or contact Envive Support.',
|
|
559
562
|
);
|
|
@@ -9,7 +9,14 @@ import { withBaseWidget } from '../withBaseWidget';
|
|
|
9
9
|
// Mock the contexts
|
|
10
10
|
const mockTrackEvent = vi.fn();
|
|
11
11
|
const mockGetHardcopy = vi.fn();
|
|
12
|
+
const mockGetWidgetConfig = vi.fn();
|
|
13
|
+
const mockGetUiConfig = vi.fn();
|
|
12
14
|
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
15
|
+
// Shared context loading flags, read at render time by the mocked hooks
|
|
16
|
+
let mockHardcopyLoading = false;
|
|
17
|
+
let mockPageLoading = false;
|
|
18
|
+
let mockWidgetConfigLoading = false;
|
|
19
|
+
let mockUiConfigLoading = false;
|
|
13
20
|
|
|
14
21
|
vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
|
|
15
22
|
useAmplitude: () => ({
|
|
@@ -25,6 +32,7 @@ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
|
|
|
25
32
|
vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
|
|
26
33
|
useHardcopy: () => ({
|
|
27
34
|
getHardcopy: mockGetHardcopy,
|
|
35
|
+
isLoading: mockHardcopyLoading,
|
|
28
36
|
}),
|
|
29
37
|
}));
|
|
30
38
|
|
|
@@ -36,18 +44,21 @@ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
|
|
|
36
44
|
created_at: '2025-01-01T00:00:00Z',
|
|
37
45
|
event_id: 'test-event-id',
|
|
38
46
|
} as UserEvent,
|
|
47
|
+
isLoading: mockPageLoading,
|
|
39
48
|
}),
|
|
40
49
|
}));
|
|
41
50
|
|
|
42
51
|
vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
|
|
43
52
|
useWidgetConfig: () => ({
|
|
44
|
-
getWidgetConfig:
|
|
53
|
+
getWidgetConfig: mockGetWidgetConfig,
|
|
54
|
+
isLoading: mockWidgetConfigLoading,
|
|
45
55
|
}),
|
|
46
56
|
}));
|
|
47
57
|
|
|
48
58
|
vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
|
|
49
59
|
useUiConfig: () => ({
|
|
50
|
-
getUiConfig:
|
|
60
|
+
getUiConfig: mockGetUiConfig,
|
|
61
|
+
isLoading: mockUiConfigLoading,
|
|
51
62
|
}),
|
|
52
63
|
}));
|
|
53
64
|
|
|
@@ -66,6 +77,7 @@ const TestWidget: React.FC<TestWidgetProps> = ({
|
|
|
66
77
|
widgetType,
|
|
67
78
|
hardcopyContent,
|
|
68
79
|
observedWidget,
|
|
80
|
+
isLoading,
|
|
69
81
|
testId = 'test-widget',
|
|
70
82
|
children,
|
|
71
83
|
}) => {
|
|
@@ -81,6 +93,7 @@ const TestWidget: React.FC<TestWidgetProps> = ({
|
|
|
81
93
|
>
|
|
82
94
|
<div data-testid="widget-config-id">{widgetConfigId}</div>
|
|
83
95
|
<div data-testid="widget-type">{widgetType}</div>
|
|
96
|
+
<div data-testid="is-loading">{String(isLoading)}</div>
|
|
84
97
|
{hardcopyContent && (
|
|
85
98
|
<div data-testid="hardcopy-content">
|
|
86
99
|
<div data-testid="hardcopy-language">{hardcopyContent.language}</div>
|
|
@@ -97,11 +110,19 @@ describe('withBaseWidget', () => {
|
|
|
97
110
|
beforeEach(() => {
|
|
98
111
|
vi.clearAllMocks();
|
|
99
112
|
|
|
113
|
+
mockHardcopyLoading = false;
|
|
114
|
+
mockPageLoading = false;
|
|
115
|
+
mockWidgetConfigLoading = false;
|
|
116
|
+
mockUiConfigLoading = false;
|
|
117
|
+
mockUseTrackComponentVisibleEvent.mockReturnValue({ isVisible: true });
|
|
118
|
+
|
|
100
119
|
// Set default mock for getHardcopy to return a resolved promise
|
|
101
120
|
mockGetHardcopy.mockResolvedValue({
|
|
102
121
|
language: 'en',
|
|
103
122
|
values: {},
|
|
104
123
|
});
|
|
124
|
+
mockGetWidgetConfig.mockResolvedValue({});
|
|
125
|
+
mockGetUiConfig.mockResolvedValue({});
|
|
105
126
|
});
|
|
106
127
|
|
|
107
128
|
describe('Visibility tracking', () => {
|
|
@@ -342,6 +363,142 @@ describe('withBaseWidget', () => {
|
|
|
342
363
|
});
|
|
343
364
|
});
|
|
344
365
|
|
|
366
|
+
describe('Loading state', () => {
|
|
367
|
+
it('stops loading once its own requests settle, even while the shared flags stay set', async () => {
|
|
368
|
+
// All four shared flags stay true for the whole test, so the only thing that can
|
|
369
|
+
// release the loading state is the widget's own requests settling.
|
|
370
|
+
mockHardcopyLoading = true;
|
|
371
|
+
mockPageLoading = true;
|
|
372
|
+
mockWidgetConfigLoading = true;
|
|
373
|
+
mockUiConfigLoading = true;
|
|
374
|
+
let resolveHardcopy!: (value: HardcopyResponse) => void;
|
|
375
|
+
mockGetHardcopy.mockReturnValue(
|
|
376
|
+
new Promise<HardcopyResponse>(resolve => {
|
|
377
|
+
resolveHardcopy = resolve;
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const WrappedWidget = withBaseWidget(TestWidget);
|
|
382
|
+
render(
|
|
383
|
+
<WrappedWidget
|
|
384
|
+
widgetConfigId="test-config-loading"
|
|
385
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
386
|
+
/>,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('true');
|
|
390
|
+
|
|
391
|
+
resolveHardcopy({ responseId: 'r', language: 'en', values: {} });
|
|
392
|
+
|
|
393
|
+
await waitFor(() => {
|
|
394
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it.each([
|
|
399
|
+
[
|
|
400
|
+
'widgetConfig',
|
|
401
|
+
() => mockGetWidgetConfig,
|
|
402
|
+
() => {
|
|
403
|
+
mockWidgetConfigLoading = true;
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
[
|
|
407
|
+
'uiConfig',
|
|
408
|
+
() => mockGetUiConfig,
|
|
409
|
+
() => {
|
|
410
|
+
mockUiConfigLoading = true;
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
])(
|
|
414
|
+
'shows loading while the %s request is pending and its shared flag is set',
|
|
415
|
+
async (_name, getGetter, setFlag) => {
|
|
416
|
+
setFlag();
|
|
417
|
+
let resolveConfig!: (value: object) => void;
|
|
418
|
+
getGetter().mockReturnValue(
|
|
419
|
+
new Promise<object>(resolve => {
|
|
420
|
+
resolveConfig = resolve;
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const WrappedWidget = withBaseWidget(TestWidget);
|
|
425
|
+
render(
|
|
426
|
+
<WrappedWidget
|
|
427
|
+
widgetConfigId="test-config-branch"
|
|
428
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
429
|
+
/>,
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('true');
|
|
433
|
+
|
|
434
|
+
resolveConfig({});
|
|
435
|
+
|
|
436
|
+
await waitFor(() => {
|
|
437
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
it('does not regress to loading when a shared flag flips after its requests settled', async () => {
|
|
443
|
+
const WrappedWidget = withBaseWidget(TestWidget);
|
|
444
|
+
const { rerender } = render(
|
|
445
|
+
<WrappedWidget
|
|
446
|
+
widgetConfigId="test-config-no-regress"
|
|
447
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
448
|
+
/>,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
await waitFor(() => {
|
|
452
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
453
|
+
expect(screen.getByTestId('hardcopy-content')).toBeInTheDocument();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Another widget elsewhere starts a fetch, flipping the shared context flags
|
|
457
|
+
mockHardcopyLoading = true;
|
|
458
|
+
mockPageLoading = true;
|
|
459
|
+
mockWidgetConfigLoading = true;
|
|
460
|
+
mockUiConfigLoading = true;
|
|
461
|
+
rerender(
|
|
462
|
+
<WrappedWidget
|
|
463
|
+
widgetConfigId="test-config-no-regress"
|
|
464
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
465
|
+
/>,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
469
|
+
expect(screen.getByTestId('hardcopy-content')).toBeInTheDocument();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('still settles and stops loading when its own requests fail', async () => {
|
|
473
|
+
mockHardcopyLoading = true;
|
|
474
|
+
mockGetHardcopy.mockRejectedValue(new Error('Failed to fetch hardcopy'));
|
|
475
|
+
|
|
476
|
+
const WrappedWidget = withBaseWidget(TestWidget);
|
|
477
|
+
const { rerender } = render(
|
|
478
|
+
<WrappedWidget
|
|
479
|
+
widgetConfigId="test-config-failed"
|
|
480
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
481
|
+
/>,
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
mockHardcopyLoading = false;
|
|
485
|
+
await waitFor(() => {
|
|
486
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// A later shared fetch still must not knock the settled widget back to loading
|
|
490
|
+
mockPageLoading = true;
|
|
491
|
+
rerender(
|
|
492
|
+
<WrappedWidget
|
|
493
|
+
widgetConfigId="test-config-failed"
|
|
494
|
+
widgetType={WidgetTypeV3.PromptCarouselV3}
|
|
495
|
+
/>,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
345
502
|
describe('Integration tests', () => {
|
|
346
503
|
it('should handle full flow: visibility tracking + hardcopy rendering', async () => {
|
|
347
504
|
const mockHardcopy: HardcopyResponse = {
|
|
@@ -30,8 +30,23 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
30
30
|
const [uiConfig, setUiConfig] = useState<OrgUIConfigV3 | undefined>(undefined);
|
|
31
31
|
const { getUiConfig, isLoading: isUiConfigLoading } = useUiConfig();
|
|
32
32
|
|
|
33
|
+
// The context isLoading flags are provider-level and shared across every widget, so a
|
|
34
|
+
// fetch issued by any widget flips them for all of them. Track when this widget's own
|
|
35
|
+
// requests have settled so a later fetch elsewhere can't knock an already-rendered
|
|
36
|
+
// widget back into its loading state.
|
|
37
|
+
//
|
|
38
|
+
// These latches (and hardcopyFetchedRef above) assume widgetConfigId is stable for the
|
|
39
|
+
// lifetime of the instance — the mounter must key widgets by widgetConfigId so a config
|
|
40
|
+
// swap on the same mount point remounts rather than reusing this state (see Widgets.tsx
|
|
41
|
+
// in injection-bundle).
|
|
42
|
+
const [hardcopySettled, setHardcopySettled] = useState(false);
|
|
43
|
+
const [widgetConfigSettled, setWidgetConfigSettled] = useState(false);
|
|
44
|
+
const [uiConfigSettled, setUiConfigSettled] = useState(false);
|
|
45
|
+
const ownRequestsSettled = hardcopySettled && widgetConfigSettled && uiConfigSettled;
|
|
46
|
+
|
|
33
47
|
const isLoading =
|
|
34
|
-
|
|
48
|
+
!ownRequestsSettled &&
|
|
49
|
+
(isHardcopyLoading || isPageLoading || isWidgetConfigLoading || isUiConfigLoading);
|
|
35
50
|
|
|
36
51
|
const visibilityEventProps = {
|
|
37
52
|
widget_config_id: widgetConfigId,
|
|
@@ -56,11 +71,18 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
56
71
|
if (!startLoading || hardcopyFetchedRef.current) {
|
|
57
72
|
return;
|
|
58
73
|
}
|
|
74
|
+
// Hardcopy is fetch-once by design: it is a network request whose deps (userEvent)
|
|
75
|
+
// can churn as the page context updates, and the served copy must not change under
|
|
76
|
+
// the shopper. The uiConfig/widgetConfig effects below need no such guard — they
|
|
77
|
+
// resolve locally from the already-loaded org config, so re-runs are free.
|
|
59
78
|
hardcopyFetchedRef.current = true;
|
|
60
79
|
getHardcopy({ widgetType, userEvent })
|
|
61
80
|
.then(setHardcopyContent)
|
|
62
81
|
.catch(() => {
|
|
63
82
|
// Silently handle errors - widget will render without hardcopy content
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
setHardcopySettled(true);
|
|
64
86
|
});
|
|
65
87
|
}, [startLoading, widgetType, getHardcopy, userEvent, widgetConfigId]);
|
|
66
88
|
|
|
@@ -72,6 +94,9 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
72
94
|
.then(setUiConfig)
|
|
73
95
|
.catch(() => {
|
|
74
96
|
// Silently handle errors - widget will render without ui config
|
|
97
|
+
})
|
|
98
|
+
.finally(() => {
|
|
99
|
+
setUiConfigSettled(true);
|
|
75
100
|
});
|
|
76
101
|
}, [startLoading, getUiConfig, widgetConfigId, widgetType]);
|
|
77
102
|
|
|
@@ -84,6 +109,9 @@ export function withBaseWidget<P extends BaseWidgetProps>(
|
|
|
84
109
|
.then(setWidgetConfig)
|
|
85
110
|
.catch(() => {
|
|
86
111
|
// Silently handle errors - widget will render without widget config
|
|
112
|
+
})
|
|
113
|
+
.finally(() => {
|
|
114
|
+
setWidgetConfigSettled(true);
|
|
87
115
|
});
|
|
88
116
|
}, [startLoading, widgetConfigId, widgetType, getWidgetConfig]);
|
|
89
117
|
|
|
@@ -5,7 +5,7 @@ import { ChatPreviewWidget } from '../ChatPreviewWidget';
|
|
|
5
5
|
|
|
6
6
|
const mockTrackEvent = vi.fn();
|
|
7
7
|
const mockGetHardcopy = vi.fn();
|
|
8
|
-
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
8
|
+
const mockUseTrackComponentVisibleEvent = vi.fn(() => ({ isVisible: true }));
|
|
9
9
|
|
|
10
10
|
vi.mock('@envive-ai/react-toolkit-v3/ChatPreview', () => ({
|
|
11
11
|
ChatPreview: () => <div data-testid="chat-preview-mock">Chat Preview</div>,
|
|
@@ -26,6 +26,10 @@ vi.mock('@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent', () => ({
|
|
|
26
26
|
useTrackComponentVisibleEvent: (...args: unknown[]) => mockUseTrackComponentVisibleEvent(...args),
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
|
+
vi.mock('@envive-ai/react-hooks/contexts/featureFlagServiceContext', () => ({
|
|
30
|
+
useFeatureFlagService: () => ({ featureFlagService: undefined }),
|
|
31
|
+
}));
|
|
32
|
+
|
|
29
33
|
vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
|
|
30
34
|
useHardcopy: () => ({
|
|
31
35
|
getHardcopy: mockGetHardcopy,
|
|
@@ -2,10 +2,7 @@ import {
|
|
|
2
2
|
ChatElementDisplayLocationV3,
|
|
3
3
|
FeatureGates,
|
|
4
4
|
} from '@envive-ai/react-hooks/application/models';
|
|
5
|
-
import {
|
|
6
|
-
EnviveMetricsEventName,
|
|
7
|
-
useAmplitude,
|
|
8
|
-
} from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
5
|
+
import { useAmplitude } from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
9
6
|
import { useSalesAgent } from '@envive-ai/react-hooks/contexts/salesAgentContext';
|
|
10
7
|
import {
|
|
11
8
|
FloatingChatConfig,
|
|
@@ -14,13 +11,7 @@ import {
|
|
|
14
11
|
} from '@envive-ai/react-hooks/contexts/typesV3';
|
|
15
12
|
import { useChatToggle } from '@envive-ai/react-hooks/hooks/ChatToggle';
|
|
16
13
|
import { useTrackComponentVisibleEvent } from '@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent';
|
|
17
|
-
import { useWidgetInteraction } from '@envive-ai/react-hooks/hooks/WidgetInteraction';
|
|
18
14
|
import { useFeatureFlagService } from '@envive-ai/react-hooks/contexts/featureFlagServiceContext';
|
|
19
|
-
import {
|
|
20
|
-
Collapse,
|
|
21
|
-
WidgetInteractionComponent,
|
|
22
|
-
WidgetInteractionType,
|
|
23
|
-
} from '@envive-ai/react-hooks/hooks/WidgetInteraction/types';
|
|
24
15
|
import {
|
|
25
16
|
FloatingButton,
|
|
26
17
|
FloatingButtonBackgroundColor,
|
|
@@ -88,7 +79,6 @@ const FloatingChatWidgetHandler = (props: FloatingChatWidgetHandlerProps) => {
|
|
|
88
79
|
const { previewButtonOnly, previewChatAlwaysOpen, previewCXButtonsEnabled, previewDisclaimer } =
|
|
89
80
|
props;
|
|
90
81
|
const salesAgentData = useSalesAgent();
|
|
91
|
-
const { trackWidgetInteraction } = useWidgetInteraction();
|
|
92
82
|
const { userHasInteractedValue } = useGetWidgetStatus();
|
|
93
83
|
|
|
94
84
|
// TODO: Get hardcopy
|
|
@@ -124,19 +114,6 @@ const FloatingChatWidgetHandler = (props: FloatingChatWidgetHandlerProps) => {
|
|
|
124
114
|
if (isOpen) setIsCXOpen(false);
|
|
125
115
|
}, [isOpen]);
|
|
126
116
|
|
|
127
|
-
const handleClose = (type: Collapse) => {
|
|
128
|
-
trackWidgetInteraction({
|
|
129
|
-
eventName: EnviveMetricsEventName.WidgetInteraction,
|
|
130
|
-
trigger: {
|
|
131
|
-
widget: WidgetInteractionComponent.FLOATING_CHAT,
|
|
132
|
-
widget_interaction: WidgetInteractionType.WIDGET_COLLAPSED,
|
|
133
|
-
widget_interaction_data: {
|
|
134
|
-
widget_collapsed: type,
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
};
|
|
139
|
-
|
|
140
117
|
const { shouldShowFloatingButton } = useFloatingButtonVisibility({
|
|
141
118
|
floatingButtonShowConfig: floatingButton?.showOption as FloatingButtonShow,
|
|
142
119
|
isChatOpen: isOpen,
|
|
@@ -191,12 +168,12 @@ const FloatingChatWidgetHandler = (props: FloatingChatWidgetHandlerProps) => {
|
|
|
191
168
|
onClose={
|
|
192
169
|
previewChatAlwaysOpen
|
|
193
170
|
? () => {}
|
|
194
|
-
: () =>
|
|
195
|
-
closeChat(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
171
|
+
: () =>
|
|
172
|
+
closeChat(
|
|
173
|
+
ChatElementDisplayLocationV3.FLOATING_CHAT_OVERLAY,
|
|
174
|
+
undefined,
|
|
175
|
+
'body_click',
|
|
176
|
+
)
|
|
200
177
|
}
|
|
201
178
|
previewMode={!!previewChatAlwaysOpen}
|
|
202
179
|
>
|
|
@@ -215,22 +192,22 @@ const FloatingChatWidgetHandler = (props: FloatingChatWidgetHandlerProps) => {
|
|
|
215
192
|
onSwipeClose={
|
|
216
193
|
previewChatAlwaysOpen
|
|
217
194
|
? () => {}
|
|
218
|
-
: () =>
|
|
219
|
-
closeChat(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
195
|
+
: () =>
|
|
196
|
+
closeChat(
|
|
197
|
+
ChatElementDisplayLocationV3.FLOATING_CHAT_CLOSE_BUTTON,
|
|
198
|
+
undefined,
|
|
199
|
+
'swipe',
|
|
200
|
+
)
|
|
224
201
|
}
|
|
225
202
|
onClose={
|
|
226
203
|
previewChatAlwaysOpen
|
|
227
204
|
? () => {}
|
|
228
|
-
: () =>
|
|
229
|
-
closeChat(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
205
|
+
: () =>
|
|
206
|
+
closeChat(
|
|
207
|
+
ChatElementDisplayLocationV3.FLOATING_CHAT_CLOSE_BUTTON,
|
|
208
|
+
undefined,
|
|
209
|
+
'close_button',
|
|
210
|
+
)
|
|
234
211
|
}
|
|
235
212
|
/>
|
|
236
213
|
</Suspense>
|
|
@@ -5,7 +5,7 @@ import { FloatingChatWidget } from '../FloatingChatWidget';
|
|
|
5
5
|
|
|
6
6
|
const mockTrackEvent = vi.fn();
|
|
7
7
|
const mockGetHardcopy = vi.fn();
|
|
8
|
-
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
8
|
+
const mockUseTrackComponentVisibleEvent = vi.fn(() => ({ isVisible: true }));
|
|
9
9
|
|
|
10
10
|
vi.mock('@envive-ai/react-toolkit-v3/FloatingButton', () => ({
|
|
11
11
|
FloatingButton: () => <div data-testid="floating-button">Floating Button</div>,
|
|
@@ -6,9 +6,10 @@ import { ProductCardWidget } from '../ProductCardWidget';
|
|
|
6
6
|
|
|
7
7
|
const mockTrackEvent = vi.fn();
|
|
8
8
|
const mockGetHardcopy = vi.fn();
|
|
9
|
-
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
9
|
+
const mockUseTrackComponentVisibleEvent = vi.fn(() => ({ isVisible: true }));
|
|
10
10
|
|
|
11
11
|
vi.mock('@envive-ai/react-toolkit-v3/ProductCard', () => ({
|
|
12
|
+
ProductCardVariant: { DARK: 'dark', LIGHT: 'light' },
|
|
12
13
|
ProductCard: ({ prompts, onSelect }: { prompts: string[]; onSelect: (text: string) => void }) => (
|
|
13
14
|
<div data-testid="product-card-mock">
|
|
14
15
|
{prompts.map(text => (
|
|
@@ -42,6 +43,10 @@ vi.mock('@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent', () => ({
|
|
|
42
43
|
useTrackComponentVisibleEvent: (...args: unknown[]) => mockUseTrackComponentVisibleEvent(...args),
|
|
43
44
|
}));
|
|
44
45
|
|
|
46
|
+
vi.mock('@envive-ai/react-hooks/contexts/featureFlagServiceContext', () => ({
|
|
47
|
+
useFeatureFlagService: () => ({ featureFlagService: undefined }),
|
|
48
|
+
}));
|
|
49
|
+
|
|
45
50
|
vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
|
|
46
51
|
useHardcopy: () => ({
|
|
47
52
|
getHardcopy: mockGetHardcopy,
|
|
@@ -6,7 +6,7 @@ import { PromptButtonCarouselWithImageWidget } from '../PromptButtonCarouselWith
|
|
|
6
6
|
|
|
7
7
|
const mockTrackEvent = vi.fn();
|
|
8
8
|
const mockGetHardcopy = vi.fn();
|
|
9
|
-
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
9
|
+
const mockUseTrackComponentVisibleEvent = vi.fn(() => ({ isVisible: true }));
|
|
10
10
|
|
|
11
11
|
vi.mock('@envive-ai/react-toolkit-v3/PromptButtonCarouselWithImage', () => ({
|
|
12
12
|
PromptButtonCarouselWithImage: ({
|
|
@@ -48,6 +48,10 @@ vi.mock('@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent', () => ({
|
|
|
48
48
|
useTrackComponentVisibleEvent: (...args: unknown[]) => mockUseTrackComponentVisibleEvent(...args),
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
|
+
vi.mock('@envive-ai/react-hooks/contexts/featureFlagServiceContext', () => ({
|
|
52
|
+
useFeatureFlagService: () => ({ featureFlagService: undefined }),
|
|
53
|
+
}));
|
|
54
|
+
|
|
51
55
|
vi.mock('@envive-ai/react-hooks/hooks/WidgetInteraction', () => ({
|
|
52
56
|
useWidgetInteraction: () => ({
|
|
53
57
|
trackWidgetInteraction: vi.fn(),
|
|
@@ -74,7 +74,9 @@ const PromptCarouselWidgetHandler = (props: BaseWidgetProps) => {
|
|
|
74
74
|
(text: string) => {
|
|
75
75
|
const rawValues = (hardcopyContent as { rawValues?: RawValues } | undefined)?.rawValues;
|
|
76
76
|
const stringId = getStringIdForText(rawValues, text);
|
|
77
|
-
onSuggestionClick
|
|
77
|
+
// Pass the raw text — onSuggestionClick runs its own idExtractor. Passing stringId here
|
|
78
|
+
// double-extracted it, the lookup missed, and suggestion_id landed empty on the event.
|
|
79
|
+
onSuggestionClick(text);
|
|
78
80
|
trackEvent({
|
|
79
81
|
eventName: EnviveMetricsEventName.WidgetTextClicked,
|
|
80
82
|
eventProps: {
|
|
@@ -6,7 +6,7 @@ import { PromptCarouselWidget } from '../PromptCarouselWidget';
|
|
|
6
6
|
|
|
7
7
|
const mockTrackEvent = vi.fn();
|
|
8
8
|
const mockGetHardcopy = vi.fn();
|
|
9
|
-
const mockUseTrackComponentVisibleEvent = vi.fn();
|
|
9
|
+
const mockUseTrackComponentVisibleEvent = vi.fn(() => ({ isVisible: true }));
|
|
10
10
|
|
|
11
11
|
vi.mock('@envive-ai/react-toolkit-v3/PromptCarousel', () => ({
|
|
12
12
|
PromptCarousel: ({
|