diff --git a/crates/cratebay-gui/src/components/ui/badge.tsx b/crates/cratebay-gui/src/components/ui/badge.tsx index 6eb2a05..0932c51 100644 --- a/crates/cratebay-gui/src/components/ui/badge.tsx +++ b/crates/cratebay-gui/src/components/ui/badge.tsx @@ -45,4 +45,4 @@ function Badge({ ) } -export { Badge, badgeVariants } +export { Badge } diff --git a/crates/cratebay-gui/src/components/ui/button.tsx b/crates/cratebay-gui/src/components/ui/button.tsx index 4d38506..4ea8a74 100644 --- a/crates/cratebay-gui/src/components/ui/button.tsx +++ b/crates/cratebay-gui/src/components/ui/button.tsx @@ -61,4 +61,4 @@ function Button({ ) } -export { Button, buttonVariants } +export { Button } diff --git a/crates/cratebay-gui/src/components/ui/tabs.tsx b/crates/cratebay-gui/src/components/ui/tabs.tsx index b463afd..549be6c 100644 --- a/crates/cratebay-gui/src/components/ui/tabs.tsx +++ b/crates/cratebay-gui/src/components/ui/tabs.tsx @@ -88,4 +88,4 @@ function TabsContent({ ) } -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/crates/cratebay-gui/src/hooks/useVolumes.ts b/crates/cratebay-gui/src/hooks/useVolumes.ts index d5c8988..04a24b2 100644 --- a/crates/cratebay-gui/src/hooks/useVolumes.ts +++ b/crates/cratebay-gui/src/hooks/useVolumes.ts @@ -64,16 +64,23 @@ export function useVolumes() { } catch (e) { if (reqId !== latestReqId.current) return setError(String(e)) - } finally { - if (reqId !== latestReqId.current) return + } + if (reqId === latestReqId.current) { setLoading(false) } }, []) useEffect(() => { - fetchVolumes() - const iv = setInterval(fetchVolumes, 5000) - return () => clearInterval(iv) + const initialFetch = setTimeout(() => { + void fetchVolumes() + }, 0) + const iv = setInterval(() => { + void fetchVolumes() + }, 5000) + return () => { + clearTimeout(initialFetch) + clearInterval(iv) + } }, [fetchVolumes]) const createVolume = useCallback(async (name: string, driver: string) => { diff --git a/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx b/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx index dbb6423..317fb00 100644 --- a/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx +++ b/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx @@ -132,7 +132,8 @@ describe("Dashboard", () => { expect(screen.getByText("web-server")).toBeInTheDocument() expect(screen.getByText("api-server")).toBeInTheDocument() - expect(screen.getByText(/Running \(2\)/)).toBeInTheDocument() + expect(screen.getByText(t("running"))).toBeInTheDocument() + expect(screen.getByText("2", { selector: ".dash-section-count" })).toBeInTheDocument() }) it("does not show running containers section when none are running", () => { @@ -156,7 +157,7 @@ describe("Dashboard", () => { /> ) - const viewAll = screen.getByText(/View all \(7\)/) + const viewAll = screen.getByText(t("viewAll")) expect(viewAll).toBeInTheDocument() await user.click(viewAll) @@ -171,8 +172,8 @@ describe("Dashboard", () => { ) - // Should only render 5 container-card elements in the running section - const containerCards = document.querySelectorAll(".container-card") + // Should only render 5 running-item elements in the running section + const containerCards = document.querySelectorAll(".dash-running-item") expect(containerCards.length).toBe(5) }) diff --git a/crates/cratebay-gui/src/pages/__tests__/Images.test.tsx b/crates/cratebay-gui/src/pages/__tests__/Images.test.tsx index 113066d..52bd058 100644 --- a/crates/cratebay-gui/src/pages/__tests__/Images.test.tsx +++ b/crates/cratebay-gui/src/pages/__tests__/Images.test.tsx @@ -58,27 +58,43 @@ const defaultProps = { t, } +const renderImages = (props: Partial = {}) => { + render() +} + +const openSearchTab = async (user: ReturnType) => { + await user.click( + screen.getByRole("button", { name: new RegExp(t("searchImages"), "i") }) + ) +} + describe("Images", () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(invoke).mockResolvedValue([]) }) - it("renders the search input and buttons", () => { - render() + it("renders the search input and buttons", async () => { + const user = userEvent.setup() + renderImages() + await openSearchTab(user) expect(screen.getByPlaceholderText(t("searchImages"))).toBeInTheDocument() expect(screen.getByText(t("search"))).toBeInTheDocument() }) it("shows the local images section header", () => { - render() + renderImages() - expect(screen.getByText(t("localImages"))).toBeInTheDocument() + expect( + screen.getByRole("button", { name: new RegExp(t("localImages"), "i") }) + ).toBeInTheDocument() }) - it("shows empty search state with hint", () => { - render() + it("shows empty search state with hint", async () => { + const user = userEvent.setup() + renderImages() + await openSearchTab(user) expect(screen.getByText(t("searchHint"))).toBeInTheDocument() }) @@ -86,7 +102,8 @@ describe("Images", () => { it("calls onSearch when the search button is clicked", async () => { const user = userEvent.setup() const onSearch = vi.fn() - render() + renderImages({ imgQuery: "nginx", onSearch }) + await openSearchTab(user) const searchBtn = screen.getByText(t("search")) await user.click(searchBtn) @@ -94,20 +111,25 @@ describe("Images", () => { expect(onSearch).toHaveBeenCalled() }) - it("disables search button when imgQuery is empty", () => { - render() + it("disables search button when imgQuery is empty", async () => { + const user = userEvent.setup() + renderImages({ imgQuery: "" }) + await openSearchTab(user) const searchBtn = screen.getByText(t("search")) expect(searchBtn).toBeDisabled() }) - it("disables search button while searching", () => { - render() + it("disables search button while searching", async () => { + const user = userEvent.setup() + renderImages({ imgQuery: "nginx", imgSearching: true }) + await openSearchTab(user) expect(screen.getByText(t("searching"))).toBeDisabled() }) - it("renders search results as cards", () => { + it("renders search results as cards", async () => { + const user = userEvent.setup() const results = [ mockSearchResult(), mockSearchResult({ @@ -120,7 +142,8 @@ describe("Images", () => { }), ] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.getByText("nginx")).toBeInTheDocument() expect(screen.getByText("Official Nginx image")).toBeInTheDocument() @@ -128,30 +151,38 @@ describe("Images", () => { expect(screen.getByText("etcd service")).toBeInTheDocument() }) - it("shows official badge for official images", () => { + it("shows official badge for official images", async () => { + const user = userEvent.setup() const results = [mockSearchResult({ official: true })] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.getByText(t("official"))).toBeInTheDocument() }) - it("does not show official badge for non-official images", () => { + it("does not show official badge for non-official images", async () => { + const user = userEvent.setup() const results = [mockSearchResult({ official: false })] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.queryByText(t("official"))).not.toBeInTheDocument() }) - it("shows source badge on result cards", () => { + it("shows source badge on result cards", async () => { + const user = userEvent.setup() const results = [mockSearchResult({ source: "dockerhub" })] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.getByText("dockerhub")).toBeInTheDocument() }) - it("renders run and tags buttons on each result card", () => { + it("renders run and tags buttons on each result card", async () => { + const user = userEvent.setup() const results = [mockSearchResult()] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.getAllByText(t("run")).length).toBeGreaterThanOrEqual(1) expect(screen.getAllByText(t("tags")).length).toBeGreaterThanOrEqual(1) @@ -162,7 +193,8 @@ describe("Images", () => { const onTags = vi.fn() const results = [mockSearchResult({ reference: "quay.io/coreos/etcd" })] - render() + renderImages({ imgResults: results, onTags }) + await openSearchTab(user) // Find the tags button within the search result card const tagsButtons = screen.getAllByText(t("tags")) @@ -173,14 +205,13 @@ describe("Images", () => { expect(onTags).toHaveBeenCalledWith("quay.io/coreos/etcd") }) - it("displays tags when available", () => { - render( - - ) + it("displays tags when available", async () => { + const user = userEvent.setup() + renderImages({ + imgTags: ["latest", "1.0", "2.0"], + imgTagsRef: "quay.io/coreos/etcd", + }) + await openSearchTab(user) expect(screen.getByText(/Tags/)).toBeInTheDocument() expect(screen.getByText("latest")).toBeInTheDocument() @@ -189,14 +220,14 @@ describe("Images", () => { }) it("shows the import image button", () => { - render() + renderImages() expect(screen.getByText(t("importImage"))).toBeInTheDocument() }) it("opens the import/push modal when import button is clicked", async () => { const user = userEvent.setup() - render() + renderImages() const importBtn = screen.getByText(t("importImage")) await user.click(importBtn) @@ -206,7 +237,7 @@ describe("Images", () => { }) it("shows error message when imgError is set", () => { - render() + renderImages({ imgError: "Something went wrong" }) expect(screen.getByText("Something went wrong")).toBeInTheDocument() }) @@ -214,9 +245,7 @@ describe("Images", () => { it("dismisses error when dismiss button is clicked", async () => { const user = userEvent.setup() const setImgError = vi.fn() - render( - - ) + renderImages({ imgError: "Some error", setImgError }) // The error inline has a dismiss button (x) const dismissBtns = document.querySelectorAll(".error-inline-dismiss") @@ -238,7 +267,7 @@ describe("Images", () => { }, ]) - render() + renderImages() // Wait for local images to load const removeBtn = await screen.findByTitle(t("removeImage")) @@ -269,7 +298,7 @@ describe("Images", () => { return undefined }) - render() + renderImages() // Wait for local images to load and click remove const removeBtn = await screen.findByTitle(t("removeImage")) @@ -282,33 +311,37 @@ describe("Images", () => { expect(invoke).toHaveBeenCalledWith("image_remove", { id: "nginx:latest" }) }) - it("shows source filter dropdown", () => { - render() + it("shows source filter dropdown", async () => { + const user = userEvent.setup() + renderImages() + await openSearchTab(user) expect(screen.getByText(t("sourceAll"))).toBeInTheDocument() }) - it("formats pull counts with K and M suffixes", () => { + it("formats pull counts with K and M suffixes", async () => { + const user = userEvent.setup() const results = [ mockSearchResult({ reference: "nginx-official", pulls: 5000000 }), mockSearchResult({ reference: "small-image", pulls: 1500, source: "quay" }), ] - render() + renderImages({ imgResults: results }) + await openSearchTab(user) expect(screen.getByText("5.0M")).toBeInTheDocument() expect(screen.getByText("1.5K")).toBeInTheDocument() }) it("shows local image filter input", () => { - render() + renderImages() expect(screen.getByPlaceholderText(t("filterLocalImages"))).toBeInTheDocument() }) it("shows no local images message when list is empty", async () => { vi.mocked(invoke).mockResolvedValue([]) - render() + renderImages() expect(await screen.findByText(t("noLocalImages"))).toBeInTheDocument() })