Skip to content
Open
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,370 changes: 10,253 additions & 2,117 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,24 @@
"tailwindcss": {
"optional": true
}
},
"devDependencies": {
"@hugeicons/core-free-icons": "^4.2.2",
"@hugeicons/react": "^1.1.9",
"@size-limit/file": "^11.2.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.0.0",
"clsx": "^2.1.1",
"jsdom": "^29.1.1",
"size-limit": "^11.2.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
}
31 changes: 31 additions & 0 deletions src/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,37 @@ describe("Sidebar", () => {
expect(navElement).toHaveAttribute("aria-label", "Main navigation");
});

it("reads localStorage on mount and pre-selects the saved section", () => {
localStorage.setItem("sorokit-active-nav", "network");

render(
<Sidebar active="wallet" onNavigate={onNavigate} open={false} onClose={onClose} />,
);

expect(onNavigate).toHaveBeenCalledWith("network");
localStorage.removeItem("sorokit-active-nav");
});

it("does not call onNavigate when localStorage has no saved section", () => {
localStorage.removeItem("sorokit-active-nav");

render(
<Sidebar active="wallet" onNavigate={onNavigate} open={false} onClose={onClose} />,
);

expect(onNavigate).not.toHaveBeenCalled();
});

it("updates localStorage when navigating to a new section", () => {
render(
<Sidebar active="wallet" onNavigate={onNavigate} open={false} onClose={onClose} />,
);

fireEvent.click(screen.getByRole("button", { name: /account/i }));

expect(localStorage.getItem("sorokit-active-nav")).toBe("account");
});

