From midnight-dapp
Use when writing unit tests for Midnight contract interaction code, integration testing without ZK proofs, E2E testing with Playwright or Cypress, or setting up CI/CD pipelines for Midnight DApps.
npx claudepluginhub aaronbassett/midnight-knowledgebase --plugin midnight-dappThis skill uses the workspace's default tool permissions.
Test Midnight DApps efficiently using mocked providers, simulated wallets, and testnet integration strategies.
examples/e2e-testnet/testnet.spec.tsexamples/e2e-testnet/testnetSetup.tsexamples/mock-proof-context/mockProofProvider.tsexamples/mock-proof-context/testUtils.tsexamples/mock-wallet/MockWallet.tsexamples/mock-wallet/walletTestUtils.tsreferences/mock-wallet-provider.mdreferences/mocking-proofs.mdreferences/testnet-workflows.mdreferences/web3-comparison.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Share bugs, ideas, or general feedback.
Test Midnight DApps efficiently using mocked providers, simulated wallets, and testnet integration strategies.
Midnight DApps face unique testing challenges:
| Challenge | Why It Matters | Solution |
|---|---|---|
| Proof generation takes seconds | Tests would be too slow | Mock proof providers |
| Wallet requires browser extension | Can't run in CI/CD | Mock wallet provider |
| Private state is local only | Hard to verify in tests | Controlled test state |
| Testnet requires real infrastructure | Flaky in automation | Mock for unit tests, testnet for E2E |
E2E (Testnet)
/ \
/ Real proofs \
/ Real wallet \
/ Slow (~min) \
/____________________\
|
Integration (Mocked)
/ \
/ Mock proof provider \
/ Mock wallet provider \
/ Fast (~seconds) \
/______________________________\
|
Unit Tests
/ \
/ Pure logic \
/ No providers \
/ Fast (~ms) \
/____________________\
| Test Type | Proof Provider | Wallet Provider | Use Case |
|---|---|---|---|
| Unit | N/A | N/A | Pure business logic |
| Component | Mock | Mock | UI components |
| Integration | Mock | Mock | Contract interactions |
| E2E (local) | Mock | Mock | Full user flows |
| E2E (testnet) | Real | Real (Lace) | Pre-deployment validation |
| Document | Description |
|---|---|
| mocking-proofs.md | Mock proof provider for fast tests |
| mock-wallet-provider.md | Simulating Lace wallet in tests |
| testnet-workflows.md | E2E testing against testnet |
| web3-comparison.md | Hardhat/Foundry testing vs Midnight |
| Example | Description |
|---|---|
| mock-proof-context/ | Mock proof provider and test utilities |
| mock-wallet/ | Fake wallet implementation for tests |
| e2e-testnet/ | Playwright E2E test against testnet |
pnpm add -D vitest @testing-library/react @playwright/test msw
import { createMockProofProvider } from "./mockProofProvider";
// Returns dummy proofs instantly (no ZK computation)
const mockProofProvider = createMockProofProvider({
latencyMs: 10, // Simulate realistic timing
});
import { MockWallet } from "./MockWallet";
const mockWallet = new MockWallet({
address: "addr_test1qz_mock_address_for_testing_purposes_xyz",
balance: 1000000n,
network: "testnet",
});
// Inject into window for components that check window.midnight
globalThis.window = {
midnight: { mnLace: mockWallet.connector },
};
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MockWallet } from "./MockWallet";
import { createMockProofProvider } from "./mockProofProvider";
import { TransferButton } from "../TransferButton";
describe("TransferButton", () => {
let mockWallet: MockWallet;
let mockProofProvider: MockProofProvider;
beforeEach(() => {
mockWallet = new MockWallet({ balance: 1000n });
mockProofProvider = createMockProofProvider();
});
it("should complete transfer with mocked providers", async () => {
render(
<TransferButton
wallet={mockWallet.api}
proofProvider={mockProofProvider}
recipient="addr_test1..."
amount={100n}
/>
);
fireEvent.click(screen.getByText("Transfer"));
// No actual proof generation - instant!
await screen.findByText("Transfer Complete");
expect(mockWallet.getBalance()).toBe(900n);
});
});
import { describe, it, expect } from "vitest";
import { createMockContract } from "./testUtils";
describe("Contract State", () => {
it("should read balance from contract state", async () => {
const contract = createMockContract({
state: {
balances: new Map([["addr_test1...", 500n]]),
totalSupply: 10000n,
},
});
const balance = await contract.state.balances.get("addr_test1...");
expect(balance).toBe(500n);
});
});
import { describe, it, expect } from "vitest";
import { witnesses, createInitialPrivateState } from "../witnesses";
describe("Witnesses", () => {
it("should return balance from private state", () => {
const privateState = createInitialPrivateState(new Uint8Array(32));
privateState.balance = 1000n;
const context = { privateState, setPrivateState: () => {} };
const balance = witnesses.get_balance(context);
expect(balance).toBe(1000n);
});
it("should throw for expired credential", () => {
const privateState = createInitialPrivateState(new Uint8Array(32));
privateState.credentials.set("abc123", {
expiry: BigInt(Date.now() / 1000 - 3600), // Expired 1 hour ago
data: new Uint8Array(32),
});
const context = { privateState, setPrivateState: () => {} };
expect(() =>
witnesses.get_credential(context, hexToBytes("abc123"))
).toThrow("expired");
});
});
import { describe, it, expect, vi } from "vitest";
import { MockWallet } from "./MockWallet";
describe("Error Handling", () => {
it("should handle user rejection", async () => {
const wallet = new MockWallet();
wallet.rejectNextTransaction("User rejected");
await expect(
wallet.api.submitTransaction(mockTx)
).rejects.toThrow("User rejected");
});
it("should handle proof server unavailable", async () => {
const proofProvider = createMockProofProvider({
shouldFail: true,
errorMessage: "Proof server unavailable",
});
await expect(
proofProvider.generateProof(mockCircuit, mockWitness)
).rejects.toThrow("Proof server unavailable");
});
});
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import { DisclosureModal } from "../DisclosureModal";
describe("DisclosureModal", () => {
it("should render disclosure summary correctly", () => {
const disclosures = [
{ field: "age", label: "Your Age", value: "25" },
{ field: "country", label: "Country", value: "US" },
];
const { container } = render(
<DisclosureModal disclosures={disclosures} onConfirm={() => {}} />
);
expect(container).toMatchSnapshot();
});
});
name: Test Midnight DApp
on: [push, pull_request]
jobs:
unit-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- run: pnpm test:unit
- run: pnpm test:integration
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- run: pnpm exec playwright install --with-deps
# E2E with mocks - fast, reliable
- run: pnpm test:e2e:mock
# Optional: E2E with testnet (slower, requires secrets)
# - run: pnpm test:e2e:testnet
# env:
# TESTNET_FAUCET_KEY: ${{ secrets.TESTNET_FAUCET_KEY }}
proof-handling - Understanding what to mock in proof generationwallet-integration - Understanding Lace wallet API to mockerror-handling - Testing error scenariosstate-management - Testing state synchronization/dapp-check - Validates test configuration/dapp-debug tests - Diagnose test failures