diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx new file mode 100644 index 00000000000..456599b4733 --- /dev/null +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx @@ -0,0 +1,74 @@ +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { getLocalStorage, setLocalStorage } from "../../../util/localStorage"; +import { renderWithProviders } from "../../../util/test/render"; +import { useOnboardingCard } from "./useOnboardingCard"; + +/** + * Minimal probe that surfaces the hook's state and actions to the DOM, so the + * hook can be exercised through the same providers the real card renders in. + */ +function OnboardingCardProbe() { + const { show, open, close } = useOnboardingCard(); + + return ( +
+ {String(show)} + + +
+ ); +} + +describe("useOnboardingCard", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("shows the card by default for a user who has not onboarded", async () => { + await renderWithProviders(); + + expect(screen.getByTestId("show").textContent).toBe("true"); + }); + + it("persists the dismissal when the card is closed", async () => { + const { user } = await renderWithProviders(); + + await user.click(screen.getByTestId("close")); + + expect(screen.getByTestId("show").textContent).toBe("false"); + expect(getLocalStorage("hasDismissedOnboardingCard")).toBe(true); + }); + + // Regression test for https://github.com/continuedev/continue/issues/12582 + it("clears the persisted dismissal when the card is reopened", async () => { + setLocalStorage("hasDismissedOnboardingCard", true); + + const { user } = await renderWithProviders(); + expect(screen.getByTestId("show").textContent).toBe("false"); + + await user.click(screen.getByTestId("open")); + + expect(screen.getByTestId("show").textContent).toBe("true"); + expect(getLocalStorage("hasDismissedOnboardingCard")).toBe(false); + }); + + // The redux `show` flag is not persisted by redux-persist, so reopening has + // to clear the localStorage flag or the card vanishes again on next load. + it("keeps the card visible after a reload once it has been reopened", async () => { + setLocalStorage("hasDismissedOnboardingCard", true); + + const first = await renderWithProviders(); + await first.user.click(screen.getByTestId("open")); + first.unmount(); + + // A fresh store stands in for a webview reload: only localStorage carries over. + await renderWithProviders(); + + expect(screen.getByTestId("show").textContent).toBe("true"); + }); +}); diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts index 75bc28adcfa..253839e83ab 100644 --- a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts @@ -38,6 +38,9 @@ export function useOnboardingCard(): UseOnboardingCard { } async function open(tab?: OnboardingModes) { + // Clear the dismissal flag set by `close`. The redux `show` flag is not + // persisted, so without this the card disappears again on the next reload. + setLocalStorage("hasDismissedOnboardingCard", false); navigate("/"); dispatch( setOnboardingCard({ diff --git a/gui/src/pages/config/sections/HelpSection.tsx b/gui/src/pages/config/sections/HelpSection.tsx index 1d1b2febc0f..1d16cafe285 100644 --- a/gui/src/pages/config/sections/HelpSection.tsx +++ b/gui/src/pages/config/sections/HelpSection.tsx @@ -8,10 +8,10 @@ import { import { useContext, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import Shortcut from "../../../components/gui/Shortcut"; +import { useOnboardingCard } from "../../../components/OnboardingCard/hooks/useOnboardingCard"; import { Card } from "../../../components/ui"; import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; -import { setOnboardingCard } from "../../../redux/slices/uiSlice"; import { saveCurrentSession } from "../../../redux/thunks/session"; import { isJetBrains } from "../../../util"; import { ROUTES } from "../../../util/navigation"; @@ -150,6 +150,7 @@ export function HelpSection() { const ideMessenger = useContext(IdeMessengerContext); const navigate = useNavigate(); const dispatch = useAppDispatch(); + const onboardingCard = useOnboardingCard(); const currentSession = useAppSelector((state) => state.session); @@ -226,12 +227,7 @@ export function HelpSection() { generateTitle: true, }), ); - dispatch( - setOnboardingCard({ - show: true, - activeTab: undefined, - }), - ); + void onboardingCard.open(); ideMessenger.post("showTutorial", undefined); }} />