create-miden-app 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +6 -6
- package/package.json +1 -6
- package/template/.claude/hooks/check-artifacts.sh +45 -0
- package/template/.claude/hooks/run-affected-tests.sh +31 -0
- package/template/.claude/hooks/typecheck.sh +27 -0
- package/template/.claude/settings.json +35 -0
- package/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
- package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/.claude/skills/miden-concepts/SKILL.md +108 -0
- package/template/.claude/skills/react-sdk-patterns/SKILL.md +296 -0
- package/template/.claude/skills/signer-integration/SKILL.md +158 -0
- package/template/.claude/skills/testing-patterns/SKILL.md +177 -0
- package/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
- package/template/.env.example +5 -0
- package/template/CLAUDE.md +210 -0
- package/template/README.md +53 -14
- package/template/create-miden-app/template/.claude/hooks/typecheck.sh +27 -0
- package/template/create-miden-app/template/.claude/settings.json +17 -0
- package/template/create-miden-app/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
- package/template/create-miden-app/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/create-miden-app/template/.claude/skills/miden-concepts/SKILL.md +108 -0
- package/template/create-miden-app/template/.claude/skills/react-sdk-patterns/SKILL.md +294 -0
- package/template/create-miden-app/template/.claude/skills/signer-integration/SKILL.md +158 -0
- package/template/create-miden-app/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
- package/template/create-miden-app/template/.env.example +5 -0
- package/template/create-miden-app/template/CLAUDE.md +116 -0
- package/template/create-miden-app/template/README.md +61 -0
- package/template/create-miden-app/template/eslint.config.js +23 -0
- package/template/create-miden-app/template/index.html +13 -0
- package/template/create-miden-app/template/package.json +34 -0
- package/template/create-miden-app/template/public/vite.svg +1 -0
- package/template/create-miden-app/template/src/App.tsx +10 -0
- package/template/create-miden-app/template/src/assets/miden.svg +3 -0
- package/template/create-miden-app/template/src/assets/react.svg +1 -0
- package/template/{src/App.css → create-miden-app/template/src/components/AppContent.css} +9 -9
- package/template/create-miden-app/template/src/components/AppContent.tsx +50 -0
- package/template/create-miden-app/template/src/components/Counter.css +27 -0
- package/template/create-miden-app/template/src/components/Counter.tsx +45 -0
- package/template/create-miden-app/template/src/config.ts +21 -0
- package/template/create-miden-app/template/src/hooks/useIncrementCounter.ts +136 -0
- package/template/create-miden-app/template/src/index.css +75 -0
- package/template/create-miden-app/template/src/lib/miden.ts +9 -0
- package/template/create-miden-app/template/src/main.tsx +10 -0
- package/template/create-miden-app/template/src/providers.tsx +31 -0
- package/template/create-miden-app/template/src/vite-env.d.ts +1 -0
- package/template/create-miden-app/template/tsconfig.app.json +32 -0
- package/template/create-miden-app/template/tsconfig.json +7 -0
- package/template/create-miden-app/template/tsconfig.node.json +24 -0
- package/template/create-miden-app/template/vite.config.ts +17 -0
- package/template/create-miden-app/template/yarn.lock +1697 -0
- package/template/index.html +1 -1
- package/template/package.json +17 -8
- package/template/public/packages/counter_account.masp +0 -0
- package/template/public/packages/increment_note.masp +0 -0
- package/template/src/App.tsx +6 -59
- package/template/src/__tests__/fixtures/accounts.ts +57 -0
- package/template/src/__tests__/fixtures/index.ts +21 -0
- package/template/src/__tests__/fixtures/notes.ts +33 -0
- package/template/src/__tests__/mocks/miden-sdk-react.ts +244 -0
- package/template/src/__tests__/patterns/README.md +44 -0
- package/template/src/__tests__/patterns/mutation-hook.test.tsx +146 -0
- package/template/src/__tests__/patterns/provider-setup.test.tsx +75 -0
- package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
- package/template/src/components/AppContent.css +45 -0
- package/template/src/components/AppContent.tsx +50 -0
- package/template/src/components/Counter.css +27 -0
- package/template/src/components/Counter.tsx +45 -0
- package/template/src/components/__tests__/AppContent.test.tsx +86 -0
- package/template/src/components/__tests__/Counter.test.tsx +114 -0
- package/template/src/config.ts +21 -0
- package/template/src/hooks/useIncrementCounter.ts +136 -0
- package/template/src/index.css +7 -0
- package/template/src/lib/miden.ts +9 -0
- package/template/src/main.tsx +6 -6
- package/template/src/providers.tsx +31 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +8 -4
- package/template/tsconfig.node.json +1 -3
- package/template/vite.config.ts +5 -17
- package/template/vitest.config.ts +26 -0
- package/template/vitest.setup.ts +1 -0
- package/template/yarn.lock +1318 -799
- package/template/src/miden/lib/demo.ts +0 -105
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST PATTERN: Provider Setup
|
|
3
|
+
*
|
|
4
|
+
* Shows how to set up the test environment for components that use Miden hooks.
|
|
5
|
+
* Copy this pattern as a starting point for any Miden component test.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Mock @miden-sdk/react at the module level (hoisted by vitest)
|
|
9
|
+
* - Override specific hooks per-test with vi.mocked()
|
|
10
|
+
* - Test ready/loading/error states that every Miden component needs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { render, screen } from "@testing-library/react";
|
|
14
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
15
|
+
|
|
16
|
+
// Mock the entire @miden-sdk/react module with realistic defaults
|
|
17
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
18
|
+
|
|
19
|
+
import { useMiden } from "@miden-sdk/react";
|
|
20
|
+
|
|
21
|
+
// A minimal component that uses Miden context
|
|
22
|
+
function StatusIndicator() {
|
|
23
|
+
const { isReady, isInitializing, error } = useMiden();
|
|
24
|
+
|
|
25
|
+
if (error) return <div role="alert">Error: {error.message}</div>;
|
|
26
|
+
if (isInitializing) return <div>Initializing WASM...</div>;
|
|
27
|
+
if (!isReady) return <div>Not ready</div>;
|
|
28
|
+
return <div>Miden Ready</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("Provider Setup Pattern", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Default mock returns isReady: true — component should render success state
|
|
37
|
+
it("renders ready state with default mocks", () => {
|
|
38
|
+
render(<StatusIndicator />);
|
|
39
|
+
expect(screen.getByText("Miden Ready")).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Override useMiden to simulate loading — tests the initializing state
|
|
43
|
+
it("renders loading state when WASM is initializing", () => {
|
|
44
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
45
|
+
client: null,
|
|
46
|
+
isReady: false,
|
|
47
|
+
isInitializing: true,
|
|
48
|
+
error: null,
|
|
49
|
+
sync: vi.fn(),
|
|
50
|
+
runExclusive: vi.fn(),
|
|
51
|
+
prover: null,
|
|
52
|
+
signerAccountId: null,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
render(<StatusIndicator />);
|
|
56
|
+
expect(screen.getByText("Initializing WASM...")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Override useMiden to simulate error — tests error handling
|
|
60
|
+
it("renders error state when initialization fails", () => {
|
|
61
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
62
|
+
client: null,
|
|
63
|
+
isReady: false,
|
|
64
|
+
isInitializing: false,
|
|
65
|
+
error: new Error("WASM load failed"),
|
|
66
|
+
sync: vi.fn(),
|
|
67
|
+
runExclusive: vi.fn(),
|
|
68
|
+
prover: null,
|
|
69
|
+
signerAccountId: null,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
render(<StatusIndicator />);
|
|
73
|
+
expect(screen.getByRole("alert")).toHaveTextContent("WASM load failed");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST PATTERN: Query Hook Component
|
|
3
|
+
*
|
|
4
|
+
* Shows how to test a component that displays data from Miden query hooks.
|
|
5
|
+
* Covers the three essential states: loading, success (with data), and error.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Override hook return values per-test with vi.mocked()
|
|
9
|
+
* - Test loading skeleton/placeholder states
|
|
10
|
+
* - Test data rendering with realistic fixtures
|
|
11
|
+
* - Test error display and recovery (refetch)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { render, screen } from "@testing-library/react";
|
|
15
|
+
import userEvent from "@testing-library/user-event";
|
|
16
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
17
|
+
|
|
18
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
19
|
+
|
|
20
|
+
import { useAccounts, useSyncState } from "@miden-sdk/react";
|
|
21
|
+
import {
|
|
22
|
+
MOCK_WALLET_HEADER,
|
|
23
|
+
MOCK_WALLET_HEADER_2,
|
|
24
|
+
MOCK_FAUCET_HEADER,
|
|
25
|
+
} from "@/__tests__/fixtures";
|
|
26
|
+
|
|
27
|
+
// Example component that lists accounts — a common Miden UI pattern
|
|
28
|
+
function AccountList() {
|
|
29
|
+
const { wallets, faucets, isLoading, error, refetch } = useAccounts();
|
|
30
|
+
const { syncHeight } = useSyncState();
|
|
31
|
+
|
|
32
|
+
if (error) {
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<p role="alert">Failed to load accounts: {error.message}</p>
|
|
36
|
+
<button onClick={refetch}>Retry</button>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isLoading) {
|
|
42
|
+
return <p>Loading accounts...</p>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<p>Synced to block {syncHeight}</p>
|
|
48
|
+
<h2>Wallets ({wallets.length})</h2>
|
|
49
|
+
<ul aria-label="wallets">
|
|
50
|
+
{wallets.map((w) => (
|
|
51
|
+
<li key={String(w.id)}>{String(w.id)}</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
<h2>Faucets ({faucets.length})</h2>
|
|
55
|
+
<ul aria-label="faucets">
|
|
56
|
+
{faucets.map((f) => (
|
|
57
|
+
<li key={String(f.id)}>{String(f.id)}</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("Query Hook Pattern", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Default mocks return realistic data — component should render account lists
|
|
70
|
+
it("renders account lists with data", () => {
|
|
71
|
+
render(<AccountList />);
|
|
72
|
+
|
|
73
|
+
// Wallet list should contain both mock wallets
|
|
74
|
+
const walletList = screen.getByRole("list", { name: "wallets" });
|
|
75
|
+
expect(walletList.children).toHaveLength(2);
|
|
76
|
+
expect(screen.getByText(MOCK_WALLET_HEADER.id)).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByText(MOCK_WALLET_HEADER_2.id)).toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
// Faucet list should contain the mock faucet
|
|
80
|
+
const faucetList = screen.getByRole("list", { name: "faucets" });
|
|
81
|
+
expect(faucetList.children).toHaveLength(1);
|
|
82
|
+
expect(screen.getByText(MOCK_FAUCET_HEADER.id)).toBeInTheDocument();
|
|
83
|
+
|
|
84
|
+
// Sync height from useSyncState mock
|
|
85
|
+
expect(screen.getByText(/Synced to block 12345/)).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Override to loading state — component should show loading indicator
|
|
89
|
+
it("shows loading state while fetching accounts", () => {
|
|
90
|
+
vi.mocked(useAccounts).mockReturnValue({
|
|
91
|
+
accounts: [],
|
|
92
|
+
wallets: [],
|
|
93
|
+
faucets: [],
|
|
94
|
+
isLoading: true,
|
|
95
|
+
error: null,
|
|
96
|
+
refetch: vi.fn(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
render(<AccountList />);
|
|
100
|
+
expect(screen.getByText("Loading accounts...")).toBeInTheDocument();
|
|
101
|
+
// Account lists should NOT be rendered during loading
|
|
102
|
+
expect(screen.queryByRole("list")).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Override to error state — component should show error with retry button
|
|
106
|
+
it("shows error with retry button on failure", async () => {
|
|
107
|
+
const mockRefetch = vi.fn();
|
|
108
|
+
vi.mocked(useAccounts).mockReturnValue({
|
|
109
|
+
accounts: [],
|
|
110
|
+
wallets: [],
|
|
111
|
+
faucets: [],
|
|
112
|
+
isLoading: false,
|
|
113
|
+
error: new Error("Network timeout"),
|
|
114
|
+
refetch: mockRefetch,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
render(<AccountList />);
|
|
118
|
+
|
|
119
|
+
// Error message should be visible and accessible
|
|
120
|
+
expect(screen.getByRole("alert")).toHaveTextContent("Network timeout");
|
|
121
|
+
|
|
122
|
+
// Clicking retry should call refetch
|
|
123
|
+
const user = userEvent.setup();
|
|
124
|
+
await user.click(screen.getByRole("button", { name: "Retry" }));
|
|
125
|
+
expect(mockRefetch).toHaveBeenCalledOnce();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Test empty state — no accounts yet (fresh install)
|
|
129
|
+
it("renders empty lists when no accounts exist", () => {
|
|
130
|
+
vi.mocked(useAccounts).mockReturnValue({
|
|
131
|
+
accounts: [],
|
|
132
|
+
wallets: [],
|
|
133
|
+
faucets: [],
|
|
134
|
+
isLoading: false,
|
|
135
|
+
error: null,
|
|
136
|
+
refetch: vi.fn(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
render(<AccountList />);
|
|
140
|
+
expect(screen.getByText("Wallets (0)")).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText("Faucets (0)")).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.logo {
|
|
2
|
+
height: 6em;
|
|
3
|
+
padding: 1.5em;
|
|
4
|
+
will-change: filter;
|
|
5
|
+
transition: filter 300ms;
|
|
6
|
+
}
|
|
7
|
+
.logo:hover {
|
|
8
|
+
filter: drop-shadow(0 0 2em #646cffaa);
|
|
9
|
+
}
|
|
10
|
+
.logo.react:hover {
|
|
11
|
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
12
|
+
}
|
|
13
|
+
.logo.miden:hover {
|
|
14
|
+
filter: drop-shadow(0 0 2em #ff5500);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes logo-spin {
|
|
18
|
+
from {
|
|
19
|
+
transform: rotate(0deg);
|
|
20
|
+
}
|
|
21
|
+
to {
|
|
22
|
+
transform: rotate(360deg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
27
|
+
a:nth-of-type(2) .logo {
|
|
28
|
+
animation: logo-spin infinite 20s linear;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.wallet-section {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
margin-bottom: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.loading {
|
|
39
|
+
padding: 2rem;
|
|
40
|
+
color: #888;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.read-the-docs {
|
|
44
|
+
color: #888;
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMiden, useSyncState } from "@miden-sdk/react";
|
|
2
|
+
import { WalletMultiButton } from "@miden-sdk/miden-wallet-adapter";
|
|
3
|
+
import reactLogo from "@/assets/react.svg";
|
|
4
|
+
import midenLogo from "@/assets/miden.svg";
|
|
5
|
+
import viteLogo from "/vite.svg";
|
|
6
|
+
import { Counter } from "@/components/Counter";
|
|
7
|
+
import "./AppContent.css";
|
|
8
|
+
|
|
9
|
+
export function AppContent() {
|
|
10
|
+
const { isReady, isInitializing, error } = useMiden();
|
|
11
|
+
const { syncHeight } = useSyncState();
|
|
12
|
+
|
|
13
|
+
if (error) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="loading">
|
|
16
|
+
<p>Failed to initialize Miden client</p>
|
|
17
|
+
<p className="error">{error.message}</p>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isInitializing || !isReady) {
|
|
23
|
+
return <div className="loading">Initializing Miden client...</div>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<div>
|
|
29
|
+
<a href="https://vite.dev" target="_blank" rel="noreferrer">
|
|
30
|
+
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
31
|
+
</a>
|
|
32
|
+
<a href="https://react.dev" target="_blank" rel="noreferrer">
|
|
33
|
+
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
34
|
+
</a>
|
|
35
|
+
<a href="https://docs.miden.io" target="_blank" rel="noreferrer">
|
|
36
|
+
<img src={midenLogo} className="logo miden" alt="Miden logo" />
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
<h1>Vite + React + Miden</h1>
|
|
40
|
+
<div className="wallet-section">
|
|
41
|
+
<WalletMultiButton />
|
|
42
|
+
</div>
|
|
43
|
+
<Counter />
|
|
44
|
+
<p className="read-the-docs">
|
|
45
|
+
Testnet block: {syncHeight ?? "syncing..."} | Click on the Vite, React,
|
|
46
|
+
and Miden logos to learn more
|
|
47
|
+
</p>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
padding: 2em;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.counter-button {
|
|
6
|
+
font-size: 1.2em;
|
|
7
|
+
padding: 0.8em 1.6em;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.account-id {
|
|
11
|
+
font-size: 0.8em;
|
|
12
|
+
color: #888;
|
|
13
|
+
font-family: monospace;
|
|
14
|
+
margin-top: 0.5rem;
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.account-id:hover {
|
|
19
|
+
text-decoration: underline;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.error {
|
|
23
|
+
color: #ff4444;
|
|
24
|
+
font-size: 0.9em;
|
|
25
|
+
margin-top: 0.5rem;
|
|
26
|
+
word-break: break-word;
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useIncrementCounter } from "@/hooks/useIncrementCounter";
|
|
2
|
+
import { COUNTER_ADDRESS } from "@/config";
|
|
3
|
+
import "./Counter.css";
|
|
4
|
+
|
|
5
|
+
export function Counter() {
|
|
6
|
+
const {
|
|
7
|
+
increment,
|
|
8
|
+
count,
|
|
9
|
+
isSubmitting,
|
|
10
|
+
isWaiting,
|
|
11
|
+
error,
|
|
12
|
+
walletConnected,
|
|
13
|
+
explorerUrl,
|
|
14
|
+
} = useIncrementCounter(COUNTER_ADDRESS);
|
|
15
|
+
|
|
16
|
+
const busy = isSubmitting || isWaiting;
|
|
17
|
+
const buttonLabel = isSubmitting
|
|
18
|
+
? "Submitting..."
|
|
19
|
+
: isWaiting
|
|
20
|
+
? "Waiting for network..."
|
|
21
|
+
: `count is ${count ?? "..."}`;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="card">
|
|
25
|
+
<button
|
|
26
|
+
className="counter-button"
|
|
27
|
+
onClick={increment}
|
|
28
|
+
disabled={busy || count === null || !walletConnected}
|
|
29
|
+
>
|
|
30
|
+
{buttonLabel}
|
|
31
|
+
</button>
|
|
32
|
+
<p>
|
|
33
|
+
<a
|
|
34
|
+
href={explorerUrl}
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noreferrer"
|
|
37
|
+
className="account-id"
|
|
38
|
+
>
|
|
39
|
+
Counter: {COUNTER_ADDRESS}
|
|
40
|
+
</a>
|
|
41
|
+
</p>
|
|
42
|
+
{error && <p className="error">{error}</p>}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
5
|
+
vi.mock("@miden-sdk/miden-wallet-adapter", () => ({
|
|
6
|
+
WalletMultiButton: () => <button>Connect Wallet</button>,
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("@/components/Counter", () => ({
|
|
9
|
+
Counter: () => <div data-testid="counter">Counter Mock</div>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { useMiden, useSyncState } from "@miden-sdk/react";
|
|
13
|
+
import { AppContent } from "../AppContent";
|
|
14
|
+
|
|
15
|
+
describe("AppContent", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders main content when Miden is ready", () => {
|
|
21
|
+
render(<AppContent />);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByText("Vite + React + Miden")).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByAltText("Vite logo")).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByAltText("React logo")).toBeInTheDocument();
|
|
26
|
+
expect(screen.getByAltText("Miden logo")).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByText("Connect Wallet")).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByTestId("counter")).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("shows sync height from testnet", () => {
|
|
32
|
+
render(<AppContent />);
|
|
33
|
+
expect(screen.getByText(/Testnet block: 12345/)).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("shows syncing indicator when syncHeight is null", () => {
|
|
37
|
+
vi.mocked(useSyncState).mockReturnValue({
|
|
38
|
+
syncHeight: null as unknown as number,
|
|
39
|
+
isSyncing: true,
|
|
40
|
+
lastSyncTime: null,
|
|
41
|
+
error: null,
|
|
42
|
+
sync: vi.fn(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
render(<AppContent />);
|
|
46
|
+
expect(screen.getByText(/syncing\.\.\./)).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows loading message during initialization", () => {
|
|
50
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
51
|
+
client: null,
|
|
52
|
+
isReady: false,
|
|
53
|
+
isInitializing: true,
|
|
54
|
+
error: null,
|
|
55
|
+
sync: vi.fn(),
|
|
56
|
+
runExclusive: vi.fn(),
|
|
57
|
+
prover: null,
|
|
58
|
+
signerAccountId: null,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
render(<AppContent />);
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByText("Initializing Miden client..."),
|
|
64
|
+
).toBeInTheDocument();
|
|
65
|
+
expect(screen.queryByText("Vite + React + Miden")).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows error message on initialization failure", () => {
|
|
69
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
70
|
+
client: null,
|
|
71
|
+
isReady: false,
|
|
72
|
+
isInitializing: false,
|
|
73
|
+
error: new Error("WASM failed to load"),
|
|
74
|
+
sync: vi.fn(),
|
|
75
|
+
runExclusive: vi.fn(),
|
|
76
|
+
prover: null,
|
|
77
|
+
signerAccountId: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
render(<AppContent />);
|
|
81
|
+
expect(
|
|
82
|
+
screen.getByText("Failed to initialize Miden client"),
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText("WASM failed to load")).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/hooks/useIncrementCounter", () => ({
|
|
6
|
+
useIncrementCounter: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { useIncrementCounter } from "@/hooks/useIncrementCounter";
|
|
10
|
+
import { Counter } from "../Counter";
|
|
11
|
+
|
|
12
|
+
const defaultHookReturn = {
|
|
13
|
+
increment: vi.fn(),
|
|
14
|
+
count: 42,
|
|
15
|
+
isSubmitting: false,
|
|
16
|
+
isWaiting: false,
|
|
17
|
+
error: null,
|
|
18
|
+
walletConnected: true,
|
|
19
|
+
explorerUrl: "https://testnet.midenscan.com/account/mtst1test",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("Counter", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
vi.mocked(useIncrementCounter).mockReturnValue(defaultHookReturn);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("displays the current count on the button", () => {
|
|
29
|
+
render(<Counter />);
|
|
30
|
+
expect(
|
|
31
|
+
screen.getByRole("button", { name: "count is 42" }),
|
|
32
|
+
).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("calls increment on button click", async () => {
|
|
36
|
+
const mockIncrement = vi.fn();
|
|
37
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
38
|
+
...defaultHookReturn,
|
|
39
|
+
increment: mockIncrement,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
render(<Counter />);
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
await user.click(screen.getByRole("button", { name: "count is 42" }));
|
|
45
|
+
expect(mockIncrement).toHaveBeenCalledOnce();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("shows submitting state", () => {
|
|
49
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
50
|
+
...defaultHookReturn,
|
|
51
|
+
isSubmitting: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
render(<Counter />);
|
|
55
|
+
const button = screen.getByRole("button", { name: "Submitting..." });
|
|
56
|
+
expect(button).toBeDisabled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("shows waiting for network state", () => {
|
|
60
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
61
|
+
...defaultHookReturn,
|
|
62
|
+
isWaiting: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
render(<Counter />);
|
|
66
|
+
const button = screen.getByRole("button", {
|
|
67
|
+
name: "Waiting for network...",
|
|
68
|
+
});
|
|
69
|
+
expect(button).toBeDisabled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("disables button when wallet not connected", () => {
|
|
73
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
74
|
+
...defaultHookReturn,
|
|
75
|
+
walletConnected: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
render(<Counter />);
|
|
79
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("disables button when count is loading (null)", () => {
|
|
83
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
84
|
+
...defaultHookReturn,
|
|
85
|
+
count: null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
render(<Counter />);
|
|
89
|
+
const button = screen.getByRole("button", { name: "count is ..." });
|
|
90
|
+
expect(button).toBeDisabled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("displays error message", () => {
|
|
94
|
+
vi.mocked(useIncrementCounter).mockReturnValue({
|
|
95
|
+
...defaultHookReturn,
|
|
96
|
+
error: "Transaction failed: insufficient funds",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
render(<Counter />);
|
|
100
|
+
expect(
|
|
101
|
+
screen.getByText("Transaction failed: insufficient funds"),
|
|
102
|
+
).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("links to explorer with counter address", () => {
|
|
106
|
+
render(<Counter />);
|
|
107
|
+
const link = screen.getByRole("link");
|
|
108
|
+
expect(link).toHaveAttribute(
|
|
109
|
+
"href",
|
|
110
|
+
"https://testnet.midenscan.com/account/mtst1test",
|
|
111
|
+
);
|
|
112
|
+
expect(link).toHaveAttribute("target", "_blank");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Network counter account deployed on Miden testnet
|
|
2
|
+
export const COUNTER_ADDRESS = "mtst1aru8adnrqspgcsr3drk2n990lyc070ll";
|
|
3
|
+
|
|
4
|
+
// StorageMap slot name for the counter
|
|
5
|
+
export const COUNTER_SLOT_NAME =
|
|
6
|
+
"miden::component::miden_counter_account::count_map";
|
|
7
|
+
|
|
8
|
+
// Block explorer base URL
|
|
9
|
+
export const EXPLORER_BASE_URL = "https://testnet.midenscan.com";
|
|
10
|
+
|
|
11
|
+
// Delay (ms) to wait for the network to process a note before re-syncing
|
|
12
|
+
export const NETWORK_SYNC_DELAY_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
// Application display name (used by wallet adapter)
|
|
15
|
+
export const APP_NAME = "Miden Template";
|
|
16
|
+
|
|
17
|
+
// Miden SDK configuration — override via environment variables
|
|
18
|
+
export const MIDEN_RPC_URL =
|
|
19
|
+
import.meta.env.VITE_MIDEN_RPC_URL ?? "testnet";
|
|
20
|
+
export const MIDEN_PROVER =
|
|
21
|
+
(import.meta.env.VITE_MIDEN_PROVER as "testnet" | "local") ?? "testnet";
|