From 8852b61d65caeb4bc6987f1b98502978f1a5b455 Mon Sep 17 00:00:00 2001 From: abimbolaalabi <139625252+abimbolaalabi@users.noreply.github.com> Date: Sun, 30 Aug 2026 09:05:55 +0100 Subject: [PATCH] test: add submission and validation tests for TopUpModal and CancelConfirmModal --- .../__tests__/CancelConfirmModal.test.tsx | 84 +++++++++++ .../__tests__/TopUpModal.test.tsx | 134 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 frontend/src/components/stream-creation/__tests__/CancelConfirmModal.test.tsx create mode 100644 frontend/src/components/stream-creation/__tests__/TopUpModal.test.tsx diff --git a/frontend/src/components/stream-creation/__tests__/CancelConfirmModal.test.tsx b/frontend/src/components/stream-creation/__tests__/CancelConfirmModal.test.tsx new file mode 100644 index 00000000..39f4381f --- /dev/null +++ b/frontend/src/components/stream-creation/__tests__/CancelConfirmModal.test.tsx @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; + +vi.mock("react-hot-toast", () => ({ + default: { success: vi.fn(), error: vi.fn(), loading: vi.fn() }, +})); + +import toast from "react-hot-toast"; +import { CancelConfirmModal } from "../CancelConfirmModal"; + +describe("CancelConfirmModal submission and validation", () => { + const baseProps = { + streamId: "stream-42", + recipient: "GDEF456ABC789GHI012JKL345MNO678PQR901STU234VWX567YZA123BCD", + token: "USDC", + deposited: 1000, + withdrawn: 200, + onConfirm: vi.fn().mockResolvedValue(undefined), + onClose: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + document.body.style.overflow = ""; + document.body.innerHTML = ""; + }); + + afterEach(() => { + document.body.style.overflow = ""; + }); + + const getCancelButton = () => screen.getByRole("button", { name: /yes, cancel stream/i }); + + it("shows a success toast when cancellation succeeds", async () => { + render(); + fireEvent.click(getCancelButton()); + await waitFor(() => { + expect(baseProps.onConfirm).toHaveBeenCalledWith("stream-42"); + }); + expect(toast.success).toHaveBeenCalledWith("Stream stream-42 cancelled successfully"); + }); + + it("shows toast.error and re-enables the confirm button when onConfirm rejects", async () => { + baseProps.onConfirm.mockRejectedValueOnce(new Error("network error")); + render(); + fireEvent.click(getCancelButton()); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Failed to cancel stream. Please try again."); + }); + expect(getCancelButton()).toBeEnabled(); + expect(toast.success).not.toHaveBeenCalled(); + }); + + it("blocks close button, keep stream, backdrop click, and Escape while submitting", async () => { + let resolveConfirm!: (value: void) => void; + const pendingConfirm = vi.fn( + () => new Promise((res) => { + resolveConfirm = res; + }), + ); + render(); + fireEvent.click(getCancelButton()); + + expect(screen.getByRole("button", { name: /cancelling/i })).toBeDisabled(); + + const closeButton = screen.getByRole("button", { name: /close/i }); + expect(closeButton).toBeDisabled(); + fireEvent.click(closeButton); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: /keep stream/i })); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("dialog")); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.keyDown(window, { key: "Escape" }); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + await act(async () => { + resolveConfirm(); + }); + }); +}); diff --git a/frontend/src/components/stream-creation/__tests__/TopUpModal.test.tsx b/frontend/src/components/stream-creation/__tests__/TopUpModal.test.tsx new file mode 100644 index 00000000..e1de3bb4 --- /dev/null +++ b/frontend/src/components/stream-creation/__tests__/TopUpModal.test.tsx @@ -0,0 +1,134 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +vi.mock("react-hot-toast", () => ({ + default: { success: vi.fn(), error: vi.fn(), loading: vi.fn() }, +})); + +import toast from "react-hot-toast"; +import { TopUpModal } from "../TopUpModal"; + +describe("TopUpModal", () => { + const baseProps = { + streamId: "stream-42", + token: "USDC", + currentDeposited: 500, + onConfirm: vi.fn().mockResolvedValue(undefined), + onClose: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + document.body.style.overflow = ""; + document.body.innerHTML = ""; + }); + + afterEach(() => { + document.body.style.overflow = ""; + }); + + const getAmountInput = () => screen.getByLabelText(/amount to add/i) as HTMLInputElement; + const getConfirmButton = () => screen.getByRole("button", { name: /confirm top up/i }); + + it("renders stream info and current balance", () => { + render(); + expect(screen.getByText("Top Up Stream")).toBeInTheDocument(); + expect(screen.getByText(/500 USDC/)).toBeInTheDocument(); + }); + + describe("amount validation", () => { + it("shows an error and does not submit when the amount is empty", () => { + render(); + fireEvent.click(getConfirmButton()); + expect(screen.getByText("Amount is required")).toBeInTheDocument(); + expect(baseProps.onConfirm).not.toHaveBeenCalled(); + }); + + it("shows an error and does not submit when the amount is zero", async () => { + const user = userEvent.setup(); + render(); + await user.type(getAmountInput(), "0"); + fireEvent.click(getConfirmButton()); + expect(screen.getByText("Amount must be greater than 0")).toBeInTheDocument(); + expect(baseProps.onConfirm).not.toHaveBeenCalled(); + }); + + it("rejects more than 7 decimal places while typing", async () => { + const user = userEvent.setup(); + render(); + await user.type(getAmountInput(), "1.123456789"); + expect(getAmountInput().value).toBe("1.1234567"); + }); + + it("does not submit when more than 7 decimal places are present", () => { + render(); + fireEvent.change(getAmountInput(), { target: { value: "1.1234567" } }); + expect(getAmountInput().value).toBe("1.1234567"); + fireEvent.change(getAmountInput(), { target: { value: "1.12345678" } }); + expect(getAmountInput().value).toBe("1.1234567"); + }); + + it("submits a valid amount and shows a success toast", async () => { + const user = userEvent.setup(); + render(); + await user.type(getAmountInput(), "100.5"); + expect(screen.getByText(/600.50 USDC/)).toBeInTheDocument(); + fireEvent.click(getConfirmButton()); + await waitFor(() => { + expect(baseProps.onConfirm).toHaveBeenCalledWith("stream-42", "100.5"); + }); + expect(toast.success).toHaveBeenCalledWith("Successfully added 100.5 USDC to stream"); + }); + }); + + describe("submission error handling", () => { + it("shows toast.error and re-enables the form when onConfirm rejects", async () => { + const user = userEvent.setup(); + baseProps.onConfirm.mockRejectedValueOnce(new Error("tx failed")); + render(); + await user.type(getAmountInput(), "10"); + fireEvent.click(getConfirmButton()); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Failed to top up stream. Please try again."); + }); + expect(getConfirmButton()).toBeEnabled(); + expect(toast.success).not.toHaveBeenCalled(); + }); + }); + + describe("close-while-submitting guard", () => { + it("blocks close button, cancel, backdrop click, and Escape while submitting", async () => { + let resolveConfirm!: (value: void) => void; + const pendingConfirm = vi.fn( + () => new Promise((res) => { + resolveConfirm = res; + }), + ); + const user = userEvent.setup(); + render(); + await user.type(getAmountInput(), "10"); + fireEvent.click(getConfirmButton()); + + expect(screen.getByRole("button", { name: /topping up/i })).toBeDisabled(); + + const closeButton = screen.getByRole("button", { name: /close/i }); + expect(closeButton).toBeDisabled(); + fireEvent.click(closeButton); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: /cancel/i })); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("dialog")); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + fireEvent.keyDown(window, { key: "Escape" }); + expect(baseProps.onClose).not.toHaveBeenCalled(); + + await act(async () => { + resolveConfirm(); + }); + }); + }); +});