feat(FN-2399): merge fusion/fn-2399

This commit is contained in:
gsxdsm
2026-04-24 00:43:15 -07:00
parent 2f76dea35f
commit 8c3aef9526
2 changed files with 103 additions and 2 deletions

View File

@@ -498,6 +498,7 @@ export function ModelOnboardingModal({
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
const [apiKeySuccess, setApiKeySuccess] = useState<Record<string, string | null>>({});
const apiKeySuccessTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
const onboardingContentRef = useRef<HTMLDivElement | null>(null);
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
@@ -982,6 +983,30 @@ export function ModelOnboardingModal({
});
}, []);
const scrollOnboardingContentToTop = useCallback(() => {
const content = onboardingContentRef.current;
if (!content) {
return;
}
const prefersReducedMotion =
typeof window !== "undefined"
&& typeof window.matchMedia === "function"
&& window.matchMedia("(prefers-reduced-motion: reduce)").matches;
try {
if (typeof content.scrollTo === "function") {
content.scrollTo({ top: 0, behavior: prefersReducedMotion ? "auto" : "smooth" });
return;
}
} catch {
// Fall through to direct scrollTop assignment for environments
// without ScrollToOptions support.
}
content.scrollTop = 0;
}, []);
// API key save handler
const handleSaveApiKey = useCallback(
async (providerId: string, keyValue?: string) => {
@@ -1019,6 +1044,7 @@ export function ModelOnboardingModal({
try {
await saveApiKey(providerId, key);
await loadAuthStatus();
scrollOnboardingContentToTop();
setApiKeyInputs((prev) => {
const next = { ...prev };
@@ -1077,7 +1103,7 @@ export function ModelOnboardingModal({
setAuthActionInProgress(null);
}
},
[apiKeyInputs, addToast, loadAuthStatus],
[apiKeyInputs, addToast, loadAuthStatus, scrollOnboardingContentToTop],
);
// API key clear handler
@@ -1507,7 +1533,7 @@ export function ModelOnboardingModal({
</div>
{/* Content */}
<div className="model-onboarding-content">
<div className="model-onboarding-content" ref={onboardingContentRef}>
{step === "ai-setup" && (
<div className="model-onboarding-ai-setup">
<p className="model-onboarding-description">

View File

@@ -512,6 +512,81 @@ describe("ModelOnboardingModal", () => {
});
});
it("scrolls onboarding content to top after successful API key save", async () => {
mockFetchAuthStatus
.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
],
})
.mockResolvedValueOnce({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
{ id: "openai", name: "OpenAI", authenticated: true, type: "api_key" },
],
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await waitFor(() => {
expect(screen.getByTestId("onboarding-apikey-input-openai")).toBeTruthy();
});
const content = document.querySelector(".model-onboarding-content") as HTMLDivElement;
expect(content).toBeTruthy();
const scrollToMock = vi.fn();
Object.defineProperty(content, "scrollTo", {
value: scrollToMock,
writable: true,
});
content.scrollTop = 240;
fireEvent.change(screen.getByTestId("onboarding-apikey-input-openai"), {
target: { value: "sk-scroll-success" },
});
fireEvent.click(screen.getByTestId("onboarding-apikey-save-openai"));
await waitFor(() => {
expect(mockSaveApiKey).toHaveBeenCalledWith("openai", "sk-scroll-success");
expect(scrollToMock).toHaveBeenCalledWith({ top: 0, behavior: "smooth" });
});
await waitFor(() => {
expect(screen.getByText("✓ API key saved")).toBeTruthy();
});
});
it("does not scroll onboarding content to top when API key save fails", async () => {
mockSaveApiKey.mockRejectedValueOnce(new Error("save failed"));
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await waitFor(() => {
expect(screen.getByTestId("onboarding-apikey-input-openai")).toBeTruthy();
});
const content = document.querySelector(".model-onboarding-content") as HTMLDivElement;
expect(content).toBeTruthy();
const scrollToMock = vi.fn();
Object.defineProperty(content, "scrollTo", {
value: scrollToMock,
writable: true,
});
content.scrollTop = 180;
fireEvent.change(screen.getByTestId("onboarding-apikey-input-openai"), {
target: { value: "sk-scroll-fail" },
});
fireEvent.click(screen.getByTestId("onboarding-apikey-save-openai"));
await waitFor(() => {
expect(screen.getByText("save failed")).toBeTruthy();
});
expect(scrollToMock).not.toHaveBeenCalled();
expect(content.scrollTop).toBe(180);
});
it("submits API key when Enter is pressed", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);