create-miden-app 1.0.4 → 1.0.7
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 -1
- package/template/.claude/commands/review-security.md +67 -0
- 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 +29 -0
- package/template/.claude/settings.local.json +24 -0
- package/template/.claude/skills/frontend-pitfalls/SKILL.md +186 -0
- package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/.claude/skills/miden-concepts/SKILL.md +110 -0
- package/template/.claude/skills/react-sdk-patterns/SKILL.md +562 -0
- package/template/.claude/skills/signer-integration/SKILL.md +177 -0
- package/template/.claude/skills/testing-patterns/SKILL.md +338 -0
- package/template/.claude/skills/vite-wasm-setup/SKILL.md +134 -0
- package/template/.claude/skills/web-client-usage/SKILL.md +454 -0
- package/template/.env.example +18 -0
- package/template/.mcp.json +9 -0
- package/template/CLAUDE.md +243 -0
- package/template/README.md +119 -14
- package/template/index.html +1 -1
- package/template/package.json +18 -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 +68 -0
- package/template/src/__tests__/fixtures/index.ts +22 -0
- package/template/src/__tests__/fixtures/notes.ts +33 -0
- package/template/src/__tests__/mocks/miden-sdk-react.ts +261 -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 +77 -0
- package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
- package/template/src/{App.css → components/AppContent.css} +9 -9
- package/template/src/components/AppContent.tsx +80 -0
- package/template/src/components/ConfiguredCounter.tsx +48 -0
- package/template/src/components/Counter.css +27 -0
- package/template/src/components/Counter.tsx +16 -0
- package/template/src/components/__tests__/AppContent.test.tsx +274 -0
- package/template/src/components/__tests__/ConfiguredCounter.test.tsx +116 -0
- package/template/src/components/__tests__/Counter.test.tsx +44 -0
- package/template/src/config.ts +41 -0
- package/template/src/hooks/__tests__/useIncrementCounter.test.tsx +257 -0
- package/template/src/hooks/useIncrementCounter.ts +195 -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 +27 -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 +25 -0
- package/template/vitest.setup.ts +1 -0
- package/template/yarn.lock +1687 -815
- package/template/src/miden/lib/demo.ts +0 -106
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock module for @miden-sdk/react.
|
|
3
|
+
*
|
|
4
|
+
* Usage in test files:
|
|
5
|
+
*
|
|
6
|
+
* vi.mock('@miden-sdk/react', () => import('@/__tests__/mocks/miden-sdk-react'));
|
|
7
|
+
*
|
|
8
|
+
* Override specific hooks in individual tests:
|
|
9
|
+
*
|
|
10
|
+
* import { useAccounts } from '@miden-sdk/react';
|
|
11
|
+
* vi.mocked(useAccounts).mockReturnValue({ wallets: [], ... });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { vi } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
MOCK_WALLET_HEADER,
|
|
17
|
+
MOCK_WALLET_HEADER_2,
|
|
18
|
+
MOCK_FAUCET_HEADER,
|
|
19
|
+
MOCK_ACCOUNT,
|
|
20
|
+
MOCK_ASSET_BALANCE,
|
|
21
|
+
MOCK_ASSET_METADATA,
|
|
22
|
+
MOCK_INPUT_NOTE_RECORD,
|
|
23
|
+
MOCK_CONSUMABLE_NOTE_RECORD,
|
|
24
|
+
MOCK_NOTE_SUMMARY,
|
|
25
|
+
MOCK_TRANSACTION_RESULT,
|
|
26
|
+
MOCK_SEND_RESULT,
|
|
27
|
+
FAUCET_ID,
|
|
28
|
+
} from "../fixtures";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Query hooks
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const useAccounts = vi.fn(() => ({
|
|
35
|
+
accounts: [MOCK_WALLET_HEADER, MOCK_WALLET_HEADER_2, MOCK_FAUCET_HEADER],
|
|
36
|
+
wallets: [MOCK_WALLET_HEADER, MOCK_WALLET_HEADER_2],
|
|
37
|
+
faucets: [MOCK_FAUCET_HEADER],
|
|
38
|
+
isLoading: false,
|
|
39
|
+
error: null,
|
|
40
|
+
refetch: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
export const useAccount = vi.fn(() => ({
|
|
44
|
+
account: MOCK_ACCOUNT,
|
|
45
|
+
assets: [MOCK_ASSET_BALANCE],
|
|
46
|
+
isLoading: false,
|
|
47
|
+
error: null,
|
|
48
|
+
refetch: vi.fn(),
|
|
49
|
+
getBalance: vi.fn((assetId: string) =>
|
|
50
|
+
assetId === FAUCET_ID ? MOCK_ASSET_BALANCE.amount : 0n,
|
|
51
|
+
),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
export const useNotes = vi.fn(() => ({
|
|
55
|
+
notes: [MOCK_INPUT_NOTE_RECORD],
|
|
56
|
+
consumableNotes: [MOCK_CONSUMABLE_NOTE_RECORD],
|
|
57
|
+
noteSummaries: [MOCK_NOTE_SUMMARY],
|
|
58
|
+
consumableNoteSummaries: [MOCK_NOTE_SUMMARY],
|
|
59
|
+
isLoading: false,
|
|
60
|
+
error: null,
|
|
61
|
+
refetch: vi.fn(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
export const useSyncState = vi.fn(() => ({
|
|
65
|
+
syncHeight: 12345,
|
|
66
|
+
isSyncing: false,
|
|
67
|
+
lastSyncTime: Date.now(),
|
|
68
|
+
error: null,
|
|
69
|
+
sync: vi.fn(),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
export const useAssetMetadata = vi.fn(() => ({
|
|
73
|
+
assetMetadata: new Map([[FAUCET_ID, MOCK_ASSET_METADATA]]),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
export const useTransactionHistory = vi.fn(() => ({
|
|
77
|
+
records: [],
|
|
78
|
+
record: null,
|
|
79
|
+
status: null,
|
|
80
|
+
isLoading: false,
|
|
81
|
+
error: null,
|
|
82
|
+
refetch: vi.fn(),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
export const useNoteStream = vi.fn(() => ({
|
|
86
|
+
notes: [],
|
|
87
|
+
latest: null,
|
|
88
|
+
markHandled: vi.fn(),
|
|
89
|
+
markAllHandled: vi.fn(),
|
|
90
|
+
snapshot: vi.fn(() => ({ ids: new Set<string>(), timestamp: Date.now() })),
|
|
91
|
+
isLoading: false,
|
|
92
|
+
error: null,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Mutation hooks
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
// Mutation hooks that resolve to TransactionResult { transactionId } —
|
|
100
|
+
// useMint / useConsume / useSwap / useMultiSend / useTransaction.
|
|
101
|
+
function createMutationMock(mutateKey: string) {
|
|
102
|
+
return vi.fn(() => ({
|
|
103
|
+
[mutateKey]: vi.fn(async () => MOCK_TRANSACTION_RESULT),
|
|
104
|
+
result: null,
|
|
105
|
+
isLoading: false,
|
|
106
|
+
stage: "idle" as const,
|
|
107
|
+
error: null,
|
|
108
|
+
reset: vi.fn(),
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// useSend resolves to SendResult { txId, note } — not TransactionResult.
|
|
113
|
+
// Keep this separate so tests that call `send()` get the correct shape.
|
|
114
|
+
function createSendMock() {
|
|
115
|
+
return vi.fn(() => ({
|
|
116
|
+
send: vi.fn(async () => MOCK_SEND_RESULT),
|
|
117
|
+
result: null,
|
|
118
|
+
isLoading: false,
|
|
119
|
+
stage: "idle" as const,
|
|
120
|
+
error: null,
|
|
121
|
+
reset: vi.fn(),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const useCreateWallet = vi.fn(() => ({
|
|
126
|
+
createWallet: vi.fn(async () => MOCK_ACCOUNT),
|
|
127
|
+
wallet: null,
|
|
128
|
+
isCreating: false,
|
|
129
|
+
error: null,
|
|
130
|
+
reset: vi.fn(),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
export const useCreateFaucet = vi.fn(() => ({
|
|
134
|
+
createFaucet: vi.fn(async () => MOCK_ACCOUNT),
|
|
135
|
+
faucet: null,
|
|
136
|
+
isCreating: false,
|
|
137
|
+
error: null,
|
|
138
|
+
reset: vi.fn(),
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
export const useSend = createSendMock();
|
|
142
|
+
export const useMultiSend = createMutationMock("sendMany");
|
|
143
|
+
export const useMint = createMutationMock("mint");
|
|
144
|
+
export const useConsume = createMutationMock("consume");
|
|
145
|
+
export const useSwap = createMutationMock("swap");
|
|
146
|
+
export const useTransaction = createMutationMock("execute");
|
|
147
|
+
|
|
148
|
+
export const useImportAccount = vi.fn(() => ({
|
|
149
|
+
importAccount: vi.fn(async () => MOCK_ACCOUNT),
|
|
150
|
+
account: null,
|
|
151
|
+
isImporting: false,
|
|
152
|
+
error: null,
|
|
153
|
+
reset: vi.fn(),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
export const useInternalTransfer = vi.fn(() => ({
|
|
157
|
+
transfer: vi.fn(async () => ({
|
|
158
|
+
createTransactionId: "0xtx1",
|
|
159
|
+
consumeTransactionId: "0xtx2",
|
|
160
|
+
noteId: "0xnote1",
|
|
161
|
+
})),
|
|
162
|
+
transferChain: vi.fn(async () => []),
|
|
163
|
+
result: null,
|
|
164
|
+
isLoading: false,
|
|
165
|
+
stage: "idle" as const,
|
|
166
|
+
error: null,
|
|
167
|
+
reset: vi.fn(),
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
export const useWaitForCommit = vi.fn(() => ({
|
|
171
|
+
waitForCommit: vi.fn(async () => undefined),
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
export const useWaitForNotes = vi.fn(() => ({
|
|
175
|
+
waitForConsumableNotes: vi.fn(async () => []),
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
export const useSessionAccount = vi.fn(() => ({
|
|
179
|
+
initialize: vi.fn(async () => undefined),
|
|
180
|
+
sessionAccountId: null,
|
|
181
|
+
isReady: false,
|
|
182
|
+
step: "idle" as const,
|
|
183
|
+
error: null,
|
|
184
|
+
reset: vi.fn(),
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Provider hooks
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export const useMiden = vi.fn(() => ({
|
|
192
|
+
client: null,
|
|
193
|
+
isReady: true,
|
|
194
|
+
isInitializing: false,
|
|
195
|
+
error: null,
|
|
196
|
+
sync: vi.fn(),
|
|
197
|
+
runExclusive: vi.fn(async <T>(fn: () => Promise<T>) => fn()),
|
|
198
|
+
prover: null,
|
|
199
|
+
signerAccountId: null,
|
|
200
|
+
signerConnected: null,
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
export const useMidenClient = vi.fn(() => ({}));
|
|
204
|
+
|
|
205
|
+
export const useSigner = vi.fn(() => null);
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Components
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
export function MidenProvider({ children }: { children: React.ReactNode }) {
|
|
212
|
+
return children;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Utility functions
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export const formatAssetAmount = vi.fn(
|
|
220
|
+
(amount: bigint, decimals = 8) =>
|
|
221
|
+
(Number(amount) / 10 ** decimals).toFixed(decimals > 4 ? 4 : decimals),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
export const parseAssetAmount = vi.fn(
|
|
225
|
+
(input: string, decimals = 8) => BigInt(Math.round(parseFloat(input) * 10 ** decimals)),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
export const toBech32AccountId = vi.fn((id: string) => id);
|
|
229
|
+
export const normalizeAccountId = vi.fn((id: string) => id);
|
|
230
|
+
export const accountIdsEqual = vi.fn((a: string, b: string) => a === b);
|
|
231
|
+
|
|
232
|
+
export const getNoteSummary = vi.fn(() => MOCK_NOTE_SUMMARY);
|
|
233
|
+
export const formatNoteSummary = vi.fn(() => "5.0 TEST");
|
|
234
|
+
|
|
235
|
+
export const readNoteAttachment = vi.fn(() => null);
|
|
236
|
+
export const createNoteAttachment = vi.fn(() => ({}));
|
|
237
|
+
|
|
238
|
+
export const clearMidenStorage = vi.fn(async () => undefined);
|
|
239
|
+
export const migrateStorage = vi.fn(async () => true);
|
|
240
|
+
export const createMidenStorage = vi.fn(() => ({
|
|
241
|
+
get: vi.fn(() => null),
|
|
242
|
+
set: vi.fn(),
|
|
243
|
+
remove: vi.fn(),
|
|
244
|
+
clear: vi.fn(),
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
export const wrapWasmError = vi.fn((e: unknown) =>
|
|
248
|
+
e instanceof Error ? e : new Error(String(e)),
|
|
249
|
+
);
|
|
250
|
+
export const waitForWalletDetection = vi.fn(async () => undefined);
|
|
251
|
+
|
|
252
|
+
export const SignerContext = {
|
|
253
|
+
Provider: vi.fn(({ children }: { children: React.ReactNode }) => children),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const bytesToBigInt = vi.fn(() => 0n);
|
|
257
|
+
export const bigIntToBytes = vi.fn(() => new Uint8Array());
|
|
258
|
+
export const concatBytes = vi.fn((...arrays: Uint8Array[]) => {
|
|
259
|
+
const len = arrays.reduce((acc, a) => acc + a.length, 0);
|
|
260
|
+
return new Uint8Array(len);
|
|
261
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Test Patterns
|
|
2
|
+
|
|
3
|
+
Copy-adaptable test patterns for Miden frontend components.
|
|
4
|
+
|
|
5
|
+
## Available Patterns
|
|
6
|
+
|
|
7
|
+
### `provider-setup.test.tsx`
|
|
8
|
+
Starting point for any component that uses Miden hooks. Shows mock setup, `vi.mocked()` overrides, and testing ready/loading/error states.
|
|
9
|
+
|
|
10
|
+
### `query-hook.test.tsx`
|
|
11
|
+
Pattern for components displaying data from query hooks (`useAccounts`, `useNotes`, etc.). Shows loading → data → error → empty state testing.
|
|
12
|
+
|
|
13
|
+
### `mutation-hook.test.tsx`
|
|
14
|
+
Pattern for components performing transactions (`useSend`, `useMint`, etc.). Shows idle → stage progression → success → error testing, plus argument verification.
|
|
15
|
+
|
|
16
|
+
## How to Use
|
|
17
|
+
|
|
18
|
+
1. Copy the closest pattern to your component's `__tests__/` directory
|
|
19
|
+
2. Rename the test file to match your component
|
|
20
|
+
3. Replace the example component with your actual component import
|
|
21
|
+
4. Adapt assertions to your component's specific behavior
|
|
22
|
+
5. Keep the `vi.mock(...)` and `beforeEach(vi.clearAllMocks)` boilerplate
|
|
23
|
+
|
|
24
|
+
## Mock Setup
|
|
25
|
+
|
|
26
|
+
All patterns use the shared mock factory:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Override specific hooks per-test:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
vi.mocked(useAccounts).mockReturnValue({ wallets: [], ... });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Fixtures
|
|
39
|
+
|
|
40
|
+
Realistic test data is in `src/__tests__/fixtures/`. Import what you need:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { WALLET_ID_1, MOCK_ASSET_BALANCE } from "@/__tests__/fixtures";
|
|
44
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST PATTERN: Mutation Hook Component
|
|
3
|
+
*
|
|
4
|
+
* Shows how to test a component that performs Miden transactions via mutation hooks.
|
|
5
|
+
* Covers: idle state, transaction stages (executing/proving/submitting), success, and error.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Mock mutation functions to resolve or reject
|
|
9
|
+
* - Test transaction stage display during multi-step operations
|
|
10
|
+
* - Test button disable during loading to prevent double-submit
|
|
11
|
+
* - Test error display and reset
|
|
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 { useSend } from "@miden-sdk/react";
|
|
21
|
+
import { WALLET_ID_1, WALLET_ID_2, FAUCET_ID } from "@/__tests__/fixtures";
|
|
22
|
+
|
|
23
|
+
// Example component: a send token form — common Miden UI pattern
|
|
24
|
+
function SendTokenForm() {
|
|
25
|
+
const { send, isLoading, stage, error, reset } = useSend();
|
|
26
|
+
|
|
27
|
+
const handleSend = async () => {
|
|
28
|
+
try {
|
|
29
|
+
await send({
|
|
30
|
+
from: WALLET_ID_1,
|
|
31
|
+
to: WALLET_ID_2,
|
|
32
|
+
assetId: FAUCET_ID,
|
|
33
|
+
amount: 100000000n, // 1.0 token
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
// Error is captured in the hook's error state
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<button onClick={handleSend} disabled={isLoading}>
|
|
43
|
+
{isLoading ? `${stage}...` : "Send 1.0 TEST"}
|
|
44
|
+
</button>
|
|
45
|
+
{stage === "complete" && <p>Transaction submitted!</p>}
|
|
46
|
+
{error && (
|
|
47
|
+
<div>
|
|
48
|
+
<p role="alert">Send failed: {error.message}</p>
|
|
49
|
+
<button onClick={reset}>Dismiss</button>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("Mutation Hook Pattern", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Default idle state — send button should be enabled
|
|
62
|
+
it("renders idle state with enabled send button", () => {
|
|
63
|
+
render(<SendTokenForm />);
|
|
64
|
+
const button = screen.getByRole("button", { name: "Send 1.0 TEST" });
|
|
65
|
+
expect(button).toBeEnabled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Simulate transaction in progress — button disabled, stage shown
|
|
69
|
+
it("shows transaction stage and disables button during send", () => {
|
|
70
|
+
vi.mocked(useSend).mockReturnValue({
|
|
71
|
+
send: vi.fn(),
|
|
72
|
+
result: null,
|
|
73
|
+
isLoading: true,
|
|
74
|
+
stage: "proving" as const,
|
|
75
|
+
error: null,
|
|
76
|
+
reset: vi.fn(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
render(<SendTokenForm />);
|
|
80
|
+
const button = screen.getByRole("button", { name: "proving..." });
|
|
81
|
+
expect(button).toBeDisabled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Simulate completed transaction — success message shown
|
|
85
|
+
it("shows success message after transaction completes", () => {
|
|
86
|
+
vi.mocked(useSend).mockReturnValue({
|
|
87
|
+
send: vi.fn(),
|
|
88
|
+
result: { txId: "0xabc123", note: null },
|
|
89
|
+
isLoading: false,
|
|
90
|
+
stage: "complete" as const,
|
|
91
|
+
error: null,
|
|
92
|
+
reset: vi.fn(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
render(<SendTokenForm />);
|
|
96
|
+
expect(screen.getByText("Transaction submitted!")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Simulate error — error message shown with dismiss button
|
|
100
|
+
it("shows error with dismiss button on failure", async () => {
|
|
101
|
+
const mockReset = vi.fn();
|
|
102
|
+
vi.mocked(useSend).mockReturnValue({
|
|
103
|
+
send: vi.fn().mockRejectedValue(new Error("Insufficient balance")),
|
|
104
|
+
result: null,
|
|
105
|
+
isLoading: false,
|
|
106
|
+
stage: "idle" as const,
|
|
107
|
+
error: new Error("Insufficient balance"),
|
|
108
|
+
reset: mockReset,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
render(<SendTokenForm />);
|
|
112
|
+
|
|
113
|
+
// Error should be visible
|
|
114
|
+
expect(screen.getByRole("alert")).toHaveTextContent("Insufficient balance");
|
|
115
|
+
|
|
116
|
+
// Dismiss should call reset
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
await user.click(screen.getByRole("button", { name: "Dismiss" }));
|
|
119
|
+
expect(mockReset).toHaveBeenCalledOnce();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Test the actual send call — verify correct arguments
|
|
123
|
+
it("calls send with correct arguments on click", async () => {
|
|
124
|
+
const mockSend = vi.fn(async () => ({ txId: "0xtx", note: null }));
|
|
125
|
+
vi.mocked(useSend).mockReturnValue({
|
|
126
|
+
send: mockSend,
|
|
127
|
+
result: null,
|
|
128
|
+
isLoading: false,
|
|
129
|
+
stage: "idle" as const,
|
|
130
|
+
error: null,
|
|
131
|
+
reset: vi.fn(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
render(<SendTokenForm />);
|
|
135
|
+
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
await user.click(screen.getByRole("button", { name: "Send 1.0 TEST" }));
|
|
138
|
+
|
|
139
|
+
expect(mockSend).toHaveBeenCalledWith({
|
|
140
|
+
from: WALLET_ID_1,
|
|
141
|
+
to: WALLET_ID_2,
|
|
142
|
+
assetId: FAUCET_ID,
|
|
143
|
+
amount: 100000000n,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
signerConnected: null,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
render(<StatusIndicator />);
|
|
57
|
+
expect(screen.getByText("Initializing WASM...")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Override useMiden to simulate error — tests error handling
|
|
61
|
+
it("renders error state when initialization fails", () => {
|
|
62
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
63
|
+
client: null,
|
|
64
|
+
isReady: false,
|
|
65
|
+
isInitializing: false,
|
|
66
|
+
error: new Error("WASM load failed"),
|
|
67
|
+
sync: vi.fn(),
|
|
68
|
+
runExclusive: vi.fn(),
|
|
69
|
+
prover: null,
|
|
70
|
+
signerAccountId: null,
|
|
71
|
+
signerConnected: null,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
render(<StatusIndicator />);
|
|
75
|
+
expect(screen.getByRole("alert")).toHaveTextContent("WASM load failed");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
#root {
|
|
2
|
-
max-width: 1280px;
|
|
3
|
-
margin: 0 auto;
|
|
4
|
-
padding: 2rem;
|
|
5
|
-
text-align: center;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
.logo {
|
|
9
2
|
height: 6em;
|
|
10
3
|
padding: 1.5em;
|
|
@@ -36,8 +29,15 @@
|
|
|
36
29
|
}
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
.
|
|
40
|
-
|
|
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
41
|
}
|
|
42
42
|
|
|
43
43
|
.read-the-docs {
|