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
63 changes: 63 additions & 0 deletions frontend/src/__tests__/create-stream-recipient-prefill.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import React from "react";

const VALID_ADDRESS = "GAV4A377RAEV6YVAWZVHXF4VZD5ZBXGIKEMNHV5YIMV5LIKSNQVYUBR7";

const searchParamsMock = { get: vi.fn() };
const push = vi.fn();

vi.mock("next/navigation", () => ({
useRouter: () => ({ push }),
useSearchParams: () => searchParamsMock,
}));

vi.mock("react-hot-toast", () => ({
toast: { success: vi.fn(), error: vi.fn() },
}));

vi.mock("@/context/wallet-context", () => ({
useWallet: () => ({ status: "disconnected", session: null }),
}));

vi.mock("@/lib/soroban", () => ({
createStream: vi.fn(),
toBaseUnits: vi.fn(),
toDurationSeconds: vi.fn(),
getTokenAddress: vi.fn(),
toSorobanErrorMessage: vi.fn(),
TOKEN_ADDRESSES: { XLM: "xlm-address" },
}));

import CreateStreamContent from "../app/streams/create/create-stream-content";

function getRecipientInput() {
return screen.getByLabelText(/recipient/i) as HTMLInputElement;
}

describe("CreateStreamContent recipient prefill", () => {
it("prefills the recipient field from a valid deep-linked query param", async () => {
searchParamsMock.get.mockImplementation((key: string) => (key === "recipient" ? VALID_ADDRESS : null));

render(<CreateStreamContent />);

await waitFor(() => expect(getRecipientInput().value).toBe(VALID_ADDRESS));
});

it("ignores a malformed recipient query param and leaves the field empty", async () => {
searchParamsMock.get.mockImplementation((key: string) => (key === "recipient" ? "not-a-stellar-address" : null));

render(<CreateStreamContent />);

await waitFor(() => expect(getRecipientInput()).toBeInTheDocument());
expect(getRecipientInput().value).toBe("");
});

it("leaves the recipient field empty when no query param is present", () => {
searchParamsMock.get.mockReturnValue(null);

render(<CreateStreamContent />);

expect(getRecipientInput().value).toBe("");
});
});
64 changes: 64 additions & 0 deletions frontend/src/__tests__/wallet-entry.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import React from "react";

const useWalletMock = vi.fn();

vi.mock("@/context/wallet-context", () => ({
useWallet: () => useWalletMock(),
}));

vi.mock("@/components/wallet/WalletModal", () => ({
WalletModal: () => <div data-testid="wallet-modal">connect your wallet</div>,
}));

vi.mock("@/components/dashboard/dashboard-view", () => ({
DashboardView: () => <div data-testid="dashboard-view">dashboard</div>,
}));

import { WalletEntry } from "../components/wallet/wallet-entry";

describe("WalletEntry", () => {
it("shows a loading state before hydration instead of the dashboard or modal", () => {
useWalletMock.mockReturnValue({
status: "disconnected",
session: null,
isHydrated: false,
disconnect: vi.fn(),
});

render(<WalletEntry />);

expect(screen.getByText(/loading wallet session/i)).toBeInTheDocument();
expect(screen.queryByTestId("wallet-modal")).not.toBeInTheDocument();
expect(screen.queryByTestId("dashboard-view")).not.toBeInTheDocument();
});

it("prompts wallet connection when hydrated with no active session", () => {
useWalletMock.mockReturnValue({
status: "disconnected",
session: null,
isHydrated: true,
disconnect: vi.fn(),
});

render(<WalletEntry />);

expect(screen.getByTestId("wallet-modal")).toBeInTheDocument();
expect(screen.queryByTestId("dashboard-view")).not.toBeInTheDocument();
});

it("renders the dashboard when hydrated with a connected session", () => {
useWalletMock.mockReturnValue({
status: "connected",
session: { publicKey: "GABC" },
isHydrated: true,
disconnect: vi.fn(),
});

render(<WalletEntry />);

expect(screen.getByTestId("dashboard-view")).toBeInTheDocument();
expect(screen.queryByTestId("wallet-modal")).not.toBeInTheDocument();
});
});
18 changes: 16 additions & 2 deletions frontend/src/app/streams/create/create-stream-content.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { logger } from "@/lib/logger";
import {
createStream,
Expand All @@ -12,7 +12,7 @@ import {
} from "@/lib/soroban";
import { hasValidPrecision, validateAmountInput } from "@/utils/amount";
import { toast } from "react-hot-toast";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { useWallet } from "@/context/wallet-context";
Expand All @@ -22,6 +22,7 @@ const TOKEN_DECIMALS = 7;
export default function CreateStreamContent() {
const { status, session } = useWallet();
const router = useRouter();
const searchParams = useSearchParams();
const [nowTimestamp] = useState(() => Date.now());
const [loading, setLoading] = useState(false);
const [txState, setTxState] = useState<"idle" | "signing" | "submitted" | "confirming">("idle");
Expand All @@ -32,6 +33,19 @@ export default function CreateStreamContent() {
duration: "30",
});

useEffect(() => {
const recipientParam = searchParams.get("recipient");
if (!recipientParam) return;

import("@stellar/stellar-sdk").then(({ StrKey }) => {
if (StrKey.isValidEd25519PublicKey(recipientParam)) {
setFormData((prev) => ({ ...prev, recipient: recipientParam }));
} else {
logger.warn("Ignoring malformed recipient query param", { recipientParam });
}
});
}, [searchParams]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (status !== "connected" || !session) {
Expand Down
Loading