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.
Files changed (56) hide show
  1. package/cli.js +6 -6
  2. package/package.json +1 -1
  3. package/template/.claude/commands/review-security.md +67 -0
  4. package/template/.claude/hooks/check-artifacts.sh +45 -0
  5. package/template/.claude/hooks/run-affected-tests.sh +31 -0
  6. package/template/.claude/hooks/typecheck.sh +27 -0
  7. package/template/.claude/settings.json +29 -0
  8. package/template/.claude/settings.local.json +24 -0
  9. package/template/.claude/skills/frontend-pitfalls/SKILL.md +186 -0
  10. package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
  11. package/template/.claude/skills/miden-concepts/SKILL.md +110 -0
  12. package/template/.claude/skills/react-sdk-patterns/SKILL.md +562 -0
  13. package/template/.claude/skills/signer-integration/SKILL.md +177 -0
  14. package/template/.claude/skills/testing-patterns/SKILL.md +338 -0
  15. package/template/.claude/skills/vite-wasm-setup/SKILL.md +134 -0
  16. package/template/.claude/skills/web-client-usage/SKILL.md +454 -0
  17. package/template/.env.example +18 -0
  18. package/template/.mcp.json +9 -0
  19. package/template/CLAUDE.md +243 -0
  20. package/template/README.md +119 -14
  21. package/template/index.html +1 -1
  22. package/template/package.json +18 -8
  23. package/template/public/packages/counter_account.masp +0 -0
  24. package/template/public/packages/increment_note.masp +0 -0
  25. package/template/src/App.tsx +6 -59
  26. package/template/src/__tests__/fixtures/accounts.ts +68 -0
  27. package/template/src/__tests__/fixtures/index.ts +22 -0
  28. package/template/src/__tests__/fixtures/notes.ts +33 -0
  29. package/template/src/__tests__/mocks/miden-sdk-react.ts +261 -0
  30. package/template/src/__tests__/patterns/README.md +44 -0
  31. package/template/src/__tests__/patterns/mutation-hook.test.tsx +146 -0
  32. package/template/src/__tests__/patterns/provider-setup.test.tsx +77 -0
  33. package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
  34. package/template/src/{App.css → components/AppContent.css} +9 -9
  35. package/template/src/components/AppContent.tsx +80 -0
  36. package/template/src/components/ConfiguredCounter.tsx +48 -0
  37. package/template/src/components/Counter.css +27 -0
  38. package/template/src/components/Counter.tsx +16 -0
  39. package/template/src/components/__tests__/AppContent.test.tsx +274 -0
  40. package/template/src/components/__tests__/ConfiguredCounter.test.tsx +116 -0
  41. package/template/src/components/__tests__/Counter.test.tsx +44 -0
  42. package/template/src/config.ts +41 -0
  43. package/template/src/hooks/__tests__/useIncrementCounter.test.tsx +257 -0
  44. package/template/src/hooks/useIncrementCounter.ts +195 -0
  45. package/template/src/index.css +7 -0
  46. package/template/src/lib/miden.ts +9 -0
  47. package/template/src/main.tsx +6 -6
  48. package/template/src/providers.tsx +27 -0
  49. package/template/src/vite-env.d.ts +1 -0
  50. package/template/tsconfig.app.json +8 -4
  51. package/template/tsconfig.node.json +1 -3
  52. package/template/vite.config.ts +5 -17
  53. package/template/vitest.config.ts +25 -0
  54. package/template/vitest.setup.ts +1 -0
  55. package/template/yarn.lock +1687 -815
  56. 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
- .card {
40
- padding: 2em;
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 {