feat(FN-1934): add GitHub onboarding status feedback states
- Derive GitHub-specific onboarding status from auth state, login outcomes, and persisted skip metadata - Replace static GitHub auth text with status badge variants and contextual success/error/info feedback blocks - Add retry, skip, and connect-anyway actions, including persistence and reset of skipped state on successful auth - Style new GitHub feedback UI and badge states using dashboard tokens with mobile touch-target adjustments - Expand ModelOnboardingModal tests to cover connected, pending, failed, skipped, and recovery flows
This commit is contained in:
@@ -284,6 +284,9 @@ export type LoginOutcome = "pending" | "success" | "timeout" | "failed" | "cance
|
|||||||
/** Provider connection status for UI display */
|
/** Provider connection status for UI display */
|
||||||
export type ProviderConnectionStatus = "connected" | "not-connected" | "skipped" | "retry";
|
export type ProviderConnectionStatus = "connected" | "not-connected" | "skipped" | "retry";
|
||||||
|
|
||||||
|
/** GitHub-specific status variants for richer connection feedback */
|
||||||
|
type GitHubConnectionStatus = "connected" | "failed" | "pending" | "skipped" | "not-connected";
|
||||||
|
|
||||||
/** Maximum number of poll cycles before timing out (150 × 2s = 5 minutes) */
|
/** Maximum number of poll cycles before timing out (150 × 2s = 5 minutes) */
|
||||||
const MAX_POLL_CYCLES = 150;
|
const MAX_POLL_CYCLES = 150;
|
||||||
|
|
||||||
@@ -322,6 +325,10 @@ export function ModelOnboardingModal({
|
|||||||
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
|
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
|
||||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
|
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
|
||||||
|
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
|
||||||
|
const state = getOnboardingState();
|
||||||
|
return state?.stepData?.github?.skipped === true;
|
||||||
|
});
|
||||||
const pollCountRef = useRef<number>(0);
|
const pollCountRef = useRef<number>(0);
|
||||||
|
|
||||||
// Initialize skippedProviders from persisted state
|
// Initialize skippedProviders from persisted state
|
||||||
@@ -408,6 +415,11 @@ export function ModelOnboardingModal({
|
|||||||
aiSetupReturnRef.current = step !== "ai-setup";
|
aiSetupReturnRef.current = step !== "ai-setup";
|
||||||
}, [step, loadAuthStatus]);
|
}, [step, loadAuthStatus]);
|
||||||
|
|
||||||
|
// Check if GitHub provider is configured and currently authenticated
|
||||||
|
const githubProvider = authProviders.find((p) => p.id === "github");
|
||||||
|
const hasGithubProvider = !!githubProvider;
|
||||||
|
const isGithubAuthenticated = githubProvider?.authenticated ?? false;
|
||||||
|
|
||||||
// Get provider connection status for UI display
|
// Get provider connection status for UI display
|
||||||
const getProviderStatus = useCallback((provider: AuthProvider): ProviderConnectionStatus => {
|
const getProviderStatus = useCallback((provider: AuthProvider): ProviderConnectionStatus => {
|
||||||
if (provider.authenticated) {
|
if (provider.authenticated) {
|
||||||
@@ -444,6 +456,46 @@ export function ModelOnboardingModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getGitHubStatus = useCallback((): GitHubConnectionStatus => {
|
||||||
|
if (isGithubAuthenticated) {
|
||||||
|
return "connected";
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubOutcome = loginOutcomes["github"];
|
||||||
|
if (githubOutcome === "pending") {
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
if (githubOutcome === "failed" || githubOutcome === "timeout") {
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
if (isGithubSkipped) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "not-connected";
|
||||||
|
}, [isGithubAuthenticated, loginOutcomes, isGithubSkipped]);
|
||||||
|
|
||||||
|
function GitHubStatusBadge({ status }: { status: GitHubConnectionStatus }) {
|
||||||
|
const config: Record<GitHubConnectionStatus, { text: string; className: string }> = {
|
||||||
|
connected: { text: "✓ Connected", className: "auth-status-badge connected" },
|
||||||
|
pending: { text: "⏳ Connecting…", className: "auth-status-badge pending" },
|
||||||
|
failed: { text: "✗ Connection failed", className: "auth-status-badge retry" },
|
||||||
|
skipped: { text: "Skipped", className: "auth-status-badge skipped" },
|
||||||
|
"not-connected": { text: "Not connected", className: "auth-status-badge not-connected" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { text, className: badgeClassName } = config[status];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-testid="github-status-badge"
|
||||||
|
className={badgeClassName}
|
||||||
|
data-status={status}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Load models
|
// Load models
|
||||||
const loadModels = useCallback(async () => {
|
const loadModels = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -517,11 +569,6 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
}, [loginOutcomes, persistLoginOutcome]);
|
}, [loginOutcomes, persistLoginOutcome]);
|
||||||
|
|
||||||
// Check if we have GitHub provider
|
|
||||||
const githubProvider = authProviders.find((p) => p.id === "github");
|
|
||||||
const hasGithubProvider = !!githubProvider;
|
|
||||||
const isGithubAuthenticated = githubProvider?.authenticated ?? false;
|
|
||||||
|
|
||||||
// Cleanup polling on unmount
|
// Cleanup polling on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -554,6 +601,25 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
}, [step]);
|
}, [step]);
|
||||||
|
|
||||||
|
const setGitHubSkippedState = useCallback((skipped: boolean) => {
|
||||||
|
setIsGithubSkipped(skipped);
|
||||||
|
saveOnboardingState(step, {
|
||||||
|
completedSteps,
|
||||||
|
stepData: {
|
||||||
|
github: {
|
||||||
|
skipped,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [step, completedSteps]);
|
||||||
|
|
||||||
|
const handleSkipGitHubStep = useCallback(() => {
|
||||||
|
if (!isGithubAuthenticated) {
|
||||||
|
setGitHubSkippedState(true);
|
||||||
|
}
|
||||||
|
handleNext();
|
||||||
|
}, [isGithubAuthenticated, setGitHubSkippedState, handleNext]);
|
||||||
|
|
||||||
// OAuth login handler
|
// OAuth login handler
|
||||||
const handleLogin = useCallback(
|
const handleLogin = useCallback(
|
||||||
async (providerId: string) => {
|
async (providerId: string) => {
|
||||||
@@ -603,6 +669,9 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
setAuthActionInProgress(null);
|
setAuthActionInProgress(null);
|
||||||
setLoginOutcomes((prev) => ({ ...prev, [providerId]: "success" }));
|
setLoginOutcomes((prev) => ({ ...prev, [providerId]: "success" }));
|
||||||
|
if (providerId === "github") {
|
||||||
|
setGitHubSkippedState(false);
|
||||||
|
}
|
||||||
addToast("Login successful", "success");
|
addToast("Login successful", "success");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -625,7 +694,7 @@ export function ModelOnboardingModal({
|
|||||||
setAuthActionInProgress(null);
|
setAuthActionInProgress(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addToast],
|
[addToast, setGitHubSkippedState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cancellation handler for in-progress logins
|
// Cancellation handler for in-progress logins
|
||||||
@@ -893,6 +962,8 @@ export function ModelOnboardingModal({
|
|||||||
const aiOauthProviders = oauthProviders.filter((p) => p.id !== "github");
|
const aiOauthProviders = oauthProviders.filter((p) => p.id !== "github");
|
||||||
const aiApiKeyProviders = apiKeyProviders.filter((p) => p.id !== "github");
|
const aiApiKeyProviders = apiKeyProviders.filter((p) => p.id !== "github");
|
||||||
|
|
||||||
|
const githubStatus = getGitHubStatus();
|
||||||
|
|
||||||
// Skip-state detection: derived state for informational banners
|
// Skip-state detection: derived state for informational banners
|
||||||
// Detects whether at least one AI provider is connected (excludes GitHub)
|
// Detects whether at least one AI provider is connected (excludes GitHub)
|
||||||
const hasAiProvider = authProviders.some((p) => p.id !== "github" && p.authenticated);
|
const hasAiProvider = authProviders.some((p) => p.id !== "github" && p.authenticated);
|
||||||
@@ -1307,13 +1378,8 @@ export function ModelOnboardingModal({
|
|||||||
<GitPullRequest size={16} style={{ marginRight: 8 }} />
|
<GitPullRequest size={16} style={{ marginRight: 8 }} />
|
||||||
GitHub
|
GitHub
|
||||||
</strong>
|
</strong>
|
||||||
<span
|
<span data-testid="onboarding-auth-status-github">
|
||||||
data-testid="onboarding-auth-status-github"
|
<GitHubStatusBadge status={githubStatus} />
|
||||||
className={`auth-status-badge ${isGithubAuthenticated ? "connected" : "not-connected"}`}
|
|
||||||
>
|
|
||||||
{isGithubAuthenticated
|
|
||||||
? "✓ Connected"
|
|
||||||
: "Not connected"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isGithubAuthenticated && (
|
{isGithubAuthenticated && (
|
||||||
@@ -1332,7 +1398,7 @@ export function ModelOnboardingModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isGithubAuthenticated && (
|
{(githubStatus === "not-connected" || githubStatus === "pending") && (
|
||||||
<div className="onboarding-github-connect-cta" data-testid="onboarding-github-connect-cta">
|
<div className="onboarding-github-connect-cta" data-testid="onboarding-github-connect-cta">
|
||||||
{authActionInProgress === "github" ? (
|
{authActionInProgress === "github" ? (
|
||||||
<div className="onboarding-github-connect-actions">
|
<div className="onboarding-github-connect-actions">
|
||||||
@@ -1355,23 +1421,56 @@ export function ModelOnboardingModal({
|
|||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show timeout message */}
|
|
||||||
{loginOutcomes["github"] === "timeout" && authActionInProgress !== "github" && (
|
|
||||||
<p className="onboarding-helper-text onboarding-github-connect-feedback">
|
|
||||||
Login timed out. Please try again.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{/* Show failure message */}
|
|
||||||
{loginOutcomes["github"] === "failed" && authActionInProgress !== "github" && (
|
|
||||||
<p className="field-error onboarding-github-connect-feedback">
|
|
||||||
Login failed. Please try again.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isGithubAuthenticated && (
|
{githubStatus === "connected" && (
|
||||||
|
<div className="onboarding-github-feedback onboarding-github-feedback--success">
|
||||||
|
GitHub is connected. You can import issues and track pull requests.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{githubStatus === "failed" && (
|
||||||
|
<div className="onboarding-github-feedback onboarding-github-feedback--error">
|
||||||
|
<p>Connection failed or timed out.</p>
|
||||||
|
<div className="onboarding-github-feedback-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => handleLogin("github")}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="onboarding-skip-step-link"
|
||||||
|
onClick={handleSkipGitHubStep}
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{githubStatus === "pending" && (
|
||||||
|
<div className="onboarding-github-feedback onboarding-github-feedback--info">
|
||||||
|
Waiting for GitHub authorization…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{githubStatus === "skipped" && (
|
||||||
|
<div className="onboarding-github-feedback onboarding-github-feedback--info">
|
||||||
|
<p>GitHub was skipped. You can connect anytime from Settings → Authentication.</p>
|
||||||
|
<div className="onboarding-github-feedback-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => handleLogin("github")}
|
||||||
|
>
|
||||||
|
Connect anyway
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{githubStatus === "not-connected" && (
|
||||||
<p className="onboarding-helper-text">
|
<p className="onboarding-helper-text">
|
||||||
No worries if you're not ready — connect GitHub anytime from Settings → Authentication.
|
No worries if you're not ready — connect GitHub anytime from Settings → Authentication.
|
||||||
</p>
|
</p>
|
||||||
@@ -1490,7 +1589,7 @@ export function ModelOnboardingModal({
|
|||||||
<button className="btn btn-sm" onClick={handleBack}>
|
<button className="btn btn-sm" onClick={handleBack}>
|
||||||
← Back
|
← Back
|
||||||
</button>
|
</button>
|
||||||
<button className="onboarding-skip-step-link" onClick={handleNext}>
|
<button className="onboarding-skip-step-link" onClick={handleSkipGitHubStep}>
|
||||||
Skip GitHub →
|
Skip GitHub →
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-primary" onClick={handleNext}>
|
<button className="btn btn-primary" onClick={handleNext}>
|
||||||
|
|||||||
@@ -540,6 +540,200 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("GitHub connection status feedback", () => {
|
||||||
|
it("shows connected status with success feedback", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
|
expect(badge).toHaveTextContent("✓ Connected");
|
||||||
|
expect(badge).toHaveClass("connected");
|
||||||
|
expect(screen.getByText("GitHub is connected. You can import issues and track pull requests.")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows not-connected status and keeps default helper text", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
|
expect(badge).toHaveTextContent("Not connected");
|
||||||
|
expect(badge).toHaveClass("not-connected");
|
||||||
|
expect(screen.queryByText("Connection failed or timed out.")).toBeNull();
|
||||||
|
expect(screen.getByText("No worries if you're not ready — connect GitHub anytime from Settings → Authentication.")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows pending status while GitHub login is in progress", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mockLoginProvider.mockImplementationOnce(() => new Promise(() => {}));
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Connect/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
|
expect(badge).toHaveTextContent("⏳ Connecting…");
|
||||||
|
expect(badge).toHaveClass("pending");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows failed status feedback with retry action", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mockLoginProvider.mockRejectedValueOnce(new Error("GitHub login failed"));
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Connect/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
|
expect(badge).toHaveTextContent("✗ Connection failed");
|
||||||
|
expect(badge).toHaveClass("retry");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("Connection failed or timed out.")).toBeTruthy();
|
||||||
|
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries login from failed feedback", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mockLoginProvider
|
||||||
|
.mockRejectedValueOnce(new Error("Initial GitHub failure"))
|
||||||
|
.mockImplementationOnce(() => new Promise(() => {}));
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Connect/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLoginProvider).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists skipped status from Skip GitHub link and restores skipped feedback", async () => {
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await navigateToGitHubStep();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Skip GitHub →"));
|
||||||
|
|
||||||
|
const persistedSkipCall = mockSaveOnboardingState.mock.calls.some((call) => {
|
||||||
|
return call[1]?.stepData?.github?.skipped === true;
|
||||||
|
});
|
||||||
|
expect(persistedSkipCall).toBe(true);
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
const persistedState = {
|
||||||
|
currentStep: "github",
|
||||||
|
completedSteps: ["ai-setup"],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {
|
||||||
|
github: {
|
||||||
|
skipped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockGetOnboardingState.mockReturnValue(persistedState);
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Connect GitHub")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
|
expect(badge).toHaveTextContent("Skipped");
|
||||||
|
expect(badge).toHaveClass("skipped");
|
||||||
|
expect(screen.getByText(/GitHub was skipped/)).toBeTruthy();
|
||||||
|
expect(screen.getByRole("button", { name: "Connect anyway" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("connects from skipped feedback via Connect anyway", async () => {
|
||||||
|
const persistedState = {
|
||||||
|
currentStep: "github",
|
||||||
|
completedSteps: ["ai-setup"],
|
||||||
|
updatedAt: "2026-04-17T00:00:00.000Z",
|
||||||
|
dismissed: false,
|
||||||
|
completed: false,
|
||||||
|
stepData: {
|
||||||
|
github: {
|
||||||
|
skipped: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockGetOnboardingState.mockReturnValue(persistedState);
|
||||||
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
|
providers: [
|
||||||
|
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Connect anyway" })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Connect anyway" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLoginProvider).toHaveBeenCalledWith("github");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows navigating to First Task step via Continue without GitHub", async () => {
|
it("allows navigating to First Task step via Continue without GitHub", async () => {
|
||||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
@@ -2175,7 +2369,7 @@ describe("ModelOnboardingModal progressive disclosure", () => {
|
|||||||
|
|
||||||
// GitHub should show Not connected
|
// GitHub should show Not connected
|
||||||
expect(screen.getByTestId("onboarding-auth-status-github")).toHaveTextContent("Not connected");
|
expect(screen.getByTestId("onboarding-auth-status-github")).toHaveTextContent("Not connected");
|
||||||
expect(screen.getByTestId("onboarding-auth-status-github")).toHaveClass("auth-status-badge");
|
expect(screen.getByTestId("github-status-badge")).toHaveClass("auth-status-badge", "not-connected");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3503,6 +3503,16 @@ body {
|
|||||||
color: var(--color-success);
|
color: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-status-badge.pending {
|
||||||
|
background: color-mix(in srgb, var(--color-info) 15%, transparent);
|
||||||
|
color: var(--color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-status-badge.not-connected {
|
||||||
|
background: color-mix(in srgb, var(--color-error) 15%, transparent);
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
.auth-status-badge.skipped {
|
.auth-status-badge.skipped {
|
||||||
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
@@ -24157,6 +24167,42 @@ html .column.drag-over * {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === GitHub Connection Status Feedback (FN-1934) === */
|
||||||
|
.onboarding-github-feedback {
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback--success {
|
||||||
|
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback--error {
|
||||||
|
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback--info {
|
||||||
|
background: color-mix(in srgb, var(--color-info) 8%, transparent);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.model-onboarding-github-optional {
|
.model-onboarding-github-optional {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -24469,6 +24515,11 @@ html .column.drag-over * {
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onboarding-github-feedback-actions .btn,
|
||||||
|
.onboarding-github-feedback-actions .onboarding-skip-step-link {
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.onboarding-provider-card {
|
.onboarding-provider-card {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
|
|||||||
Reference in New Issue
Block a user