it("traps focus and handles escape/restoration on mobile", () => {
vi.stubGlobal("innerWidth", 375);

Expand Down
8 changes: 8 additions & 0 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ export function Sidebar({ active, onNavigate, open, onClose }: SidebarProps) {
const sidebarRef = useRef<HTMLElement | null>(null);
const triggerRef = useRef<HTMLElement | null>(null);

useEffect(() => {
const saved = localStorage.getItem("sorokit-active-nav");
if (saved && saved !== active) {
onNavigate(saved as NavSection);
}
}, []);

function handleNav(id: NavSection) {
localStorage.setItem("sorokit-active-nav", id);
onNavigate(id);
onClose();
}
Expand Down
29 changes: 29 additions & 0 deletions src/components/TopBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,33 @@ describe("TopBar", () => {
fireEvent.click(screen.getByRole("button", { name: /open menu/i }));
expect(onMenuToggle).toHaveBeenCalledTimes(1);
});

it("renders the title as an h1 element", () => {
vi.mocked(useSorokit).mockReturnValue({
error: null,
clearError,
} as ReturnType<typeof useSorokit>);
const { container } = render(<TopBar active="wallet" onMenuToggle={onMenuToggle} />);
const heading = screen.getByRole("heading", { level: 1 });
expect(heading).toBeInTheDocument();
expect(container.querySelector("h1")).toBe(heading);
});

it("renders the title text matching the active nav label", () => {
vi.mocked(useSorokit).mockReturnValue({
error: null,
clearError,
} as ReturnType<typeof useSorokit>);
render(<TopBar active="account" onMenuToggle={onMenuToggle} />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Account");
});

it("renders only one h1 element", () => {
vi.mocked(useSorokit).mockReturnValue({
error: null,
clearError,
} as ReturnType<typeof useSorokit>);
const { container } = render(<TopBar active="wallet" onMenuToggle={onMenuToggle} />);
expect(container.querySelectorAll("h1")).toHaveLength(1);
});
});
68 changes: 67 additions & 1 deletion src/context/SorokitProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ const TestComponent = () => {
);
};

const IsLoadingTestComponent = () => {
const { isConnecting, isLoadingAccount, isLoading, connectWallet } = useSorokit();

return (
<div>
<div data-testid="isConnecting">{isConnecting ? "true" : "false"}</div>
<div data-testid="isLoadingAccount">{isLoadingAccount ? "true" : "false"}</div>
<div data-testid="isLoading">{isLoading ? "true" : "false"}</div>
<button onClick={() => connectWallet()}>Connect</button>
</div>
);
};

const MemoTestComponent = () => {
const value = useSorokit();
const prevValueRef = useRef<ReturnType<typeof useSorokit> | null>(null);
Expand Down Expand Up @@ -132,11 +145,18 @@ describe("SorokitProvider", () => {
expect(screen.getByTestId("render-count")).toHaveTextContent("1");
expect(screen.getByTestId("ref-equal")).toHaveTextContent("false");

// Wait for network loading effect to settle
await waitFor(() => {
expect(screen.getByTestId("render-count")).toHaveTextContent("2");
});

expect(screen.getByTestId("ref-equal")).toHaveTextContent("false");

// Now trigger a parent re-render with no provider state changes
await act(async () => {
fireEvent.click(screen.getByText("Trigger Parent Render"));
});

expect(screen.getByTestId("render-count")).toHaveTextContent("2");
expect(screen.getByTestId("ref-equal")).toHaveTextContent("true");

vi.useRealTimers();
Expand All @@ -163,4 +183,50 @@ describe("SorokitProvider", () => {
});
expect(screen.getByTestId("address")).toHaveTextContent("GABC");
});

it("isLoading is true when isConnecting is true", async () => {
mockClient.wallet.connect = vi.fn().mockImplementation(() => {
return new Promise(() => {});
});

renderWithProvider(<IsLoadingTestComponent />, { client: mockClient });

await act(async () => {
fireEvent.click(screen.getByText("Connect"));
});

expect(screen.getByTestId("isConnecting")).toHaveTextContent("true");
expect(screen.getByTestId("isLoading")).toHaveTextContent("true");
});

it("isLoading is true when isLoadingAccount is true", async () => {
mockClient.wallet.connect = vi.fn().mockResolvedValue({ data: { address: "GABC" }, error: null });
mockClient.account.getAccount = vi.fn().mockImplementation(() => {
return new Promise(() => {});
});
mockClient.account.getBalances = vi.fn().mockImplementation(() => {
return new Promise(() => {});
});

renderWithProvider(<IsLoadingTestComponent />, { client: mockClient });

await act(async () => {
fireEvent.click(screen.getByText("Connect"));
});

expect(screen.getByTestId("isConnecting")).toHaveTextContent("false");

await waitFor(() => {
expect(screen.getByTestId("isLoadingAccount")).toHaveTextContent("true");
expect(screen.getByTestId("isLoading")).toHaveTextContent("true");
});
});

it("isLoading is false when both isConnecting and isLoadingAccount are false", async () => {
renderWithProvider(<IsLoadingTestComponent />, { client: mockClient });

expect(screen.getByTestId("isConnecting")).toHaveTextContent("false");
expect(screen.getByTestId("isLoadingAccount")).toHaveTextContent("false");
expect(screen.getByTestId("isLoading")).toHaveTextContent("false");
});
});
3 changes: 2 additions & 1 deletion src/context/SorokitProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export function SorokitProvider({ client, children }: SorokitProviderProps) {
address,
isConnected: !!address,
isConnecting,
isLoading: isConnecting || isLoadingAccount,
connectWallet,
disconnectWallet,
account,
Expand All @@ -141,11 +142,11 @@ export function SorokitProvider({ client, children }: SorokitProviderProps) {
[
address,
isConnecting,
isLoadingAccount,
connectWallet,
disconnectWallet,
account,
balances,
isLoadingAccount,
refreshAccount,
network,
switchNetwork,
Expand Down
1 change: 1 addition & 0 deletions src/context/sorokit-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface SorokitState {
address: string | null;
isConnected: boolean;
isConnecting: boolean;
isLoading: boolean;
connectWallet: () => Promise<void>;
disconnectWallet: () => Promise<void>;
account: AccountData | null;
Expand Down
8 changes: 8 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import path from "path";
Expand All @@ -12,6 +16,10 @@ export default defineConfig({
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
},
})
environment: "jsdom",
setupFiles: ["./src/setupTests.ts"],
},
Expand Down