diff --git a/domains/testing/skills/visual-testing/references/cli-reference.md b/domains/testing/skills/visual-testing/references/cli-reference.md new file mode 100644 index 00000000..31494bc2 --- /dev/null +++ b/domains/testing/skills/visual-testing/references/cli-reference.md @@ -0,0 +1,140 @@ +# MM CLI Command Reference + +Complete command reference for the `mm` CLI. For core workflow and getting started, see the main skill. + +## Contents + +- [Syntax Rules](#syntax-rules) +- [Lifecycle](#lifecycle) +- [Interaction](#interaction) +- [Navigation & Tabs](#navigation--tabs) +- [Context](#context) +- [State, Knowledge & Seeding](#state-knowledge--seeding) +- [Advanced](#advanced) + +## Syntax Rules + +The CLI is strict about flag names and argument positions. Mistyped flags are silently treated as element targets, producing confusing timeout errors. + +### Flag spelling is exact — lowercase, no variants + +```bash +# CORRECT +mm click --testid unlock-submit +mm type --testid unlock-password "correct horse battery staple" + +# WRONG — all of these silently fail with a timeout +mm click --testId unlock-submit # capital I — treated as testId "--testId" +mm click --test-id unlock-submit # hyphenated — treated as testId "--test-id" +mm click --TestId unlock-submit # PascalCase — same problem +``` + +The only recognized interaction flags are: `--testid`, `--selector`, `--timeout`, `--within`. Anything else (including `--testId` with a capital I) is parsed as a positional argument and used as a literal testId value, producing a locator like `[data-testid="--testId"]` which always times out. + +### a11yRefs are positional arguments — no `--ref` flag + +```bash +# CORRECT — ref is the first positional arg +mm click e5 +mm type e2 "correct horse battery staple" + +# WRONG — no --ref flag exists; "--ref" becomes the testId value +mm click --ref e5 +``` + +### One targeting method per call + +Each interaction command accepts exactly one of: positional a11yRef, `--testid`, or `--selector`. Do not combine them. + +```bash +mm click e5 # a11yRef (positional) +mm click --testid unlock-submit # testId (flag) +mm click --selector "#connectButton" # CSS selector (flag) +``` + +### Quick reference + +| What you want | Correct syntax | Common mistake | +|---|---|---| +| Click by testId | `mm click --testid X` | `mm click --testId X` (capital I) | +| Click by a11y ref | `mm click e5` | `mm click --ref e5` (no such flag) | +| Click by CSS | `mm click --selector ".btn"` | `mm click --css ".btn"` (no such flag) | +| Type by testId | `mm type --testid X "text"` | `mm type --testId X "text"` | +| Type by a11y ref | `mm type e2 "text"` | `mm type --ref e2 "text"` | + +## Lifecycle + +| Command | Description | +| ----------------------- | -------------------------------------- | +| `mm launch` | Launch MetaMask in headless Chrome | +| `mm cleanup` | Stop browser and services | +| `mm cleanup --shutdown` | Stop browser, services, and the daemon | +| `mm status` | Show current daemon and session status | + +## Interaction + +| Command | Description | +| ---------------------------- | ---------------------------------------------------- | +| `mm click ` | Click element by a11y ref, testId, or selector | +| `mm type ` | Type text into element (uses `fill()`, clears first) | +| `mm get-text ` | Read text content of element | +| `mm describe-screen` | Combined state + activeTab + testIds + a11y snapshot | +| `mm screenshot [--name ]` | Take and save screenshot | +| `mm wait-for ` | Wait for element to be visible | +| `mm wait-for-notification` | Wait for sidepanel confirmation route, set as active | +| `mm accessibility-snapshot` | Get trimmed a11y tree with refs | +| `mm list-testids` | List visible `data-testid` attributes | +| `mm clipboard ` | Read from or write to browser clipboard | + +All interaction commands accept `--timeout ` (default 15000). This is a **single deadline budget** covering visibility wait + action combined — not a per-phase timeout. + +## Navigation & Tabs + +| Command | Description | +| ------------------------- | --------------------------------------------- | +| `mm navigate ` | Navigate to a specific URL | +| `mm navigate-home` | Navigate to the extension home | +| `mm navigate-settings` | Navigate to the extension settings | +| `mm switch-to-tab ` | Switch active page to a different tab by role | +| `mm close-tab ` | Close a tab | + +Tab roles: `extension`, `notification`, `dapp`, `other`. + +`mm switch-to-tab dapp` is equivalent to `mm switch-to-tab --role dapp`. + +## Context + +| Command | Description | +| ---------------------------- | ---------------------------------------------- | +| `mm get-context` | Get current context and available capabilities | +| `mm set-context ` | Switch workflow context | + +Two contexts: `e2e` (default — local Anvil, fixtures, seeding) and `prod` (no fixtures, no local chain). Cannot switch during active session — run `mm cleanup` first. + +## State, Knowledge & Seeding + +| Command | Description | +| ----------------------------- | ----------------------------------------- | +| `mm get-state` | Get current extension state | +| `mm knowledge-search ` | Search steps across sessions | +| `mm knowledge-last` | Get last N step records from this session | +| `mm knowledge-sessions` | List recent sessions with metadata | +| `mm knowledge-summarize` | Generate session recipe | +| `mm run-steps ` | Execute multiple tools in sequence | +| `mm seed-contract ` | Deploy a test contract | +| `mm seed-contracts` | Deploy multiple test contracts | +| `mm get-contract-address` | Get deployed contract address | +| `mm list-contracts` | List all deployed contracts | + +## Advanced + +| Command | Description | +| ------------------------------------------------- | ----------------------------------------------------------- | +| `mm mock-network add ''` | Add Playwright route mocks during active session | +| `mm mock-network clear` | Clear network mocks and recorded requests | +| `mm mock-network list` | List active network mock rules | +| `mm mock-network requests [--limit ]` | Show recorded matched and missed mocked-origin requests | +| `mm cdp [params-json] [--timeout ]` | Send raw Chrome DevTools Protocol command against active page| + +For mock network details, see [mock-network.md](mock-network.md). +For CDP state manipulation, see [state-manipulation.md](state-manipulation.md). diff --git a/domains/testing/skills/visual-testing/references/error-recovery.md b/domains/testing/skills/visual-testing/references/error-recovery.md new file mode 100644 index 00000000..0919f698 --- /dev/null +++ b/domains/testing/skills/visual-testing/references/error-recovery.md @@ -0,0 +1,67 @@ +# Error Recovery & Troubleshooting + +## Contents + +- [On Failure](#on-failure) +- [Error Codes](#error-codes) +- [Common Failures & Solutions](#common-failures--solutions) + +## On Failure + +1. Run `mm describe-screen` +2. Check the current screen: + - `unlock` → enter password and submit + - `home` → continue, but check for modals or blockers + - `onboarding-*` → complete onboarding + - `unknown` → take a screenshot and investigate +3. Query prior runs: + ```bash + mm knowledge-search "send" + mm knowledge-sessions + mm knowledge-last + ``` +4. Capture `mm screenshot --name "debug"` for diagnosis + +## Error Codes + +| Code | Meaning | +| ---------------------------- | ------------------------------------------------------------------------------ | +| `MM_SESSION_ALREADY_RUNNING` | Session exists — `mm cleanup` first | +| `MM_NO_ACTIVE_SESSION` | No session — `mm launch` first | +| `MM_LAUNCH_FAILED` | Browser launch failed | +| `MM_INVALID_INPUT` | Invalid parameters | +| `MM_TARGET_NOT_FOUND` | Element not visible | +| `MM_TAB_NOT_FOUND` | Tab not found | +| `MM_CLICK_FAILED` | Click failed (post-find) | +| `MM_CLICK_TIMEOUT` | Click hung — may have completed; run `mm describe-screen` | +| `MM_TYPE_FAILED` | Type failed (post-find) | +| `MM_TYPE_TIMEOUT` | `fill()` timed out — `mm describe-screen` and retry | +| `MM_GETTEXT_FAILED` | `get-text` failed (element detached) — re-target | +| `MM_GETTEXT_TIMEOUT` | `textContent()` timed out | +| `MM_WAIT_TIMEOUT` | Wait timeout exceeded | +| `MM_PAGE_CLOSED` | Page closed during interaction — normal after some confirmations | +| `MM_SCREENSHOT_FAILED` | Screenshot capture failed | +| `MM_BATCH_TIMEOUT` | `batchTimeoutMs` deadline exceeded | +| `MM_CONTEXT_SWITCH_BLOCKED` | Cannot switch context during active session | +| `MM_SET_CONTEXT_FAILED` | Context switch failed | +| `MM_CDP_BLOCKED` | CDP method is in the destructive-blocklist | +| `MM_CDP_FAILED` | CDP command execution failed or timed out | + +## Common Failures & Solutions + +| Symptom | Likely Cause | Solution | +| ------------------------------------------- | ---------------------------- | ------------------------------------------------------------- | +| `MM_SESSION_ALREADY_RUNNING` | Previous session not cleaned | `mm cleanup` | +| `MM_NO_ACTIVE_SESSION` | No browser running | `mm launch` | +| Extension not loading | Extension not built | `yarn build:test:webpack` then `mm launch` | +| `EADDRINUSE` port error | Orphan processes | Check `.mm-server` for ports, kill orphaned process | +| `MM_TARGET_NOT_FOUND` | Element not visible | `mm describe-screen` to check state | +| `MM_WAIT_TIMEOUT` | Slow environment | Increase `--timeout`, inspect screenshot | +| `MM_CLICK_TIMEOUT` / `MM_TYPE_TIMEOUT` | Action hung | `mm describe-screen` to verify; retry with larger `--timeout` | +| `MM_GETTEXT_TIMEOUT` / `MM_GETTEXT_FAILED` | Element detached | `mm describe-screen` and re-target | +| `MM_PAGE_CLOSED` | Confirmation auto-closed | Expected — `mm describe-screen` to find active page | +| `MM_CDP_BLOCKED` | Destructive CDP method | Use a non-blocked method | +| `MM_CDP_FAILED` | Invalid CDP params | Check method & params; retry with `--timeout` (max 30000) | +| `MM_CONTEXT_SWITCH_BLOCKED` | Active session | `mm cleanup` before `mm set-context` | +| Fixtures not available | Running in prod context | `mm set-context e2e` | +| Stale a11yRefs after navigate | Refs not refreshed | `mm describe-screen` for fresh refs | diff --git a/domains/testing/skills/visual-testing/references/mock-network.md b/domains/testing/skills/visual-testing/references/mock-network.md new file mode 100644 index 00000000..1e2f60b2 --- /dev/null +++ b/domains/testing/skills/visual-testing/references/mock-network.md @@ -0,0 +1,81 @@ +# Mock Network Requests + +Use `mm mock-network` to stub browser network requests during an active session. Prefer this over raw CDP when you need deterministic API responses. + +## Contents + +- [Rules](#rules) +- [Rule Shape](#rule-shape) +- [Examples](#examples) +- [Verification Pattern](#verification-pattern) +- [In mm run-steps](#in-mm-run-steps) + +## Rules + +Mock rules are installed on the active Playwright browser context: + +- Run `mm launch` first. +- Add rules **before** the UI action that triggers the request. +- `mm cleanup` removes all rules; `mm mock-network clear` removes rules and request history without ending the session. +- Unmatched requests on a mocked origin continue unchanged and are recorded as misses. +- **Cannot** intercept requests during extension startup before the session is fully active. + +## Rule Shape + +```json +{ + "id": "token-prices", + "method": "GET", + "url": "https://price.api.metamask.io/v1/**", + "response": { + "status": 200, + "json": { "ethereum": { "usd": 1234.56 } } + } +} +``` + +| Field | Description | +| ------------------ | --------------------------------------------------------------------------------- | +| `id` | Stable identifier. Same `id` replaces the previous rule. | +| `method` | HTTP method; normalized to uppercase. | +| `url` | Absolute URL or glob. `*` matches within a segment; `**` matches any path suffix. | +| `response.status` | Optional HTTP status; defaults to `200`. | +| `response.json` | JSON response payload. | +| `response.body` | Text response payload. Use either `json` or `body`. | +| `response.headers` | Optional. JSON/text defaults include `access-control-allow-origin: *`. | + +## Examples + +```bash +# Single rule +mm mock-network add '{"id":"token-prices","method":"GET","url":"https://price.api.metamask.io/v1/**","response":{"status":200,"json":{"ethereum":{"usd":1234.56}}}}' + +# Multiple rules +mm mock-network add '{"routes":[ + {"id":"feature-flags","method":"GET","url":"https://client-config.api.cx.metamask.io/**","response":{"json":{"flags":{}}}}, + {"id":"empty-nfts","method":"POST","url":"https://nft.api.metamask.io/**","response":{"status":200,"json":{"nfts":[]}}} +]}' + +# Inspect and manage +mm mock-network list +mm mock-network requests --limit 20 +mm mock-network clear +``` + +## Verification Pattern + +1. Add the rule with `mm mock-network add ...` +2. Trigger the UI flow that makes the request +3. Run `mm mock-network requests --limit 20` +4. Confirm the expected request has `matched: true` and the expected `ruleId` + +## In mm run-steps + +Use tool name `mock_network` with the same input shape: + +```bash +mm run-steps '{"steps":[ + {"tool":"mock_network","args":{"action":"add","rule":{"id":"prices","method":"GET","url":"https://price.api.metamask.io/v1/**","response":{"json":{"ok":true}}}}}, + {"tool":"navigate","args":{"screen":"url","url":"https://test-dapp.io"}} +]}' +``` diff --git a/domains/testing/skills/visual-testing/references/state-manipulation.md b/domains/testing/skills/visual-testing/references/state-manipulation.md new file mode 100644 index 00000000..9962bad7 --- /dev/null +++ b/domains/testing/skills/visual-testing/references/state-manipulation.md @@ -0,0 +1,212 @@ +# Runtime State Manipulation + +Use `mm cdp` to read and write wallet state mid-session when fixtures and presets don't cover your scenario. All operations use `Runtime.evaluate` against the active extension page and work on every build type. + +## Contents + +- [CDP Basics](#cdp-basics) +- [Five Operations](#five-operations) +- [Verify State After Mutation](#verify-state-after-mutation) +- [When to Use CDP](#when-to-use-cdp) + +## CDP Basics + +`mm cdp` sends a raw [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) command against the active page. Use this **only when structured commands cannot express what you need**. + +```bash +mm cdp Runtime.evaluate '{"expression":"document.title"}' +mm cdp Network.enable +mm cdp DOM.getDocument '{"depth":2}' --timeout 60000 +``` + +| Argument | Description | +| --------------- | ----------------------------------------------------------------- | +| `` | CDP method name (e.g., `Runtime.evaluate`, `DOM.getDocument`) | +| `[params-json]` | Optional JSON object with method-specific parameters | +| `--timeout` | Per-command timeout in ms. Default: 30000. Min: 1000. Max: 30000 | + +**Blocked methods** (returns `MM_CDP_BLOCKED`): `Browser.close`, `Target.closeTarget`, `Target.disposeBrowserContext`, `Browser.crashGpuProcess`. + +CDP calls are **mutating** — run `mm describe-screen` afterward to re-sync the a11y ref map. + +## Five Operations + +| Operation | API | Scope | +| -------------------- | ------------------------------------ | -------------------------------------------------------- | +| Read Redux state | `stateHooks.getCleanAppState()` | In-memory UI state (what the user sees) | +| Read persisted state | `stateHooks.getPersistedState()` | On-disk controller state (`chrome.storage.local`) | +| Write Redux state | Fiber dispatch via CDP | Instant UI update, lost on reload | +| Write persisted state| `chrome.storage.local.set()` via CDP | Survives reload, eventually reflected in UI | +| Inject in-memory data| Fiber walk + `pushData()` via CDP | Reaches components that bypass Redux (streams, channels) | + +To change what the user sees: write Redux state. +To make changes survive a reload: write persisted state too. +For most visual testing: writing Redux state alone is sufficient. +If a component doesn't respond to Redux changes: it reads from an in-memory source — use operation 5. + +### 1. Read Redux State + +```bash +# Full state keys +mm cdp Runtime.evaluate '{"expression":"stateHooks.getCleanAppState().then(s => JSON.stringify(Object.keys(s.metamask).sort()))", "awaitPromise":true, "returnByValue":true}' + +# Specific values +mm cdp Runtime.evaluate '{"expression":"stateHooks.getCleanAppState().then(s => JSON.stringify({ privacyMode: s.metamask.preferences?.privacyMode, selectedNetwork: s.metamask.selectedNetworkClientId }))", "awaitPromise":true, "returnByValue":true}' +``` + +### 2. Read Persisted State + +```bash +mm cdp Runtime.evaluate '{"expression":"stateHooks.getPersistedState().then(s => JSON.stringify(Object.keys(s.data)))", "awaitPromise":true, "returnByValue":true}' + +# Specific controller +mm cdp Runtime.evaluate '{"expression":"stateHooks.getPersistedState().then(s => JSON.stringify(s.data.PreferencesController))", "awaitPromise":true, "returnByValue":true}' +``` + +### 3. Write Redux State + +Locate the Redux store via the React fiber tree and dispatch `UPDATE_METAMASK_STATE`: + +```bash +mm cdp Runtime.evaluate '{"expression":"(function(){var r=document.getElementById(\"app-content\");var k=Object.keys(r).find(function(k){return k.startsWith(\"__reactFiber$\")});var f=r[k];while(f){if(f.stateNode&&f.stateNode.store&&typeof f.stateNode.store.dispatch===\"function\"){var store=f.stateNode.store;var s=store.getState();var next=JSON.parse(JSON.stringify(s.metamask));next.preferences=Object.assign({},next.preferences,{privacyMode:true});store.dispatch({type:\"UPDATE_METAMASK_STATE\",value:next});return\"ok\"}f=f.return}return\"store not found\"})()","returnByValue":true}' +``` + +**To modify for other state changes**, change the line that modifies `next`: + +```javascript +// Toggle privacy mode +next.preferences = Object.assign({}, next.preferences, {privacyMode: true}); + +// Change a nested controller value +next.PreferencesController = Object.assign({}, next.PreferencesController, { + preferences: Object.assign({}, next.PreferencesController.preferences, { + showTestNetworks: true + }) +}); +``` + +Notes: +- The `__reactFiber$` key suffix is a random hash per build. The traversal pattern is stable. +- `UPDATE_METAMASK_STATE` does not persist. Change is lost on reload. +- If the fiber walk fails (e.g., LavaMoat scuttling), returns `"store not found"`. + +### 4. Write Persisted State + +Write directly to `chrome.storage.local`. The extension uses a split format where each controller is a separate key tracked by a `manifest` array. + +```bash +# Modify an existing controller +mm cdp Runtime.evaluate '{"expression":"chrome.storage.local.get([\"PreferencesController\"]).then(function(r){r.PreferencesController.preferences.privacyMode=true;return chrome.storage.local.set(r)})","awaitPromise":true}' + +# Add a new controller (must update manifest) +mm cdp Runtime.evaluate '{"expression":"chrome.storage.local.get([\"manifest\"]).then(function(r){var m=r.manifest;if(m.indexOf(\"MyController\")<0)m.push(\"MyController\");return chrome.storage.local.set({manifest:m,MyController:{key:\"value\"}})})","awaitPromise":true}' +``` + +Storage schema: + +``` +chrome.storage.local = { + manifest: ['PreferencesController', 'NetworkController', ...], + PreferencesController: { preferences: { privacyMode: false, ... }, ... }, + meta: { version: 175, storageKind: 'split' } +} +``` + +Rules: +- Always read before write. Merge with existing state, never blind-write. +- Update `manifest` when adding new keys. Keys not in the manifest are invisible on next load. +- Do not delete or corrupt `manifest` or `meta`. + +### 5. Inject Into In-Memory Data Sources (React Fiber Walk) + +Some components read from in-memory caches, stream managers, or data channels that are **not in Redux state**. Dispatching `UPDATE_METAMASK_STATE` updates Redux but these components won't re-render because they subscribe to a different data source. + +**How to detect this:** After a Redux dispatch, if the component still shows stale data, it reads from an in-memory source — not Redux. + +#### Step 1 — Identify the data source + +Read the component source to find what hook it uses: +- `useSelector(...)` → reads from Redux → use Write Redux (operation 3) +- `useSomeChannel(...)` / `useSomeStream(...)` → reads from an in-memory provider → needs fiber injection +- `useContext(SomeContext)` → reads from React context → find the provider via fiber walk + +#### Step 2 — Find the object via fiber walk + +Walk the React fiber tree looking for the target object by its **shape** (the methods and properties it exposes). Shape-matching is more robust than looking for named exports because it works regardless of bundling, minification, or scoping. + +```bash +mm cdp Runtime.evaluate '{"expression":"(function(){var r=document.getElementById(\"app-content\");var k=Object.keys(r).find(function(k){return k.startsWith(\"__reactFiber$\")});var f=r[k];var visited=0;function walk(node){if(!node||visited>500)return null;visited++;var s=node.memoizedState;while(s){if(s.queue&&s.queue.lastRenderedState){var obj=s.queue.lastRenderedState;if(obj&&typeof obj.pushData===\"function\"&&typeof obj.getCachedData===\"function\"){return obj}}s=s.next}if(node.memoizedProps){for(var pk in node.memoizedProps){var pv=node.memoizedProps[pk];if(pv&&typeof pv===\"object\"&&typeof pv.pushData===\"function\"){return pv}}}return walk(node.child)||walk(node.sibling)}var result=walk(f);return result?\"found at fiber \"+visited:\"not found after \"+visited+\" fibers\"})()","returnByValue":true}' +``` + +**Adapt the detection predicate** to match your target object's shape. Common predicates: + +| Target type | Predicate | +|---|---| +| Data channel | `typeof obj.pushData === "function" && typeof obj.getCachedData === "function"` | +| Redux store | `typeof obj.dispatch === "function" && typeof obj.getState === "function"` | +| Stream manager | Check for named channel properties, e.g. `obj.account && typeof obj.account.pushData === "function"` | +| React context value | Match on the unique combination of properties the context provides | + +#### Step 3 — Disconnect the live source + +If the data source is fed by a live connection (WebSocket, polling, subscription), disconnect it first to prevent your injected data from being overwritten. TypeScript `private` fields compile to regular JS properties at runtime, so they are accessible: + +```javascript +// Inside the Runtime.evaluate expression: +if (channel.unsubscribeFromSource) { + channel.unsubscribeFromSource(); + channel.unsubscribeFromSource = null; +} +channel.isConnected = false; +``` + +#### Step 4 — Push data + +Call the channel's update method with data matching the expected type: + +```javascript +channel.pushData({ + // fields matching the component's expected data shape +}); +``` + +`pushData` updates the internal cache AND notifies all subscribers — React components re-render immediately. + +#### Complete example + +Find a data channel by shape, disconnect its live source, and inject test data: + +```bash +mm cdp Runtime.evaluate '{"expression":"(function(){var r=document.getElementById(\"app-content\");var k=Object.keys(r).find(function(k){return k.startsWith(\"__reactFiber$\")});var f=r[k];var visited=0;function walk(n){if(!n||visited>500)return null;visited++;var s=n.memoizedState;while(s){if(s.queue&&s.queue.lastRenderedState){var obj=s.queue.lastRenderedState;if(obj&&typeof obj.pushData===\"function\"&&typeof obj.getCachedData===\"function\"){return obj}}s=s.next}var p=n.memoizedProps;if(p){for(var pk in p){var pv=p[pk];if(pv&&typeof pv===\"object\"){if(typeof pv.pushData===\"function\")return pv;for(var ck in pv){if(pv[ck]&&typeof pv[ck].pushData===\"function\")return pv[ck]}}}}return walk(n.child)||walk(n.sibling)}var ch=walk(f);if(!ch)return\"channel not found after \"+visited+\" fibers\";if(ch.unsubscribeFromSource){ch.unsubscribeFromSource();ch.unsubscribeFromSource=null}ch.isConnected=false;ch.pushData({totalBalance:\"10000.00\",unrealizedPnl:\"500.00\",marginUsed:\"2000.00\",spendableBalance:\"8000.00\",withdrawableBalance:\"8000.00\",returnOnEquity:\"0.05\"});return\"data pushed at fiber \"+visited})()","returnByValue":true}' +``` + +Then `mm describe-screen` and `mm screenshot` to verify the component updated. + +#### When to use this vs other operations + +| Symptom | Cause | Solution | +|---------|-------|----------| +| Redux dispatch updated state but component shows stale data | Component reads from in-memory source, not Redux | Fiber injection (this section) | +| Feature needs live streaming data but no preset provides it | Data comes from external service via WebSocket/polling | Disconnect source + push mock data via fiber | +| Component shows loading skeleton despite state being set | The in-memory channel's `isInitialLoading` is still true | Push data via `pushData()` — it marks the channel as loaded | + +## Verify State After Mutation + +```bash +# Check Redux state +mm cdp Runtime.evaluate '{"expression":"stateHooks.getCleanAppState().then(function(s){return JSON.stringify({privacyMode:s.metamask.preferences.privacyMode})})","awaitPromise":true,"returnByValue":true}' + +# Check persisted state +mm cdp Runtime.evaluate '{"expression":"stateHooks.getPersistedState().then(function(s){return JSON.stringify({privacyMode:s.data.PreferencesController.preferences.privacyMode})})","awaitPromise":true,"returnByValue":true}' +``` + +## When to Use CDP + +| Need | Suggested CDP method | +| --------------------------------------------- | --------------------------------------------------------- | +| Read a JS value / `window` property | `Runtime.evaluate` with `{ "expression": "..." }` | +| Inspect / traverse DOM | `DOM.getDocument`, `DOM.querySelector` | +| Capture network traffic | `Network.enable` | +| Inject cookies or storage | `Network.setCookie`, `Storage.setLocalStorage*` | +| Low-level input beyond `mm click` / `mm type` | `Input.dispatchKeyEvent`, `Input.dispatchMouseEvent` | +| Inject data into non-Redux in-memory sources | Fiber walk + `pushData()` (see [operation 5](#5-inject-into-in-memory-data-sources-react-fiber-walk)) | diff --git a/domains/testing/skills/visual-testing/repos/metamask-extension.md b/domains/testing/skills/visual-testing/repos/metamask-extension.md index 43c1eb34..48115b84 100644 --- a/domains/testing/skills/visual-testing/repos/metamask-extension.md +++ b/domains/testing/skills/visual-testing/repos/metamask-extension.md @@ -6,383 +6,182 @@ metadata: type: browser-testing --- -# MetaMask Visual Testing — Agent Skill +# MetaMask Visual Testing — Extension -## When to Use This Skill - -Use this skill when you need to: +## When to Use - Visually validate MetaMask UI changes in a real browser - Capture screenshots as evidence - Verify onboarding, unlock, transaction, swap, or dapp-confirmation flows -- Click through extension behavior instead of reasoning from code alone - Debug unexpected UI state in the extension or sidepanel +- Inject test data into features that read from in-memory streams or data channels (not Redux) when fixtures and WebSocket mocks can't reach the data source -For architecture and developer-facing implementation details, see `test/e2e/playwright/llm-workflow/README.md`. - -## Prerequisites - -**CLI invocation**: The `mm` CLI is a project-local dependency. Use one of: - -```bash -yarn mm -npx mm -./node_modules/.bin/mm -``` - -All examples in this skill use `mm` for brevity. - -**Validate that there is an extension build** before `mm launch`: - -- Build output is in `dist/chrome/` -- If there is no build, build the extension -- If there is a build and the user explicitly asked to rebuild, rebuild it -- If there is a build and the user explicitly asked not to rebuild, reuse it -- If reuse vs rebuild affects the task and the user was explicit, follow that instruction - -**Build command**: - -```bash -yarn install -yarn build:test:webpack -``` - -`mm launch` validates the build and returns an actionable error if it is missing. - -If ports are stuck from a previous run, do not assume fixed port numbers. The current daemon and sub-service ports are persisted in the worktree-local `.mm-server` file: - -```bash -cat .mm-server -``` - -Look under `subPorts` for the active `anvil` and `fixture` ports, then target those specific ports if you need to clean up orphan processes. +For architecture details, see `test/e2e/playwright/llm-workflow/README.md`. ## Gotchas - `a11yRef`s (`e1`, `e2`, ...) are **ephemeral**. After `mm describe-screen`, `mm accessibility-snapshot`, or major navigation, re-describe before reusing refs. - `mm type` uses Playwright `fill()` and **clears the field first**. - After confirm or reject in sidepanel mode, the page does **not** close. It stays open and navigates back to the home route. -- `mm wait-for-notification` waits for the sidepanel confirmation route. It does **not** wait for a legacy popup window. +- `mm wait-for-notification` waits for the sidepanel confirmation route, not a legacy popup window. - `mm run-steps` expects a JSON **object** with a `steps` key, not a bare array. -- In `mm run-steps`, prefer `a11yRef`, `testId`, or `selector` in args. `ref` is accepted as shorthand, but explicit keys are clearer. - You cannot switch context during an active session. Run `mm cleanup` first, or use `mm launch --context ...`. - The default password for built-in fixtures is `correct horse battery staple`. -- Network mocks are session-scoped. Add `mm mock-network` rules after `mm launch` and before the UI action that triggers the request; `mm cleanup` removes them. -- `mm mock-network` uses Playwright route interception and can mock requests from both page and extension service-worker contexts. However, it **cannot** intercept requests made during extension startup before the session is fully active. Pre-launch mocking is not currently supported and will be added in a future update. - -## CLI Commands Overview - -The `mm` CLI is the primary interface. - -### Lifecycle - -| Command | Description | -| ----------------------- | -------------------------------------- | -| `mm launch` | Launch MetaMask in headless Chrome | -| `mm cleanup` | Stop browser and services | -| `mm cleanup --shutdown` | Stop browser, services, and the daemon | -| `mm status` | Show current daemon and session status | - -### Interaction - -| Command | Description | -| ---------------------------- | ---------------------------------------------------- | -| `mm click ` | Click element by a11y ref, testId, or selector | -| `mm type ` | Type text into element | -| `mm get-text ` | Read text content of element | -| `mm describe-screen` | Combined state + activeTab + testIds + a11y snapshot | -| `mm screenshot [--name ]` | Take and save screenshot | -| `mm wait-for ` | Wait for element to be visible | -| `mm wait-for-notification` | Wait for sidepanel confirmation route, set as active | -| `mm accessibility-snapshot` | Get trimmed a11y tree with refs | -| `mm list-testids` | List visible `data-testid` attributes | -| `mm clipboard ` | Read from or write to browser clipboard | - -### Navigation & Tabs - -| Command | Description | -| ------------------------- | --------------------------------------------- | -| `mm navigate ` | Navigate to a specific URL | -| `mm navigate-home` | Navigate to the extension home | -| `mm navigate-settings` | Navigate to the extension settings | -| `mm switch-to-tab ` | Switch active page to a different tab by role | -| `mm close-tab ` | Close a tab | - -### Context - -| Command | Description | -| ---------------------------- | ---------------------------------------------- | -| `mm get-context` | Get current context and available capabilities | -| `mm set-context ` | Switch workflow context | - -### State, Knowledge, and Seeding - -| Command | Description | -| ----------------------------- | ----------------------------------------- | -| `mm get-state` | Get current extension state | -| `mm knowledge-search ` | Search steps across sessions | -| `mm knowledge-last` | Get last N step records from this session | -| `mm knowledge-sessions` | List recent sessions with metadata | -| `mm knowledge-summarize` | Generate session recipe | -| `mm run-steps ` | Execute multiple tools in sequence | -| `mm seed-contract ` | Deploy a test contract | -| `mm seed-contracts` | Deploy multiple test contracts | -| `mm get-contract-address` | Get deployed contract address | -| `mm list-contracts` | List all deployed contracts | - -### Advanced - -| Command | Description | -| ----------------------------------------------- | ------------------------------------------------------------- | -| `mm mock-network add ''` | Add one or more Playwright route mocks during active session | -| `mm mock-network clear` | Clear network mocks and recorded requests | -| `mm mock-network list` | List active network mock rules | -| `mm mock-network requests [--limit ]` | Show recorded matched and missed mocked-origin requests | -| `mm cdp [params-json] [--timeout ]` | Send raw Chrome DevTools Protocol command against active page | - -## Launch Modes & Fixtures - -### Default: Pre-Onboarded Wallet - -Wallet is pre-configured with 25 ETH on local Anvil. +- Network mocks are session-scoped. Add rules after `mm launch` and before the triggering UI action; `mm cleanup` removes them. +- `mm mock-network` **cannot** intercept requests during extension startup before the session is fully active. +- There is **no `mm scroll` command**. If an element is below the viewport and `mm click` times out, scroll it into view via CDP before retrying: + ```bash + mm cdp Runtime.evaluate '{"expression":"document.querySelector(\"[data-testid=import-token-button]\").scrollIntoView({block:\"center\"})"}' + mm click --testid import-token-button + ``` -```bash -mm launch -mm launch --state default -mm launch --context prod -``` +## Prerequisites -### Onboarding: Fresh Wallet +**CLI invocation** — `mm` is a project-local dependency: ```bash -mm launch --state onboarding +yarn mm # or: npx mm ``` -### Custom Fixture +**Build validation** before `mm launch`: + +- Build output is in `dist/chrome/`. If missing, build first. +- If a build exists and the user explicitly asked to rebuild, rebuild it. +- If a build exists and the user explicitly asked not to rebuild, reuse it. +- `mm launch` validates the build and returns an actionable error if missing. ```bash -mm launch --state custom --preset withMultipleAccounts +yarn install && yarn build:test:webpack ``` -### Available Presets - -| Preset | Description | -| ---------------------- | --------------------------------- | -| `withMultipleAccounts` | Wallet with 2 accounts | -| `withERC20Tokens` | Wallet with test ERC-20 tokens | -| `withConnectedDapp` | Wallet pre-connected to test dapp | -| `withPopularNetworks` | Popular L2 networks added | -| `withMainnet` | Switched to Ethereum Mainnet | -| `withNFTs` | Wallet with test NFTs | -| `withFiatDisabled` | Fiat conversion display disabled | -| `withHSTToken` | Wallet with HST token | - -## Context Switching (e2e vs prod) - -Two execution contexts are supported: - -| Context | Description | -| ------- | ------------------------------------------------------------------------ | -| `e2e` | Default. Local Anvil blockchain, pre-onboarded wallet, fixtures, seeding | -| `prod` | Production-like mode. No fixtures, no local chain, limited capabilities | - -Use: +**Port conflicts** — if ports are stuck from a previous run, check `.mm-server` for active ports: ```bash -mm get-context -mm set-context prod -mm set-context e2e +cat .mm-server # look under subPorts for anvil and fixture ports ``` -Rules: - -1. Cannot switch during an active session — run `mm cleanup` first -2. Default context is `e2e` -3. Context persists until changed or daemon restart -4. `mm launch --context prod` sets context and launches in one step - -## Sidepanel Mode (Default) - -The extension runs in **headless browser mode** by default, using `sidepanel.html` instead of the legacy popup. - -What matters operationally: - -1. After confirm or reject, the sidepanel stays open and navigates back to home -2. `mm wait-for-notification` waits for a confirmation route in the sidepanel URL hash -3. After a confirmation action, use `mm describe-screen` to verify the return to home state -4. Known confirmation routes include: - - `/connect` - - `/confirm-transaction` - - `/confirmation` - - `/confirm-import-token` - - `/confirm-add-suggested-token` - - `/confirm-add-suggested-nft` - ## Core Workflow -### 1. Build Extension +### 1. Launch ```bash -yarn build:test:webpack +mm launch # pre-onboarded wallet, 25 ETH on local Anvil +mm launch --state onboarding # fresh wallet +mm launch --state custom --preset withERC20Tokens # custom fixture +mm launch --context prod --state onboarding # production-like mode ``` -Skip if already built and reuse is acceptable for the task. - -### 2. Launch Extension +Available presets: `withMultipleAccounts`, `withERC20Tokens`, `withConnectedDapp`, `withPopularNetworks`, `withMainnet`, `withNFTs`, `withFiatDisabled`, `withHSTToken`. -```bash -mm launch -mm launch --state default -mm launch --state onboarding -mm launch --context prod --state onboarding -mm launch --state custom --preset withMultipleAccounts -``` +Two contexts: **e2e** (default — local Anvil, fixtures, seeding) and **prod** (no fixtures, no local chain). Use `mm get-context` / `mm set-context` to switch. Cannot switch during active session. -### 3. Reuse Existing Knowledge (Mandatory) - -Before interacting, query prior knowledge: +### 2. Reuse Knowledge (Mandatory) ```bash mm knowledge-search "" mm knowledge-sessions ``` -If knowledge exists, reuse the discovered sequence. If not, proceed with discovery and let this session record the new steps. +Reuse discovered sequences when they exist. If none exist, proceed with discovery and let this session record the new steps. -### 4. Describe Current Screen +### 3. Describe Screen ```bash mm describe-screen ``` -This returns the current screen, active tab info, visible testIds, and an accessibility tree with refs. - -**Observation efficiency:** +Returns current screen, active tab, visible testIds, and accessibility tree with refs. -- Mutating actions like `click`, `type`, and `navigate` return compact observations -- After the first mutation, later mutations return diff-based observations until `mm describe-screen` resets the baseline -- Use mutation responses for quick next-step targeting when they already contain the needed refs -- Call `mm describe-screen` when you need the full a11y tree, screenshots, or priorKnowledge +**Observation efficiency:** Mutating actions (`click`, `type`, `navigate`) return compact diff-based observations after the first mutation. Use these for quick next-step targeting. Call `mm describe-screen` when you need the full a11y tree, screenshots, or to reset the baseline. -### 5. Interact with UI +### 4. Interact -Use exactly one targeting method per call. - -#### Timeouts (deadline-based) - -`mm click`, `mm type`, `mm get-text`, and `mm wait-for` all accept `--timeout ` (default 15000). This is a **single deadline budget** covering the entire operation — visibility wait + action combined — not a per-phase timeout. - -Phase-specific error codes: - -- `MM_WAIT_TIMEOUT` — element never became visible within the budget. -- `MM_CLICK_TIMEOUT` — element was found but the click action hung. The click may still complete in the background; run `mm describe-screen` to verify before retrying. -- `MM_TYPE_TIMEOUT` — element was found but `fill()` hung. -- `MM_GETTEXT_TIMEOUT` — element was found but `textContent()` hung. -- `MM_PAGE_CLOSED` — page closed during the action (expected for some confirmation flows; `mm click` may instead succeed with `pageClosedAfterClick: true` when the closure was a natural consequence of the click). - -Examples: +Use exactly **one targeting method** per call. Priority: `testId` (stable) > `a11yRef` (discovery) > `selector` (fallback). ```bash -mm click --testid onboarding-complete-done --timeout 60000 -mm type --testid send-amount "0.01" --timeout 10000 -mm get-text --testid balance-display --timeout 5000 +# By testId — preferred for known flows and batching +mm click --testid unlock-submit +mm type --testid unlock-password "correct horse battery staple" mm wait-for --testid account-menu-icon --timeout 10000 -``` - -#### By a11yRef - -Use refs from `mm describe-screen` or `mm accessibility-snapshot` during discovery. +mm get-text --testid balance-display -```bash +# By a11yRef — from describe-screen, during discovery mm click e5 mm type e2 "correct horse battery staple" -mm wait-for e3 --timeout 10000 -``` -#### Scoped targeting with `--within` +# By CSS selector — fallback when testIds or a11y refs are unavailable +mm click --selector "text=Rename" +mm click --selector "role=button[name='Submit']" +``` -Use `--within` when duplicate names or testIds exist and you need to target inside a specific container. +**Scoped targeting** with `--within` when duplicate names or testIds exist: ```bash mm click --testid end-accessory --within "testid:account-list-item/0" mm click e3 --within "testid:dialog-container" -mm wait-for --testid confirm-btn --within "selector:.modal-content" +mm click --selector ".popover-menu-item" --within "selector:.account-list-item:first-child" ``` -The `--within` value accepts an a11y ref, `testid:`, or `selector:`. +`--within` accepts an a11yRef, `testid:`, or `selector:`. -#### By testId - -Prefer `testId` for stable, known flows and batching. +**List views (account list, token list, network list):** These screens contain many elements with identical names and testIds. The a11y tree will show dozens of entries like `"Open multichain account address menu"` with no distinguishing context. You must scope with `--within` to target the correct item: ```bash -mm click --testid unlock-submit -mm type --testid unlock-password "correct horse battery staple" -mm wait-for --testid account-menu-icon --timeout 10000 -mm get-text --testid balance-display -``` +# Target the 3-dot menu on the first account in the account list +mm click --testid account-list-item-menu-button --within "testid:account-list-item/0" -#### By CSS selector +# Target an element inside a specific dialog or popover +mm click --selector "text=Rename" --within "testid:popover-content" +``` -Use selectors as a fallback when testIds or a11y refs are unavailable. +If `--within` isn't working or you can't identify the right parent testId, use a CSS selector with `:nth-child()` or other structural selectors as a fallback. -Supported forms: +#### Timeouts -- CSS: `button.primary` -- Text: `text=Rename` -- Role: `role=button[name='Submit']` +All interaction commands accept `--timeout ` (default 15000). This is a **single deadline budget** covering visibility wait + action combined. -Do not use the unsupported `:text()` pseudo-class. +Phase-specific error codes on timeout: +- `MM_WAIT_TIMEOUT` — element never became visible. +- `MM_CLICK_TIMEOUT` — element found, click hung. May have completed; run `mm describe-screen`. +- `MM_TYPE_TIMEOUT` — element found, `fill()` hung. +- `MM_GETTEXT_TIMEOUT` — element found, `textContent()` hung. +- `MM_PAGE_CLOSED` — page closed during action (expected for some confirmation flows). -```bash -mm click --selector "button.primary" -mm click --selector "text=Rename" -mm click --selector "role=button[name='Submit']" -mm type --selector "input[name='amount']" "0.1" -mm wait-for --selector ".transaction-list-item" --timeout 10000 -mm get-text --selector ".balance-value" -``` +For the full error code table, see [references/error-recovery.md](references/error-recovery.md). -#### Reading Element Text +### 5. Verify-Fix Loop -```bash -mm get-text e5 -mm get-text --testid balance-display -mm get-text --selector ".tx-amount" -mm get-text --testid amount --within "testid:tx-row" -``` +After any interaction sequence: -Start with a11y refs during discovery, then prefer testIds once the flow is known. +1. `mm describe-screen` to verify expected state +2. If wrong: `mm screenshot --name "debug-"`, check `mm knowledge-search ""`, retry +3. Continue only when expected state is confirmed -### 6. Verify-Fix Loop +### 5b. Edge Case: Component Not Responding to State Changes -After any interaction sequence: +If you change Redux state via CDP but the target component doesn't update: -1. Run `mm describe-screen` to verify the expected state -2. If the state is wrong: - - capture `mm screenshot --name "debug-"` - - check `mm knowledge-search ""` - - retry the failed step or adjust targeting -3. Only continue when the expected screen or state is confirmed +1. **The component reads from a different data source** — an in-memory stream, data channel, or React context that is NOT backed by Redux. +2. Read the component's source to identify the hook it uses (`useSelector` = Redux; anything else = likely in-memory). +3. Use the [React Fiber Data Injection](references/state-manipulation.md#5-inject-into-in-memory-data-sources-react-fiber-walk) pattern from the State Manipulation reference. +4. Disconnect the live data feed first (prevents overwrite), then push your test data. +5. Re-verify with `mm describe-screen`. -### 7. Handle Confirmations (Dapp Flows) +### 6. Confirmations (Dapp Flows) ```bash -mm navigate https://test-dapp.io -mm click e1 -mm wait-for-notification -mm describe-screen -mm click e2 -mm describe-screen -mm switch-to-tab dapp -mm describe-screen +mm navigate https://test-dapp.io # navigate to dapp +mm click e1 # trigger dapp action +mm wait-for-notification # wait for sidepanel confirmation +mm describe-screen # see the confirmation +mm click e2 # confirm or reject +mm describe-screen # verify return to home +mm switch-to-tab dapp # back to dapp ``` -`mm switch-to-tab dapp` is equivalent to `mm switch-to-tab --role dapp`. - Tab roles: `extension`, `notification`, `dapp`, `other`. -### 8. Navigate +### 7. Navigate ```bash mm navigate-home @@ -390,212 +189,50 @@ mm navigate-settings mm navigate https://test-dapp.io ``` -### 9. Take Screenshots +### 8. Screenshots ```bash mm screenshot --name "after-unlock" ``` -For visual validation, capture screenshots before and after meaningful state changes. - -### 10. Cleanup (Always Required) - -```bash -mm cleanup -mm cleanup --shutdown -``` - -## Mock Network Requests - -Use `mm mock-network` to stub browser network requests during an active session. Prefer this over raw CDP when you need deterministic API responses. - -Rules are installed on the active Playwright browser context: - -- Run `mm launch` first. -- Add rules before the UI action that triggers the request. -- Rules are removed by `mm cleanup`; `mm mock-network clear` removes rules and request history without ending the session. -- Unmatched requests on an origin with a mock rule continue unchanged and are recorded as misses. - -Rule shape: - -```json -{ - "id": "token-prices", - "method": "GET", - "url": "https://price.api.metamask.io/v1/**", - "response": { - "status": 200, - "json": { - "ethereum": { - "usd": 1234.56 - } - } - } -} -``` - -Fields: - -| Field | Description | -| ------------------ | --------------------------------------------------------------------------------------------------- | -| `id` | Stable identifier. Adding another rule with the same `id` replaces the previous rule. | -| `method` | HTTP method to match; normalized to uppercase. | -| `url` | Absolute `http`/`https` URL or URL glob. `*` matches within a segment; `**` matches any path suffix. | -| `response.status` | Optional HTTP status; defaults to `200`. | -| `response.json` | JSON response payload. | -| `response.body` | Text response payload. Use either `json` or `body`. | -| `response.headers` | Optional response headers. JSON/text defaults include `access-control-allow-origin: *`. | - -Examples: - -```bash -mm mock-network add '{"id":"token-prices","method":"GET","url":"https://price.api.metamask.io/v1/**","response":{"status":200,"json":{"ethereum":{"usd":1234.56}}}}' - -mm mock-network add '{"routes":[ - {"id":"feature-flags","method":"GET","url":"https://client-config.api.cx.metamask.io/**","response":{"json":{"flags":{}}}}, - {"id":"empty-nfts","method":"POST","url":"https://nft.api.metamask.io/**","response":{"status":200,"json":{"nfts":[]}}} -]}' - -mm mock-network list -mm mock-network requests --limit 20 -mm mock-network clear -``` - -Verification pattern: - -1. Add the rule with `mm mock-network add ...` -2. Trigger the UI flow that makes the request -3. Run `mm mock-network requests --limit 20` -4. Confirm the expected request has `matched: true` and the expected `ruleId` - -In `mm run-steps`, use tool name `mock_network` with the same input shape: - -```bash -mm run-steps '{"steps":[ - {"tool":"mock_network","args":{"action":"add","rule":{"id":"prices","method":"GET","url":"https://price.api.metamask.io/v1/**","response":{"json":{"ok":true}}}}}, - {"tool":"navigate","args":{"screen":"url","url":"https://test-dapp.io"}} -]}' -``` - -## Raw CDP Commands +Capture before and after meaningful state changes for visual validation evidence. -`mm cdp` sends a raw [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) command against the active page. Reach for this **only when structured commands (`mm click`, `mm type`, `mm navigate`, etc.) cannot express what you need** — for example, evaluating JavaScript, enabling network tracking, or inspecting the DOM tree directly. +### 9. Cleanup (Always Required) ```bash -mm cdp Runtime.evaluate '{"expression":"document.title"}' -mm cdp Network.enable -mm cdp DOM.getDocument '{"depth":2}' --timeout 60000 +mm cleanup # stop browser and services +mm cleanup --shutdown # also stop the daemon ``` -Arguments: - -| Argument | Description | -| --------------- | ---------------------------------------------------------------------------- | -| `` | CDP method name (e.g., `Runtime.evaluate`, `DOM.getDocument`, `Network.enable`) | -| `[params-json]` | Optional JSON object with method-specific parameters | -| `--timeout` | Per-command timeout in ms. Default: 30000. Min: 1000. Max: 30000 | - -**Blocked methods** (would destroy the session — returns `MM_CDP_BLOCKED`): - -- `Browser.close` -- `Target.closeTarget` -- `Target.disposeBrowserContext` -- `Browser.crashGpuProcess` +## Sidepanel Mode -**Behavior:** +The extension runs in sidepanel mode by default (`sidepanel.html`). -- Categorized as **mutating** — state-changing CDP calls bypass session tracking. Run `mm describe-screen` afterward to re-sync the a11y ref map and screen state. -- CDP failures (invalid method, malformed params, timeout) return `MM_CDP_FAILED`. -- This is an **escape hatch, not a sandbox**: prefer structured commands whenever they cover your use case. - -**When to reach for `mm cdp`:** - -| Need | Suggested CDP method | -| ----------------------------------------------------- | ------------------------------------------------------------- | -| Read a JS value / `window` property | `Runtime.evaluate` with `{ "expression": "..." }` | -| Inspect / traverse DOM | `DOM.getDocument`, `DOM.querySelector` | -| Capture network traffic | `Network.enable` (then read events via subsequent CDP calls) | -| Inject cookies or storage | `Network.setCookie`, `Storage.setLocalStorage*` | -| Low-level input beyond `mm click` / `mm type` | `Input.dispatchKeyEvent`, `Input.dispatchMouseEvent` | +Key behavior: +- After confirm/reject, the sidepanel stays open and navigates back to home +- `mm wait-for-notification` waits for a confirmation route in the URL hash +- After a confirmation action, use `mm describe-screen` to verify the return to home state +- Confirmation routes: `/connect`, `/confirm-transaction`, `/confirmation`, `/confirm-import-token`, `/confirm-add-suggested-token`, `/confirm-add-suggested-nft` ## Batching with mm run-steps -Use `mm run-steps` for known, deterministic sequences. Use individual commands for discovery, debugging, or when intermediate state changes the next action. - -Important details: - -- `mm run-steps` expects a JSON object with a `steps` key -- Prefer `a11yRef`, `testId`, or `selector` in args -- Use `within` in args to scope a target within a parent element -- Add `batchTimeoutMs` for an overall timeout -- Tool aliases such as `navigate_home` and `navigate-home` are supported +Use for known, deterministic sequences. Use individual commands for discovery or when intermediate state changes the next action. ```bash mm run-steps '{"steps":[ - { "tool": "type", "args": { "testId": "unlock-password", "text": "correct horse battery staple" } }, - { "tool": "click", "args": { "testId": "unlock-submit" } }, - { "tool": "wait_for", "args": { "testId": "account-menu-icon", "timeoutMs": 10000 } }, - { "tool": "get_text", "args": { "testId": "account-balance", "within": { "testId": "account-overview" } } } + {"tool":"type","args":{"testId":"unlock-password","text":"correct horse battery staple"}}, + {"tool":"click","args":{"testId":"unlock-submit"}}, + {"tool":"wait_for","args":{"testId":"account-menu-icon","timeoutMs":10000}} ]}' ``` -Pattern: - -1. Discover with `mm describe-screen` -2. Reuse prior successful steps from `mm knowledge-search` -3. Batch the known sequence with `mm run-steps` -4. Re-verify with `mm describe-screen` - -## Capabilities - -- **Anvil**: Local blockchain on port 8545 -- **Fixture Server**: Wallet state management on port 12345 -- **Contract Seeding**: Deploy test contracts with `mm seed-contract` - -## Error Recovery - -### On Failure - -1. Run `mm describe-screen` -2. Check the current screen: - - `unlock` → enter password and submit - - `home` → continue, but check for modals or blockers - - `onboarding-*` → complete onboarding - - `unknown` → take a screenshot and investigate -3. Query prior runs if needed: - -```bash -mm knowledge-search "send" -mm knowledge-sessions -mm knowledge-last -``` +Details: +- Prefer `testId`, `a11yRef`, or `selector` in args (not `ref`) +- Use `within` in args to scope targeting within a parent element +- Add `batchTimeoutMs` for overall timeout +- Tool aliases like `navigate_home` and `navigate-home` are supported -4. Capture `mm screenshot --name "debug"` for diagnosis - -### Error Codes - -| Code | Meaning | -| ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `MM_SESSION_ALREADY_RUNNING` | Session exists, call `mm cleanup` first | -| `MM_NO_ACTIVE_SESSION` | No session, call `mm launch` first | -| `MM_LAUNCH_FAILED` | Browser launch failed | -| `MM_INVALID_INPUT` | Invalid parameters | -| `MM_TARGET_NOT_FOUND` | Element not found | -| `MM_TAB_NOT_FOUND` | Tab not found | -| `MM_CLICK_FAILED` | Click operation failed (post-find, not a timeout) | -| `MM_CLICK_TIMEOUT` | Click action timed out (element found, click hung) — may have completed in background; run `mm describe-screen` | -| `MM_TYPE_FAILED` | Type operation failed (post-find, not a timeout) | -| `MM_TYPE_TIMEOUT` | `fill()` action timed out; run `mm describe-screen` and retry | -| `MM_GETTEXT_FAILED` | `get-text` failed (non-timeout, e.g. element detached) — re-target | -| `MM_GETTEXT_TIMEOUT` | `textContent()` action timed out | -| `MM_WAIT_TIMEOUT` | Wait timeout exceeded | -| `MM_PAGE_CLOSED` | Browser page closed during interaction — normal after some confirmations | -| `MM_SCREENSHOT_FAILED` | Screenshot capture failed | -| `MM_BATCH_TIMEOUT` | `batchTimeoutMs` deadline exceeded | -| `MM_CONTEXT_SWITCH_BLOCKED` | Cannot switch context during active session | -| `MM_SET_CONTEXT_FAILED` | Context switch failed | -| `MM_CDP_BLOCKED` | CDP method is in the destructive-blocklist (e.g. `Browser.close`) | -| `MM_CDP_FAILED` | CDP command execution failed or timed out | +Pattern: discover with `mm describe-screen` → reuse from `mm knowledge-search` → batch with `mm run-steps` → re-verify with `mm describe-screen`. ## Default Credentials @@ -605,21 +242,17 @@ mm knowledge-last | Chain ID | `1337` | | Balance | 25 ETH | -## Common Failures & Solutions - -| Symptom | Likely Cause | Solution | -| ------------------------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `MM_SESSION_ALREADY_RUNNING` | Previous session not cleaned | Call `mm cleanup` first | -| `MM_NO_ACTIVE_SESSION` | No browser running | Call `mm launch` first | -| Extension not loading | Extension not built | Run `yarn build:test:webpack` then retry `mm launch` | -| `EADDRINUSE` port error | Orphan processes | Check `.mm-server` for the active daemon/sub-service ports, then kill the specific orphaned process on that port | -| `MM_TARGET_NOT_FOUND` | Element not visible | Use `mm describe-screen` to check state | -| `MM_WAIT_TIMEOUT` | Slow environment or UI delay | Increase `--timeout`, inspect screenshot | -| `MM_CLICK_TIMEOUT` / `MM_TYPE_TIMEOUT` | Element found but action hung (side effect) | Run `mm describe-screen` first to verify it didn't already complete; retry with larger `--timeout` | -| `MM_GETTEXT_TIMEOUT` / `MM_GETTEXT_FAILED` | Element detached or content not ready | Run `mm describe-screen` and re-target; bump `--timeout` if the value is async | -| `MM_PAGE_CLOSED` | Confirmation popup auto-closed, dapp tab gone | Expected after some confirmations — run `mm describe-screen` to find the new active page | -| `MM_CDP_BLOCKED` | Attempted a destructive CDP method | Use a non-blocked CDP method; see the blocked list in the Raw CDP Commands section | -| `MM_CDP_FAILED` | Invalid CDP method / params, or CDP timed out | Check method name & params shape; retry with a larger `--timeout` (max 30000) | -| `MM_CONTEXT_SWITCH_BLOCKED` | Switching during active session | Call `mm cleanup` before `mm set-context` | -| Fixtures not available | Running in prod context | Switch to e2e: `mm set-context e2e` | -| Stale a11yRefs after navigate | Refs not refreshed | Call `mm describe-screen` to get fresh refs | +## Capabilities + +- **Anvil**: Local blockchain on port 8545 +- **Fixture Server**: Wallet state management +- **Contract Seeding**: Deploy test contracts with `mm seed-contract` + +## Reference Guides + +Load these on demand — not required for standard visual testing: + +- **[CLI Command Reference](references/cli-reference.md)** — full command tables for all `mm` commands +- **[State Manipulation](references/state-manipulation.md)** — read/write Redux and persisted state via CDP when fixtures don't cover your scenario +- **[Mock Network](references/mock-network.md)** — stub network requests for deterministic API responses +- **[Error Recovery](references/error-recovery.md)** — error codes, common failures, and troubleshooting