2026-09-27 08:54:02 -07:00
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
|
|
import { useAppState } from "../../store/appState";
|
|
|
|
|
import type { AppSettings, CatalogItem, MarketplaceSnapshot } from "../../lib/types";
|
|
|
|
|
import type { MarketplaceApi } from "../../hooks/useMarketplace";
|
|
|
|
|
|
2026-09-27 10:11:19 -07:00
|
|
|
vi.mock("./InstallControls", () => ({
|
|
|
|
|
default: ({ headCommit }: { headCommit: string | null }) => <div>install controls at {headCommit}</div>,
|
|
|
|
|
}));
|
2026-09-27 08:54:02 -07:00
|
|
|
vi.mock("./AddMarketplaceModal", () => ({ default: () => <div>add modal</div> }));
|
|
|
|
|
|
|
|
|
|
import BrowsePane from "./BrowsePane";
|
|
|
|
|
|
|
|
|
|
const it_ = (kind: CatalogItem["kind"], key: string, patch: Partial<CatalogItem> = {}): CatalogItem => ({
|
|
|
|
|
kind,
|
|
|
|
|
key,
|
|
|
|
|
name: key,
|
|
|
|
|
description: `${key} description`,
|
|
|
|
|
path: key,
|
|
|
|
|
invalid: null,
|
|
|
|
|
hook_commands: [],
|
|
|
|
|
preview: `${key} preview body`,
|
|
|
|
|
...patch,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const snapshot: MarketplaceSnapshot = {
|
|
|
|
|
marketplace_id: "m1",
|
|
|
|
|
head_commit: "a".repeat(40),
|
|
|
|
|
fetched_at: "2026-09-27T12:00:00Z",
|
|
|
|
|
fetch_error: "network unreachable",
|
|
|
|
|
items: [it_("agent", "code-reviewer"), it_("hook", "notify-on-stop"), it_("skill", "broken", { invalid: "SKILL.md missing" })],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function api(patch: Partial<MarketplaceApi> = {}): MarketplaceApi {
|
|
|
|
|
return {
|
|
|
|
|
snapshots: [snapshot],
|
|
|
|
|
updates: [],
|
|
|
|
|
loading: false,
|
|
|
|
|
refreshing: [],
|
|
|
|
|
load: vi.fn(),
|
|
|
|
|
refresh: vi.fn(),
|
|
|
|
|
reloadState: vi.fn(),
|
|
|
|
|
install: vi.fn(),
|
|
|
|
|
uninstall: vi.fn(),
|
|
|
|
|
setDisabled: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
forget: vi.fn(),
|
|
|
|
|
remove: vi.fn(async () => true),
|
|
|
|
|
...patch,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("BrowsePane", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
useAppState.setState({
|
|
|
|
|
appSettings: {
|
|
|
|
|
marketplaces: [{ id: "m1", name: "Starter", url: "https://github.com/s/m.git", branch: null, account_id: null }],
|
|
|
|
|
marketplace_accounts: [],
|
|
|
|
|
global_marketplace_installs: [],
|
|
|
|
|
} as unknown as AppSettings,
|
|
|
|
|
projects: [],
|
|
|
|
|
marketplaceFilterProjectId: null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("lists items, filters by kind and search, and shows detail", () => {
|
|
|
|
|
render(<BrowsePane mp={api()} />);
|
|
|
|
|
expect(screen.getByText("network unreachable")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole("button", { name: /code-reviewer/ })).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole("button", { name: /notify-on-stop/ })).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: "Hooks" }));
|
|
|
|
|
expect(screen.queryByRole("button", { name: /code-reviewer/ })).not.toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: "All" }));
|
|
|
|
|
fireEvent.change(screen.getByLabelText("Search items"), { target: { value: "review" } });
|
|
|
|
|
expect(screen.queryByRole("button", { name: /notify-on-stop/ })).not.toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /code-reviewer/ }));
|
|
|
|
|
expect(screen.getByText("code-reviewer preview body")).toBeInTheDocument();
|
2026-09-27 10:11:19 -07:00
|
|
|
expect(screen.getByText(`install controls at ${"a".repeat(40)}`)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("I2: installs pin the head the shown item was read at, not a later one", () => {
|
|
|
|
|
const mp = api();
|
|
|
|
|
const { rerender } = render(<BrowsePane mp={mp} />);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /code-reviewer/ }));
|
|
|
|
|
rerender(<BrowsePane mp={{ ...mp, snapshots: [{ ...snapshot, head_commit: "b".repeat(40) }] }} />);
|
|
|
|
|
expect(screen.getByText(`install controls at ${"a".repeat(40)}`)).toBeInTheDocument();
|
2026-09-27 08:54:02 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows why an item is invalid", () => {
|
|
|
|
|
render(<BrowsePane mp={api()} />);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /broken/ }));
|
|
|
|
|
expect(screen.getByText("SKILL.md missing")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("refreshes one marketplace", () => {
|
|
|
|
|
const mp = api();
|
|
|
|
|
render(<BrowsePane mp={mp} />);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Refresh Starter" }));
|
|
|
|
|
expect(mp.refresh).toHaveBeenCalledWith("m1");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("offers Add when there are no marketplaces", () => {
|
|
|
|
|
useAppState.setState({
|
|
|
|
|
appSettings: { marketplaces: [], marketplace_accounts: [], global_marketplace_installs: [] } as unknown as AppSettings,
|
|
|
|
|
});
|
|
|
|
|
render(<BrowsePane mp={api({ snapshots: [] })} />);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Add marketplace" }));
|
|
|
|
|
expect(screen.getByText("add modal")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("confirms before removing a marketplace (F6)", () => {
|
|
|
|
|
const mp = api();
|
|
|
|
|
render(<BrowsePane mp={mp} />);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Remove Starter" }));
|
|
|
|
|
expect(screen.getByText(/Source removed/)).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText(/Forget/)).toBeInTheDocument();
|
|
|
|
|
expect(mp.remove).not.toHaveBeenCalled();
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Remove marketplace" }));
|
|
|
|
|
expect(mp.remove).toHaveBeenCalledWith("m1");
|
|
|
|
|
});
|
|
|
|
|
});
|