Files
kandev/apps/web/components/github/github-connection-dialog.test.tsx
T

211 lines
7.5 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ToastProvider } from "@/components/toast-provider";
import type { GitHubAppRegistrationCatalogItem, GitHubStatus } from "@/lib/types/github";
import { GitHubAppConnectionPanel } from "./github-app-connection-panel";
import { GitHubConnectionDialog } from "./github-connection-dialog";
const mocks = vi.hoisted(() => ({
mobile: false,
registrations: [] as GitHubAppRegistrationCatalogItem[],
}));
const changeConnectionLabel = "Change connection";
const registrationDisplayName = "Work automation";
const githubAppLabel = "GitHub App";
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
useResponsiveBreakpoint: () => ({ isMobile: mocks.mobile }),
}));
vi.mock("@/hooks/domains/github/use-github-app-registrations", () => ({
useGitHubAppRegistrations: (workspaceId: string) => ({
workspaceId,
catalog: { workspace_id: workspaceId, registrations: mocks.registrations },
registrations: mocks.registrations,
selected: mocks.registrations.find((item) => item.selected) ?? null,
loaded: true,
loading: false,
error: null,
mutating: false,
refresh: vi.fn(),
startManifest: vi.fn(),
prepareImport: vi.fn(),
importRegistration: vi.fn(),
rename: vi.fn(),
remove: vi.fn(),
startInstall: vi.fn(),
}),
}));
vi.mock("./github-app-import-form", () => ({
GitHubAppImportForm: ({ onImported }: { onImported: (registrationId: string) => void }) => (
<button onClick={() => onImported("registration-1")}>Complete test import</button>
),
}));
const status: GitHubStatus = {
workspace_id: "workspace-1",
automation: {
workspace_id: "workspace-1",
source: "pat",
github_host: "github.com",
login: "octocat",
status: "active",
credential_generation: 1,
},
personal: null,
authenticated: true,
username: "octocat",
auth_method: "pat",
token_configured: true,
required_scopes: [],
};
const registration: GitHubAppRegistrationCatalogItem = {
id: "registration-1",
source: "managed",
display_name: registrationDisplayName,
github_host: "github.com",
app_id: 42,
client_id: "Iv1.test",
slug: "work-automation",
owner_login: "acme",
owner_type: "Organization",
visibility: "private",
public_base_url: "https://kandev.example",
credential_generation: 1,
status: "active",
webhook_status: "verified",
created_at: "2026-07-21T00:00:00Z",
updated_at: "2026-07-21T00:00:00Z",
selected: true,
binding_count: 3,
workspace_binding_count: 2,
shared: true,
manifest_callback_url: "https://kandev.example/manifest/callback",
install_callback_url: "https://kandev.example/install/callback",
personal_callback_url: "https://kandev.example/personal/callback",
webhook_url: "https://kandev.example/webhook",
};
function view(workspaceId = "workspace-1") {
return render(
<ToastProvider>
<GitHubConnectionDialog
status={{ ...status, workspace_id: workspaceId }}
workspaceId={workspaceId}
onSaved={vi.fn()}
/>
</ToastProvider>,
);
}
beforeEach(() => {
mocks.mobile = false;
mocks.registrations = [registration];
});
afterEach(() => cleanup());
describe("GitHubConnectionDialog", () => {
it("uses a method list and shows workspace App choices and sharing impact", async () => {
view();
fireEvent.click(screen.getByRole("button", { name: changeConnectionLabel }));
expect(await screen.findByTestId("github-connection-desktop")).toBeTruthy();
expect(screen.getByLabelText("Connection method")).toBeTruthy();
expect(screen.getAllByText("Personal access token")).not.toHaveLength(0);
expect(screen.getByText("GitHub CLI account")).toBeTruthy();
fireEvent.click(screen.getByText(githubAppLabel, { selector: "span" }));
expect(await screen.findByText(registrationDisplayName)).toBeTruthy();
expect(screen.getByText(/Used by 2 workspaces/)).toBeTruthy();
expect(screen.getByRole("button", { name: /Install for this workspace/ })).toBeTruthy();
expect(screen.getByRole("button", { name: "Add existing App" })).toBeTruthy();
expect(screen.getByRole("button", { name: "Create new App" })).toBeTruthy();
expect(screen.queryByTestId("github-app-system-setup-link")).toBeNull();
});
it("renders one bounded mobile drawer with a single scroll body", async () => {
mocks.mobile = true;
view();
fireEvent.click(screen.getByRole("button", { name: changeConnectionLabel }));
const drawer = await screen.findByTestId("github-connection-mobile");
expect(drawer.className).toContain("100dvh");
expect(drawer.querySelectorAll(".overflow-y-auto")).toHaveLength(1);
expect(screen.queryByTestId("github-connection-desktop")).toBeNull();
});
it("returns to the registration catalog after importing an App", async () => {
view();
fireEvent.click(screen.getByRole("button", { name: changeConnectionLabel }));
fireEvent.click(await screen.findByText(githubAppLabel, { selector: "span" }));
fireEvent.click(screen.getByRole("button", { name: "Add existing App" }));
fireEvent.click(screen.getByRole("button", { name: "Complete test import" }));
expect(await screen.findByText(registrationDisplayName)).toBeTruthy();
const installButton = screen.getByRole("button", { name: /Install for this workspace/ });
expect((installButton as HTMLButtonElement).disabled).toBe(false);
});
it("does not install invalid App registrations", async () => {
mocks.registrations = [
{
...registration,
status: "invalid",
last_error: "Credentials could not be restored.",
},
];
view();
fireEvent.click(screen.getByRole("button", { name: changeConnectionLabel }));
fireEvent.click(await screen.findByText(githubAppLabel, { selector: "span" }));
expect(await screen.findByText("Needs attention")).toBeTruthy();
expect(screen.getByText("Credentials could not be restored.")).toBeTruthy();
expect(
(screen.getByRole("button", { name: /Install for this workspace/ }) as HTMLButtonElement)
.disabled,
).toBe(true);
});
it("clears a selected App when it disappears from the catalog", async () => {
const rendered = render(
<ToastProvider>
<GitHubAppConnectionPanel workspaceId="workspace-1" />
</ToastProvider>,
);
expect(
(screen.getByRole("button", { name: /Install for this workspace/ }) as HTMLButtonElement)
.disabled,
).toBe(false);
mocks.registrations = [];
rendered.rerender(
<ToastProvider>
<GitHubAppConnectionPanel workspaceId="workspace-1" />
</ToastProvider>,
);
await waitFor(() =>
expect(
(screen.getByRole("button", { name: /Install for this workspace/ }) as HTMLButtonElement)
.disabled,
).toBe(true),
);
});
it("closes and resets local setup state when the workspace changes", async () => {
const rendered = view();
fireEvent.click(screen.getByRole("button", { name: changeConnectionLabel }));
fireEvent.click(await screen.findByText(githubAppLabel, { selector: "span" }));
expect(await screen.findByText(registrationDisplayName)).toBeTruthy();
rendered.rerender(
<ToastProvider>
<GitHubConnectionDialog
status={{ ...status, workspace_id: "workspace-2" }}
workspaceId="workspace-2"
onSaved={vi.fn()}
/>
</ToastProvider>,
);
await waitFor(() => expect(screen.queryByTestId("github-connection-desktop")).toBeNull());
});
});