205 lines
9.5 KiB
TypeScript
205 lines
9.5 KiB
TypeScript
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("makes every learning abbreviation keyboard-focusable with its explanation intact", () => {
|
|
const { container } = render(<App dashboard={ready()} />);
|
|
const abbreviations = Array.from(container.querySelectorAll("abbr"));
|
|
expect(abbreviations).toHaveLength(5);
|
|
|
|
for (const abbreviation of abbreviations) {
|
|
expect(abbreviation.getAttribute("title")?.trim().length).toBeGreaterThan(0);
|
|
expect(abbreviation.getAttribute("tabindex")).toBe("0");
|
|
(abbreviation as HTMLElement).focus();
|
|
expect(document.activeElement).toBe(abbreviation);
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|