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();
+ });
+ });
+ });
+});