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
12 changes: 12 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ GEMINI.md
# Playwright
test-results/
playwright-report/

# Generated lint / test / bundle reports (run locally, never commit)
lint-output*.txt
lint.txt
test-results.txt
test-output.txt
test_output.json
vitest-results*.json
bundle-stats.html

# Local backups
*.bak
4 changes: 4 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ npm run test:run
npm run test:e2e
```

## Generated Reports

Lint, test and bundle reports are **generated locally and must not be committed**. Write them to the git-ignored `test-results/` directory, or delete them before committing. The common artifacts (`lint-output*.txt`, `lint.txt`, `test-results.txt`, `test-output.txt`, `test_output.json`, `vitest-results*.json`, `bundle-stats.html`) are covered by `.gitignore`.

## Related Docs

- API docs output: [`docs/api/frontend`](/Users/macbook/stellar/YieldVault-RWA/docs/api/frontend)
Expand Down
4,950 changes: 0 additions & 4,950 deletions frontend/bundle-stats.html

This file was deleted.

Binary file removed frontend/lint-output.txt
Binary file not shown.
32 changes: 0 additions & 32 deletions frontend/lint-output2.txt

This file was deleted.

12 changes: 0 additions & 12 deletions frontend/lint-output3.txt

This file was deleted.

26 changes: 0 additions & 26 deletions frontend/lint.txt

This file was deleted.

19 changes: 4 additions & 15 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,6 @@ function AppContent() {
</RouteErrorBoundary>
}
/>
<Route path="/transactions" element={<ErrorBoundary><LazyTransactionHistory walletAddress={walletAddress} /></ErrorBoundary>} />
<Route path="/compare" element={<ErrorBoundary><LazyVaultComparison /></ErrorBoundary>} />
<Route path="/strategies/:strategyId" element={<ErrorBoundary><StrategyDetail walletAddress={walletAddress} /></ErrorBoundary>} />
<Route path="/receipt/:txHash" element={<ErrorBoundary><TransactionReceipt /></ErrorBoundary>} />
<Route path="/settings" element={<ErrorBoundary><LazySettings /></ErrorBoundary>} />
<Route path="/vault-health" element={<ErrorBoundary><VaultHealthDashboard /></ErrorBoundary>} />
<Route path="/audit-log" element={<ErrorBoundary><AuditLog /></ErrorBoundary>} />
<Route path="/ui-kit" element={<ErrorBoundary><LazyUIPreview /></ErrorBoundary>} />
<Route
path="/admin"
element={
Expand Down Expand Up @@ -304,16 +296,13 @@ function AppContent() {
}
/>
<Route
path="/admin"
path="*"
element={
<ProtectedRoute role={role} allow={["admin"]}>
<RouteErrorBoundary routeName="admin">
<Admin walletAddress={walletAddress} />
</RouteErrorBoundary>
</ProtectedRoute>
<RouteErrorBoundary routeName="not-found">
<Navigate to="/" replace />
</RouteErrorBoundary>
}
/>
<Route path="*" element={<ErrorBoundary><Navigate to="/" replace /></ErrorBoundary>} />
</SentryRoutes>
</Suspense>
</PageTransition>
Expand Down
125 changes: 125 additions & 0 deletions frontend/src/components/ConfirmationModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useState } from "react";
import ConfirmationModal from "./ConfirmationModal";

const baseProps = {
onClose: vi.fn(),
onConfirm: vi.fn(),
actionType: "deposit" as const,
amount: 100,
expectedOutput: 99.5,
estimatedFee: 0.5,
};

function Harness({ isProcessing = false }: { isProcessing?: boolean }) {
const [open, setOpen] = useState(false);
return (
<div>
<button data-testid="trigger" onClick={() => setOpen(true)}>
open
</button>
<ConfirmationModal
{...baseProps}
isOpen={open}
isProcessing={isProcessing}
onClose={() => {
baseProps.onClose();
setOpen(false);
}}
/>
</div>
);
}

const FOCUSABLE_SELECTOR =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

describe("ConfirmationModal", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("renders nothing while closed", () => {
render(<ConfirmationModal {...baseProps} isOpen={false} />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});

it("renders the dialog with translated copy when open", () => {
render(<ConfirmationModal {...baseProps} isOpen />);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(screen.getByRole("heading", { name: "Confirm Deposit" })).toBeInTheDocument();
expect(screen.getByText("Estimated Fee")).toBeInTheDocument();
expect(screen.getByText("Total Cost")).toBeInTheDocument();
});

it("shows the large-amount alert for amounts over the threshold", () => {
render(<ConfirmationModal {...baseProps} isOpen amount={5000} />);
expect(screen.getByRole("alert")).toHaveTextContent("Large transaction amount detected");
});

it("moves focus into the dialog on open", async () => {
render(<ConfirmationModal {...baseProps} isOpen />);
const dialog = screen.getByRole("dialog");
await waitFor(() => {
expect(dialog.contains(document.activeElement)).toBe(true);
});
});

it("calls onConfirm when the confirm button is clicked", () => {
render(<ConfirmationModal {...baseProps} isOpen />);
fireEvent.click(screen.getByLabelText("Confirm deposit"));
expect(baseProps.onConfirm).toHaveBeenCalledTimes(1);
});

it("closes on Escape", async () => {
render(<ConfirmationModal {...baseProps} isOpen />);
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => expect(baseProps.onClose).toHaveBeenCalledTimes(1));
});

it("closes on backdrop click", () => {
render(<ConfirmationModal {...baseProps} isOpen />);
fireEvent.click(screen.getByRole("dialog"));
expect(baseProps.onClose).toHaveBeenCalledTimes(1);
});

it("ignores backdrop clicks while processing", () => {
render(<ConfirmationModal {...baseProps} isOpen isProcessing />);
fireEvent.click(screen.getByRole("dialog"));
expect(baseProps.onClose).not.toHaveBeenCalled();
});

it("ignores Escape while processing", () => {
render(<ConfirmationModal {...baseProps} isOpen isProcessing />);
fireEvent.keyDown(document, { key: "Escape" });
expect(baseProps.onClose).not.toHaveBeenCalled();
});

it("does not close when clicking inside the panel", () => {
render(<ConfirmationModal {...baseProps} isOpen />);
fireEvent.click(screen.getByRole("heading", { name: "Confirm Deposit" }));
expect(baseProps.onClose).not.toHaveBeenCalled();
});

it("wraps Tab focus from the last element back to the first", () => {
render(<ConfirmationModal {...baseProps} isOpen />);
const dialog = screen.getByRole("dialog");
const focusables = dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
const last = focusables[focusables.length - 1];
last.focus();
fireEvent.keyDown(document, { key: "Tab" });
expect(document.activeElement).toBe(focusables[0]);
});

it("restores focus to the trigger after closing", async () => {
render(<Harness />);
const trigger = screen.getByTestId("trigger");
trigger.focus();
fireEvent.click(trigger);
await screen.findByRole("dialog");
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => expect(trigger).toHaveFocus());
});
});
Loading
Loading