Help us improve
Share bugs, ideas, or general feedback.
From midnight-cq
This skill should be used when the user asks to test my dapp, write e2e tests, test wallet connection, playwright midnight, test transaction UI, integration test frontend, end-to-end test, browser test, test UI with contract, write Playwright tests for a Midnight dapp, mock ContractProvider, test wallet disconnect, test confirmation screen, test rejected transaction, test contract state displayed in the UI, test React components with contract simulator, mock wallet in tests, or set up e2e testing. NOT for testing Compact contract logic in isolation — see midnight-cq:compact-testing.
npx claudepluginhub devrelaicom/midnight-expert --plugin midnight-cqHow this skill is triggered — by the user, by Claude, or both
Slash command
/midnight-cq:dapp-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> "If you're testing contract logic, use `midnight-cq:compact-testing`. If
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
"If you're testing contract logic, use
midnight-cq:compact-testing. If you're testing that the UI correctly calls contracts and displays results, you're here."
Every Midnight DApp has three testing layers. Use the right tool at each layer — do not collapse them.
┌────────────────────────────────────────────────────────────┐
│ Layer 3 – E2E Tests │
│ Tool: Playwright (always headless) │
│ Scope: Full browser flows — wallet connect, submit tx, │
│ confirmation screen, error modals │
├────────────────────────────────────────────────────────────┤
│ Layer 2 – Integration Tests │
│ Tool: Vitest │
│ Scope: Frontend components + contract simulator │
│ ContractProvider mocked to wrap simulator │
├────────────────────────────────────────────────────────────┤
│ Layer 1 – Unit Tests │
│ Tool: Vitest (contracts via midnight-cq:compact-testing) │
│ Scope: Contract logic in isolation; pure TypeScript │
│ utilities; component logic without DOM │
└────────────────────────────────────────────────────────────┘
Layer 1 is covered by midnight-cq:compact-testing. This skill starts at Layer 2.
| Question | Answer |
|---|---|
| Does the test call a Compact circuit directly? | Use midnight-cq:compact-testing |
| Does the test verify UI renders correct contract state? | You are here |
| Does the test simulate a user clicking through a flow? | You are here (E2E) |
| Does the test check a pure utility function? | Plain Vitest, no skill needed |
The Midnight SDK's ContractProvider bridges frontend components and the
blockchain. In integration tests, swap it for a mock that wraps the contract
simulator instead of hitting a live network.
// tests/integration/setup.ts
import { createSimulator } from '@openzeppelin-compact/contracts-simulator';
import { MyContractSimulator } from '../simulators/MyContractSimulator';
export function createMockContractProvider() {
const simulator = new MyContractSimulator(/* initial args */);
return {
// Mirror the real ContractProvider interface
callCircuit: vi.fn(async (circuit: string, args: unknown[]) => {
const fn = (simulator.circuits.impure as Record<string, (...a: unknown[]) => unknown>)[circuit];
if (!fn) throw new Error(`Unknown circuit: ${circuit}`);
return fn(...args);
}),
queryState: vi.fn(async () => simulator.getPublicState()),
};
}
// tests/integration/MyComponent.test.ts
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ContractContext } from '../../src/context/ContractContext';
import { createMockContractProvider } from './setup';
import { MyComponent } from '../../src/components/MyComponent';
describe('MyComponent — integration', () => {
let provider: ReturnType<typeof createMockContractProvider>;
beforeEach(() => {
provider = createMockContractProvider();
});
it('displays contract state after mount', async () => {
render(
<ContractContext.Provider value={provider}>
<MyComponent />
</ContractContext.Provider>,
);
await waitFor(() =>
expect(screen.getByTestId('contract-value')).toHaveTextContent('42'),
);
});
it('calls callCircuit on button click', async () => {
render(
<ContractContext.Provider value={provider}>
<MyComponent />
</ContractContext.Provider>,
);
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(provider.callCircuit).toHaveBeenCalledOnce();
});
});
headless: false must never appear in
committed test code or CI configuration.page.locator() directly
inside test blocks.tests/e2e/.tests/
e2e/
pages/
WalletPage.ts # Page object: wallet connect/disconnect
TransactionPage.ts # Page object: tx submission + confirmation
ErrorPage.ts # Page object: error state helpers
wallet-connection.spec.ts
transaction-flow.spec.ts
error-states.spec.ts
integration/
... # Vitest integration tests (Layer 2)
playwright.config.ts
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
use: {
headless: true, // always — never change this
baseURL: 'http://localhost:3000',
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
// tests/e2e/pages/WalletPage.ts
import { type Page, type Locator } from '@playwright/test';
export class WalletPage {
readonly connectButton: Locator;
readonly disconnectButton: Locator;
readonly walletAddress: Locator;
constructor(private page: Page) {
this.connectButton = page.getByRole('button', { name: /connect wallet/i });
this.disconnectButton = page.getByRole('button', { name: /disconnect/i });
this.walletAddress = page.getByTestId('wallet-address');
}
async connect() {
await this.connectButton.click();
// Handle wallet extension popup if present
await this.page.waitForSelector('[data-testid="wallet-address"]');
}
async disconnect() {
await this.disconnectButton.click();
await this.connectButton.waitFor();
}
}
Follow the same pattern for TransactionPage and ErrorPage: locators in
the constructor, multi-step flows in named methods.
| Scenario | Layer | Notes |
|---|---|---|
| Wallet connects successfully | E2E | Assert address displayed |
| Wallet disconnects | E2E | Assert connect button returns |
| Transaction submission flow | E2E | Pending → confirmed state |
| Confirmation UI shown | E2E | Check confirmation banner/hash |
| Transaction rejected by user | E2E | Assert rejection error message |
| Network error during submission | E2E | Assert error modal, retry option |
| Contract state displayed on mount | Integration | Provider mock returns simulator state |
| State updates after transaction | Integration | callCircuit → re-query → UI refresh |
| Error state from bad contract call | Integration | Provider throws → UI shows error |
When a real browser wallet extension is unavailable in CI, inject a stub via
page.addInitScript() before any app code runs. The stub must match the real
DApp Connector API shape (window.midnight.mnLace.enable()) — see
references/playwright-patterns.md for the full fixture pattern.
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
(window as any).midnight = {
mnLace: {
enable: async () => ({
isEnabled: async () => true,
state: async () => ({
address: 'mn_test_addr_deadbeef',
coinPublicKey: '0'.repeat(64),
}),
submitTransaction: async (_tx: unknown) => ({
txHash: 'mock_tx_hash_' + Date.now(),
}),
}),
apiVersion: '1.0.0',
name: 'MockLace',
},
};
});
});
Blockchain state transitions are async. Always use waitFor or Playwright's
built-in auto-waiting rather than asserting on immediately-resolved values:
// GOOD — waits up to 30 s for confirmation banner
await expect(txPage.confirmationBanner).toBeVisible({ timeout: 30_000 });
// BAD — races the async state update
expect(await page.locator('[data-testid="tx-confirmed"]').count()).toBe(1);
| Reference | Contents |
|---|---|
references/playwright-patterns.md | Page objects, wallet mocking, async blockchain assertions |
references/integration-testing.md | Simulator in frontend tests, ContractProvider mocking |