/* Copyright 2026 Marimo. All rights reserved. */ import { vi } from "vitest"; // MSW 2.x requires localStorage for cookie persistence. // vi.hoisted runs before all imports, so localStorage is available when MSW loads. vi.hoisted(() => { const store: Record = {}; globalThis.localStorage = { getItem: (key: string) => store[key] ?? null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { Reflect.deleteProperty(store, key); }, clear: () => { for (const key of Object.keys(store)) { Reflect.deleteProperty(store, key); } }, key: (index: number) => Object.keys(store)[index] ?? null, get length() { return Object.keys(store).length; }, }; }); import { renderHook, waitFor } from "@testing-library/react"; import * as msw from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { type PyPiPackageResponse, usePackageMetadata, } from "../usePackageMetadata"; function createPackageMock(options: { extras?: string[] | null; versions: string[]; }): PyPiPackageResponse { return { info: { provides_extra: options.extras ?? null, }, releases: Object.fromEntries(options.versions.map((v) => [v, []])), }; } describe("usePackageMetadata", () => { // https://mswjs.io/docs/api/setup-server/#usage const server = setupServer(); beforeAll(() => { // Start the interception. server.listen(); }); afterEach(() => { // Remove any handlers you may have added in individual tests (runtime handlers). server.resetHandlers(); // Clear localStorage to prevent state leakage between tests. localStorage.clear(); }); afterAll(() => { // Disable request interception and clean up. server.close(); }); it("should return loading state initially", async () => { server.use( msw.http.get("https://pypi.org/pypi/numpy/json", async () => { msw.delay(); return msw.HttpResponse.json( createPackageMock({ extras: ["test", "dev"], versions: ["1.21.0", "1.20.0"], }), ); }), ); const { result } = renderHook(() => usePackageMetadata("numpy")); expect(result.current.isPending).toBe(true); expect(result.current.data).toBeUndefined(); expect(result.current.error).toBeUndefined(); }); it("should fetch and return package metadata successfully", async () => { server.use( msw.http.get("https://pypi.org/pypi/pandas/json", () => { return msw.HttpResponse.json( createPackageMock({ extras: ["test", "performance", "plotting"], versions: ["2.0.0", "1.5.3", "1.4.0"], }), ); }), ); const { result } = renderHook(() => usePackageMetadata("pandas")); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toEqual({ versions: ["2.0.0", "1.5.3", "1.4.0"], extras: ["test", "performance", "plotting"], }); expect(result.current.error).toBeUndefined(); }); it("should handle packages with no extras", async () => { server.use( msw.http.get("https://pypi.org/pypi/requests/json", () => msw.HttpResponse.json( createPackageMock({ extras: null, versions: ["2.28.0", "2.27.1"], }), ), ), ); const { result } = renderHook(() => usePackageMetadata("requests")); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toEqual({ versions: ["2.28.0", "2.27.1"], extras: [], }); expect(result.current.error).toBeUndefined(); }); it("should handle network errors", async () => { server.use( msw.http.get("https://pypi.org/pypi/nonexistent/json", () => msw.HttpResponse.error(), ), ); const { result } = renderHook(() => usePackageMetadata("nonexistent")); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toBeUndefined(); expect(result.current.error).toBeInstanceOf(Error); }); it("should clean package names with extras syntax", async () => { server.use( msw.http.get("https://pypi.org/pypi/package-name/json", () => msw.HttpResponse.json( createPackageMock({ extras: ["extra1", "extra2"], versions: ["1.0.0"], }), ), ), ); const { result } = renderHook(() => usePackageMetadata("package-name[extra1,extra2]"), ); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toEqual({ versions: ["1.0.0"], extras: ["extra1", "extra2"], }); }); it("should sort versions in reverse semver order", async () => { server.use( msw.http.get("https://pypi.org/pypi/scipy/json", () => msw.HttpResponse.json( createPackageMock({ extras: [], versions: ["1.9.0", "1.10.1", "1.8.1", "2.0.0"], }), ), ), ); const { result } = renderHook(() => usePackageMetadata("scipy")); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toMatchInlineSnapshot(` { "extras": [], "versions": [ "2.0.0", "1.10.1", "1.9.0", "1.8.1", ], } `); }); it("should handle 404 package not found error", async () => { server.use( msw.http.get( "https://pypi.org/pypi/package-not-found/json", () => new msw.HttpResponse(null, { status: 404 }), ), ); const { result } = renderHook(() => usePackageMetadata("package-not-found"), ); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.data).toBeUndefined(); expect(result.current.error).toBeInstanceOf(Error); }); it("should use cached data on subsequent calls", async () => { let callCount = 0; server.use( msw.http.get("https://pypi.org/pypi/cached-package/json", () => { callCount++; return msw.HttpResponse.json( createPackageMock({ extras: ["test"], versions: ["1.0.0"], }), ); }), ); // First const { result: result1 } = renderHook(() => usePackageMetadata("cached-package"), ); await waitFor(() => expect(result1.current.isPending).toBe(false)); expect(result1.current.data).toEqual({ versions: ["1.0.0"], extras: ["test"], }); expect(callCount).toBe(1); // Second const { result: result2 } = renderHook(() => usePackageMetadata("cached-package"), ); await waitFor(() => expect(result2.current.isPending).toBe(false)); expect(result2.current.data).toEqual({ versions: ["1.0.0"], extras: ["test"], }); expect(callCount).toBe(1); }); });