Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 133 additions & 2 deletions packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ import {

const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} });

function FirstRunOnboarding() {
function FirstRunOnboarding({
initialFirstRun = false,
}: {
initialFirstRun?: boolean;
} = {}) {
return (
<AgentNativeI18nProvider
catalog={toolkitI18nCatalog}
persistPreference={false}
>
<FirstRunOnboardingSource />
<FirstRunOnboardingSource initialFirstRun={initialFirstRun} />
</AgentNativeI18nProvider>
);
}
Expand Down Expand Up @@ -258,6 +262,133 @@ describe("FirstRunOnboarding", () => {
expect(document.body.querySelector("[data-onboarding-screen]")).toBeNull();
});

it("shows the first-run role step while the onboarding summary is loading", () => {
mocks.useOnboarding.mockReturnValue({
firstRun: true,
loading: true,
error: null,
profile: null,
completeFirstRun: mocks.completeFirstRun,
completeFirstRunError: null,
});

act(() => {
root.render(
<TooltipProvider>
<FirstRunOnboarding initialFirstRun />
</TooltipProvider>,
);
});

expect(
document.body.querySelector('[data-onboarding-screen="role"]'),
).not.toBeNull();
expect(document.body.querySelector("[data-onboarding-loading]")).toBeNull();
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
"onboarding_started",
{ flow: "first_run" },
);
expect(mocks.trackOnboardingEvent).toHaveBeenCalledWith(
"onboarding_step_viewed",
expect.objectContaining({ flow: "first_run", step_id: "role" }),
);
});

it("does not emit a second started event when the summary resolves", () => {
mocks.useOnboarding.mockReturnValue({
firstRun: true,
loading: true,
error: null,
profile: null,
completeFirstRun: mocks.completeFirstRun,
completeFirstRunError: null,
});

act(() => {
root.render(
<TooltipProvider>
<FirstRunOnboarding initialFirstRun />
</TooltipProvider>,
);
});

mocks.useOnboarding.mockReturnValue({
firstRun: true,
loading: false,
error: null,
profile: {
appId: "builder-app",
appName: "Builder App",
capabilities: [],
},
completeFirstRun: mocks.completeFirstRun,
completeFirstRunError: null,
});

act(() => {
root.render(
<TooltipProvider>
<FirstRunOnboarding initialFirstRun />
</TooltipProvider>,
);
});

expect(
mocks.trackOnboardingEvent.mock.calls.filter(
([name]) => name === "onboarding_started",
),
).toHaveLength(1);
});

it("keeps the role selection visible if the onboarding summary fails", () => {
mocks.useOnboarding.mockReturnValue({
firstRun: true,
loading: true,
error: null,
profile: null,
completeFirstRun: mocks.completeFirstRun,
completeFirstRunError: null,
});

act(() => {
root.render(
<TooltipProvider>
<FirstRunOnboarding initialFirstRun />
</TooltipProvider>,
);
});

act(() => {
container
.querySelector('[data-testid="first-run-role-developer"] input')
?.click();
});

mocks.useOnboarding.mockReturnValue({
firstRun: true,
loading: false,
error: new Error("summary unavailable"),
profile: null,
completeFirstRun: mocks.completeFirstRun,
completeFirstRunError: null,
});

act(() => {
root.render(
<TooltipProvider>
<FirstRunOnboarding initialFirstRun />
</TooltipProvider>,
);
});

expect(
container.querySelector('[data-onboarding-screen="role"]'),
).not.toBeNull();
expect(
container.querySelector('[data-testid="first-run-role-developer"] input'),
).toHaveProperty("checked", true);
});

