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
33 changes: 33 additions & 0 deletions docs-site/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,36 @@ events.onmessage = (e) => console.log(JSON.parse(e.data));

Next: [Core concepts](/core-concepts) or the [interactive
playground](/api-reference/playground).

## Running in Local Sandbox Mode

The local mock sandbox lets you run the frontend and backend with seeded demo
data, without a funded Testnet wallet or live Soroban RPC. From the repository
root, install dependencies once and start the full stack:

```bash
npm install
npm run dev:mock
```

This starts PostgreSQL, Redis, and the mock Soroban RPC, prepares and seeds the
local database with `scripts/seed-mock-data.ts`, then starts the backend and
frontend. The backend is launched with `SANDBOX_MODE_ENABLED=true` by the
`dev:mock:start` script. The backend configuration uses this variable name
(rather than `SANDBOX_MODE`).

To run the seed script directly, first ensure the local database is running and
its schema has been prepared, then run this command from the repository root:

```bash
npx tsx scripts/seed-mock-data.ts
```

The script uses `DATABASE_URL` if set; otherwise, it defaults to the local
sandbox database at `******localhost:5433/flowfi`.

Once the services are running, open the app at [http://localhost:3000](http://localhost:3000).
Select **Sandbox** in the frontend network selector to use the local mock RPC
for network-dependent features. The mock RPC does not support transaction
simulation, so use the Testnet workflow above when testing real Stellar
transactions.
60 changes: 60 additions & 0 deletions frontend/src/components/ModeToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { afterEach, describe, expect, it } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ThemeProvider } from "next-themes";
import { ModeToggle } from "./ModeToggle";

function renderModeToggle() {
return render(
<ThemeProvider attribute="class" defaultTheme="light" storageKey="theme">
<ModeToggle />
</ThemeProvider>,
);
}

afterEach(() => {
localStorage.clear();
document.documentElement.className = "";
});

describe("ModeToggle", () => {
it("toggles themes and persists the selected preference", async () => {
const user = userEvent.setup();
renderModeToggle();

const toggle = await screen.findByRole("button", { name: "Switch to dark mode" });
expect(toggle.querySelector("svg.text-yellow-500")).toBeInTheDocument();
expect(toggle.querySelector("svg.text-blue-400")).toBeInTheDocument();

await user.click(toggle);

await waitFor(() => {
expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeInTheDocument();
expect(localStorage.getItem("theme")).toBe("dark");
expect(document.documentElement).toHaveClass("dark");
});

await user.click(screen.getByRole("button", { name: "Switch to light mode" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Switch to dark mode" })).toBeInTheDocument();
expect(localStorage.getItem("theme")).toBe("light");
expect(document.documentElement).not.toHaveClass("dark");
});
});

it("can be focused and activated from the keyboard", async () => {
const user = userEvent.setup();
renderModeToggle();

const toggle = await screen.findByRole("button", { name: "Switch to dark mode" });
await user.tab();
expect(toggle).toHaveFocus();

await user.keyboard("{Enter}");

await waitFor(() => {
expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeInTheDocument();
expect(localStorage.getItem("theme")).toBe("dark");
});
});
});
78 changes: 78 additions & 0 deletions frontend/src/components/NetworkSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const useNetworkMock = vi.fn();

vi.mock("@/context/NetworkContext", () => ({
useNetwork: () => useNetworkMock(),
}));

import { NetworkSelector } from "./NetworkSelector";

describe("NetworkSelector", () => {
const setNetworkId = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
useNetworkMock.mockReturnValue({ networkId: "testnet", setNetworkId });
});

it("shows the active Testnet badge and available Stellar networks", () => {
const { container } = render(<NetworkSelector />);
const selector = screen.getByRole("combobox", {
name: "Active Stellar network",
});
const badge = container.querySelector("label span");

expect(selector).toHaveValue("testnet");
expect(badge).toHaveClass("bg-orange-400");
expect(screen.getByRole("option", { name: "Testnet" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Mainnet" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Futurenet" })).toBeInTheDocument();
});

it("uses the active network's badge color", () => {
useNetworkMock.mockReturnValue({ networkId: "mainnet", setNetworkId });
const { container } = render(<NetworkSelector />);

expect(container.querySelector("label span")).toHaveClass("bg-emerald-400");
});

it("sets the selected network", async () => {
const user = userEvent.setup();
render(<NetworkSelector />);

await user.selectOptions(
screen.getByRole("combobox", { name: "Active Stellar network" }),
"mainnet",
);

expect(setNetworkId).toHaveBeenCalledWith("mainnet");
});

it("keeps the native selector keyboard accessible", async () => {
const user = userEvent.setup();
render(<NetworkSelector />);
const selector = screen.getByRole("combobox", {
name: "Active Stellar network",
});
const handleKeyDown = vi.fn();
selector.addEventListener("keydown", handleKeyDown);

await user.tab();
expect(selector).toHaveFocus();
await user.keyboard("{ArrowDown}");
await user.keyboard("{Enter}");

expect(selector).toHaveFocus();
expect(handleKeyDown).toHaveBeenCalledWith(expect.objectContaining({ key: "ArrowDown" }));
expect(handleKeyDown).toHaveBeenCalledWith(expect.objectContaining({ key: "Enter" }));
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
"Testnet",
"Futurenet",
"Mainnet",
"Sandbox",
]);
});
});
50 changes: 50 additions & 0 deletions frontend/src/components/wallet/DisconnectConfirmModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DisconnectConfirmModal } from "./DisconnectConfirmModal";

describe("DisconnectConfirmModal", () => {
it("confirms wallet disconnection when Disconnect is selected", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
const onClose = vi.fn();

render(
<DisconnectConfirmModal
onConfirm={onConfirm}
onClose={onClose}
walletAddress="GTESTWALLETADDRESS"
/>,
);

expect(screen.getByText("GTESTWALLETADDRESS")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Disconnect" }));

expect(onConfirm).toHaveBeenCalledOnce();
expect(onClose).not.toHaveBeenCalled();
});

it("closes without disconnecting when Cancel is selected", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
const onClose = vi.fn();

render(<DisconnectConfirmModal onConfirm={onConfirm} onClose={onClose} />);
await user.click(screen.getByRole("button", { name: "Cancel" }));

expect(onClose).toHaveBeenCalledOnce();
expect(onConfirm).not.toHaveBeenCalled();
});

it("closes without disconnecting when Escape is pressed", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
const onClose = vi.fn();

render(<DisconnectConfirmModal onConfirm={onConfirm} onClose={onClose} />);
await user.keyboard("{Escape}");

expect(onClose).toHaveBeenCalledOnce();
expect(onConfirm).not.toHaveBeenCalled();
});
});
Loading