From b17563eeb406daecf171359c77adeda15cf8d9ba Mon Sep 17 00:00:00 2001 From: Levi-Ojukwu Date: Sat, 27 Jun 2026 23:57:28 +0100 Subject: [PATCH] fix: align accent colors to indigo, remove dead Button stub, lazy-load soroban, add remittance e2e --- frontend/e2e/send-remittance.spec.ts | 136 ++++++++++++++++++ frontend/src/app/[locale]/globals.css | 2 +- .../src/app/[locale]/repay/[loanId]/page.tsx | 2 +- .../app/components/global_ui/Button.test.tsx | 17 --- .../src/app/components/global_ui/Button.tsx | 16 --- frontend/src/app/components/ui/Button.tsx | 4 +- frontend/src/app/components/ui/Input.tsx | 2 +- 7 files changed, 141 insertions(+), 38 deletions(-) create mode 100644 frontend/e2e/send-remittance.spec.ts delete mode 100644 frontend/src/app/components/global_ui/Button.test.tsx delete mode 100644 frontend/src/app/components/global_ui/Button.tsx diff --git a/frontend/e2e/send-remittance.spec.ts b/frontend/e2e/send-remittance.spec.ts new file mode 100644 index 00000000..a04bde37 --- /dev/null +++ b/frontend/e2e/send-remittance.spec.ts @@ -0,0 +1,136 @@ +import { test, expect, type Page, type Route } from "@playwright/test"; + +const MOCK_SENDER_ADDRESS = "GCJPBXSE6WCQDCEYZW6C3YVZCSSCHC4AE72L5KWKCYL2CLLL7NH5VSCI"; +const VALID_RECIPIENT = "GADR4OJZ2A6Q7V4YHLQED7XN3YXC2B5S6T7U8A9B0C1D2E3F4G5H6J7K8L9M"; + +function seedConnectedWallet(state: { status: string; address: string }) { + return { + state: { + ...state, + network: { chainId: 2, name: "TESTNET", isSupported: true }, + balances: [ + { symbol: "USDC", amount: "5000.00", usdValue: 5000 }, + { symbol: "XLM", amount: "100.00", usdValue: 12.5 }, + ], + shouldAutoReconnect: true, + }, + version: 0, + }; +} + +async function setupMocks(page: Page) { + await page.route("**/api/user/profile", async (route: Route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + id: "user_1", + email: "alice@example.com", + walletAddress: MOCK_SENDER_ADDRESS, + kycVerified: true, + }), + }); + }); + + await page.route("**/api/pool/stats", async (route: Route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + success: true, + data: { + totalDeposits: 1000000, + totalOutstanding: 450000, + utilizationRate: 0.45, + apy: 0.12, + activeLoansCount: 154, + }, + }), + }); + }); +} + +test.describe("Send Remittance Flow", () => { + test("shows connect-wallet warning when wallet is not connected", async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem( + "remitlend-wallet", + JSON.stringify({ + state: { status: "disconnected", address: null }, + version: 0, + }), + ); + }); + await setupMocks(page); + await page.goto("/en/send-remittance"); + + await expect( + page.locator("text=Please connect your Stellar wallet to send remittances"), + ).toBeVisible(); + }); + + test("validates recipient address on submit", async ({ page }) => { + const walletState = JSON.stringify( + seedConnectedWallet({ status: "connected", address: MOCK_SENDER_ADDRESS }), + ); + await page.addInitScript((stateJson: string) => { + window.localStorage.setItem("remitlend-wallet", stateJson); + }, walletState); + await setupMocks(page); + await page.goto("/en/send-remittance"); + + await expect(page.locator("text=Send Remittance")).toBeVisible(); + + await page.fill('#recipientAddress', "invalid-address"); + await page.fill('#amount', "100"); + + await page.getByRole("button", { name: /Review & Send/i }).click(); + + await expect( + page.locator("text=Invalid Stellar address format"), + ).toBeVisible(); + }); + + test("sends a remittance successfully and redirects to history", async ({ page }) => { + const walletState = JSON.stringify( + seedConnectedWallet({ status: "connected", address: MOCK_SENDER_ADDRESS }), + ); + await page.addInitScript((stateJson: string) => { + window.localStorage.setItem("remitlend-wallet", stateJson); + }, walletState); + await setupMocks(page); + + await page.route("**/api/remittances", async (route: Route) => { + if (route.request().method() === "POST") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true, txHash: "tx_rem_abc" }), + }); + } else { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + } + }); + + await page.goto("/en/send-remittance"); + + await expect(page.locator("text=Send Remittance")).toBeVisible(); + + await page.fill('#recipientAddress', VALID_RECIPIENT); + await page.fill('#amount', "250"); + + await page.getByRole("button", { name: /Review & Send/i }).click(); + + const confirmBtn = page.getByRole("button", { name: /Confirm|Send/i }).first(); + await expect(confirmBtn).toBeVisible({ timeout: 5000 }); + await confirmBtn.click(); + + await expect(page.locator("text=Remittance sent successfully")).toBeVisible({ timeout: 10000 }); + + await page.waitForURL("**/remittances", { timeout: 5000 }); + }); +}); diff --git a/frontend/src/app/[locale]/globals.css b/frontend/src/app/[locale]/globals.css index 943e73cd..4e8ea71b 100644 --- a/frontend/src/app/[locale]/globals.css +++ b/frontend/src/app/[locale]/globals.css @@ -6,7 +6,7 @@ /* WCAG AA Compliant: #171717 on #ffffff (15.9:1) */ --background: #ffffff; --foreground: #171717; - --focus-ring: #2563eb; + --focus-ring: #4f46e5; } @theme inline { diff --git a/frontend/src/app/[locale]/repay/[loanId]/page.tsx b/frontend/src/app/[locale]/repay/[loanId]/page.tsx index e53210e1..ffb21f5a 100644 --- a/frontend/src/app/[locale]/repay/[loanId]/page.tsx +++ b/frontend/src/app/[locale]/repay/[loanId]/page.tsx @@ -21,7 +21,6 @@ import { import { useContractToast } from "../../../hooks/useContractToast"; import { TransactionPreviewModal } from "../../../components/transaction/TransactionPreviewModal"; import { useTransactionPreview } from "../../../hooks/useTransactionPreview"; -import { buildUnsignedRepaymentXdr } from "../../../utils/soroban"; import { buildAmountHelperText, getPrecisionError, @@ -86,6 +85,7 @@ export default function RepayLoanPage() { throw new Error("Contract configuration missing"); } + const { buildUnsignedRepaymentXdr } = await import("../../../utils/soroban"); const xdr = await buildUnsignedRepaymentXdr({ borrower: walletAddress, loanId, diff --git a/frontend/src/app/components/global_ui/Button.test.tsx b/frontend/src/app/components/global_ui/Button.test.tsx deleted file mode 100644 index ad0c5e25..00000000 --- a/frontend/src/app/components/global_ui/Button.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import Button from "./Button"; - -describe("Button Component", () => { - it("renders button text", () => { - render(); - expect(screen.getByText("Click Me")).toBeInTheDocument(); - }); - - it("calls onClick when clicked", () => { - const handleClick = jest.fn(); - render(); - - fireEvent.click(screen.getByText("Click")); - expect(handleClick).toHaveBeenCalledTimes(1); - }); -}); diff --git a/frontend/src/app/components/global_ui/Button.tsx b/frontend/src/app/components/global_ui/Button.tsx deleted file mode 100644 index 54f3b44a..00000000 --- a/frontend/src/app/components/global_ui/Button.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -interface ButtonProps { - children: React.ReactNode; - onClick?: () => void; -} - -const Button: React.FC = ({ children, onClick }) => { - return ( - - ); -}; - -export default Button; diff --git a/frontend/src/app/components/ui/Button.tsx b/frontend/src/app/components/ui/Button.tsx index cb0a39c2..2fa734a2 100644 --- a/frontend/src/app/components/ui/Button.tsx +++ b/frontend/src/app/components/ui/Button.tsx @@ -30,7 +30,7 @@ const Button = React.forwardRef( const Component = "button"; const variants = { - primary: "bg-blue-600 text-white hover:bg-blue-700 shadow-sm", + primary: "bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm", secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700", outline: @@ -50,7 +50,7 @@ const Button = React.forwardRef( return ( ( aria-invalid={error ? true : undefined} aria-describedby={error ? errorId : helperText ? helperId : undefined} className={cn( - "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-500 dark:text-zinc-100", + "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-500 dark:text-zinc-100", leftIcon && "pl-10", rightIcon && "pr-10", error && "border-red-500 focus-visible:ring-red-500",