Files
fusion/packages/dashboard/app/components/__tests__/PostOnboardingRecommendations.test.tsx
Fusion f5268019f9 feat(FN-3260): document static slot-host rendering contract
Documents the static slot-host rendering contract in `docs/PLUGIN_AUTHORING.md`, clarifying how plugins should interact with the rendering system.

Fusion-Task-Id: FN-3260
2026-05-04 21:21:22 -07:00

249 lines
8.7 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations";
const mockFetchAuthStatus = vi.fn();
const mockFetchGlobalSettings = vi.fn();
vi.mock("../../api", () => ({
fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args),
fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args),
}));
const mockIsOnboardingCompleted = vi.fn();
const mockIsPostOnboardingDismissed = vi.fn();
const mockDismissPostOnboardingRecommendations = vi.fn();
vi.mock("../model-onboarding-state", () => ({
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
}));
vi.mock("../PluginSlot", () => ({
PluginSlot: ({ slotId, actions }: { slotId: string; actions?: { openSettingsSection?: (section: string) => void; openModelOnboarding?: () => void } }) => (
<div data-testid={`plugin-slot-${slotId}`}>
<button type="button" onClick={() => actions?.openSettingsSection?.("authentication")}>plugin-open-settings</button>
<button type="button" onClick={() => actions?.openModelOnboarding?.()}>plugin-open-onboarding</button>
</div>
),
}));
vi.mock("lucide-react", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
X: () => <span data-testid="icon-x">X</span>,
Lightbulb: () => <span data-testid="icon-lightbulb">Lightbulb</span>,
CheckCircle: () => <span data-testid="icon-check-circle">CheckCircle</span>,
AlertCircle: () => <span data-testid="icon-alert-circle">AlertCircle</span>,
Key: () => <span data-testid="icon-key">Key</span>,
GitPullRequest: () => <span data-testid="icon-git-pull-request">GitPullRequest</span>,
Zap: () => <span data-testid="icon-zap">Zap</span>,
};
});
describe("PostOnboardingRecommendations", () => {
const onOpenSettings = vi.fn();
const onOpenModelOnboarding = vi.fn();
const renderComponent = () => render(
<PostOnboardingRecommendations
onOpenSettings={onOpenSettings}
onOpenModelOnboarding={onOpenModelOnboarding}
/>,
);
beforeEach(() => {
vi.clearAllMocks();
mockIsOnboardingCompleted.mockReturnValue(true);
mockIsPostOnboardingDismissed.mockReturnValue(false);
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: true },
],
});
mockFetchGlobalSettings.mockResolvedValue({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
});
it("renders nothing when onboarding is not completed", () => {
mockIsOnboardingCompleted.mockReturnValue(false);
const { container } = renderComponent();
expect(container.firstChild).toBeNull();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
});
it("renders nothing when post-onboarding is dismissed", () => {
mockIsPostOnboardingDismissed.mockReturnValue(true);
const { container } = renderComponent();
expect(container.firstChild).toBeNull();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
});
it("renders nothing when all setup items are complete", async () => {
const { container } = renderComponent();
await waitFor(() => {
expect(mockFetchAuthStatus).toHaveBeenCalledTimes(1);
expect(mockFetchGlobalSettings).toHaveBeenCalledTimes(1);
});
expect(container.firstChild).toBeNull();
});
it("renders Connect AI Provider recommendation when no AI provider is authenticated", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
expect(await screen.findByText("Connect AI Provider")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Set Up AI" })).toBeInTheDocument();
});
it("renders Select Default Model recommendation when no default model is set", async () => {
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
expect(await screen.findByText("Select Default Model")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Choose Model" })).toBeInTheDocument();
});
it("renders Connect GitHub recommendation when GitHub exists but is not authenticated", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: false },
],
});
renderComponent();
expect(await screen.findByText("Connect GitHub to import issues and track pull requests")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect GitHub" })).toBeInTheDocument();
});
it("renders multiple recommendations when multiple setup items are incomplete", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: false },
],
});
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
expect(await screen.findByText("Connect AI Provider")).toBeInTheDocument();
expect(screen.getByText("Select Default Model")).toBeInTheDocument();
expect(screen.getAllByText("Connect GitHub").length).toBeGreaterThan(0);
});
it("clicking Set Up AI calls onOpenModelOnboarding", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
const button = await screen.findByRole("button", { name: "Set Up AI" });
fireEvent.click(button);
expect(onOpenModelOnboarding).toHaveBeenCalledTimes(1);
});
it("clicking Choose Model calls onOpenSettings with global-models", async () => {
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
const button = await screen.findByRole("button", { name: "Choose Model" });
fireEvent.click(button);
expect(onOpenSettings).toHaveBeenCalledWith("global-models");
});
it("clicking Connect GitHub calls onOpenSettings with authentication", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: false },
],
});
renderComponent();
const button = await screen.findByRole("button", { name: "Connect GitHub" });
fireEvent.click(button);
expect(onOpenSettings).toHaveBeenCalledWith("authentication");
});
it("clicking dismiss calls dismissPostOnboardingRecommendations", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
const dismissButton = await screen.findByRole("button", { name: "Dismiss recommendations" });
fireEvent.click(dismissButton);
expect(mockDismissPostOnboardingRecommendations).toHaveBeenCalledTimes(1);
});
it("passes host callbacks to plugin recommendation slot", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
await screen.findByTestId("plugin-slot-post-onboarding-recommendation");
fireEvent.click(screen.getByRole("button", { name: "plugin-open-settings" }));
fireEvent.click(screen.getByRole("button", { name: "plugin-open-onboarding" }));
expect(onOpenSettings).toHaveBeenCalledWith("authentication");
expect(onOpenModelOnboarding).toHaveBeenCalledTimes(1);
});
it("returns null on API error", async () => {
mockFetchAuthStatus.mockRejectedValue(new Error("network failure"));
const { container } = renderComponent();
await waitFor(() => {
expect(mockFetchAuthStatus).toHaveBeenCalledTimes(1);
});
expect(container.firstChild).toBeNull();
});
});