import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { RegistryClientConfig, RegistryPackageView, RegistryReleaseView, } from "../../src/lib/api/registry"; import { render } from "../utils/render.tsx"; vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); return { ...actual, Link: ({ children, to, ...props }: any) => ( {children} ), useNavigate: () => vi.fn(), }; }); const mockGetRegistryPackage = vi.fn(); const mockResolveRegistryPackage = vi.fn(); const mockListRegistryReleases = vi.fn(); vi.mock("../../src/lib/api/registry", async () => { const actual = await vi.importActual( "../../src/lib/api/registry", ); return { ...actual, getRegistryPackage: (...a: unknown[]) => mockGetRegistryPackage(...a), resolveRegistryPackage: (...a: unknown[]) => mockResolveRegistryPackage(...a), listRegistryReleases: (...a: unknown[]) => mockListRegistryReleases(...a), resolveDidToHandle: vi.fn(async () => ({ status: "ok", handle: "acme.dev" })), }; }); vi.mock("../../src/lib/api/client", async () => { const actual = await vi.importActual( "../../src/lib/api/client", ); return { ...actual, fetchManifest: vi.fn(async () => ({ version: "1.0.0", astroVersion: "5.0.0" })), }; }); vi.mock("../../src/lib/api/plugins", () => ({ fetchPlugins: vi.fn(async () => []), })); const { RegistryPluginDetail } = await import("../../src/components/RegistryPluginDetail"); const CONFIG: RegistryClientConfig = { aggregatorUrl: "https://aggregator.test" }; interface PkgOverrides { sections?: Record; lastUpdated?: string; labels?: { val?: string; src?: string }[]; } function makePackage(overrides: PkgOverrides = {}): RegistryPackageView { return { did: "did:plc:acme", handle: "acme.dev", slug: "myplugin", labels: overrides.labels ?? [], profile: { name: "My Plugin", description: "A short description.", license: "MIT", authors: [{ name: "Acme" }], security: [], keywords: [], sections: overrides.sections, lastUpdated: overrides.lastUpdated, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test fixture cast to the validated view shape } as any; } interface ReleaseOverrides { sbom?: { format?: string; url?: string; checksum?: string }; extensions?: Record; } function makeRelease(overrides: ReleaseOverrides = {}): RegistryReleaseView { return { version: "1.2.3", indexedAt: "2025-03-01T00:00:00Z", labels: [], release: { sbom: overrides.sbom, extensions: overrides.extensions, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test fixture cast to the validated view shape } as any; } const RELEASE_EXTENSION_NSID = "com.emdashcms.experimental.package.releaseExtension"; function Wrapper({ children }: { children: React.ReactNode }) { const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return {children}; } function setup(pkg: RegistryPackageView, releases: RegistryReleaseView[]) { mockGetRegistryPackage.mockResolvedValue(pkg); mockResolveRegistryPackage.mockResolvedValue(pkg); mockListRegistryReleases.mockResolvedValue({ releases }); } describe("RegistryPluginDetail sections", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders one pane per non-empty section and suppresses empty ones", async () => { setup( makePackage({ sections: { description: "Description body text.", installation: "Installation body text.", faq: " ", security: "", }, }), [makeRelease()], ); const screen = await render( , ); // Tabs for present sections. await expect.element(screen.getByRole("tab", { name: "Description" })).toBeInTheDocument(); await expect.element(screen.getByRole("tab", { name: "Installation" })).toBeInTheDocument(); // Empty/whitespace sections produce no tab. expect(screen.getByRole("tab", { name: "FAQ" }).query()).toBeNull(); expect(screen.getByRole("tab", { name: "Security" }).query()).toBeNull(); // Default pane is the first present section (description). await expect.element(screen.getByText("Description body text.")).toBeInTheDocument(); }); it("renders sanitized markdown — a ", }, }), [makeRelease()], ); const screen = await render( , ); await expect.element(screen.getByText("Safe paragraph.")).toBeInTheDocument(); expect(screen.container.querySelector("script")).toBeNull(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- probe for the XSS side-effect expect((window as any).__pwned).toBeUndefined(); }); it("renders nothing (no tab bar) when there are no sections", async () => { setup(makePackage({ sections: undefined }), [makeRelease()]); const screen = await render( , ); await expect.element(screen.getByRole("heading", { name: "My Plugin" })).toBeInTheDocument(); expect(screen.container.querySelector('[role="tab"]')).toBeNull(); }); }); describe("RegistryPluginDetail SBOM", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows the SBOM badge and a download link for an https url", async () => { setup(makePackage(), [ makeRelease({ sbom: { format: "cyclonedx", url: "https://x/sbom.json" } }), ]); const screen = await render( , ); await expect.element(screen.getByText("SBOM · cyclonedx")).toBeInTheDocument(); const link = screen.getByRole("link", { name: "Download SBOM" }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute("href", "https://x/sbom.json"); }); it("renders the badge but no download link for an unsafe (javascript:) url", async () => { setup(makePackage(), [makeRelease({ sbom: { format: "spdx", url: "javascript:alert(1)" } })]); const screen = await render( , ); await expect.element(screen.getByText("SBOM · spdx")).toBeInTheDocument(); expect(screen.getByRole("link", { name: "Download SBOM" }).query()).toBeNull(); }); }); describe("RegistryPluginDetail declared permissions", () => { beforeEach(() => { vi.clearAllMocks(); }); it("derives the consent list faithfully from declaredAccess, including hook facets", async () => { // declaredAccess carries the hook facets; the consent list must show the // canonical capability strings the install handler enforces, derived via // the shared converter rather than a component-local flattener. setup(makePackage(), [ makeRelease({ extensions: { [RELEASE_EXTENSION_NSID]: { declaredAccess: { network: { request: { allowedHosts: ["api.cloudflare.com"] } }, email: { transport: {}, events: {} }, }, }, }, }), ]); const screen = await render( , ); await expect.element(screen.getByText("hooks.email-transport:register")).toBeInTheDocument(); await expect.element(screen.getByText("hooks.email-events:register")).toBeInTheDocument(); await expect.element(screen.getByText("network:request")).toBeInTheDocument(); }); }); describe("RegistryPluginDetail lastUpdated + verified tooltip", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders the publisher lastUpdated label when present", async () => { setup(makePackage({ lastUpdated: "2025-02-15T00:00:00Z" }), [makeRelease()]); const screen = await render( , ); await expect.element(screen.getByText("Updated")).toBeInTheDocument(); await expect.element(screen.getByText("Indexed")).toBeInTheDocument(); }); it("exposes the labeller DID through the verified shield trigger", async () => { setup(makePackage({ labels: [{ val: "verified", src: "did:plc:labeller" }] }), [makeRelease()]); const screen = await render( , ); // The shield trigger is a focusable button whose accessible name names the labeller. const trigger = screen.getByRole("button", { name: /Verified publisher/ }); await expect.element(trigger).toBeInTheDocument(); await expect.element(trigger).toHaveAccessibleName(/did:plc:labeller/); }); });