@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-native.eject.json +16 -1
- package/dist/app/agentkit/react/components.d.ts +5 -3
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +103 -42
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +93 -65
- package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
- package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
- package/dist/app/agentkit/react/voice-transcripts.js +209 -8
- package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +55 -40
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/index.js +1 -1
- package/dist/app/chat/agentkit-chat/index.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +5 -1
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.js +2 -0
- package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
- package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
- package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
- package/dist/app/chat/reply-connection-suggestion.js +42 -0
- package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +7 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +7 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +7 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +7 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +7 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +7 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +7 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +7 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +7 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +7 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +7 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
- package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
- package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
- package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.js +1 -1
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +14 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
- package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.js +59 -36
- package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.js +14 -1
- package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
- package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
- package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
- package/dist/app/settings/account/search-entries.d.ts +1 -0
- package/dist/app/settings/account/search-entries.d.ts.map +1 -1
- package/dist/app/settings/account/search-entries.js +5 -0
- package/dist/app/settings/account/search-entries.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +26 -2
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
- package/dist/app/shared/AppShellSkeleton.js +4 -10
- package/dist/app/shared/AppShellSkeleton.js.map +1 -1
- package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
- package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
- package/dist/app/shared/EnvironmentBadge.js +2 -3
- package/dist/app/shared/EnvironmentBadge.js.map +1 -1
- package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
- package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
- package/dist/app/shared/is-builder-io-employee.js +10 -0
- package/dist/app/shared/is-builder-io-employee.js.map +1 -0
- package/dist/app/styles/agent-conversation.css +30 -17
- package/dist/app/styles/theme-families.css +22 -5
- package/dist/app/styles/tokens/agent-kit.css +3 -3
- package/dist/composer/PromptComposer.d.ts +15 -3
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +74 -16
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +9 -2
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +12 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +5 -4
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/index.d.ts +2 -2
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/prompt-attachments.d.ts +20 -0
- package/dist/composer/prompt-attachments.d.ts.map +1 -1
- package/dist/composer/prompt-attachments.js +188 -11
- package/dist/composer/prompt-attachments.js.map +1 -1
- package/dist/composer/realtime-voice-transcript.d.ts +27 -0
- package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
- package/dist/composer/realtime-voice-transcript.js +34 -1
- package/dist/composer/realtime-voice-transcript.js.map +1 -1
- package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
- package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
- package/dist/composer/realtime-voice-turn-metrics.js +132 -0
- package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
- package/dist/composer/runtime-adapters.d.ts +4 -1
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js +2 -1
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +536 -99
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/voice-chat-icon.d.ts +7 -0
- package/dist/composer/voice-chat-icon.d.ts.map +1 -0
- package/dist/composer/voice-chat-icon.js +14 -0
- package/dist/composer/voice-chat-icon.js.map +1 -0
- package/dist/editor/DragHandle.d.ts +12 -1
- package/dist/editor/DragHandle.d.ts.map +1 -1
- package/dist/editor/DragHandle.js +165 -41
- package/dist/editor/DragHandle.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts +20 -0
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +58 -13
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/ui/dropdown-menu.js +5 -1
- package/dist/ui/dropdown-menu.js.map +1 -1
- package/package.json +14 -4
- package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
- package/src/app/agentkit/react/components.spec.tsx +24 -4
- package/src/app/agentkit/react/components.tsx +174 -44
- package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
- package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
- package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
- package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
- package/src/app/agentkit/react/styles.css +93 -65
- package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
- package/src/app/agentkit/react/voice-transcripts.ts +245 -8
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
- package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
- package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
- package/src/app/chat/agentkit-chat/index.ts +1 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
- package/src/app/chat/chat/run-recovery.tsx +6 -1
- package/src/app/chat/composer/runtime-adapters.tsx +2 -0
- package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
- package/src/app/chat/reply-connection-suggestion.ts +76 -0
- package/src/app/i18n/catalogs/ar-SA.ts +10 -0
- package/src/app/i18n/catalogs/de-DE.ts +10 -0
- package/src/app/i18n/catalogs/en-US.ts +10 -0
- package/src/app/i18n/catalogs/es-ES.ts +10 -0
- package/src/app/i18n/catalogs/fr-FR.ts +10 -0
- package/src/app/i18n/catalogs/hi-IN.ts +10 -0
- package/src/app/i18n/catalogs/ja-JP.ts +10 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +10 -0
- package/src/app/i18n/catalogs/zh-CN.ts +10 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
- package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
- package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
- package/src/app/onboarding/GettingStartedCard.tsx +1 -1
- package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
- package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
- package/src/app/providers/AppProviders.spec.tsx +109 -0
- package/src/app/providers/AppProviders.tsx +53 -1
- package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
- package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
- package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
- package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
- package/src/app/settings/account/PreferencesSettings.tsx +24 -7
- package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
- package/src/app/settings/account/search-entries.spec.ts +14 -0
- package/src/app/settings/account/search-entries.ts +9 -0
- package/src/app/settings/shell/SettingsShell.tsx +29 -2
- package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
- package/src/app/shared/AppShellSkeleton.tsx +3 -16
- package/src/app/shared/EnvironmentBadge.tsx +3 -4
- package/src/app/shared/is-builder-io-employee.ts +17 -0
- package/src/app/styles/agent-conversation.css +30 -17
- package/src/app/styles/theme-families.css +22 -5
- package/src/app/styles/theme-families.spec.ts +73 -0
- package/src/app/styles/tokens/agent-kit.css +3 -3
- package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
- package/src/composer/AttachmentChip.spec.tsx +87 -0
- package/src/composer/PromptComposer.context.spec.tsx +8 -4
- package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
- package/src/composer/PromptComposer.tsx +125 -14
- package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
- package/src/composer/RealtimeVoiceMode.tsx +45 -2
- package/src/composer/TiptapComposer.tsx +25 -0
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
- package/src/composer/VoiceButton.tsx +8 -3
- package/src/composer/index.ts +4 -0
- package/src/composer/prompt-attachments.spec.ts +160 -8
- package/src/composer/prompt-attachments.ts +221 -12
- package/src/composer/realtime-voice-transcript.spec.ts +36 -0
- package/src/composer/realtime-voice-transcript.ts +56 -1
- package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
- package/src/composer/realtime-voice-turn-metrics.ts +192 -0
- package/src/composer/runtime-adapters.tsx +5 -1
- package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
- package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
- package/src/composer/useRealtimeVoiceMode.tsx +716 -139
- package/src/composer/voice-chat-icon.tsx +19 -0
- package/src/editor/DragHandle.spec.ts +217 -0
- package/src/editor/DragHandle.ts +214 -55
- package/src/editor/RegistryBlockNode.spec.ts +172 -2
- package/src/editor/RegistryBlockNode.tsx +91 -13
- package/src/editor/index.ts +4 -0
- package/src/styles.css +2 -0
- package/src/ui/dropdown-menu.tsx +4 -0
|
@@ -41,6 +41,8 @@ function FirstRunOnboarding({
|
|
|
41
41
|
|
|
42
42
|
const mocks = vi.hoisted(() => ({
|
|
43
43
|
completeFirstRun: vi.fn(),
|
|
44
|
+
recordCreditsV2Exposure: vi.fn(),
|
|
45
|
+
useActionMutation: vi.fn(),
|
|
44
46
|
useBuilderConnectFlow: vi.fn(),
|
|
45
47
|
routePathname: "/",
|
|
46
48
|
useActualRouter: false,
|
|
@@ -52,6 +54,11 @@ const mocks = vi.hoisted(() => ({
|
|
|
52
54
|
useOnboardingPreviewStep: vi.fn(),
|
|
53
55
|
setOnboardingExperiment: vi.fn(),
|
|
54
56
|
setupVariant: "control" as "control" | "credits" | null,
|
|
57
|
+
creditsV2FlagState: { status: "ready", enabled: false } as {
|
|
58
|
+
status: "ready" | "loading" | "unavailable";
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
},
|
|
61
|
+
recordCreditsV2Exposure: vi.fn(),
|
|
55
62
|
}));
|
|
56
63
|
|
|
57
64
|
vi.mock("react-router", async (importOriginal) => {
|
|
@@ -69,7 +76,23 @@ vi.mock("react-router", async (importOriginal) => {
|
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
vi.mock("@agent-native/core/client/feature-flags/use-feature-flag", () => ({
|
|
72
|
-
useFeatureFlagState: () =>
|
|
79
|
+
useFeatureFlagState: (key: string) =>
|
|
80
|
+
key === "clips-first-run-credits-v2"
|
|
81
|
+
? mocks.creditsV2FlagState
|
|
82
|
+
: { status: "ready", enabled: true },
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
vi.mock("@agent-native/core/client/use-action", () => ({
|
|
86
|
+
useActionMutation: () => ({
|
|
87
|
+
mutateAsync: (...args: unknown[]) => mocks.recordCreditsV2Exposure(...args),
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
vi.mock("@agent-native/core/client/use-action", async (importOriginal) => ({
|
|
92
|
+
...(await importOriginal<
|
|
93
|
+
typeof import("@agent-native/core/client/use-action")
|
|
94
|
+
>()),
|
|
95
|
+
useActionMutation: mocks.useActionMutation,
|
|
73
96
|
}));
|
|
74
97
|
|
|
75
98
|
vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
|
|
@@ -87,10 +110,6 @@ vi.mock("@agent-native/core/client/onboarding/use-preview-mode", () => ({
|
|
|
87
110
|
useOnboardingPreviewStep: mocks.useOnboardingPreviewStep,
|
|
88
111
|
}));
|
|
89
112
|
|
|
90
|
-
vi.mock("@agent-native/core/client/feature-flags/use-feature-flag", () => ({
|
|
91
|
-
useFeatureFlagState: () => ({ status: "ready", enabled: true }),
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
113
|
vi.mock("./first-run-experiment.js", async (importOriginal) => ({
|
|
95
114
|
...(await importOriginal<typeof import("./first-run-experiment.js")>()),
|
|
96
115
|
assignFirstRunSetupVariant: () => mocks.setupVariant,
|
|
@@ -107,6 +126,12 @@ describe("FirstRunOnboarding", () => {
|
|
|
107
126
|
beforeEach(() => {
|
|
108
127
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
109
128
|
mocks.completeFirstRun.mockReset();
|
|
129
|
+
mocks.recordCreditsV2Exposure.mockReset().mockResolvedValue({
|
|
130
|
+
assignment: null,
|
|
131
|
+
});
|
|
132
|
+
mocks.useActionMutation.mockReset().mockReturnValue({
|
|
133
|
+
mutateAsync: mocks.recordCreditsV2Exposure,
|
|
134
|
+
});
|
|
110
135
|
mocks.routePathname = "/";
|
|
111
136
|
mocks.useActualRouter = false;
|
|
112
137
|
mocks.navigate.mockReset();
|
|
@@ -120,6 +145,9 @@ describe("FirstRunOnboarding", () => {
|
|
|
120
145
|
mocks.useOnboarding.mockReset();
|
|
121
146
|
mocks.useOnboardingPreviewMode.mockReset();
|
|
122
147
|
mocks.useOnboardingPreviewStep.mockReset();
|
|
148
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: false };
|
|
149
|
+
mocks.recordCreditsV2Exposure.mockReset();
|
|
150
|
+
mocks.recordCreditsV2Exposure.mockResolvedValue({ assignment: null });
|
|
123
151
|
mocks.useOnboardingPreviewMode.mockReturnValue(false);
|
|
124
152
|
mocks.useOnboardingPreviewStep.mockReturnValue(null);
|
|
125
153
|
mocks.useBuilderConnectFlow.mockReturnValue({
|
|
@@ -978,6 +1006,7 @@ describe("FirstRunOnboarding", () => {
|
|
|
978
1006
|
])(
|
|
979
1007
|
"restarts the prior $mode mode after a $failure",
|
|
980
1008
|
({
|
|
1009
|
+
mode,
|
|
981
1010
|
choiceId,
|
|
982
1011
|
provisionAccount,
|
|
983
1012
|
statusUnavailable,
|
|
@@ -1018,12 +1047,25 @@ describe("FirstRunOnboarding", () => {
|
|
|
1018
1047
|
});
|
|
1019
1048
|
|
|
1020
1049
|
expect(start).toHaveBeenCalledOnce();
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1050
|
+
if (mode === "provision" && terminalError) {
|
|
1051
|
+
expect(document.body.textContent).toContain(
|
|
1052
|
+
"Builder.io setup didn't finish. Try again, or use your own keys.",
|
|
1053
|
+
);
|
|
1054
|
+
act(() => {
|
|
1055
|
+
document.body
|
|
1056
|
+
.querySelector<HTMLButtonElement>(
|
|
1057
|
+
'[data-testid="first-run-retry-builder"]',
|
|
1058
|
+
)
|
|
1059
|
+
?.click();
|
|
1060
|
+
});
|
|
1061
|
+
} else {
|
|
1062
|
+
expect(document.body.textContent).toContain(error);
|
|
1063
|
+
act(() => {
|
|
1064
|
+
[...document.body.querySelectorAll("button")]
|
|
1065
|
+
.find((button) => button.textContent === "Try again")
|
|
1066
|
+
?.click();
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1027
1069
|
|
|
1028
1070
|
expect(start).toHaveBeenCalledTimes(2);
|
|
1029
1071
|
expect(start).toHaveBeenLastCalledWith(
|
|
@@ -1563,7 +1605,8 @@ describe("FirstRunOnboarding", () => {
|
|
|
1563
1605
|
).toBeTruthy();
|
|
1564
1606
|
});
|
|
1565
1607
|
|
|
1566
|
-
it("
|
|
1608
|
+
it("offers retry, own-key, and skip actions after Builder provisioning fails", () => {
|
|
1609
|
+
const start = vi.fn();
|
|
1567
1610
|
const flow = {
|
|
1568
1611
|
hasFetchedStatus: true,
|
|
1569
1612
|
statusResolved: true,
|
|
@@ -1572,7 +1615,7 @@ describe("FirstRunOnboarding", () => {
|
|
|
1572
1615
|
connecting: false,
|
|
1573
1616
|
terminalError: "Builder account provisioning failed (503).",
|
|
1574
1617
|
error: "Builder account provisioning failed (503).",
|
|
1575
|
-
start
|
|
1618
|
+
start,
|
|
1576
1619
|
retry: vi.fn(),
|
|
1577
1620
|
};
|
|
1578
1621
|
mocks.useBuilderConnectFlow.mockReturnValue(flow);
|
|
@@ -1597,26 +1640,138 @@ describe("FirstRunOnboarding", () => {
|
|
|
1597
1640
|
?.click();
|
|
1598
1641
|
});
|
|
1599
1642
|
|
|
1643
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
1644
|
+
"onboarding_step_viewed",
|
|
1645
|
+
expect.objectContaining({ step_id: "connecting", optional: true }),
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1600
1648
|
expect(
|
|
1601
|
-
document.body.querySelector('[data-testid="first-run-
|
|
1649
|
+
document.body.querySelector('[data-testid="first-run-retry-builder"]'),
|
|
1602
1650
|
).toBeTruthy();
|
|
1651
|
+
expect(
|
|
1652
|
+
document.body.querySelector(
|
|
1653
|
+
'[data-testid="first-run-open-key-settings-after-failure"]',
|
|
1654
|
+
),
|
|
1655
|
+
).toBeTruthy();
|
|
1656
|
+
expect(
|
|
1657
|
+
document.body.querySelector(
|
|
1658
|
+
'[data-testid="first-run-skip-builder-after-failure"]',
|
|
1659
|
+
),
|
|
1660
|
+
).toBeTruthy();
|
|
1661
|
+
act(() => {
|
|
1662
|
+
document.body
|
|
1663
|
+
.querySelector<HTMLButtonElement>(
|
|
1664
|
+
'[data-testid="first-run-retry-builder"]',
|
|
1665
|
+
)
|
|
1666
|
+
?.click();
|
|
1667
|
+
});
|
|
1668
|
+
expect(start).toHaveBeenCalledTimes(2);
|
|
1669
|
+
expect(start).toHaveBeenLastCalledWith(
|
|
1670
|
+
expect.objectContaining({ provisionAccount: true }),
|
|
1671
|
+
);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
it("opens AI key settings directly from a failed Builder connection", async () => {
|
|
1675
|
+
mocks.useBuilderConnectFlow.mockReturnValue({
|
|
1676
|
+
hasFetchedStatus: true,
|
|
1677
|
+
statusResolved: true,
|
|
1678
|
+
configured: false,
|
|
1679
|
+
agentNativeProvisioningEnabled: true,
|
|
1680
|
+
connecting: false,
|
|
1681
|
+
terminalError: "Builder account provisioning failed (503).",
|
|
1682
|
+
error: "Builder account provisioning failed (503).",
|
|
1683
|
+
start: vi.fn(),
|
|
1684
|
+
retry: vi.fn(),
|
|
1685
|
+
});
|
|
1603
1686
|
|
|
1687
|
+
await act(async () => {
|
|
1688
|
+
root.render(
|
|
1689
|
+
<TooltipProvider>
|
|
1690
|
+
<FirstRunOnboarding />
|
|
1691
|
+
</TooltipProvider>,
|
|
1692
|
+
);
|
|
1693
|
+
});
|
|
1694
|
+
act(() => {
|
|
1695
|
+
document.body
|
|
1696
|
+
.querySelector<HTMLButtonElement>('[data-testid="first-run-role-skip"]')
|
|
1697
|
+
?.click();
|
|
1698
|
+
});
|
|
1604
1699
|
act(() => {
|
|
1605
1700
|
document.body
|
|
1606
1701
|
.querySelector<HTMLButtonElement>(
|
|
1607
|
-
'[data-testid="first-run-
|
|
1702
|
+
'[data-testid="first-run-builder-create-account"]',
|
|
1608
1703
|
)
|
|
1609
1704
|
?.click();
|
|
1610
1705
|
});
|
|
1611
1706
|
|
|
1612
|
-
|
|
1613
|
-
document.body
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
)
|
|
1619
|
-
|
|
1707
|
+
await act(async () => {
|
|
1708
|
+
document.body
|
|
1709
|
+
.querySelector<HTMLButtonElement>(
|
|
1710
|
+
'[data-testid="first-run-open-key-settings-after-failure"]',
|
|
1711
|
+
)
|
|
1712
|
+
?.click();
|
|
1713
|
+
await Promise.resolve();
|
|
1714
|
+
await Promise.resolve();
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
expect(mocks.navigate).toHaveBeenCalledWith(
|
|
1718
|
+
manualSetupSettingsRoute({ redesign: true }),
|
|
1719
|
+
);
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it("can skip onboarding setup after a failed Builder connection", async () => {
|
|
1723
|
+
mocks.useBuilderConnectFlow.mockReturnValue({
|
|
1724
|
+
hasFetchedStatus: true,
|
|
1725
|
+
statusResolved: true,
|
|
1726
|
+
configured: false,
|
|
1727
|
+
agentNativeProvisioningEnabled: true,
|
|
1728
|
+
connecting: false,
|
|
1729
|
+
terminalError: "Builder account provisioning failed (503).",
|
|
1730
|
+
error: "Builder account provisioning failed (503).",
|
|
1731
|
+
start: vi.fn(),
|
|
1732
|
+
retry: vi.fn(),
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
await act(async () => {
|
|
1736
|
+
root.render(
|
|
1737
|
+
<TooltipProvider>
|
|
1738
|
+
<FirstRunOnboarding />
|
|
1739
|
+
</TooltipProvider>,
|
|
1740
|
+
);
|
|
1741
|
+
});
|
|
1742
|
+
act(() => {
|
|
1743
|
+
document.body
|
|
1744
|
+
.querySelector<HTMLButtonElement>('[data-testid="first-run-role-skip"]')
|
|
1745
|
+
?.click();
|
|
1746
|
+
});
|
|
1747
|
+
act(() => {
|
|
1748
|
+
document.body
|
|
1749
|
+
.querySelector<HTMLButtonElement>(
|
|
1750
|
+
'[data-testid="first-run-builder-create-account"]',
|
|
1751
|
+
)
|
|
1752
|
+
?.click();
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
await act(async () => {
|
|
1756
|
+
document.body
|
|
1757
|
+
.querySelector<HTMLButtonElement>(
|
|
1758
|
+
'[data-testid="first-run-skip-builder-after-failure"]',
|
|
1759
|
+
)
|
|
1760
|
+
?.click();
|
|
1761
|
+
await Promise.resolve();
|
|
1762
|
+
await Promise.resolve();
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
expect(mocks.completeFirstRun).toHaveBeenCalledOnce();
|
|
1766
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
1767
|
+
"onboarding_step_skipped",
|
|
1768
|
+
expect.objectContaining({
|
|
1769
|
+
flow: "first_run",
|
|
1770
|
+
step_id: "connecting",
|
|
1771
|
+
optional: true,
|
|
1772
|
+
reason: "builder_provision_failed",
|
|
1773
|
+
}),
|
|
1774
|
+
);
|
|
1620
1775
|
});
|
|
1621
1776
|
|
|
1622
1777
|
it("shows the role step first", () => {
|
|
@@ -1639,6 +1794,13 @@ describe("FirstRunOnboarding", () => {
|
|
|
1639
1794
|
});
|
|
1640
1795
|
|
|
1641
1796
|
it("labels Clips setup as Builder, custom keys, or an optional skip", async () => {
|
|
1797
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
1798
|
+
mocks.recordCreditsV2Exposure.mockResolvedValue({
|
|
1799
|
+
assignment: {
|
|
1800
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
1801
|
+
experiment_variant: "control",
|
|
1802
|
+
},
|
|
1803
|
+
});
|
|
1642
1804
|
mocks.useOnboarding.mockReturnValue({
|
|
1643
1805
|
firstRun: true,
|
|
1644
1806
|
loading: false,
|
|
@@ -1684,8 +1846,137 @@ describe("FirstRunOnboarding", () => {
|
|
|
1684
1846
|
customKeysAction: "Open AI key settings",
|
|
1685
1847
|
skip: "Skip for now",
|
|
1686
1848
|
});
|
|
1849
|
+
expect(mocks.useActionMutation).toHaveBeenCalledWith(
|
|
1850
|
+
"record-first-run-builder-credits-v2-exposure",
|
|
1851
|
+
{ uiOnly: true, skipActionQueryInvalidation: true },
|
|
1852
|
+
);
|
|
1853
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(2);
|
|
1854
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(1, {});
|
|
1855
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(2, {
|
|
1856
|
+
confirmedVariant: "control",
|
|
1857
|
+
});
|
|
1858
|
+
expect(mocks.setOnboardingExperiment).toHaveBeenCalledWith({
|
|
1859
|
+
id: "first_run_builder_credits_v2",
|
|
1860
|
+
variant: "control",
|
|
1861
|
+
});
|
|
1862
|
+
expect(mocks.setOnboardingExperiment).not.toHaveBeenCalledWith({
|
|
1863
|
+
id: "first_run_builder_credits_v2",
|
|
1864
|
+
variant: "credits",
|
|
1865
|
+
});
|
|
1866
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
1867
|
+
"onboarding_experiment_exposed",
|
|
1868
|
+
{
|
|
1869
|
+
flow: "first_run",
|
|
1870
|
+
step_id: "choice",
|
|
1871
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
1872
|
+
experiment_variant: "control",
|
|
1873
|
+
},
|
|
1874
|
+
);
|
|
1875
|
+
expect(
|
|
1876
|
+
document.body.querySelector("[data-testid='first-run-credits-offer']"),
|
|
1877
|
+
).toBeNull();
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
it("does not hold another app's first-run screen while the v2 flag loads", async () => {
|
|
1881
|
+
mocks.creditsV2FlagState = { status: "loading", enabled: false };
|
|
1882
|
+
mocks.useOnboarding.mockReturnValue({
|
|
1883
|
+
firstRun: true,
|
|
1884
|
+
loading: false,
|
|
1885
|
+
error: null,
|
|
1886
|
+
profile: { appId: "forms", appName: "Forms", capabilities: [] },
|
|
1887
|
+
completeFirstRun: mocks.completeFirstRun,
|
|
1888
|
+
completeFirstRunError: null,
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
await act(async () => {
|
|
1892
|
+
root.render(
|
|
1893
|
+
<TooltipProvider>
|
|
1894
|
+
<FirstRunOnboarding />
|
|
1895
|
+
</TooltipProvider>,
|
|
1896
|
+
);
|
|
1897
|
+
});
|
|
1898
|
+
await act(async () => {
|
|
1899
|
+
document.body
|
|
1900
|
+
.querySelector<HTMLButtonElement>("[data-testid='first-run-role-skip']")
|
|
1901
|
+
?.click();
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
expect(
|
|
1905
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
1906
|
+
).toBeNull();
|
|
1907
|
+
expect(
|
|
1908
|
+
document.body.querySelector('[data-onboarding-screen="choice"]'),
|
|
1909
|
+
).not.toBeNull();
|
|
1910
|
+
expect(mocks.recordCreditsV2Exposure).not.toHaveBeenCalled();
|
|
1687
1911
|
});
|
|
1688
1912
|
|
|
1913
|
+
it.each(["design", "slides", "clips"])(
|
|
1914
|
+
"shows the v2 credits actions in %s",
|
|
1915
|
+
async (appId) => {
|
|
1916
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
1917
|
+
mocks.recordCreditsV2Exposure.mockResolvedValue({
|
|
1918
|
+
assignment: {
|
|
1919
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
1920
|
+
experiment_variant: "credits",
|
|
1921
|
+
},
|
|
1922
|
+
});
|
|
1923
|
+
mocks.useOnboarding.mockReturnValue({
|
|
1924
|
+
firstRun: true,
|
|
1925
|
+
loading: false,
|
|
1926
|
+
error: null,
|
|
1927
|
+
profile: { appId, appName: appId, capabilities: [] },
|
|
1928
|
+
completeFirstRun: mocks.completeFirstRun,
|
|
1929
|
+
completeFirstRunError: null,
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
await act(async () => {
|
|
1933
|
+
root.render(
|
|
1934
|
+
<TooltipProvider>
|
|
1935
|
+
<FirstRunOnboarding />
|
|
1936
|
+
</TooltipProvider>,
|
|
1937
|
+
);
|
|
1938
|
+
});
|
|
1939
|
+
await act(async () => {
|
|
1940
|
+
document.body
|
|
1941
|
+
.querySelector<HTMLButtonElement>(
|
|
1942
|
+
"[data-testid='first-run-role-skip']",
|
|
1943
|
+
)
|
|
1944
|
+
?.click();
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
const primary = document.body.querySelector<HTMLButtonElement>(
|
|
1948
|
+
"[data-testid='first-run-credits-activate']",
|
|
1949
|
+
);
|
|
1950
|
+
const existingAccount = document.body.querySelector<HTMLButtonElement>(
|
|
1951
|
+
"[data-testid='first-run-credits-connect-existing']",
|
|
1952
|
+
);
|
|
1953
|
+
const ownKeys = document.body.querySelector<HTMLButtonElement>(
|
|
1954
|
+
"[data-testid='first-run-credits-own-api-keys']",
|
|
1955
|
+
);
|
|
1956
|
+
expect(primary?.textContent).toBe("Activate free credits");
|
|
1957
|
+
expect(existingAccount?.textContent).toBe("I have a Builder.io account");
|
|
1958
|
+
expect(ownKeys?.textContent).toBe("Use my own API keys");
|
|
1959
|
+
expect(
|
|
1960
|
+
document.body.querySelector(
|
|
1961
|
+
"[data-testid='first-run-credits-skip-confirm']",
|
|
1962
|
+
),
|
|
1963
|
+
).toBeNull();
|
|
1964
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
1965
|
+
"onboarding_experiment_exposed",
|
|
1966
|
+
expect.objectContaining({
|
|
1967
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
1968
|
+
experiment_variant: "credits",
|
|
1969
|
+
}),
|
|
1970
|
+
);
|
|
1971
|
+
|
|
1972
|
+
await act(async () => {
|
|
1973
|
+
ownKeys?.click();
|
|
1974
|
+
await Promise.resolve();
|
|
1975
|
+
});
|
|
1976
|
+
expect(mocks.navigate).toHaveBeenCalled();
|
|
1977
|
+
},
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1689
1980
|
it("opens the app directly after a configured Builder connection", () => {
|
|
1690
1981
|
mocks.useBuilderConnectFlow.mockReturnValue({
|
|
1691
1982
|
hasFetchedStatus: true,
|
|
@@ -2865,7 +3156,12 @@ describe("FirstRunOnboarding", () => {
|
|
|
2865
3156
|
});
|
|
2866
3157
|
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
2867
3158
|
"onboarding_experiment_exposed",
|
|
2868
|
-
{
|
|
3159
|
+
expect.objectContaining({
|
|
3160
|
+
flow: "first_run",
|
|
3161
|
+
step_id: "choice",
|
|
3162
|
+
experiment_id: "first_run_builder_credits",
|
|
3163
|
+
experiment_variant: "credits",
|
|
3164
|
+
}),
|
|
2869
3165
|
);
|
|
2870
3166
|
});
|
|
2871
3167
|
|
|
@@ -2932,8 +3228,13 @@ describe("FirstRunOnboarding", () => {
|
|
|
2932
3228
|
expect(mocks.setOnboardingExperiment).not.toHaveBeenCalled();
|
|
2933
3229
|
});
|
|
2934
3230
|
|
|
2935
|
-
const openClipsChoice = (
|
|
2936
|
-
|
|
3231
|
+
const openClipsChoice = (
|
|
3232
|
+
start = vi.fn(),
|
|
3233
|
+
overrides: Record<string, unknown> = {},
|
|
3234
|
+
) => {
|
|
3235
|
+
mocks.useBuilderConnectFlow.mockReturnValue(
|
|
3236
|
+
flow({ start, ...overrides }),
|
|
3237
|
+
);
|
|
2937
3238
|
mocks.useOnboarding.mockReturnValue({
|
|
2938
3239
|
...mocks.useOnboarding(),
|
|
2939
3240
|
profile: { ...mocks.useOnboarding().profile, appId: "clips" },
|
|
@@ -2942,6 +3243,176 @@ describe("FirstRunOnboarding", () => {
|
|
|
2942
3243
|
return start;
|
|
2943
3244
|
};
|
|
2944
3245
|
|
|
3246
|
+
it("does not expose the credits v2 experiment when connection status is unavailable", () => {
|
|
3247
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3248
|
+
openClipsChoice(vi.fn(), { statusUnavailable: true });
|
|
3249
|
+
|
|
3250
|
+
expect(mocks.recordCreditsV2Exposure).not.toHaveBeenCalled();
|
|
3251
|
+
expect(mocks.trackOnboardingEvent).not.toHaveBeenCalledWith(
|
|
3252
|
+
"onboarding_experiment_exposed",
|
|
3253
|
+
expect.objectContaining({
|
|
3254
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3255
|
+
}),
|
|
3256
|
+
);
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
it("retries credits v2 exposure after a transient mutation failure", async () => {
|
|
3260
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3261
|
+
mocks.recordCreditsV2Exposure
|
|
3262
|
+
.mockRejectedValueOnce(new Error("temporary failure"))
|
|
3263
|
+
.mockResolvedValue({
|
|
3264
|
+
assignment: {
|
|
3265
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3266
|
+
experiment_variant: "credits",
|
|
3267
|
+
},
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
openClipsChoice();
|
|
3271
|
+
await act(async () => {
|
|
3272
|
+
await Promise.resolve();
|
|
3273
|
+
await Promise.resolve();
|
|
3274
|
+
});
|
|
3275
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledOnce();
|
|
3276
|
+
|
|
3277
|
+
mocks.useBuilderConnectFlow.mockReturnValue(flow({ configured: true }));
|
|
3278
|
+
await act(async () => {
|
|
3279
|
+
root.render(
|
|
3280
|
+
<TooltipProvider>
|
|
3281
|
+
<FirstRunOnboarding />
|
|
3282
|
+
</TooltipProvider>,
|
|
3283
|
+
);
|
|
3284
|
+
});
|
|
3285
|
+
mocks.useBuilderConnectFlow.mockReturnValue(flow());
|
|
3286
|
+
await act(async () => {
|
|
3287
|
+
root.render(
|
|
3288
|
+
<TooltipProvider>
|
|
3289
|
+
<FirstRunOnboarding />
|
|
3290
|
+
</TooltipProvider>,
|
|
3291
|
+
);
|
|
3292
|
+
await Promise.resolve();
|
|
3293
|
+
await Promise.resolve();
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(3);
|
|
3297
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3298
|
+
"onboarding_experiment_exposed",
|
|
3299
|
+
{
|
|
3300
|
+
flow: "first_run",
|
|
3301
|
+
step_id: "choice",
|
|
3302
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3303
|
+
experiment_variant: "credits",
|
|
3304
|
+
},
|
|
3305
|
+
);
|
|
3306
|
+
});
|
|
3307
|
+
|
|
3308
|
+
it("retries exposure persistence after tracking the rendered v2 arm", async () => {
|
|
3309
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3310
|
+
let confirmationAttempts = 0;
|
|
3311
|
+
mocks.recordCreditsV2Exposure.mockImplementation((...args: unknown[]) => {
|
|
3312
|
+
const input = args[0] as { confirmedVariant?: string } | undefined;
|
|
3313
|
+
if (input?.confirmedVariant) {
|
|
3314
|
+
confirmationAttempts += 1;
|
|
3315
|
+
if (confirmationAttempts === 1) {
|
|
3316
|
+
return Promise.reject(new Error("temporary confirmation failure"));
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
return Promise.resolve({
|
|
3320
|
+
assignment: {
|
|
3321
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3322
|
+
experiment_variant: "credits",
|
|
3323
|
+
},
|
|
3324
|
+
});
|
|
3325
|
+
});
|
|
3326
|
+
mocks.trackOnboardingEvent.mockImplementation((name: string) => {
|
|
3327
|
+
if (name === "onboarding_experiment_exposed") {
|
|
3328
|
+
expect(q("first-run-credits-offer")).not.toBeNull();
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
openClipsChoice();
|
|
3333
|
+
await act(async () => {
|
|
3334
|
+
await Promise.resolve();
|
|
3335
|
+
await Promise.resolve();
|
|
3336
|
+
await Promise.resolve();
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(3);
|
|
3340
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(1, {});
|
|
3341
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(2, {
|
|
3342
|
+
confirmedVariant: "credits",
|
|
3343
|
+
});
|
|
3344
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(3, {
|
|
3345
|
+
confirmedVariant: "credits",
|
|
3346
|
+
});
|
|
3347
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3348
|
+
"onboarding_experiment_exposed",
|
|
3349
|
+
{
|
|
3350
|
+
flow: "first_run",
|
|
3351
|
+
step_id: "choice",
|
|
3352
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3353
|
+
experiment_variant: "credits",
|
|
3354
|
+
},
|
|
3355
|
+
);
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
it("reconciles failed exposure persistence without blocking the rendered v2 arm", async () => {
|
|
3359
|
+
vi.useFakeTimers();
|
|
3360
|
+
try {
|
|
3361
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3362
|
+
mocks.recordCreditsV2Exposure
|
|
3363
|
+
.mockResolvedValueOnce({
|
|
3364
|
+
assignment: {
|
|
3365
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3366
|
+
experiment_variant: "credits",
|
|
3367
|
+
},
|
|
3368
|
+
})
|
|
3369
|
+
.mockRejectedValueOnce(new Error("first confirmation unavailable"))
|
|
3370
|
+
.mockRejectedValueOnce(new Error("second confirmation unavailable"))
|
|
3371
|
+
.mockResolvedValueOnce({
|
|
3372
|
+
assignment: {
|
|
3373
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3374
|
+
experiment_variant: "credits",
|
|
3375
|
+
},
|
|
3376
|
+
});
|
|
3377
|
+
|
|
3378
|
+
openClipsChoice();
|
|
3379
|
+
await act(async () => {
|
|
3380
|
+
await Promise.resolve();
|
|
3381
|
+
await Promise.resolve();
|
|
3382
|
+
await Promise.resolve();
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(3);
|
|
3386
|
+
expect(q("first-run-credits-offer")).not.toBeNull();
|
|
3387
|
+
expect(q("first-run-credits-own-api-keys")).not.toBeNull();
|
|
3388
|
+
expect(
|
|
3389
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
3390
|
+
).toBeNull();
|
|
3391
|
+
expect(mocks.setOnboardingExperiment).toHaveBeenCalledWith({
|
|
3392
|
+
id: "first_run_builder_credits_v2",
|
|
3393
|
+
variant: "credits",
|
|
3394
|
+
});
|
|
3395
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3396
|
+
"onboarding_experiment_exposed",
|
|
3397
|
+
expect.objectContaining({
|
|
3398
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3399
|
+
}),
|
|
3400
|
+
);
|
|
3401
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3402
|
+
"onboarding_step_viewed",
|
|
3403
|
+
expect.objectContaining({ step_id: "choice" }),
|
|
3404
|
+
);
|
|
3405
|
+
|
|
3406
|
+
await act(async () => {
|
|
3407
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(4);
|
|
3411
|
+
} finally {
|
|
3412
|
+
vi.useRealTimers();
|
|
3413
|
+
}
|
|
3414
|
+
});
|
|
3415
|
+
|
|
2945
3416
|
it("shows one activate action instead of the two setup cards", () => {
|
|
2946
3417
|
mocks.useBuilderConnectFlow.mockReturnValue(flow());
|
|
2947
3418
|
openChoice();
|
|
@@ -3121,6 +3592,102 @@ describe("FirstRunOnboarding", () => {
|
|
|
3121
3592
|
},
|
|
3122
3593
|
);
|
|
3123
3594
|
|
|
3595
|
+
it("keeps a late Builder status response out of Clips v2 after timeout", () => {
|
|
3596
|
+
vi.useFakeTimers();
|
|
3597
|
+
try {
|
|
3598
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3599
|
+
openClipsChoice(vi.fn(), { hasFetchedStatus: false });
|
|
3600
|
+
|
|
3601
|
+
expect(mocks.recordCreditsV2Exposure).not.toHaveBeenCalled();
|
|
3602
|
+
act(() => {
|
|
3603
|
+
vi.advanceTimersByTime(4000);
|
|
3604
|
+
});
|
|
3605
|
+
expect(q("first-run-open-key-settings")).not.toBeNull();
|
|
3606
|
+
expect(q("first-run-credits-offer")).toBeNull();
|
|
3607
|
+
|
|
3608
|
+
mocks.useBuilderConnectFlow.mockReturnValue(flow());
|
|
3609
|
+
act(() => {
|
|
3610
|
+
root.render(
|
|
3611
|
+
<TooltipProvider>
|
|
3612
|
+
<FirstRunOnboarding />
|
|
3613
|
+
</TooltipProvider>,
|
|
3614
|
+
);
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3617
|
+
expect(q("first-run-open-key-settings")).not.toBeNull();
|
|
3618
|
+
expect(q("first-run-credits-offer")).toBeNull();
|
|
3619
|
+
expect(mocks.recordCreditsV2Exposure).not.toHaveBeenCalled();
|
|
3620
|
+
expect(mocks.setOnboardingExperiment).not.toHaveBeenCalled();
|
|
3621
|
+
expect(mocks.trackOnboardingEvent).not.toHaveBeenCalledWith(
|
|
3622
|
+
"onboarding_experiment_exposed",
|
|
3623
|
+
expect.objectContaining({
|
|
3624
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3625
|
+
}),
|
|
3626
|
+
);
|
|
3627
|
+
} finally {
|
|
3628
|
+
vi.useRealTimers();
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
it("falls back if the Clips v2 exposure mutation stalls and ignores a late assignment", async () => {
|
|
3633
|
+
vi.useFakeTimers();
|
|
3634
|
+
try {
|
|
3635
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3636
|
+
let resolveAssignment:
|
|
3637
|
+
| ((result: {
|
|
3638
|
+
assignment: {
|
|
3639
|
+
experiment_id: string;
|
|
3640
|
+
experiment_variant: string;
|
|
3641
|
+
};
|
|
3642
|
+
}) => void)
|
|
3643
|
+
| undefined;
|
|
3644
|
+
mocks.recordCreditsV2Exposure.mockImplementation(
|
|
3645
|
+
() =>
|
|
3646
|
+
new Promise((resolve) => {
|
|
3647
|
+
resolveAssignment = resolve;
|
|
3648
|
+
}),
|
|
3649
|
+
);
|
|
3650
|
+
|
|
3651
|
+
openClipsChoice();
|
|
3652
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledOnce();
|
|
3653
|
+
expect(
|
|
3654
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
3655
|
+
).not.toBeNull();
|
|
3656
|
+
|
|
3657
|
+
act(() => {
|
|
3658
|
+
vi.advanceTimersByTime(4000);
|
|
3659
|
+
});
|
|
3660
|
+
expect(
|
|
3661
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
3662
|
+
).toBeNull();
|
|
3663
|
+
expect(q("first-run-open-key-settings")).not.toBeNull();
|
|
3664
|
+
expect(q("first-run-credits-offer")).toBeNull();
|
|
3665
|
+
|
|
3666
|
+
await act(async () => {
|
|
3667
|
+
resolveAssignment?.({
|
|
3668
|
+
assignment: {
|
|
3669
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3670
|
+
experiment_variant: "credits",
|
|
3671
|
+
},
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
|
|
3675
|
+
expect(q("first-run-open-key-settings")).not.toBeNull();
|
|
3676
|
+
expect(q("first-run-credits-offer")).toBeNull();
|
|
3677
|
+
expect(mocks.setOnboardingExperiment).not.toHaveBeenCalled();
|
|
3678
|
+
expect(mocks.trackOnboardingEvent).not.toHaveBeenCalledWith(
|
|
3679
|
+
"onboarding_experiment_exposed",
|
|
3680
|
+
expect.objectContaining({
|
|
3681
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3682
|
+
}),
|
|
3683
|
+
);
|
|
3684
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledOnce();
|
|
3685
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledWith({});
|
|
3686
|
+
} finally {
|
|
3687
|
+
vi.useRealTimers();
|
|
3688
|
+
}
|
|
3689
|
+
});
|
|
3690
|
+
|
|
3124
3691
|
it("falls back to the full setup where activation is unavailable", () => {
|
|
3125
3692
|
mocks.useBuilderConnectFlow.mockReturnValue(
|
|
3126
3693
|
flow({ agentNativeProvisioningEnabled: false }),
|
|
@@ -3130,5 +3697,148 @@ describe("FirstRunOnboarding", () => {
|
|
|
3130
3697
|
expect(q("first-run-open-key-settings")).not.toBeNull();
|
|
3131
3698
|
expect(q("first-run-credits-more-info")).toBeNull();
|
|
3132
3699
|
});
|
|
3700
|
+
|
|
3701
|
+
it("renders and attributes the v2 choice while exposure persistence is pending", async () => {
|
|
3702
|
+
mocks.creditsV2FlagState = { status: "ready", enabled: true };
|
|
3703
|
+
mocks.useBuilderConnectFlow.mockReturnValue(flow());
|
|
3704
|
+
mocks.useOnboarding.mockReturnValue({
|
|
3705
|
+
...mocks.useOnboarding(),
|
|
3706
|
+
profile: { ...mocks.useOnboarding().profile, appId: "clips" },
|
|
3707
|
+
});
|
|
3708
|
+
let resolveAssignment:
|
|
3709
|
+
| ((result: {
|
|
3710
|
+
assignment: {
|
|
3711
|
+
experiment_id: string;
|
|
3712
|
+
experiment_variant: string;
|
|
3713
|
+
};
|
|
3714
|
+
}) => void)
|
|
3715
|
+
| undefined;
|
|
3716
|
+
let resolveConfirmation:
|
|
3717
|
+
| ((result: {
|
|
3718
|
+
assignment: {
|
|
3719
|
+
experiment_id: string;
|
|
3720
|
+
experiment_variant: string;
|
|
3721
|
+
};
|
|
3722
|
+
}) => void)
|
|
3723
|
+
| undefined;
|
|
3724
|
+
mocks.recordCreditsV2Exposure.mockImplementation((...args: unknown[]) => {
|
|
3725
|
+
const input = args[0] as { confirmedVariant?: string } | undefined;
|
|
3726
|
+
if (input?.confirmedVariant) {
|
|
3727
|
+
return new Promise((resolve) => {
|
|
3728
|
+
resolveConfirmation = resolve;
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
return new Promise((resolve) => {
|
|
3732
|
+
resolveAssignment = resolve;
|
|
3733
|
+
});
|
|
3734
|
+
});
|
|
3735
|
+
mocks.trackOnboardingEvent.mockImplementation(
|
|
3736
|
+
(name: string, properties?: Record<string, unknown>) => {
|
|
3737
|
+
if (name === "onboarding_experiment_exposed") {
|
|
3738
|
+
expect(q("first-run-credits-offer")).not.toBeNull();
|
|
3739
|
+
}
|
|
3740
|
+
if (
|
|
3741
|
+
name === "onboarding_step_viewed" &&
|
|
3742
|
+
properties?.step_id === "choice"
|
|
3743
|
+
) {
|
|
3744
|
+
expect(mocks.setOnboardingExperiment).toHaveBeenCalledWith({
|
|
3745
|
+
id: "first_run_builder_credits_v2",
|
|
3746
|
+
variant: "credits",
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
},
|
|
3750
|
+
);
|
|
3751
|
+
|
|
3752
|
+
await act(async () => {
|
|
3753
|
+
root.render(
|
|
3754
|
+
<TooltipProvider>
|
|
3755
|
+
<FirstRunOnboarding />
|
|
3756
|
+
</TooltipProvider>,
|
|
3757
|
+
);
|
|
3758
|
+
});
|
|
3759
|
+
await act(async () => {
|
|
3760
|
+
document.body
|
|
3761
|
+
.querySelector<HTMLButtonElement>(
|
|
3762
|
+
"[data-testid='first-run-role-skip']",
|
|
3763
|
+
)
|
|
3764
|
+
?.click();
|
|
3765
|
+
});
|
|
3766
|
+
|
|
3767
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledOnce();
|
|
3768
|
+
expect(
|
|
3769
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
3770
|
+
).not.toBeNull();
|
|
3771
|
+
expect(q("first-run-credits-offer")).toBeNull();
|
|
3772
|
+
|
|
3773
|
+
await act(async () => {
|
|
3774
|
+
resolveAssignment?.({
|
|
3775
|
+
assignment: {
|
|
3776
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3777
|
+
experiment_variant: "credits",
|
|
3778
|
+
},
|
|
3779
|
+
});
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenNthCalledWith(2, {
|
|
3783
|
+
confirmedVariant: "credits",
|
|
3784
|
+
});
|
|
3785
|
+
expect(
|
|
3786
|
+
document.body.querySelector("[data-onboarding-loading='true']"),
|
|
3787
|
+
).toBeNull();
|
|
3788
|
+
expect(q("first-run-credits-offer")).not.toBeNull();
|
|
3789
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3790
|
+
"onboarding_step_viewed",
|
|
3791
|
+
expect.objectContaining({ step_id: "choice" }),
|
|
3792
|
+
);
|
|
3793
|
+
expect(mocks.setOnboardingExperiment).toHaveBeenCalledWith({
|
|
3794
|
+
id: "first_run_builder_credits_v2",
|
|
3795
|
+
variant: "credits",
|
|
3796
|
+
});
|
|
3797
|
+
|
|
3798
|
+
await act(async () => {
|
|
3799
|
+
resolveConfirmation?.({
|
|
3800
|
+
assignment: {
|
|
3801
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3802
|
+
experiment_variant: "credits",
|
|
3803
|
+
},
|
|
3804
|
+
});
|
|
3805
|
+
});
|
|
3806
|
+
|
|
3807
|
+
expect(mocks.setOnboardingExperiment).toHaveBeenCalledWith({
|
|
3808
|
+
id: "first_run_builder_credits_v2",
|
|
3809
|
+
variant: "credits",
|
|
3810
|
+
});
|
|
3811
|
+
expect(q("first-run-credits-offer")).not.toBeNull();
|
|
3812
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3813
|
+
"onboarding_experiment_exposed",
|
|
3814
|
+
{
|
|
3815
|
+
flow: "first_run",
|
|
3816
|
+
step_id: "choice",
|
|
3817
|
+
experiment_id: "first_run_builder_credits_v2",
|
|
3818
|
+
experiment_variant: "credits",
|
|
3819
|
+
},
|
|
3820
|
+
);
|
|
3821
|
+
expect(q("first-run-credits-skip")).not.toBeNull();
|
|
3822
|
+
expect(q("first-run-credits-skip-confirm")).toBeNull();
|
|
3823
|
+
const exposureIndex = mocks.trackOnboardingEvent.mock.calls.findIndex(
|
|
3824
|
+
([name]) => name === "onboarding_experiment_exposed",
|
|
3825
|
+
);
|
|
3826
|
+
const choiceViewIndex = mocks.trackOnboardingEvent.mock.calls.findIndex(
|
|
3827
|
+
([name, properties]) =>
|
|
3828
|
+
name === "onboarding_step_viewed" &&
|
|
3829
|
+
(properties as Record<string, unknown> | undefined)?.step_id ===
|
|
3830
|
+
"choice",
|
|
3831
|
+
);
|
|
3832
|
+
expect(exposureIndex).toBeGreaterThanOrEqual(0);
|
|
3833
|
+
expect(choiceViewIndex).toBeGreaterThan(exposureIndex);
|
|
3834
|
+
expect(mocks.recordCreditsV2Exposure).toHaveBeenCalledTimes(2);
|
|
3835
|
+
expect(document.body.textContent).not.toContain("Are you sure?");
|
|
3836
|
+
|
|
3837
|
+
click("first-run-credits-skip");
|
|
3838
|
+
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
|
|
3839
|
+
"onboarding_step_skipped",
|
|
3840
|
+
expect.objectContaining({ flow: "first_run", step_id: "choice" }),
|
|
3841
|
+
);
|
|
3842
|
+
});
|
|
3133
3843
|
});
|
|
3134
3844
|
});
|