From 2f625f001f13f88b4aa05b8e97290cd079dde294 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 13 Oct 2026 18:36:34 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A4=96=20tests:=20pin=20the=20onboard?= =?UTF-8?q?ing=20wizard's=20steps=20and=20entry=20animations=20(T3=20C=20s?= =?UTF-8?q?tep=202=20PR=201c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test only, green on main: without #xum-pre the wizard shows its loading step, then the gateway step, with the dialog, overlay and step entry animations. --- tests/ui/onboarding/wizardHandoff.test.ts | 93 +++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/ui/onboarding/wizardHandoff.test.ts diff --git a/tests/ui/onboarding/wizardHandoff.test.ts b/tests/ui/onboarding/wizardHandoff.test.ts new file mode 100644 index 00000000000..ed7b08f5e79 --- /dev/null +++ b/tests/ui/onboarding/wizardHandoff.test.ts @@ -0,0 +1,93 @@ +/** + * The onboarding wizard and the static first-run view (T3 #5971, C step 2, plan section 5.6). + * The server will put a static copy of the wizard's gateway step in #xum-pre before the app JS runs. + * The live wizard takes over in one commit. Without #xum-pre, the wizard must render exactly as before. + */ +import "../dom"; +import React from "react"; +import { act, cleanup, fireEvent, render } from "@testing-library/react"; + +let mockProviders: { config: Record | null; loading: boolean }; + +jest.mock("@/browser/hooks/useProvidersConfig", () => ({ + useProvidersConfig: () => ({ + ...mockProviders, + refresh: jest.fn(), + updateOptimistically: jest.fn(), + }), +})); +jest.mock("@/browser/features/SplashScreens/SplashScreenProvider", () => ({ + ...jest.requireActual( + "@/browser/features/SplashScreens/SplashScreenProvider" + ), + useOnboardingPause: () => ({ paused: null, pause: jest.fn(), resume: jest.fn() }), +})); +jest.mock("@/browser/contexts/SettingsContext", () => ({ + useSettings: () => ({ open: jest.fn(), registerOnClose: () => () => undefined }), +})); +jest.mock("@/browser/contexts/ProjectContext", () => ({ + useProjectContext: () => ({ addProject: jest.fn(), userProjects: new Map() }), +})); +jest.mock("@/browser/contexts/API", () => ({ useAPI: () => ({ api: null }) })); +jest.mock("@/browser/hooks/useRouting", () => ({ + useRouting: () => ({ navigateToProject: jest.fn() }), +})); +jest.mock("@/browser/hooks/useMuxGatewayAccountStatus", () => ({ + ...jest.requireActual( + "@/browser/hooks/useMuxGatewayAccountStatus" + ), + useMuxGatewayAccountStatus: () => ({ + data: null, + error: null, + isLoading: false, + refresh: jest.fn(), + }), +})); + +import { OnboardingWizardSplash } from "@/browser/features/SplashScreens/OnboardingWizardSplash"; + +const LOADING_TEXT = "Checking your provider configuration…"; +const GATEWAY_TEXT = "OSS contributors with GitHub accounts"; + +const dialog = () => document.body.querySelector("[role=dialog]"); +const overlay = () => document.body.querySelector("[data-modal-dialog-overlay]"); +const stepBody = () => dialog()?.querySelector("[class*='slide-in-from']") ?? null; +const showsText = (text: string) => document.body.textContent?.includes(text) === true; + +function renderWizard() { + return render(React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() })); +} + +afterEach(() => { + cleanup(); + document.body.innerHTML = ""; +}); + +describe("the onboarding wizard without a static first-run view", () => { + test("shows its loading step, then the gateway step, with the dialog's entry animation", () => { + mockProviders = { config: null, loading: true }; + const view = renderWizard(); + expect(showsText(LOADING_TEXT)).toBe(true); + expect(dialog()?.className).toContain("data-[state=open]:zoom-in-95"); + expect(overlay()?.className).toContain("data-[state=open]:fade-in-0"); + expect(dialog()?.style.animation).toBe(""); + expect(overlay()?.style.animation).toBe(""); + + mockProviders = { config: {}, loading: false }; + act(() => view.rerender(React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() }))); + expect(showsText(LOADING_TEXT)).toBe(false); + expect(showsText(GATEWAY_TEXT)).toBe(true); + expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + expect(dialog()?.style.animation).toBe(""); + }); + + test("animates each step it moves to", () => { + mockProviders = { config: {}, loading: false }; + renderWizard(); + fireEvent.click( + Array.from(dialog()!.querySelectorAll("button")).find((b) => b.textContent === "Next")! + ); + expect(showsText(GATEWAY_TEXT)).toBe(false); + expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + }); +}); From 2b12de0ef25277c24cc2db818127abfacc6e9e4a Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 13 Oct 2026 18:53:30 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A4=96=20perf:=20swap=20the=20static?= =?UTF-8?q?=20first-run=20view=20for=20the=20live=20onboarding=20wizard=20?= =?UTF-8?q?in=20one=20commit=20(T3=20C=20step=202=20PR=201c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under #xum-pre the wizard renders nothing while it loads, so its loading step never paints over the view. Its first real commit removes the view in a layout effect and opens the dialog, overlay and step without entry animations, which return once the user moves on. A wizard that opens past the gateway step removes the view too. Dialog and SplashScreen get a skipEntryAnimation prop. Inert without #xum-pre. --- src/browser/components/Dialog/Dialog.tsx | 14 ++++- .../SplashScreens/OnboardingWizardSplash.tsx | 42 ++++++++++++--- .../features/SplashScreens/SplashScreen.tsx | 3 ++ .../SplashScreens/SplashScreenProvider.tsx | 5 ++ tests/ui/onboarding/wizardHandoff.test.ts | 52 +++++++++++++++++++ 5 files changed, 106 insertions(+), 10 deletions(-) diff --git a/src/browser/components/Dialog/Dialog.tsx b/src/browser/components/Dialog/Dialog.tsx index 393e6edba8e..b49763a2f7d 100644 --- a/src/browser/components/Dialog/Dialog.tsx +++ b/src/browser/components/Dialog/Dialog.tsx @@ -42,6 +42,8 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; +const NO_ANIMATION: React.CSSProperties = { animation: "none" }; + const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { @@ -56,6 +58,8 @@ const DialogContent = React.forwardRef< * cancel an inline edit) instead of dismissing the dialog. */ allowEditableEscape?: boolean; + /** Open without the entry animation, when the same frame is already on screen. */ + skipEntryAnimation?: boolean; } >( ( @@ -66,6 +70,7 @@ const DialogContent = React.forwardRef< maxWidth, maxHeight, allowEditableEscape = false, + skipEntryAnimation = false, style, onEscapeKeyDown, ...props @@ -73,7 +78,7 @@ const DialogContent = React.forwardRef< ref ) => ( - + { @@ -101,7 +106,12 @@ const DialogContent = React.forwardRef< maxHeight && "overflow-y-auto", className )} - style={{ ...(maxWidth && { maxWidth }), ...(maxHeight && { maxHeight }), ...style }} + style={{ + ...(maxWidth && { maxWidth }), + ...(maxHeight && { maxHeight }), + ...(skipEntryAnimation && NO_ANIMATION), + ...style, + }} {...props} > {children} diff --git a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx index b923e607055..1673c87c866 100644 --- a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx +++ b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, Bot, @@ -9,7 +9,7 @@ import { Sparkles, } from "lucide-react"; import { SplashScreen } from "./SplashScreen"; -import { useOnboardingPause } from "./SplashScreenProvider"; +import { hasFirstRunView, removeFirstRunView, useOnboardingPause } from "./SplashScreenProvider"; import { DocsLink } from "@/browser/components/DocsLink/DocsLink"; import { ProviderWithIcon } from "@/browser/components/ProviderIcon/ProviderIcon"; import { @@ -193,6 +193,32 @@ function CommandPalettePreview(props: { shortcut: string }) { ); } +/** + * T3 #5971, C step 2: the server's static first-run view (#xum-pre) already shows the gateway step. + * The live wizard renders nothing while it loads under that view, then swaps it out in its first + * real commit (a layout effect runs before paint), opening without entry animations until the user + * moves on. A wizard that starts past the gateway step replaces the view too. + */ +function useFirstRunViewHandoff(isLoading: boolean) { + const [startsUnderFirstRunView] = useState(hasFirstRunView); + const [skipEntryAnimation, setSkipEntryAnimation] = useState(startsUnderFirstRunView); + useLayoutEffect(() => { + if (!isLoading) removeFirstRunView(); + }, [isLoading]); + return { + hideWhileLoading: isLoading && startsUnderFirstRunView, + skipEntryAnimation, + stopSkipping: () => setSkipEntryAnimation(false), + }; +} + +function stepEntryClassName(direction: Direction, skipEntryAnimation: boolean): string { + if (skipEntryAnimation) return ""; + return direction === "forward" + ? "animate-in fade-in-0 slide-in-from-right-2" + : "animate-in fade-in-0 slide-in-from-left-2"; +} + export function OnboardingWizardSplash(props: { onDismiss: () => void }) { const onboardingPause = useOnboardingPause(); const [stepIndex, setStepIndex] = useState(onboardingPause.paused?.stepIndex ?? 0); @@ -1043,6 +1069,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { const totalSteps = steps.length; const currentStep = steps[stepIndex] ?? steps[0]; + const firstRunView = useFirstRunViewHandoff(isLoading); useEffect(() => { if (currentStep?.key !== "mux-gateway" && muxGatewayLoginInProgress) { @@ -1050,7 +1077,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { } }, [cancelMuxGatewayLogin, currentStep?.key, muxGatewayLoginInProgress]); - if (!currentStep) { + if (!currentStep || firstRunView.hideWhileLoading) { return null; } @@ -1062,6 +1089,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { return; } setDirection("back"); + firstRunView.stopSkipping(); // Changing steps remounts the project form without its inline error, so // clear the stale flag or the "click Next" nudge stays hidden on return. setProjectFormHasError(false); @@ -1073,6 +1101,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { return; } setDirection("forward"); + firstRunView.stopSkipping(); setProjectFormHasError(false); setStepIndex((i) => Math.min(totalSteps - 1, i + 1)); }; @@ -1091,6 +1120,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { props.onDismiss(); }} dismissLabel={null} + skipEntryAnimation={firstRunView.skipEntryAnimation} footerClassName="justify-between" footer={ <> @@ -1152,11 +1182,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
diff --git a/src/browser/features/SplashScreens/SplashScreen.tsx b/src/browser/features/SplashScreens/SplashScreen.tsx index 0ba4827a4d7..e11649200a3 100644 --- a/src/browser/features/SplashScreens/SplashScreen.tsx +++ b/src/browser/features/SplashScreens/SplashScreen.tsx @@ -25,6 +25,8 @@ interface SplashScreenProps { footer?: React.ReactNode; /** Optional class name for the DialogFooter container. */ footerClassName?: string; + /** Open without the dialog's entry animation (the static first-run view shows this frame). */ + skipEntryAnimation?: boolean; } export function SplashScreen(props: SplashScreenProps) { @@ -47,6 +49,7 @@ export function SplashScreen(props: SplashScreenProps) { e.preventDefault()} > diff --git a/src/browser/features/SplashScreens/SplashScreenProvider.tsx b/src/browser/features/SplashScreens/SplashScreenProvider.tsx index 29aa42e16ad..9f639cd71e6 100644 --- a/src/browser/features/SplashScreens/SplashScreenProvider.tsx +++ b/src/browser/features/SplashScreens/SplashScreenProvider.tsx @@ -21,6 +21,11 @@ import { updatePersistedState } from "@/browser/hooks/usePersistedState"; import { FIRST_RUN_VIEW_ID } from "@/common/constants/firstRunView"; import { ONBOARDING_VIEWED_KEY } from "@/common/constants/storage"; +/** Whether the server's static first-run view is on screen (T3 #5971, C step 2). */ +export function hasFirstRunView(): boolean { + return document.getElementById(FIRST_RUN_VIEW_ID) != null; +} + /** Removes the static first-run view. Callers do it whenever the live wizard will not replace it. */ export function removeFirstRunView(): void { document.getElementById(FIRST_RUN_VIEW_ID)?.remove(); diff --git a/tests/ui/onboarding/wizardHandoff.test.ts b/tests/ui/onboarding/wizardHandoff.test.ts index ed7b08f5e79..ce90a37a648 100644 --- a/tests/ui/onboarding/wizardHandoff.test.ts +++ b/tests/ui/onboarding/wizardHandoff.test.ts @@ -91,3 +91,55 @@ describe("the onboarding wizard without a static first-run view", () => { expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); }); }); + +describe("the onboarding wizard under the static first-run view (#xum-pre)", () => { + const firstRunView = () => document.getElementById("xum-pre"); + const clickNext = () => + fireEvent.click( + Array.from(dialog()!.querySelectorAll("button")).find((b) => b.textContent === "Next")! + ); + + beforeEach(() => { + const view = document.createElement("div"); + view.id = "xum-pre"; + document.body.append(view); + }); + + test("never paints its loading step, and no commit shows the gateway step and the static view together", () => { + // A Profiler's onRender runs in each commit of the wizard, after its layout effects and + // before passive effects: the point where the browser would paint. + const viewAtGatewayCommit: boolean[] = []; + const onRender = () => { + if (showsText(GATEWAY_TEXT)) viewAtGatewayCommit.push(firstRunView() != null); + }; + const tree = () => + React.createElement( + React.Profiler, + { id: "wizard", onRender }, + React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() }) + ); + mockProviders = { config: null, loading: true }; + const view = render(tree()); + expect(dialog()).toBeNull(); + expect(firstRunView()).not.toBeNull(); + + mockProviders = { config: {}, loading: false }; + act(() => view.rerender(tree())); + expect(viewAtGatewayCommit.length).toBeGreaterThan(0); + expect(viewAtGatewayCommit).not.toContain(true); + expect(firstRunView()).toBeNull(); + // The static view showed this frame, so nothing animates in, until the user moves on. + expect(dialog()?.style.animation).toBe("none"); + expect(overlay()?.style.animation).toBe("none"); + expect(stepBody()).toBeNull(); + clickNext(); + expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + }); + + test("removes the static view when it opens past the gateway step", () => { + mockProviders = { config: { anthropic: { isConfigured: true } }, loading: false }; + renderWizard(); + expect(showsText(GATEWAY_TEXT)).toBe(false); + expect(firstRunView()).toBeNull(); + }); +}); From d7cf9385b45a0327e0f52fa1436fa336d30c77cb Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 13 Oct 2026 20:19:22 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A4=96=20fix:=20keep=20the=20dialog's?= =?UTF-8?q?=20entry=20animation=20off=20for=20the=20whole=20handoff=20moun?= =?UTF-8?q?t=20(PR=201c=20review)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex P2 on #6381: clicking Next cleared the dialog and overlay's animation:none while both stayed open, so their entry animation replayed on step 2. Only the step's skip clears on Next now. The test checks the dialog and overlay stay without animation. --- .../SplashScreens/OnboardingWizardSplash.tsx | 17 ++++++++++------- tests/ui/onboarding/wizardHandoff.test.ts | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx index 1673c87c866..dcf528d56cd 100644 --- a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx +++ b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx @@ -196,19 +196,22 @@ function CommandPalettePreview(props: { shortcut: string }) { /** * T3 #5971, C step 2: the server's static first-run view (#xum-pre) already shows the gateway step. * The live wizard renders nothing while it loads under that view, then swaps it out in its first - * real commit (a layout effect runs before paint), opening without entry animations until the user - * moves on. A wizard that starts past the gateway step replaces the view too. + * real commit (a layout effect runs before paint). The dialog and overlay never play their entry + * animation in this mount: they stay open, so clearing the override later would replay it. The + * step skips its entry animation only until the user moves on. A wizard that starts past the + * gateway step replaces the view too. */ function useFirstRunViewHandoff(isLoading: boolean) { const [startsUnderFirstRunView] = useState(hasFirstRunView); - const [skipEntryAnimation, setSkipEntryAnimation] = useState(startsUnderFirstRunView); + const [skipStepEntry, setSkipStepEntry] = useState(startsUnderFirstRunView); useLayoutEffect(() => { if (!isLoading) removeFirstRunView(); }, [isLoading]); return { hideWhileLoading: isLoading && startsUnderFirstRunView, - skipEntryAnimation, - stopSkipping: () => setSkipEntryAnimation(false), + skipDialogEntry: startsUnderFirstRunView, + skipStepEntry, + stopSkipping: () => setSkipStepEntry(false), }; } @@ -1120,7 +1123,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { props.onDismiss(); }} dismissLabel={null} - skipEntryAnimation={firstRunView.skipEntryAnimation} + skipEntryAnimation={firstRunView.skipDialogEntry} footerClassName="justify-between" footer={ <> @@ -1182,7 +1185,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
diff --git a/tests/ui/onboarding/wizardHandoff.test.ts b/tests/ui/onboarding/wizardHandoff.test.ts index ce90a37a648..9c411c71957 100644 --- a/tests/ui/onboarding/wizardHandoff.test.ts +++ b/tests/ui/onboarding/wizardHandoff.test.ts @@ -134,6 +134,9 @@ describe("the onboarding wizard under the static first-run view (#xum-pre)", () expect(stepBody()).toBeNull(); clickNext(); expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + // The dialog and overlay stay open, so dropping their override would replay their entry. + expect(dialog()?.style.animation).toBe("none"); + expect(overlay()?.style.animation).toBe("none"); }); test("removes the static view when it opens past the gateway step", () => { From 2490f12fff4952f2f8169815293fba6c114558e1 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 13 Oct 2026 20:47:04 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A4=96=20tests:=20find=20onboarding?= =?UTF-8?q?=20steps=20by=20key,=20not=20by=20their=20copy=20(PR=201c=20rev?= =?UTF-8?q?iew)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex P2 on #6381: the handoff tests matched the loading and gateway prose, so a reword would fail them with no behavior change. The step element now carries data-onboarding-step={key}, and the tests read it. --- .../SplashScreens/OnboardingWizardSplash.tsx | 1 + tests/ui/onboarding/wizardHandoff.test.ts | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx index dcf528d56cd..35dc6ffebe6 100644 --- a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx +++ b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx @@ -1185,6 +1185,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
diff --git a/tests/ui/onboarding/wizardHandoff.test.ts b/tests/ui/onboarding/wizardHandoff.test.ts index 9c411c71957..c087c437aeb 100644 --- a/tests/ui/onboarding/wizardHandoff.test.ts +++ b/tests/ui/onboarding/wizardHandoff.test.ts @@ -46,13 +46,16 @@ jest.mock("@/browser/hooks/useMuxGatewayAccountStatus", () => ({ import { OnboardingWizardSplash } from "@/browser/features/SplashScreens/OnboardingWizardSplash"; -const LOADING_TEXT = "Checking your provider configuration…"; -const GATEWAY_TEXT = "OSS contributors with GitHub accounts"; +// Steps are found by their key (data-onboarding-step), so rewording the copy does not break these tests. +const LOADING_STEP = "loading"; +const GATEWAY_STEP = "mux-gateway"; const dialog = () => document.body.querySelector("[role=dialog]"); const overlay = () => document.body.querySelector("[data-modal-dialog-overlay]"); -const stepBody = () => dialog()?.querySelector("[class*='slide-in-from']") ?? null; -const showsText = (text: string) => document.body.textContent?.includes(text) === true; +const step = () => document.body.querySelector("[data-onboarding-step]"); +const shows = (key: string) => step()?.dataset.onboardingStep === key; +const animatesIn = () => + step()?.className.includes("animate-in fade-in-0 slide-in-from-right-2") === true; function renderWizard() { return render(React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() })); @@ -67,7 +70,7 @@ describe("the onboarding wizard without a static first-run view", () => { test("shows its loading step, then the gateway step, with the dialog's entry animation", () => { mockProviders = { config: null, loading: true }; const view = renderWizard(); - expect(showsText(LOADING_TEXT)).toBe(true); + expect(shows(LOADING_STEP)).toBe(true); expect(dialog()?.className).toContain("data-[state=open]:zoom-in-95"); expect(overlay()?.className).toContain("data-[state=open]:fade-in-0"); expect(dialog()?.style.animation).toBe(""); @@ -75,9 +78,9 @@ describe("the onboarding wizard without a static first-run view", () => { mockProviders = { config: {}, loading: false }; act(() => view.rerender(React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() }))); - expect(showsText(LOADING_TEXT)).toBe(false); - expect(showsText(GATEWAY_TEXT)).toBe(true); - expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + expect(shows(LOADING_STEP)).toBe(false); + expect(shows(GATEWAY_STEP)).toBe(true); + expect(animatesIn()).toBe(true); expect(dialog()?.style.animation).toBe(""); }); @@ -87,8 +90,8 @@ describe("the onboarding wizard without a static first-run view", () => { fireEvent.click( Array.from(dialog()!.querySelectorAll("button")).find((b) => b.textContent === "Next")! ); - expect(showsText(GATEWAY_TEXT)).toBe(false); - expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + expect(shows(GATEWAY_STEP)).toBe(false); + expect(animatesIn()).toBe(true); }); }); @@ -110,7 +113,7 @@ describe("the onboarding wizard under the static first-run view (#xum-pre)", () // before passive effects: the point where the browser would paint. const viewAtGatewayCommit: boolean[] = []; const onRender = () => { - if (showsText(GATEWAY_TEXT)) viewAtGatewayCommit.push(firstRunView() != null); + if (shows(GATEWAY_STEP)) viewAtGatewayCommit.push(firstRunView() != null); }; const tree = () => React.createElement( @@ -131,9 +134,9 @@ describe("the onboarding wizard under the static first-run view (#xum-pre)", () // The static view showed this frame, so nothing animates in, until the user moves on. expect(dialog()?.style.animation).toBe("none"); expect(overlay()?.style.animation).toBe("none"); - expect(stepBody()).toBeNull(); + expect(animatesIn()).toBe(false); clickNext(); - expect(stepBody()?.className).toContain("animate-in fade-in-0 slide-in-from-right-2"); + expect(animatesIn()).toBe(true); // The dialog and overlay stay open, so dropping their override would replay their entry. expect(dialog()?.style.animation).toBe("none"); expect(overlay()?.style.animation).toBe("none"); @@ -142,7 +145,7 @@ describe("the onboarding wizard under the static first-run view (#xum-pre)", () test("removes the static view when it opens past the gateway step", () => { mockProviders = { config: { anthropic: { isConfigured: true } }, loading: false }; renderWizard(); - expect(showsText(GATEWAY_TEXT)).toBe(false); + expect(shows(GATEWAY_STEP)).toBe(false); expect(firstRunView()).toBeNull(); }); }); From 930eb4031cebaba16ffdcadbe5b991bdd29e4ef6 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 14 Oct 2026 03:43:11 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A4=96=20tests:=20mock=20useLiveAPI?= =?UTF-8?q?=20in=20the=20wizard=20handoff=20tests=20(rebase=20after=20PR?= =?UTF-8?q?=201a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After 1a's squash merge, the stack sits on main, where #6323 made OnboardingWizardSplash call useLiveAPI(). The test's API mock exported only useAPI, so every wizard handoff test threw "useLiveAPI is not a function". The mock now returns null for it, as useLiveAPI does with no API. --- tests/ui/onboarding/wizardHandoff.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/ui/onboarding/wizardHandoff.test.ts b/tests/ui/onboarding/wizardHandoff.test.ts index c087c437aeb..cfe01647b81 100644 --- a/tests/ui/onboarding/wizardHandoff.test.ts +++ b/tests/ui/onboarding/wizardHandoff.test.ts @@ -28,7 +28,11 @@ jest.mock("@/browser/contexts/SettingsContext", () => ({ jest.mock("@/browser/contexts/ProjectContext", () => ({ useProjectContext: () => ({ addProject: jest.fn(), userProjects: new Map() }), })); -jest.mock("@/browser/contexts/API", () => ({ useAPI: () => ({ api: null }) })); +// useLiveAPI is main's live-client hook (#6323): with no API it is null, like useAPI's api. +jest.mock("@/browser/contexts/API", () => ({ + useAPI: () => ({ api: null }), + useLiveAPI: () => null, +})); jest.mock("@/browser/hooks/useRouting", () => ({ useRouting: () => ({ navigateToProject: jest.fn() }), }));