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:
Fusion
2026-04-17 01:09:16 -07:00
committed by gsxdsm
parent 1a10a03c26
commit d793b9cba1
3 changed files with 374 additions and 30 deletions

View File

@@ -540,6 +540,200 @@ describe("ModelOnboardingModal", () => {
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 () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
@@ -2175,7 +2369,7 @@ describe("ModelOnboardingModal progressive disclosure", () => {
// GitHub should show 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");
});
});