feat: add read-only bank operations console
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { Address, Hex } from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import shellHtml from "../index.html?raw";
|
||||
import { App } from "./App";
|
||||
import type { DashboardState } from "./hooks/useBankDashboard";
|
||||
import type { DashboardSnapshot, DeploymentManifest } from "./types/dashboard";
|
||||
|
||||
const address = (digit: string) => `0x${digit.repeat(40)}` as Address;
|
||||
const transactionHash = (digit: string) => `0x${digit.repeat(64)}` as Hex;
|
||||
|
||||
const localManifest: DeploymentManifest = {
|
||||
schemaVersion: 1,
|
||||
network: "anvil",
|
||||
chainId: 31337,
|
||||
deploymentBlock: 3n,
|
||||
rpcUrl: "http://127.0.0.1:8545",
|
||||
token: address("1"),
|
||||
proxy: address("2"),
|
||||
implementation: address("3"),
|
||||
owner: address("4"),
|
||||
actors: [
|
||||
{ label: "Alice", address: address("5") },
|
||||
{ label: "Bob", address: address("6") },
|
||||
],
|
||||
};
|
||||
|
||||
const snapshot: DashboardSnapshot = {
|
||||
blockNumber: 18n,
|
||||
synchronizedAt: new Date("2026-08-21T10:00:00.000Z"),
|
||||
version: 1,
|
||||
paused: true,
|
||||
asset: localManifest.token,
|
||||
owner: localManifest.owner,
|
||||
proxy: localManifest.proxy,
|
||||
implementation: localManifest.implementation,
|
||||
reserves: 1_500_000_000n,
|
||||
liabilities: 1_400_000_000n,
|
||||
surplus: 100_000_000n,
|
||||
actors: [
|
||||
{ label: "Alice", address: address("5"), balance: 900_000_000n },
|
||||
{ label: "Bob", address: address("6"), balance: 500_000_000n },
|
||||
],
|
||||
activity: [
|
||||
{ kind: "deposit", account: address("5"), amount: 1_000_000_000n, blockNumber: 4n, logIndex: 0, transactionHash: transactionHash("a") },
|
||||
{ kind: "withdrawal", account: address("5"), amount: 100_000_000n, blockNumber: 5n, logIndex: 0, transactionHash: transactionHash("b") },
|
||||
{ kind: "paused", account: address("4"), blockNumber: 6n, logIndex: 0, transactionHash: transactionHash("c") },
|
||||
{ kind: "ownershipTransferred", previousOwner: address("7"), newOwner: address("4"), blockNumber: 3n, logIndex: 1, transactionHash: transactionHash("d") },
|
||||
{ kind: "upgraded", implementation: address("3"), blockNumber: 3n, logIndex: 0, transactionHash: transactionHash("d") },
|
||||
],
|
||||
diagnostics: [{ blockNumber: 7n, logIndex: 2, transactionHash: transactionHash("e"), message: "Could not decode a proxy event log." }],
|
||||
};
|
||||
|
||||
function ready(manifest = localManifest, value = snapshot): DashboardState {
|
||||
return { status: "ready", manifest, snapshot: value };
|
||||
}
|
||||
|
||||
describe("read-only operations console", () => {
|
||||
it("loads the console entry point from the browser document", () => {
|
||||
const document = new DOMParser().parseFromString(shellHtml, "text/html");
|
||||
expect(document.querySelector('script[type="module"]')?.getAttribute("src")).toBe("/src/main.tsx");
|
||||
});
|
||||
|
||||
it("keeps the exact educational funds warning and complete trust disclosure visible", () => {
|
||||
render(<App dashboard={ready()} />);
|
||||
|
||||
expect(screen.getByText("Educational demo — mock token — never use real funds.")).toBeTruthy();
|
||||
expect(screen.getByText(/educational and unaudited/i)).toBeTruthy();
|
||||
expect(screen.getByText(/owner can pause customer actions and install arbitrary future logic/i)).toBeTruthy();
|
||||
expect(screen.getByText(/UUPS mistakes can corrupt state or permanently brick upgradeability/i)).toBeTruthy();
|
||||
expect(screen.getByText(/professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders network, synchronization, lifecycle, pause, and V1 status", () => {
|
||||
render(<App dashboard={ready()} />);
|
||||
|
||||
expect(screen.getByText("Local Anvil")).toBeTruthy();
|
||||
expect(screen.getByText("Ready")).toBeTruthy();
|
||||
expect(screen.getByText("Block 18")).toBeTruthy();
|
||||
expect(screen.getByText("Paused")).toBeTruthy();
|
||||
expect(screen.getByText("Version 1")).toBeTruthy();
|
||||
expect(screen.getByText(/Aug 21, 2026/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders accounting, contract identity, and local tracked accounts", () => {
|
||||
render(<App dashboard={ready()} />);
|
||||
|
||||
expect(screen.getByText("1,500.00 mUSDC")).toBeTruthy();
|
||||
expect(screen.getByText("1,400.00 mUSDC")).toBeTruthy();
|
||||
expect(screen.getByText("100.00 mUSDC")).toBeTruthy();
|
||||
expect(screen.getByText("107.14%")).toBeTruthy();
|
||||
expect(screen.getByText("Proxy")).toBeTruthy();
|
||||
expect(screen.getByText("Implementation")).toBeTruthy();
|
||||
expect(screen.getByText("Token")).toBeTruthy();
|
||||
expect(screen.getByText("Owner")).toBeTruthy();
|
||||
expect(screen.getByText("Deployment block")).toBeTruthy();
|
||||
expect(screen.getByText("Alice")).toBeTruthy();
|
||||
expect(screen.getByText("Bob")).toBeTruthy();
|
||||
expect(screen.getByText("900.00 mUSDC")).toBeTruthy();
|
||||
expect(screen.getByText("500.00 mUSDC")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders valid V1 activity newest first beside an isolated decode warning", () => {
|
||||
render(<App dashboard={ready()} />);
|
||||
|
||||
const timeline = screen.getByLabelText("Proxy activity");
|
||||
expect(timeline.textContent).toContain("Deposited");
|
||||
expect(timeline.textContent).toContain("Withdrawn");
|
||||
expect(timeline.textContent).toContain("Paused by");
|
||||
expect(timeline.textContent).toContain("Ownership transferred");
|
||||
expect(timeline.textContent).toContain("Implementation upgraded");
|
||||
expect(timeline.textContent).toContain("Block 7");
|
||||
expect(timeline.textContent).toContain("Could not decode a proxy event log.");
|
||||
expect(timeline.textContent?.indexOf("Paused by")).toBeLessThan(timeline.textContent?.indexOf("Withdrawn") ?? 0);
|
||||
expect(timeline.textContent).toContain("0xcccc…cccc");
|
||||
});
|
||||
|
||||
it("uses validated Base Sepolia explorer links and public shortened account labels", () => {
|
||||
const baseManifest: DeploymentManifest = {
|
||||
...localManifest,
|
||||
network: "baseSepolia",
|
||||
chainId: 84532,
|
||||
rpcUrl: "https://sepolia.base.org",
|
||||
explorerBaseUrl: "https://sepolia.basescan.org",
|
||||
};
|
||||
render(<App dashboard={ready(baseManifest)} />);
|
||||
|
||||
expect(screen.getByText("Base Sepolia")).toBeTruthy();
|
||||
expect(screen.queryByText("Alice")).toBeNull();
|
||||
expect(screen.getAllByText("0x5555…5555").length).toBeGreaterThan(0);
|
||||
const proxyLink = screen.getByRole("link", { name: /proxy.*0x2222…2222/i });
|
||||
expect(proxyLink.getAttribute("href")).toBe(`https://sepolia.basescan.org/address/${snapshot.proxy}`);
|
||||
const transactionLink = screen.getByRole("link", { name: /transaction 0xcccc…cccc/i });
|
||||
expect(transactionLink.getAttribute("href")).toBe(`https://sepolia.basescan.org/tx/${transactionHash("c")}`);
|
||||
});
|
||||
|
||||
it("does not create explorer links for local Anvil", () => {
|
||||
render(<App dashboard={ready()} />);
|
||||
expect(screen.queryAllByRole("link")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("shows No deposits instead of dividing zero liabilities", () => {
|
||||
render(<App dashboard={ready(localManifest, { ...snapshot, reserves: 0n, liabilities: 0n, surplus: 0n })} />);
|
||||
expect(screen.getByText("No deposits")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("distinguishes stale and disconnected conditions without hiding the persistent warnings", () => {
|
||||
const { rerender } = render(<App dashboard={{
|
||||
status: "stale",
|
||||
manifest: localManifest,
|
||||
snapshot,
|
||||
failedAt: new Date("2026-08-21T10:05:00.000Z"),
|
||||
error: "RPC timeout",
|
||||
}} />);
|
||||
expect(screen.getByText("Stale")).toBeTruthy();
|
||||
expect(screen.getByText(/RPC timeout/)).toBeTruthy();
|
||||
expect(screen.getByText("1,500.00 mUSDC")).toBeTruthy();
|
||||
|
||||
rerender(<App dashboard={{
|
||||
status: "disconnected",
|
||||
manifest: localManifest,
|
||||
failedAt: new Date("2026-08-21T10:06:00.000Z"),
|
||||
error: "connection refused",
|
||||
}} />);
|
||||
expect(screen.getAllByText("Disconnected")).toHaveLength(2);
|
||||
expect(screen.getByText(/connection refused/)).toBeTruthy();
|
||||
expect(screen.getByText("Educational demo — mock token — never use real funds.")).toBeTruthy();
|
||||
expect(screen.getByText(/educational and unaudited/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders explanatory invalid-manifest and chain-mismatch terminal screens", () => {
|
||||
const { rerender } = render(<App dashboard={{ status: "invalid-manifest", error: "manifest is missing required field proxy" }} />);
|
||||
expect(screen.getAllByText("Invalid manifest")).toHaveLength(2);
|
||||
expect(screen.getByText(/missing required field proxy/)).toBeTruthy();
|
||||
|
||||
rerender(<App dashboard={{
|
||||
status: "chain-mismatch",
|
||||
manifest: localManifest,
|
||||
error: "endpoint chain ID 84532 does not match manifest chain ID 31337",
|
||||
}} />);
|
||||
expect(screen.getAllByText("Chain mismatch")).toHaveLength(2);
|
||||
expect(screen.getByText(/84532.*31337/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("exposes no forms, buttons, wallet connection, signing, or transaction controls", () => {
|
||||
const { container } = render(<App dashboard={ready()} />);
|
||||
expect(container.querySelector("button")).toBeNull();
|
||||
expect(container.querySelector("form")).toBeNull();
|
||||
expect(container.textContent).not.toMatch(/connect wallet|sign transaction|submit transaction|deposit funds|withdraw funds/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user