test(ui): add vitest coverage for the 3 new OAuth login components

CONTRIBUTING.md requires corresponding tests for new UI components.
Adds:

- OAuthDeviceLoginButton.test.tsx (6 cases) — Sign-in button disabled
  state, startCall invocation, user-code + verification-URL rendering,
  onSuccess callback on poll success, error-state rendering on poll
  failure, and cancel flow.
- ChatGPTLoginButton.test.tsx (1 case) — renders with the ChatGPT label.
- CopilotLoginButton.test.tsx (1 case) — renders with the GitHub Copilot
  label.

Real timers throughout; polling tests wait up to 10s for the 3s-interval
first poll to fire. 20/20 local tests pass across my new + adjacent
existing UI test files; ``npm run build`` succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Cook 2026-04-17 11:31:52 -04:00
parent d663094202
commit c3d29a1ded
3 changed files with 222 additions and 0 deletions

View file

@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import ChatGPTLoginButton from "./ChatGPTLoginButton";
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({ accessToken: "test-token" }),
}));
vi.mock("@/components/networking", async () => {
const actual = await vi.importActual("@/components/networking");
return {
...actual,
chatgptOauthStartCall: vi.fn(),
chatgptOauthStatusCall: vi.fn(),
chatgptOauthCancelCall: vi.fn(),
};
});
describe("ChatGPTLoginButton", () => {
it("renders with the ChatGPT provider label", () => {
render(<ChatGPTLoginButton credentialName="c" onSuccess={vi.fn()} />);
expect(
screen.getByRole("button", { name: /Sign in with ChatGPT/i }),
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import CopilotLoginButton from "./CopilotLoginButton";
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({ accessToken: "test-token" }),
}));
vi.mock("@/components/networking", async () => {
const actual = await vi.importActual("@/components/networking");
return {
...actual,
copilotOauthStartCall: vi.fn(),
copilotOauthStatusCall: vi.fn(),
copilotOauthCancelCall: vi.fn(),
};
});
describe("CopilotLoginButton", () => {
it("renders with the GitHub Copilot provider label", () => {
render(<CopilotLoginButton credentialName="c" onSuccess={vi.fn()} />);
expect(
screen.getByRole("button", { name: /Sign in with GitHub Copilot/i }),
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,170 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import OAuthDeviceLoginButton from "./OAuthDeviceLoginButton";
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({ accessToken: "test-token" }),
}));
describe("OAuthDeviceLoginButton", () => {
beforeEach(() => {
vi.useRealTimers();
});
const baseProps = {
providerLabel: "ChatGPT",
startCall: vi.fn(),
statusCall: vi.fn(),
cancelCall: vi.fn(),
onSuccess: vi.fn(),
};
it("disables Sign-in when no credential name is provided", () => {
render(<OAuthDeviceLoginButton {...baseProps} onSuccess={vi.fn()} />);
const button = screen.getByRole("button", { name: /Sign in with ChatGPT/i });
expect(button).toBeDisabled();
});
it("enables Sign-in once a credential name is provided", () => {
render(
<OAuthDeviceLoginButton
{...baseProps}
credentialName="my-creds"
onSuccess={vi.fn()}
/>,
);
expect(
screen.getByRole("button", { name: /Sign in with ChatGPT/i }),
).toBeEnabled();
});
it("on Sign-in click, shows the user code and verification URL returned by startCall", async () => {
const startCall = vi.fn().mockResolvedValue({
session_id: "sess-1",
user_code: "ABCD-1234",
verification_url: "https://auth.example/code",
interval: 5,
});
const statusCall = vi.fn().mockResolvedValue({ status: "pending" });
render(
<OAuthDeviceLoginButton
{...baseProps}
credentialName="my-creds"
startCall={startCall}
statusCall={statusCall}
cancelCall={vi.fn()}
onSuccess={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i }));
await waitFor(() => {
expect(screen.getByText("ABCD-1234")).toBeInTheDocument();
});
expect(
screen.getByRole("link", { name: "https://auth.example/code" }),
).toHaveAttribute("href", "https://auth.example/code");
expect(startCall).toHaveBeenCalledWith("test-token", "my-creds");
});
it("calls onSuccess when polling reports success", async () => {
const onSuccess = vi.fn();
const startCall = vi.fn().mockResolvedValue({
session_id: "sess-1",
user_code: "CODE",
verification_url: "https://auth.example/code",
interval: 5,
});
const statusCall = vi.fn().mockResolvedValue({ status: "success" });
render(
<OAuthDeviceLoginButton
{...baseProps}
credentialName="my-creds"
startCall={startCall}
statusCall={statusCall}
cancelCall={vi.fn()}
onSuccess={onSuccess}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i }));
// Poll interval is 3s; allow up to 10s of real time for the first poll
// to fire and resolve with status=success.
await waitFor(
() => {
expect(onSuccess).toHaveBeenCalledTimes(1);
},
{ timeout: 10000 },
);
}, 15000);
it("surfaces error status from polling", async () => {
const startCall = vi.fn().mockResolvedValue({
session_id: "sess-1",
user_code: "CODE",
verification_url: "https://auth.example/code",
interval: 5,
});
const statusCall = vi
.fn()
.mockResolvedValue({ status: "error", message: "upstream failure" });
render(
<OAuthDeviceLoginButton
{...baseProps}
credentialName="my-creds"
startCall={startCall}
statusCall={statusCall}
cancelCall={vi.fn()}
onSuccess={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i }));
await waitFor(
() => {
expect(screen.getByText("upstream failure")).toBeInTheDocument();
},
{ timeout: 10000 },
);
}, 15000);
it("on Cancel click, calls cancelCall and returns to idle state", async () => {
const cancelCall = vi.fn().mockResolvedValue(undefined);
const startCall = vi.fn().mockResolvedValue({
session_id: "sess-1",
user_code: "CODE",
verification_url: "https://auth.example/code",
interval: 5,
});
const statusCall = vi.fn().mockResolvedValue({ status: "pending" });
render(
<OAuthDeviceLoginButton
{...baseProps}
credentialName="my-creds"
startCall={startCall}
statusCall={statusCall}
cancelCall={cancelCall}
onSuccess={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i }));
await waitFor(() => {
expect(screen.getByText("CODE")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
await waitFor(() => {
expect(cancelCall).toHaveBeenCalledWith("test-token", "sess-1");
});
// Back to idle — Sign-in button visible again
expect(
screen.getByRole("button", { name: /Sign in with ChatGPT/i }),
).toBeInTheDocument();
});
});