diff --git a/src/__tests__/BatchPayQueue.test.tsx b/src/__tests__/BatchPayQueue.test.tsx
new file mode 100644
index 0000000..67619d0
--- /dev/null
+++ b/src/__tests__/BatchPayQueue.test.tsx
@@ -0,0 +1,239 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import BatchPayQueue from "@/components/BatchPayQueue";
+import type { Invoice } from "@stellar-split/sdk";
+
+vi.mock("@stellar-split/sdk", () => ({
+ formatAmount: (value: bigint) => (Number(value) / 10_000_000).toFixed(2),
+ parseAmount: (value: string) => {
+ const num = Number(value);
+ return isNaN(num) ? 0n : BigInt(Math.round(num * 10_000_000));
+ },
+}));
+
+const SCALE = 10_000_000n;
+
+const createMockInvoice = (id: string, status: string = "Pending"): Invoice => ({
+ id,
+ creator: "GCREATOR",
+ recipients: [
+ { address: "GRECIPIENT1", amount: 50n * SCALE },
+ { address: "GRECIPIENT2", amount: 50n * SCALE },
+ ],
+ token: "CUSDC",
+ deadline: 0,
+ funded: 0n,
+ status,
+ payments: [],
+});
+
+describe("Issue #597: BatchPayQueue error messages", () => {
+ it("should render queue items with empty state message when queue is empty", () => {
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/search for invoices above to add them/i)
+ ).toBeInTheDocument();
+ });
+
+ it("should display queue items with invoice details", () => {
+ const invoice = createMockInvoice("inv-123");
+ const queue = [{ invoice, amount: "100" }];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ const invoiceText = screen.getAllByText(/invoice #inv-123/i)[0];
+ expect(invoiceText).toBeInTheDocument();
+ expect(screen.getByText(/status: pending/i)).toBeInTheDocument();
+ });
+
+ it("should allow changing amount for queue items", async () => {
+ const invoice = createMockInvoice("inv-456");
+ const queue = [{ invoice, amount: "" }];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ const amountInput = screen.getByPlaceholderText(/amount \(usdc\)/i) as HTMLInputElement;
+ fireEvent.change(amountInput, { target: { value: "75" } });
+
+ expect(onAmountChange).toHaveBeenCalledWith("inv-456", "75");
+ });
+
+ it("should allow removing queue items", async () => {
+ const invoice = createMockInvoice("inv-789");
+ const queue = [{ invoice, amount: "100" }];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ const removeButton = screen.getByLabelText(/remove invoice/i);
+ fireEvent.click(removeButton);
+
+ expect(onRemove).toHaveBeenCalledWith("inv-789");
+ });
+
+ it("should calculate and display running total", () => {
+ const invoice1 = createMockInvoice("inv-1");
+ const invoice2 = createMockInvoice("inv-2");
+ const queue = [
+ { invoice: invoice1, amount: "50" },
+ { invoice: invoice2, amount: "75" },
+ ];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/running total/i)).toBeInTheDocument();
+ expect(screen.getByText(/125 usdc/i)).toBeInTheDocument();
+ });
+
+ it("should handle drag and drop reordering", async () => {
+ const invoice1 = createMockInvoice("inv-1");
+ const invoice2 = createMockInvoice("inv-2");
+ const queue = [
+ { invoice: invoice1, amount: "50" },
+ { invoice: invoice2, amount: "75" },
+ ];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+ const onReorder = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const firstItem = container.querySelector('[draggable="true"]');
+ if (firstItem) {
+ fireEvent.dragStart(firstItem);
+ const items = container.querySelectorAll('[draggable="true"]');
+ if (items.length > 1) {
+ fireEvent.dragOver(items[1]);
+ fireEvent.drop(items[1]);
+ }
+ }
+
+ await waitFor(() => {
+ expect(onReorder).toHaveBeenCalled();
+ });
+ });
+
+ it("should support keyboard reordering with Alt+ArrowUp", async () => {
+ const invoice1 = createMockInvoice("inv-1");
+ const invoice2 = createMockInvoice("inv-2");
+ const queue = [
+ { invoice: invoice1, amount: "50" },
+ { invoice: invoice2, amount: "75" },
+ ];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+ const onReorder = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const items = container.querySelectorAll('[draggable="true"]');
+ if (items.length > 1) {
+ fireEvent.keyDown(items[1], { key: "ArrowUp", altKey: true });
+ expect(onReorder).toHaveBeenCalled();
+ }
+ });
+
+ it("should support keyboard reordering with Alt+ArrowDown", async () => {
+ const invoice1 = createMockInvoice("inv-1");
+ const invoice2 = createMockInvoice("inv-2");
+ const queue = [
+ { invoice: invoice1, amount: "50" },
+ { invoice: invoice2, amount: "75" },
+ ];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+ const onReorder = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const items = container.querySelectorAll('[draggable="true"]');
+ if (items.length > 1) {
+ fireEvent.keyDown(items[0], { key: "ArrowDown", altKey: true });
+ expect(onReorder).toHaveBeenCalled();
+ }
+ });
+
+ it("should handle NaN amounts in running total calculation", () => {
+ const invoice1 = createMockInvoice("inv-1");
+ const invoice2 = createMockInvoice("inv-2");
+ const queue = [
+ { invoice: invoice1, amount: "50" },
+ { invoice: invoice2, amount: "" },
+ ];
+ const onAmountChange = vi.fn();
+ const onRemove = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/50 usdc/i)).toBeInTheDocument();
+ });
+});
diff --git a/src/__tests__/QRModalDownload.test.tsx b/src/__tests__/QRModalDownload.test.tsx
new file mode 100644
index 0000000..e6d9e71
--- /dev/null
+++ b/src/__tests__/QRModalDownload.test.tsx
@@ -0,0 +1,366 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import QRModal from "@/components/QRModal";
+
+vi.mock("@/lib/freighter", () => ({
+ isWalletConnected: vi.fn().mockResolvedValue(false),
+}));
+
+vi.mock("@/components/FocusTrap", () => ({
+ __esModule: true,
+ default: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+vi.mock("qrcode.react", () => ({
+ QRCodeCanvas: ({ value }: { value: string }) => (
+
+ ),
+}));
+
+describe("Issue #600: QRModal Download QR Code", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.clearAllMocks();
+ });
+
+ it("should render QR modal when open is true", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/scan to connect/i)).toBeInTheDocument();
+ });
+
+ it("should render QR code canvas", () => {
+ const onClose = vi.fn();
+ const testUri = "wc:test-uri-123";
+
+ render(
+
+ );
+
+ expect(screen.getByTestId(`qr-code-${testUri}`)).toBeInTheDocument();
+ });
+
+ it("should display the URI as plain text", () => {
+ const testUri = "wc:1234567890abcdef";
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(testUri)).toBeInTheDocument();
+ });
+
+ it("should render Copy URI button", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByRole("button", { name: /copy uri/i })).toBeInTheDocument();
+ });
+
+ it("should allow copying URI to clipboard", async () => {
+ const testUri = "wc:test-uri";
+ const onClose = vi.fn();
+ const onCopied = vi.fn();
+
+ const mockClipboard = {
+ writeText: vi.fn().mockResolvedValue(undefined),
+ };
+
+ Object.defineProperty(navigator, "clipboard", {
+ value: mockClipboard,
+ configurable: true,
+ });
+
+ render(
+
+ );
+
+ const copyButton = screen.getByRole("button", { name: /copy uri/i });
+ fireEvent.click(copyButton);
+
+ await waitFor(() => {
+ expect(mockClipboard.writeText).toHaveBeenCalledWith(testUri);
+ });
+ });
+
+ it("should call onCopied callback after copying URI", async () => {
+ const testUri = "wc:test-uri";
+ const onClose = vi.fn();
+ const onCopied = vi.fn();
+
+ const mockClipboard = {
+ writeText: vi.fn().mockResolvedValue(undefined),
+ };
+
+ Object.defineProperty(navigator, "clipboard", {
+ value: mockClipboard,
+ configurable: true,
+ });
+
+ render(
+
+ );
+
+ const copyButton = screen.getByRole("button", { name: /copy uri/i });
+ fireEvent.click(copyButton);
+
+ await waitFor(() => {
+ expect(onCopied).toHaveBeenCalled();
+ });
+ });
+
+ it("should render Dismiss button", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const dismissButtons = screen.getAllByRole("button", { name: /dismiss/i });
+ expect(dismissButtons.length).toBeGreaterThan(0);
+ });
+
+ it("should close modal when Dismiss button is clicked", async () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const dismissButton = screen.getAllByRole("button", { name: /dismiss/i })[0];
+ fireEvent.click(dismissButton);
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it("should close modal when close button in header is clicked", async () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const closeButton = screen.getByLabelText(/close qr modal/i);
+ fireEvent.click(closeButton);
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it("should not render when open is false", () => {
+ const onClose = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("should have proper ARIA attributes", () => {
+ const onClose = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog).toHaveAttribute("aria-modal", "true");
+ });
+
+ it("should poll wallet connection status", async () => {
+ const onConnected = vi.fn();
+ const onClose = vi.fn();
+
+ const { isWalletConnected } = await import("@/lib/freighter");
+
+ render(
+
+ );
+
+ vi.advanceTimersByTime(2000);
+
+ await waitFor(() => {
+ expect(isWalletConnected).toHaveBeenCalled();
+ });
+ });
+
+ it("should call onConnected when wallet becomes connected", async () => {
+ const onConnected = vi.fn();
+ const onClose = vi.fn();
+
+ const { isWalletConnected } = await import("@/lib/freighter");
+ vi.mocked(isWalletConnected).mockResolvedValueOnce(true);
+
+ render(
+
+ );
+
+ vi.advanceTimersByTime(2000);
+
+ await waitFor(() => {
+ expect(onConnected).toHaveBeenCalled();
+ });
+ });
+
+ it("should stop polling when modal is closed", async () => {
+ const onConnected = vi.fn();
+ const onClose = vi.fn();
+
+ const { isWalletConnected } = await import("@/lib/freighter");
+ vi.mocked(isWalletConnected).mockResolvedValue(true);
+
+ const { rerender } = render(
+
+ );
+
+ vi.advanceTimersByTime(2000);
+
+ rerender(
+
+ );
+
+ const callCountAfterClose = vi.mocked(isWalletConnected).mock.calls.length;
+
+ vi.advanceTimersByTime(2000);
+
+ const callCountAfterWait = vi.mocked(isWalletConnected).mock.calls.length;
+
+ expect(callCountAfterWait).toBe(callCountAfterClose);
+ });
+
+ it("should respect custom polling interval", async () => {
+ const onConnected = vi.fn();
+ const onClose = vi.fn();
+
+ const { isWalletConnected } = await import("@/lib/freighter");
+ vi.mocked(isWalletConnected).mockResolvedValue(false);
+
+ render(
+
+ );
+
+ vi.advanceTimersByTime(500);
+
+ await waitFor(() => {
+ expect(vi.mocked(isWalletConnected).mock.calls.length).toBeGreaterThan(0);
+ });
+ });
+
+ it("should display heading with scan instruction", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/scan to connect/i)).toBeInTheDocument();
+ });
+
+ it("should have visual styling for draggable QR container", () => {
+ const onClose = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const qrContainer = container.querySelector(".bg-white.rounded-xl");
+ expect(qrContainer).toBeInTheDocument();
+ });
+});
diff --git a/src/__tests__/TxConfirmModal.test.tsx b/src/__tests__/TxConfirmModal.test.tsx
new file mode 100644
index 0000000..85d61b5
--- /dev/null
+++ b/src/__tests__/TxConfirmModal.test.tsx
@@ -0,0 +1,234 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import TxConfirmModal from "@/components/TxConfirmModal";
+
+vi.mock("@/components/FocusTrap", () => ({
+ __esModule: true,
+ default: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+describe("Issue #598: TxConfirmModal with Stellar Expert link", () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("should render transaction confirmation message", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/✓ payment confirmed/i)).toBeInTheDocument();
+ expect(screen.getByText("Transaction hash")).toBeInTheDocument();
+ });
+
+ it("should display transaction hash", () => {
+ const txHash = "1234567890abcdef";
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(txHash)).toBeInTheDocument();
+ });
+
+ it("should render link to Stellar Expert with proper URL structure", () => {
+ const txHash = "abc123hash";
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const link = screen.getByRole("link", {
+ name: /view on stellar expert/i,
+ });
+ expect(link).toHaveAttribute("href");
+ const href = link.getAttribute("href");
+ expect(href).toContain("stellar.expert/explorer");
+ expect(href).toContain(`tx/${txHash}`);
+ });
+
+ it("should open Stellar Expert link in new tab with security attributes", () => {
+ const txHash = "abc123hash";
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const link = screen.getByRole("link", {
+ name: /view on stellar expert/i,
+ });
+ expect(link).toHaveAttribute("target", "_blank");
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("should allow copying transaction hash", async () => {
+ const txHash = "abcdef123456";
+ const onClose = vi.fn();
+
+ const mockClipboard = {
+ writeText: vi.fn().mockResolvedValue(undefined),
+ };
+
+ Object.defineProperty(navigator, "clipboard", {
+ value: mockClipboard,
+ configurable: true,
+ });
+
+ render(
+
+ );
+
+ const copyButton = screen.getByRole("button", { name: /copy/i });
+ fireEvent.click(copyButton);
+
+ await waitFor(() => {
+ expect(mockClipboard.writeText).toHaveBeenCalledWith(txHash);
+ });
+ });
+
+ it("should show 'Copied!' feedback after copying", async () => {
+ const txHash = "abcdef123456";
+ const onClose = vi.fn();
+
+ const mockClipboard = {
+ writeText: vi.fn().mockResolvedValue(undefined),
+ };
+
+ Object.defineProperty(navigator, "clipboard", {
+ value: mockClipboard,
+ configurable: true,
+ });
+
+ vi.useFakeTimers();
+
+ render(
+
+ );
+
+ const copyButton = screen.getByRole("button", { name: /copy/i });
+ fireEvent.click(copyButton);
+
+ await waitFor(() => {
+ expect(screen.getByText("Copied!")).toBeInTheDocument();
+ });
+
+ vi.advanceTimersByTime(2001);
+
+ await waitFor(() => {
+ expect(screen.getByText("Copy")).toBeInTheDocument();
+ });
+
+ vi.useRealTimers();
+ });
+
+ it("should close modal when close button is clicked", async () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const closeButton = screen.getByLabelText("Close");
+ fireEvent.click(closeButton);
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it("should close modal when clicking overlay background", async () => {
+ const onClose = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const overlayContainer = container.querySelector(".fixed.inset-0");
+ if (overlayContainer) {
+ fireEvent.click(overlayContainer);
+ }
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it("should render success indicator with green checkmark", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ const title = screen.getByText(/✓ payment confirmed/i);
+ expect(title).toHaveClass("text-green-400");
+ });
+
+ it("should display the correct action text in confirmation message", () => {
+ const onClose = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/✓ withdrawal confirmed/i)).toBeInTheDocument();
+ });
+
+ it("should be accessible with proper ARIA attributes", () => {
+ const onClose = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog).toHaveAttribute("aria-modal", "true");
+ expect(dialog).toHaveAttribute("aria-labelledby", "tx-modal-title");
+ });
+});
diff --git a/src/__tests__/WalletErrorModal.test.tsx b/src/__tests__/WalletErrorModal.test.tsx
new file mode 100644
index 0000000..444ee21
--- /dev/null
+++ b/src/__tests__/WalletErrorModal.test.tsx
@@ -0,0 +1,321 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import WalletErrorModal from "@/components/WalletErrorModal";
+
+describe("Issue #599: WalletErrorModal retry functionality", () => {
+ it("should not render when errorType is null", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("should render not installed error state", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/freighter not installed/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/freighter is a browser extension/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: /install freighter/i })
+ ).toBeInTheDocument();
+ });
+
+ it("should render locked wallet error state", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/freighter is locked/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/your freighter wallet is locked/i)
+ ).toBeInTheDocument();
+ });
+
+ it("should render locked wallet error with Try Again button", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const tryAgainButton = screen.getByRole("button", { name: /try again/i });
+ expect(tryAgainButton).toBeInTheDocument();
+ });
+
+ it("should call onRetry when Try Again button is clicked", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const tryAgainButton = screen.getByRole("button", { name: /try again/i });
+ fireEvent.click(tryAgainButton);
+
+ expect(onRetry).toHaveBeenCalled();
+ });
+
+ it("should render network mismatch error state", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/wrong network/i)).toBeInTheDocument();
+ const mainnetTexts = screen.getAllByText(/mainnet/i);
+ expect(mainnetTexts.length).toBeGreaterThan(0);
+ });
+
+ it("should render network mismatch error with Got it button", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const gotItButton = screen.getByRole("button", { name: /got it/i });
+ expect(gotItButton).toBeInTheDocument();
+ });
+
+ it("should call onDismiss when Dismiss button is clicked", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const dismissButton = screen.getByRole("button", { name: /cancel/i });
+ fireEvent.click(dismissButton);
+
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ it("should dismiss modal when Escape key is pressed", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ fireEvent.keyDown(document, { key: "Escape" });
+
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ it("should dismiss modal when clicking on overlay background", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const overlayBackground = container.querySelector(".fixed.inset-0");
+ if (overlayBackground) {
+ fireEvent.click(overlayBackground, {
+ target: overlayBackground,
+ currentTarget: overlayBackground,
+ });
+ }
+
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ it("should have proper ARIA attributes for accessibility", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ const { container } = render(
+
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog).toHaveAttribute("aria-modal", "true");
+ expect(dialog).toHaveAttribute("aria-labelledby", "wallet-error-title");
+ });
+
+ it("should focus first interactive element when error is shown", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ const firstButton = screen.getByRole("button", { name: /try again/i });
+ expect(firstButton).toHaveFocus();
+ });
+ });
+
+ it("should restore focus when error is cleared", async () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ const { rerender } = render(
+
+ );
+
+ const previouslyFocused = document.activeElement;
+
+ rerender(
+
+ );
+
+ await waitFor(() => {
+ expect(document.activeElement).not.toBe(previouslyFocused);
+ });
+ });
+
+ it("should use default expectedNetwork when not provided", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const testnetTexts = screen.getAllByText(/testnet/i);
+ expect(testnetTexts.length).toBeGreaterThan(0);
+ });
+
+ it("should use custom expectedNetwork when provided", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const customChainTexts = screen.getAllByText(/custom chain/i);
+ expect(customChainTexts.length).toBeGreaterThan(0);
+ });
+
+ it("should display install link for not_installed error", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ const installLink = screen.getByRole("link", {
+ name: /install freighter/i,
+ });
+ expect(installLink).toHaveAttribute(
+ "href",
+ "https://www.freighter.app/"
+ );
+ expect(installLink).toHaveAttribute("target", "_blank");
+ expect(installLink).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("should display instructions for locked wallet", () => {
+ const onDismiss = vi.fn();
+ const onRetry = vi.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByText(/click the freighter icon/i)).toBeInTheDocument();
+ expect(screen.getByText(/enter your freighter password/i)).toBeInTheDocument();
+ expect(screen.getByText(/return here and click/i)).toBeInTheDocument();
+ });
+});