diff --git a/src/context/SorokitProvider.test.tsx b/src/context/SorokitProvider.test.tsx
index 629888c..4706708 100644
--- a/src/context/SorokitProvider.test.tsx
+++ b/src/context/SorokitProvider.test.tsx
@@ -9,6 +9,8 @@ import { SorokitProvider } from "./SorokitProvider";
import { useSorokit } from "./useSorokit";
const TestComponent = () => {
+ const { address, account, balances, error, connectWallet, disconnectWallet, switchNetwork } = useSorokit();
+
const { address, account, balances, connectWallet, disconnectWallet, switchNetwork, refreshAccount, isLoadingAccount, error, errorHistory } = useSorokit();
return (
@@ -16,6 +18,7 @@ const TestComponent = () => {
{address || "none"}
{account ? account.sequence : "none"}
{balances.length}
+ {error || "none"}
{isLoadingAccount ? "true" : "false"}
{error || "none"}
{errorHistory.length}
@@ -83,7 +86,10 @@ describe("SorokitProvider", () => {
} as unknown as ReturnType;
});
- it("disconnectWallet clears address, account, and balances", async () => {
+ it("disconnectWallet clears address, account, balances, and error", async () => {
+ mockClient.account.getAccount = vi.fn().mockResolvedValue({ data: null, error: "Account error" });
+ mockClient.account.getBalances = vi.fn().mockResolvedValue({ data: null, error: "Balances error" });
+
renderWithProvider(, { client: mockClient });
const connectBtn = screen.getByText("Connect");
@@ -96,8 +102,7 @@ describe("SorokitProvider", () => {
expect(screen.getByTestId("address")).toHaveTextContent("GABC");
await waitFor(() => {
- expect(screen.getByTestId("account")).toHaveTextContent("100");
- expect(screen.getByTestId("balances")).toHaveTextContent("1");
+ expect(screen.getByTestId("error")).toHaveTextContent("Account error; Balances error");
});
await act(async () => {
@@ -107,6 +112,84 @@ describe("SorokitProvider", () => {
expect(screen.getByTestId("address")).toHaveTextContent("none");
expect(screen.getByTestId("account")).toHaveTextContent("none");
expect(screen.getByTestId("balances")).toHaveTextContent("0");
+ expect(screen.getByTestId("error")).toHaveTextContent("none");
+ });
+
+ it("combines error strings when both getAccount and getBalances fail", async () => {
+ mockClient.account.getAccount = vi.fn().mockResolvedValue({ data: null, error: "Account failed" });
+ mockClient.account.getBalances = vi.fn().mockResolvedValue({ data: null, error: "Balances failed" });
+
+ renderWithProvider(, { client: mockClient });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toHaveTextContent("Account failed; Balances failed");
+ });
+ });
+
+ it("shows single error when only getAccount fails", async () => {
+ mockClient.account.getAccount = vi.fn().mockResolvedValue({ data: null, error: "Account not found" });
+ mockClient.account.getBalances = vi.fn().mockResolvedValue({ data: [{ asset: "XLM", balance: "10" }], error: null });
+
+ renderWithProvider(, { client: mockClient });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toHaveTextContent("Account not found");
+ expect(screen.getByTestId("balances")).toHaveTextContent("1");
+ });
+ });
+
+ it("shows single error when only getBalances fails", async () => {
+ mockClient.account.getAccount = vi.fn().mockResolvedValue({ data: { sequence: "100" }, error: null });
+ mockClient.account.getBalances = vi.fn().mockResolvedValue({ data: null, error: "Failed to fetch balances" });
+
+ renderWithProvider(, { client: mockClient });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toHaveTextContent("Failed to fetch balances");
+ expect(screen.getByTestId("account")).toHaveTextContent("100");
+ });
+ });
+
+ it("clears error from previous session on reconnect", async () => {
+ mockClient.account.getAccount = vi.fn().mockResolvedValueOnce({ data: null, error: "Old error" });
+ mockClient.account.getBalances = vi.fn().mockResolvedValueOnce({ data: [], error: null });
+
+ renderWithProvider(, { client: mockClient });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toHaveTextContent("Old error");
+ });
+
+ // Next connect with different address succeeds
+ mockClient.wallet.connect = vi.fn().mockResolvedValue({ data: { address: "GDEF" }, error: null });
+ mockClient.account.getAccount = vi.fn().mockResolvedValue({ data: { sequence: "200" }, error: null });
+ mockClient.account.getBalances = vi.fn().mockResolvedValue({ data: [{ asset: "XLM", balance: "50" }], error: null });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("address")).toHaveTextContent("GDEF");
+ expect(screen.getByTestId("error")).toHaveTextContent("none");
+ expect(screen.getByTestId("account")).toHaveTextContent("200");
+ });
});
it("connectWallet populates address on success", async () => {
diff --git a/src/context/SorokitProvider.tsx b/src/context/SorokitProvider.tsx
index ce78c5b..3777315 100644
--- a/src/context/SorokitProvider.tsx
+++ b/src/context/SorokitProvider.tsx
@@ -149,7 +149,12 @@ export function SorokitProvider({
// Load account when address changes
useEffect(() => {
- if (!address) return;
+ setError(null);
+ if (!address) {
+ setAccount(null);
+ setBalances([]);
+ return;
+ }
let active = true;
const timerId = window.setTimeout(() => {
@@ -162,6 +167,10 @@ export function SorokitProvider({
if (!active) return;
if (accountRes.data) setAccount(accountRes.data);
if (balancesRes.data) setBalances(balancesRes.data);
+ const combined = [accountRes.error, balancesRes.error]
+ .filter(Boolean)
+ .join("; ");
+ if (combined) setError(combined);
if (accountRes.error && balancesRes.error) {
reportError(
`${accountRes.error}; ${balancesRes.error}`,
@@ -227,6 +236,12 @@ export function SorokitProvider({
}, [reportError]);
const disconnectWallet = useCallback(async () => {
+ await client.wallet.disconnect();
+ setAddress(null);
+ setAccount(null);
+ setBalances([]);
+ setError(null);
+ }, [client]);
setIsDisconnecting(true);
try {
// A wallet adapter that throws (e.g. the extension went away
diff --git a/src/screens/Dashboard.test.tsx b/src/screens/Dashboard.test.tsx
index de37900..94e0e40 100644
--- a/src/screens/Dashboard.test.tsx
+++ b/src/screens/Dashboard.test.tsx
@@ -1,3 +1,72 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { Dashboard } from "./Dashboard";
+
+vi.mock("@/context/useSorokit", () => ({
+ useSorokit: vi.fn(() => ({
+ isConnected: true,
+ address: "GABC",
+ account: null,
+ balances: [],
+ network: { name: "testnet" },
+ error: null,
+ clearError: vi.fn(),
+ })),
+}));
+
+vi.mock("@/screens/WalletScreen", () => ({
+ WalletScreen: () => WalletScreen
,
+}));
+
+vi.mock("@/screens/AccountScreen", () => ({
+ AccountScreen: () => AccountScreen
,
+}));
+
+vi.mock("@/screens/TransactionsScreen", () => ({
+ TransactionsScreen: () => TransactionsScreen
,
+}));
+
+vi.mock("@/screens/SorobanScreen", () => ({
+ SorobanScreen: () => SorobanScreen
,
+}));
+
+vi.mock("@/screens/NetworkScreen", () => ({
+ NetworkScreen: () => NetworkScreen
,
+}));
+
+describe("Dashboard screen mounting", () => {
+ it("mounts only the default active screen (wallet) on load", () => {
+ render();
+
+ expect(screen.getByTestId("wallet-screen")).toBeInTheDocument();
+ expect(screen.queryByTestId("account-screen")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("transactions-screen")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("soroban-screen")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("network-screen")).not.toBeInTheDocument();
+ });
+
+ it("unmounts previous screen and mounts only the new active screen when navigating", () => {
+ render();
+
+ // Click Account in sidebar
+ fireEvent.click(screen.getByRole("button", { name: /account/i }));
+ expect(screen.getByTestId("account-screen")).toBeInTheDocument();
+ expect(screen.queryByTestId("wallet-screen")).not.toBeInTheDocument();
+
+ // Click Transactions
+ fireEvent.click(screen.getByRole("button", { name: /transactions/i }));
+ expect(screen.getByTestId("transactions-screen")).toBeInTheDocument();
+ expect(screen.queryByTestId("account-screen")).not.toBeInTheDocument();
+
+ // Click Soroban
+ fireEvent.click(screen.getByRole("button", { name: /soroban/i }));
+ expect(screen.getByTestId("soroban-screen")).toBeInTheDocument();
+ expect(screen.queryByTestId("transactions-screen")).not.toBeInTheDocument();
+
+ // Click Network
+ fireEvent.click(screen.getByRole("button", { name: /network/i }));
+ expect(screen.getByTestId("network-screen")).toBeInTheDocument();
+ expect(screen.queryByTestId("soroban-screen")).not.toBeInTheDocument();
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx
index 84e017b..44c1a75 100644
--- a/src/screens/Dashboard.tsx
+++ b/src/screens/Dashboard.tsx
@@ -1,3 +1,6 @@
+import { useState } from "react";
+import { Sidebar, type NavSection } from "@/components/Sidebar";
+import { TopBar } from "@/components/TopBar";
import { type ComponentType, lazy, Suspense, useCallback, useEffect, useState } from "react";
import { ErrorBoundary } from "@/components/ErrorBoundary";
@@ -51,6 +54,10 @@ const PAGE_TITLES: Record = {
nfts: "NFTs — Sorokit",
};
+export function Dashboard() {
+ const [active, setActive] = useState("wallet");
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
const SCREENS: Record = {
wallet: WalletScreen,
account: AccountScreen,
@@ -177,6 +184,12 @@ export function Dashboard({
/>
+
+ {active === "wallet" &&
}
+ {active === "account" &&
}
+ {active === "transactions" &&
}
+ {active === "soroban" &&
}
+ {active === "network" &&
}