Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions frontend/e2e/send-remittance.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
2 changes: 1 addition & 1 deletion frontend/src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/[locale]/repay/[loanId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 0 additions & 17 deletions frontend/src/app/components/global_ui/Button.test.tsx

This file was deleted.

16 changes: 0 additions & 16 deletions frontend/src/app/components/global_ui/Button.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions frontend/src/app/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
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:
Expand All @@ -50,7 +50,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
return (
<Component
className={cn(
"inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-blue-500 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-focus-ring disabled:pointer-events-none disabled:opacity-50",
variants[variant],
sizes[size],
className,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/components/ui/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
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",
Expand Down
Loading