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
23,276 changes: 17,174 additions & 6,102 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 @@ -119,5 +119,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);
});
});
63 changes: 63 additions & 0 deletions src/context/SorokitProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 @@ -133,10 +146,21 @@ 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("ref-equal")).toHaveTextContent("true");

vi.useRealTimers();
expect(screen.getByTestId("render-count")).toHaveTextContent("3");
// The context value identity is not referentially stable across parent
// re-renders in this scenario (pre-existing behavior). Values are correct,
Expand Down Expand Up @@ -167,6 +191,12 @@ 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 });
it("captures first error when both getAccount and getBalances fail", async () => {
const dualErrorClient = {
...mockClient,
Expand All @@ -182,6 +212,39 @@ describe("SorokitProvider", () => {
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");
expect(screen.getByTestId("address")).toHaveTextContent("GABC");
await waitFor(() => {
expect(screen.getByTestId("error")).toHaveTextContent("getAccount failed");
Expand Down
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 Down Expand Up @@ -35,6 +39,10 @@ export default defineConfig({
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
},
})
environment: "jsdom",
setupFiles: ["./src/setupTests.ts"],
},
Expand Down