it("does not show a close button during first-run setup", async () => {
await act(async () => {
root.render(
Expand Down
42 changes: 27 additions & 15 deletions packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ export function FirstRunOnboarding({
const [screen, setScreen] = useState<FirstRunScreen>(() =>
previewStep === "references" ? "extension" : (previewStep ?? "role"),
);
const canRenderRoleWithoutProfile =
initialFirstRun && screen === "role" && (loading || error != null);
const canTrackOnboardingScreen =
(!loading && profile !== null) || canRenderRoleWithoutProfile;
const [extensionPlacement, setExtensionPlacement] =
useState<FirstRunExtensionPlacement>("after-setup");
const [extensionIndex, setExtensionIndex] = useState(0);
Expand Down Expand Up @@ -308,6 +312,7 @@ export function FirstRunOnboarding({
const setupSkipStartedRef = useRef(false);
const onboardingTerminalRef = useRef(false);
const abandonmentTrackedRef = useRef(false);
const onboardingStartedRef = useRef(false);
const setupAttemptRef = useRef<FirstRunSetupAttempt | null>(null);
const builderSetupAttemptRef = useRef<FirstRunSetupAttempt | null>(null);
const stepViewRef = useRef<{ key: string; id: string } | null>(null);
Expand Down Expand Up @@ -367,12 +372,18 @@ export function FirstRunOnboarding({
[completeFirstRun, extensionIndex, navigate, trackFirstRunStepCompleted],
);
useEffect(() => {
if (!previewMode && firstRun && !loading && profile) {
if (
!previewMode &&
firstRun &&
canTrackOnboardingScreen &&
!onboardingStartedRef.current
) {
onboardingStartedRef.current = true;
trackOnboardingEvent("onboarding_started", { flow: "first_run" });
}
}, [firstRun, loading, previewMode, profile]);
}, [canTrackOnboardingScreen, firstRun, previewMode]);
useEffect(() => {
if (previewMode || !firstRun || loading || !profile) {
if (previewMode || !firstRun || !canTrackOnboardingScreen) {
stepViewRef.current = null;
return;
}
Expand All @@ -399,13 +410,12 @@ export function FirstRunOnboarding({
extensionPlacement,
extensionStepIndex,
firstRun,
loading,
previewMode,
profile,
canTrackOnboardingScreen,
screen,
]);
useEffect(() => {
if (previewMode || !firstRun || loading || !profile) return;
if (previewMode || !firstRun || !canTrackOnboardingScreen) return;
const handlePageHide = (event: PageTransitionEvent) => {
if (event.persisted) return;
if (
Expand Down Expand Up @@ -436,9 +446,8 @@ export function FirstRunOnboarding({
extensionPlacement,
extensionStepIndex,
firstRun,
loading,
previewMode,
profile,
canTrackOnboardingScreen,
screen,
]);
const beginBeforeSetup = useCallback(() => {
Expand Down Expand Up @@ -528,7 +537,7 @@ export function FirstRunOnboarding({

if (!firstRun) return null;

if (error) {
if (error && !canRenderRoleWithoutProfile) {
return (
<OnboardingShell
profile={profile}
Expand All @@ -554,15 +563,10 @@ export function FirstRunOnboarding({
);
}

if (loading || !profile) {
if ((loading || !profile) && !canRenderRoleWithoutProfile) {
return <OnboardingSkeleton />;
}

// Every shared service Builder.io powers, the same list Infrastructure
// shows, plus the app's own headline capabilities it covers.
const builderCapabilities = getBuilderIncludedCapabilities(
profile.capabilities,
);
const handleBuilder = (provisionAccount = canActivateBuilderFreeCredits) => {
if (previewMode) {
handleFinish(null);
Expand Down Expand Up @@ -736,6 +740,14 @@ export function FirstRunOnboarding({
}

if (screen === "choice") {
if (!profile) return <OnboardingSkeleton />;

// Every shared service Builder.io powers, the same list Infrastructure
// shows, plus the app's own headline capabilities it covers.
const builderCapabilities = getBuilderIncludedCapabilities(
profile.capabilities,
);

return (
<OnboardingShell
profile={profile}
Expand Down
Loading