Skip to content

feat: Keyboard-first order entry UX: auto-focus, auto-select, Tab navigation & inline validation#41949

Merged
abretonc7s merged 33 commits into
mainfrom
feat/tat-2802-keyboard-order-entry-ux
May 4, 2026
Merged

feat: Keyboard-first order entry UX: auto-focus, auto-select, Tab navigation & inline validation#41949
abretonc7s merged 33 commits into
mainfrom
feat/tat-2802-keyboard-order-entry-ux

Conversation

@abretonc7s
Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s commented Apr 20, 2026

Description

Delivers the keyboard-first order entry UX across every perps order surface:

  • Auto-focus on the primary input when each screen mounts (size on Trade, limit price on Limit, margin on Add-margin, TP trigger on TP/SL, Close button on Position close).
  • Auto-select the existing value on focus so typing replaces it immediately. Applies to the size input, the leverage input, and every modal input.
  • Disabled submit + Order size must be at least $10 button copy until the amount clears the minimum.
  • Inline validation is real-time — no deferred "min order" errors on submit.
  • Internal refocus when the user toggles between market and limit order types.
  • Pressing Enter from any primary input submits the form when the submit button is enabled. The Trade/Limit page renders as a native <form> with an onSubmit handler and a type="submit" button, so the browser's built-in Enter-to-submit behavior drives it (disabled buttons naturally block submission). Add margin and TP/SL modals wire an Enter keydown directly on their numeric inputs; both paths guard against Shift+Enter and IME composition.
  • Leverage input: ArrowUp/ArrowDown now step leverage by 1 (clamped to minLeverage..maxLeverage), matching how native numeric inputs behave.

Submit flow is wired so the in-progress toast transitions cleanly to the success state: the perps RPC is awaited before navigating off the order-entry page, and the in-progress toast is emitted once from the page itself rather than re-emitted via route state on the market-detail page. The shared background RPC client (metaRPCClientFactory) also tolerates error responses with a missing or empty message field so a pending request is always settled instead of orphaned.

Changelog

CHANGELOG entry: Improved perps order entry with auto-focus, auto-select-on-focus, real-time minimum-order-size validation, and Enter-to-submit keyboard shortcut.

Related issues

Fixes: TAT-2802

Manual testing steps

  1. Open the extension and navigate to the Perps tab.
  2. Tap any market (e.g. ETH) → press Long. Confirm the $ size input is already focused and the submit button reads Order size must be at least $10 and is disabled.
  3. Type 5 — submit stays disabled and still reads Order size must be at least $10.
  4. Type 15 — submit enables and reads Open long ETH.
  5. Blur and re-focus the size input — existing value should be fully selected.
  6. Switch the order type to Limit — the limit price input takes focus automatically.
  7. Open an existing position, tap Edit margin → margin input is focused. Open TP/SL → TP trigger input is focused. Open Close position → Close button is focused.
  8. From any primary input (size, limit price, margin, TP/SL), press Enter with a valid value — the submit button fires. Press Enter while disabled (empty / below-min / invalid) — nothing happens. Shift+Enter and IME-composition Enter are ignored.
  9. Click into the leverage input — its current value should be fully selected so typing replaces it. Press ArrowUp to step leverage by +1 (clamped at maxLeverage); press ArrowDown to step by −1 (clamped at minLeverage).
  10. Submit path does not hang. Press Enter (or click the submit button) on a Long ETH $15 market order — the in-progress toast should flip to "Order submitted" once Hyperliquid confirms, and the UI lands on the market detail page with the new position visible. Repeat a few times to confirm no stuck "Submitting your trade" toast across repeated submits.

Screenshots/Recordings

Screenshots/close Position ETH 100pct 1777559535679
Screenshots/close Position ETH 100pct 1777559535679
caption confidence: LOW — generic filename — no state-specific suffix

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Validation Recipe

Task-local bundle convention: one thin orchestrator recipe + nine AC-focused self-runnable subflows, invoked via explicit bundle/<name> refs. Live runs on CDP 6662:

  • Main recipe end-to-end: 9/9 PASS (all subflows green, guarded teardown closes the ETH position opened by AC7 positive).

  • AC7 positive standalone (full Enter path): 17/17 PASS — wait-position-in-state (state-based wait via perpsGetPositions) typically resolves in ~3s.

  • AC7 positive standalone (idempotent skip branch, pre-existing position): 6/6 PASS — branch-position short-circuits to verify-position-dom.

  • Main recipe: temp/.task/feat/tat-2802-0420-210839/artifacts/recipe.json — pure orchestrator, 10 nodes (9 call nodes to bundle/<name> subflows + done + one-step teardown calling perps/close-position).

  • Subflow bundle: temp/.task/feat/tat-2802-0420-210839/artifacts/recipe-flows/ — 9 flat subflow files, each self-runnable and generically parametrized (inputs: symbol/side/sideLabel/amount with sensible defaults). Each delegates the entire form-open prelude to a single { call: perps/open-order-form, params: { symbol, side } } step — no duplicated setup nodes. Reviewers can run any subflow in isolation via node temp/agentic/recipes/validate-recipe.js --recipe temp/.task/feat/tat-2802-0420-210839/artifacts/recipe-flows/<ac>.json --cdp-port 6662 --skip-manual or validate any (symbol, side) combination by passing --input symbol=BTC --input side=short --input sideLabel=Short.

  • New shared canonical flow: temp/agentic/recipes/domains/perps/flows/open-order-form.json — extracted so every subflow (and future callers) can open a fresh {side} order form on {symbol} in one line. Composition: perps/ensure-perps-networkperps/prime-perps-stateperps/navigate-to-market-detailperps/close-positionwait-{side}-ctapress-{side}wait-order-form.

AC-to-subflow map
AC Subflow ref Inline nodes Purpose
AC1 bundle/ac1-size-autofocus 2 Size input auto-focus + focused testid proof
AC1 (limit) bundle/ac1-limit-autofocus 5 Switch to limit, wait mount, autofocus proof + screenshot
AC2 bundle/ac2-select-on-focus 3 Blur + refocus + poll selectionStart==0 && selectionEnd==length
AC5 bundle/ac5-below-min 2 Type 5, assert submit stays disabled with min-order copy
AC5/AC6 bundle/ac5-valid-amount 3 Type {{amount}}, assert Open {{sideLabel}} {{symbol}} + enabled + screenshot (parametric — defaults validate Open long ETH).
AC6 bundle/ac6-empty-button 2 Empty-state Order size must be at least $10 disabled copy + screenshot
AC7 (−) bundle/ac7-enter-blocked-assert self-runnable Primes perps, opens fresh empty order form, focuses amount input, presses Enter, asserts formMounted + hashStillNew + buttonStillDisabled. Canonical standalone covers the empty-form case.
AC7 (+) bundle/ac7-enter-submit-success self-runnable Resilient idempotent + parametric. pre_conditions + balance gate via perpsGetAccountState + branch-position (if {{symbol}} position already exists → verify and pass; else full submit path). Switch to market, type {{amount}}, focus, press Enter, wait market-detail + state-based wait on perpsGetPositions + position-live + direction regex /{{sideLabel}}\s+\d+(?:\.\d+)?x/, assert route locked to #/perps/market/{{symbol}}, form unmounted, screenshot. Mirrors canonical perps/open-long-position.
Leverage (added during PR) bundle/leverage-keyboard self-runnable Capture initial leverage; focus the leverage input (blur+refocus); poll selectionStart==0 && selectionEnd==length; press ArrowUp; assert value increments (or clamps at maxLeverage); refocus; press ArrowDown; assert value decrements (or clamps at minLeverage). State-agnostic about the initial value.

Each subflow delegates its entire form-open prelude to the shared canonical flow perps/open-order-form — which itself composes perps/ensure-perps-network + perps/prime-perps-state + perps/navigate-to-market-detail + perps/close-position + parameterized CTA press. No subflow repeats plumbing. Main recipe teardown simply calls perps/close-position, whose own internal branch makes it a no-op when no position is visible. AC7 positive proof is UI-success-state driven AND backend-state driven (no network probe): redirect + state-wait on perpsGetPositions + live position + direction regex + route lock.

Reproduce locally — copy-paste recipe bundle (click to expand)

The recipe + 9 subflows below are inlined verbatim. They live under temp/ in the working slot (gitignored), so they don't ship via the PR diff — copy them into your local checkout to run.

Quickstart

# Set your slot/repo paths
REPO=/path/to/your/metamask-extension
mkdir -p "$REPO/temp/tasks/fix/41949-repro/artifacts/recipe-flows"

# 1. Save each JSON block below into the matching file path (e.g. recipe.json
#    → $REPO/temp/tasks/fix/41949-repro/artifacts/recipe.json)
# 2. Make sure your slot is prepared with the latest branch tip:
bash /path/to/farmslot/scripts/prepare-slot.sh \
  --branch feat/tat-2802-keyboard-order-entry-ux \
  --var watch=off <your-slot-id>

# 3. Run the full bundle
cd "$REPO/temp/recipes"
node validate-recipe.js \
  --recipe "$REPO/temp/tasks/fix/41949-repro/artifacts/recipe.json" \
  --cdp-port <your-cdp-port> --skip-manual

# 4. (Optional) Run a single subflow in isolation
node validate-recipe.js \
  --recipe "$REPO/temp/tasks/fix/41949-repro/artifacts/recipe-flows/ac7-enter-submit-success.json" \
  --cdp-port <your-cdp-port> --skip-manual

# 5. (Optional) Run the narrated demo recipe (single form-open, deliberate pauses,
#    auto-screenshots per AC) for video capture / live-walkthrough
node validate-recipe.js \
  --recipe "$REPO/temp/tasks/fix/41949-repro/artifacts/recipe-demo.json" \
  --cdp-port <your-cdp-port> --skip-manual --slow 800

Each subflow accepts --param symbol=BTC --param side=short (and sideLabel=Short) for free re-parameterization — the validator only ships ETH-long defaults.

Files

artifacts/recipe.json
{
  "title": "TAT-2802 — Keyboard-first order entry UX (composable bundle)",
  "description": "Orchestrator recipe. Each AC subflow under recipe-flows/ is fully self-contained (pre_conditions + setup that primes perps, navigates, closes any lingering ETH position, opens a fresh order form). The main recipe just calls each subflow in sequence via bundle/<name> refs and runs a safety teardown to close any ETH position left behind.",
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "teardown": [
        {
          "id": "teardown-close-eth",
          "action": "call",
          "ref": "perps/close-position",
          "params": { "symbol": "ETH", "percent": "100" }
        }
      ],
      "entry": "ac1-size-autofocus",
      "nodes": {
        "ac1-size-autofocus": {
          "action": "call",
          "ref": "bundle/ac1-size-autofocus",
          "description": "AC1 — size input auto-focus + focused testid proof (self-contained)",
          "next": "ac6-empty-button"
        },
        "ac6-empty-button": {
          "action": "call",
          "ref": "bundle/ac6-empty-button",
          "description": "AC6 — 'Order size must be at least $10' disabled button copy while size empty + screenshot (self-contained)",
          "next": "ac7-neg-empty"
        },
        "ac7-neg-empty": {
          "action": "call",
          "ref": "bundle/ac7-enter-blocked-assert",
          "description": "AC7 negative — Enter blocked while button disabled (empty case, self-contained)",
          "next": "ac5-below-min"
        },
        "ac5-below-min": {
          "action": "call",
          "ref": "bundle/ac5-below-min",
          "description": "AC5 — typing $5 keeps button disabled with min-order copy (self-contained)",
          "next": "ac5-valid-amount"
        },
        "ac5-valid-amount": {
          "action": "call",
          "ref": "bundle/ac5-valid-amount",
          "description": "AC5/AC6 — typing $15 enables button with 'Open long ETH' copy + screenshot (self-contained)",
          "next": "ac2-select-on-focus"
        },
        "ac2-select-on-focus": {
          "action": "call",
          "ref": "bundle/ac2-select-on-focus",
          "description": "AC2 — blur + refocus selects existing value in full (self-contained, types $15 first)",
          "next": "leverage-keyboard"
        },
        "leverage-keyboard": {
          "action": "call",
          "ref": "bundle/leverage-keyboard",
          "description": "Leverage input — select-on-focus + ArrowUp/ArrowDown step (self-contained)",
          "next": "ac1-limit-autofocus"
        },
        "ac1-limit-autofocus": {
          "action": "call",
          "ref": "bundle/ac1-limit-autofocus",
          "description": "AC1 (limit variant) — switching to limit order type auto-focuses limit-price-input + screenshot (self-contained)",
          "next": "ac7-enter-submit-success"
        },
        "ac7-enter-submit-success": {
          "action": "call",
          "ref": "bundle/ac7-enter-submit-success",
          "description": "AC7 positive — resilient idempotent. Pre-conditions + balance gate + branch-position: if ETH position already exists from a prior run, treat as prior AC7 proof and skip resubmission; otherwise press Long CTA → open order form → switch to market → type $15 → focus size input → press Enter → wait market-detail → state-based wait on perpsGetPositions until ETH position appears → verify position DOM + direction regex + route lock + screenshot",
          "next": "done"
        },
        "done": {
          "action": "end",
          "status": "pass"
        }
      }
    }
  }
}
artifacts/recipe-flows/ac1-size-autofocus.json
{
  "title": "AC1 — Size input auto-focus on order form mount ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}} via perps/open-order-form, then asserts document.activeElement is inside amount-input-field (market mode default).",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "wait-autofocus",
      "nodes": {
        "wait-autofocus": {
          "action": "wait_for",
          "expression": "!!document.activeElement && document.activeElement.closest('[data-testid=\"amount-input-field\"]') !== null",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 3000,
          "next": "autofocus-proof"
        },
        "autofocus-proof": {
          "action": "eval_sync",
          "expression": "JSON.stringify({ focusedTestId: document.activeElement?.closest('[data-testid]')?.getAttribute('data-testid'), tag: document.activeElement?.tagName })",
          "assert": { "operator": "eq", "field": "focusedTestId", "value": "amount-input-field" },
          "save_as": "ac1_market",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac1-limit-autofocus.json
{
  "title": "AC1 (limit variant) — Switching to limit order type auto-focuses limit price input ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, presses the limit toggle, waits for limit-price-input to mount, asserts focus lands inside it, screenshots evidence.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "switch-limit",
      "nodes": {
        "switch-limit": {
          "action": "press",
          "test_id": "order-type-limit",
          "next": "wait-limit-mount"
        },
        "wait-limit-mount": {
          "action": "wait_for",
          "test_id": "limit-price-input",
          "timeout_ms": 5000,
          "next": "wait-limit-autofocus"
        },
        "wait-limit-autofocus": {
          "action": "wait_for",
          "expression": "!!document.activeElement && document.activeElement.closest('[data-testid=\"limit-price-input\"]') !== null",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 3000,
          "next": "limit-autofocus-proof"
        },
        "limit-autofocus-proof": {
          "action": "eval_sync",
          "expression": "JSON.stringify({ focusedTestId: document.activeElement?.closest('[data-testid]')?.getAttribute('data-testid') })",
          "assert": { "operator": "eq", "field": "focusedTestId", "value": "limit-price-input" },
          "save_as": "ac1_limit",
          "next": "screenshot-limit"
        },
        "screenshot-limit": {
          "action": "screenshot",
          "filename": "evidence-limit-autofocus",
          "note": "AC1 (limit variant) — switching to limit order type auto-focuses the limit price input",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac2-select-on-focus.json
{
  "title": "AC2 — Existing value auto-selected on input refocus ({{side}} {{symbol}} ${{amount}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, types {{amount}} so the input has a value, then blurs + refocuses the amount-input-field inner input and polls until selectionStart/selectionEnd span the full value length.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" },
    "amount": { "type": "string", "default": "15", "description": "USD notional pre-typed so the input has a value for the select-on-focus proof" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "prelude-type-value",
      "nodes": {
        "prelude-type-value": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "{{amount}}",
          "next": "blur-input"
        },
        "blur-input": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');if(!i)return JSON.stringify({blurred:false});i.blur();return JSON.stringify({blurred:document.activeElement!==i});})()",
          "assert": { "operator": "eq", "field": "blurred", "value": true },
          "next": "refocus-input"
        },
        "refocus-input": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i});})()",
          "assert": { "operator": "eq", "field": "focused", "value": true },
          "next": "select-on-focus"
        },
        "select-on-focus": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');var len=(i&&i.value||'').length;return JSON.stringify({selected: !!i && len>0 && i.selectionStart===0 && i.selectionEnd===len, len:len, start:i&&i.selectionStart, end:i&&i.selectionEnd});})()",
          "assert": { "operator": "eq", "field": "selected", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac5-below-min.json
{
  "title": "AC5 — Below-min amount keeps submit disabled with min-order copy ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, types {{amount}} into the amount-input-field and asserts submit-order-button still reads 'Order size must be at least $10' and is disabled.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" },
    "amount": { "type": "string", "default": "5", "description": "Below-min USD notional" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "type-below-min",
      "nodes": {
        "type-below-min": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "{{amount}}",
          "next": "assert-still-disabled"
        },
        "assert-still-disabled": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text: (b?.textContent||'').trim(), disabled: !!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Order size must be at least $10" },
            { "field": "disabled", "operator": "eq", "value": true }
          ]},
          "save_as": "ac5_below_min",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac5-valid-amount.json
{
  "title": "AC5/AC6 — Valid amount enables submit with action-specific copy ({{side}} {{symbol}} ${{amount}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, types {{amount}} into the amount-input-field and asserts submit-order-button enables and reads the expected side+symbol copy. Captures evidence screenshot. expectedCopy is derived from side+symbol so BTC short still asserts correctly.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" },
    "amount": { "type": "string", "default": "15", "description": "Valid USD notional at or above the minimum" },
    "sideLabel": { "type": "string", "default": "Long", "description": "Title-cased side label retained for backwards compatibility; not used in the submit-button assertion (locale is sentence-case post #41881)" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "type-valid",
      "nodes": {
        "type-valid": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "{{amount}}",
          "next": "assert-enabled"
        },
        "assert-enabled": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text: (b?.textContent||'').trim(), disabled: !!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Open {{side}} {{symbol}}" },
            { "field": "disabled", "operator": "eq", "value": false }
          ]},
          "save_as": "ac5_enabled",
          "next": "screenshot-valid"
        },
        "screenshot-valid": {
          "action": "screenshot",
          "filename": "evidence-valid-amount",
          "note": "AC5 — submit button reads 'Open {{side}} {{symbol}}' (sentence-case per locale perpsOpenLong/Short) and is enabled after a valid amount (>= $10)",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac6-empty-button.json
{
  "title": "AC6 — 'Order size must be at least $10' disabled button copy when size empty ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, asserts submit-order-button textContent equals 'Order size must be at least $10' and the button is disabled while the amount input is empty. Captures evidence screenshot.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "assert-empty-copy",
      "nodes": {
        "assert-empty-copy": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text: (b?.textContent||'').trim(), disabled: !!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Order size must be at least $10" },
            { "field": "disabled", "operator": "eq", "value": true }
          ]},
          "save_as": "ac6_empty",
          "next": "screenshot-min-order-copy"
        },
        "screenshot-min-order-copy": {
          "action": "screenshot",
          "filename": "evidence-min-order-empty",
          "note": "AC6 — submit button shows 'Order size must be at least $10' and is disabled while size is empty",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac7-enter-blocked-assert.json
{
  "title": "AC7 (negative) — Enter is blocked while submit button is disabled ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}} (empty amount), focuses the amount input, dispatches Enter, asserts the order form is still mounted, route still carries mode=new, and the submit button is still disabled.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "focus-input",
      "nodes": {
        "focus-input": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i,preHash:window.location.hash});})()",
          "assert": { "operator": "eq", "field": "focused", "value": true },
          "save_as": "ac7_neg_pre",
          "next": "press-enter"
        },
        "press-enter": {
          "action": "key_press",
          "key": "Enter",
          "next": "assert-blocked"
        },
        "assert-blocked": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({formMounted:!!document.querySelector('[data-testid=\"perps-order-entry-page\"]'),hashStillNew:window.location.hash.indexOf('mode=new')!==-1,buttonStillDisabled:!!b?.disabled});})()",
          "assert": { "all": [
            { "field": "formMounted", "operator": "eq", "value": true },
            { "field": "hashStillNew", "operator": "eq", "value": true },
            { "field": "buttonStillDisabled", "operator": "eq", "value": true }
          ]},
          "save_as": "ac7_neg_proof",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/ac7-enter-submit-success.json
{
  "title": "AC7 (positive) — Enter key from focused amount input submits order and lands on market detail ({{side}} {{symbol}} ${{amount}})",
  "description": "Resilient idempotent subflow proving keyboard Enter submits the perps order form. Mirrors canonical `perps/open-long-position` pattern: pre-conditions gate, balance check via perpsGetAccountState, idempotent branch-position (if a {{symbol}} position already exists from a prior run treat as prior AC7 proof and skip resubmission), otherwise the setup opens a fresh {{side}} order form on {{symbol}} → switch to market → type {{amount}} → focus size input → press Enter → wait for market-detail redirect → state-wait on perpsGetPositions until the {{symbol}} position appears → verify on-screen position + direction regex + route lock. Self-contained: run standalone via `validate-recipe.js --recipe artifacts/recipe-flows/ac7-enter-submit-success.json`.",
  "inputs": {
    "symbol": {
      "type": "string",
      "default": "ETH",
      "description": "Market symbol"
    },
    "side": {
      "type": "string",
      "default": "long",
      "description": "Trade side: long or short"
    },
    "sideLabel": {
      "type": "string",
      "default": "Long",
      "description": "Title-cased side label used in the direction regex and assertion messages"
    },
    "amount": {
      "type": "string",
      "default": "15",
      "description": "USD notional amount — must clear $10 minimum for AC7"
    }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled",
        "perps.ready_to_trade",
        "perps.sufficient_balance"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "gate-check-balance",
      "nodes": {
        "gate-check-balance": {
          "action": "eval_async",
          "expression": "(async()=>{var r=await stateHooks.submitRequestToBackground('perpsGetAccountState',[]);var bal=parseFloat(r?.totalBalance??'0');return JSON.stringify({ok:bal>0,bal:bal,reason:bal>0?'ok':'Insufficient perps balance ($'+bal+') — deposit or use funded testnet account'})})()",
          "assert": { "operator": "eq", "field": "ok", "value": true },
          "save_as": "ac7_balance_gate",
          "next": "check-existing-position"
        },
        "check-existing-position": {
          "action": "eval_async",
          "expression": "(async()=>{var ps=await stateHooks.submitRequestToBackground('perpsGetPositions',[]);ps=Array.isArray(ps)?ps:[];var hit=ps.find(function(p){return (p?.symbol||p?.coin||p?.asset||'').toString().toUpperCase()==='{{symbol}}'});return JSON.stringify({ok:true,hasPosition:!!hit,positionsCount:ps.length,symbol:hit?.symbol||hit?.coin||hit?.asset||null})})()",
          "assert": { "operator": "eq", "field": "ok", "value": true },
          "save_as": "ac7_pre_position",
          "next": "branch-position"
        },
        "branch-position": {
          "action": "switch",
          "cases": [
            {
              "label": "position already exists — treat prior AC7 submit as proof, skip duplicate order",
              "when": { "field": "vars.ac7_pre_position.hasPosition", "operator": "truthy" },
              "next": "verify-position-dom"
            }
          ],
          "default": "switch-market"
        },
        "switch-market": {
          "action": "press",
          "test_id": "order-type-market",
          "next": "wait-market-mount"
        },
        "wait-market-mount": {
          "action": "wait_for",
          "test_id": "amount-input-field",
          "timeout_ms": 5000,
          "next": "capture-form-wiring"
        },
        "capture-form-wiring": {
          "action": "eval_sync",
          "expression": "(function(){var f=document.querySelector('[data-testid=\"perps-order-entry-page\"]');var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({formTag:f&&f.tagName,buttonType:b&&b.type,buttonForm:b&&b.form&&b.form===f,preHash:window.location.hash});})()",
          "assert": { "all": [
            { "field": "formTag", "operator": "eq", "value": "FORM" },
            { "field": "buttonType", "operator": "eq", "value": "submit" },
            { "field": "buttonForm", "operator": "eq", "value": true }
          ]},
          "save_as": "ac7_wiring",
          "next": "type-valid"
        },
        "type-valid": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "{{amount}}",
          "next": "focus-input"
        },
        "focus-input": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i,preHash:window.location.hash});})()",
          "assert": { "operator": "eq", "field": "focused", "value": true },
          "save_as": "ac7_focus",
          "next": "press-enter"
        },
        "press-enter": {
          "action": "key_press",
          "key": "Enter",
          "next": "wait-market-detail"
        },
        "wait-market-detail": {
          "action": "wait_for",
          "test_id": "perps-market-detail-page",
          "timeout_ms": 15000,
          "next": "wait-position-in-state"
        },
        "wait-position-in-state": {
          "action": "wait_for",
          "expression": "(async()=>{var ps=await stateHooks.submitRequestToBackground('perpsGetPositions',[]);ps=Array.isArray(ps)?ps:[];return ps.some(function(p){return (p?.symbol||p?.coin||p?.asset||'').toString().toUpperCase()==='{{symbol}}'})})()",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 45000,
          "poll_ms": 750,
          "next": "verify-position-dom"
        },
        "verify-position-dom": {
          "action": "wait_for",
          "test_id": "perps-position-cta-buttons",
          "timeout_ms": 15000,
          "next": "wait-position-direction"
        },
        "wait-position-direction": {
          "action": "wait_for",
          "expression": "(function(){var dir=document.querySelector('[data-testid=\"perps-position-leverage\"]');return !!dir && /{{sideLabel}}\\s+\\d+(?:\\.\\d+)?x/.test((dir.textContent||'').trim());})()",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 15000,
          "poll_ms": 500,
          "next": "enter-submit-proof"
        },
        "enter-submit-proof": {
          "action": "eval_sync",
          "expression": "(function(){var dirEl=document.querySelector('[data-testid=\"perps-position-leverage\"]');var dirTxt=(dirEl?.textContent||'').trim();return JSON.stringify({leftOrderForm:!document.querySelector('[data-testid=\"perps-order-entry-page\"]'),onMarketDetail:!!document.querySelector('[data-testid=\"perps-market-detail-page\"]'),positionLive:!!document.querySelector('[data-testid=\"perps-position-cta-buttons\"]'),directionMatches:/{{sideLabel}}\\s+\\d+(?:\\.\\d+)?x/.test(dirTxt),onRoute:window.location.hash==='#/perps/market/{{symbol}}',directionText:dirTxt,postHash:window.location.hash});})()",
          "assert": { "all": [
            { "field": "leftOrderForm", "operator": "eq", "value": true },
            { "field": "onMarketDetail", "operator": "eq", "value": true },
            { "field": "positionLive", "operator": "eq", "value": true },
            { "field": "directionMatches", "operator": "eq", "value": true },
            { "field": "onRoute", "operator": "eq", "value": true }
          ]},
          "save_as": "ac7_redirect",
          "next": "screenshot-post-submit"
        },
        "screenshot-post-submit": {
          "action": "screenshot",
          "filename": "evidence-enter-submit-redirect",
          "note": "AC7 — Enter from focused amount input (${{amount}}) dispatches form submit, order is placed, page redirects to market detail, and the new {{symbol}} position is visible. Idempotent: if a position already existed on entry the subflow treated that as prior AC7 proof and skipped resubmission.",
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-flows/leverage-keyboard.json
{
  "title": "Leverage input — select-on-focus + ArrowUp/ArrowDown step ({{side}} {{symbol}})",
  "description": "Self-runnable. Opens a fresh {{side}} order form on {{symbol}}, then proves the leverage-input (1) auto-selects its current value on focus so typing replaces it, (2) steps up by 1 on ArrowUp, (3) steps down by 1 on ArrowDown. State-agnostic about the initial leverage value.",
  "inputs": {
    "symbol": { "type": "string", "default": "ETH", "description": "Market symbol" },
    "side": { "type": "string", "default": "long", "description": "Trade side: long or short" }
  },
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "setup": [
        { "id": "open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "{{symbol}}", "side": "{{side}}" } }
      ],
      "entry": "capture-initial",
      "nodes": {
        "capture-initial": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');return JSON.stringify({hasInput:!!i,initial:i?parseInt(i.value,10):null});})()",
          "assert": { "all": [
            { "field": "hasInput", "operator": "eq", "value": true },
            { "field": "initial", "operator": "gt", "value": 0 }
          ]},
          "save_as": "lev_initial",
          "next": "focus-leverage"
        },
        "focus-leverage": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');if(!i)return JSON.stringify({focused:false});i.blur();i.focus();return JSON.stringify({focused:document.activeElement===i});})()",
          "assert": { "operator": "eq", "field": "focused", "value": true },
          "next": "assert-select-on-focus"
        },
        "assert-select-on-focus": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');var len=(i&&i.value||'').length;return JSON.stringify({selected:!!i && len>0 && i.selectionStart===0 && i.selectionEnd===len, len:len, start:i&&i.selectionStart, end:i&&i.selectionEnd});})()",
          "assert": { "operator": "eq", "field": "selected", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "press-arrow-up"
        },
        "press-arrow-up": {
          "action": "key_press",
          "key": "ArrowUp",
          "next": "assert-arrow-up"
        },
        "assert-arrow-up": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');var cur=i?parseInt(i.value,10):NaN;return JSON.stringify({current:cur,matches:cur==={{vars.lev_initial.initial}}+1 || cur==={{vars.lev_initial.initial}}});})()",
          "assert": { "operator": "eq", "field": "matches", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "save_as": "lev_after_up",
          "next": "refocus-leverage"
        },
        "refocus-leverage": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i,value:parseInt(i.value,10)});})()",
          "assert": { "operator": "eq", "field": "focused", "value": true },
          "save_as": "lev_pre_down",
          "next": "press-arrow-down"
        },
        "press-arrow-down": {
          "action": "key_press",
          "key": "ArrowDown",
          "next": "assert-arrow-down"
        },
        "assert-arrow-down": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');var cur=i?parseInt(i.value,10):NaN;return JSON.stringify({current:cur,matches:cur==={{vars.lev_pre_down.value}}-1 || cur==={{vars.lev_pre_down.value}}});})()",
          "assert": { "operator": "eq", "field": "matches", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "done"
        },
        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}
artifacts/recipe-demo.json
{
  "title": "TAT-2802 — Keyboard-first order entry UX (NARRATED DEMO RECIPE)",
  "description": "Single-pass demo flow for video capture. Opens one Long ETH order form, then walks through every AC inline with deliberate pauses (`wait`) so a human reviewer can read the screen between steps. Each AC ends with an evidence screenshot. Run with `--slow 800` for extra breathing room. NOT a CI gate — the validation bundle (`recipe.json` + `recipe-flows/`) is the authoritative pass/fail recipe.",
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled",
        "perps.ready_to_trade",
        "perps.sufficient_balance"
      ],
      "setup": [
        { "id": "setup-prime", "action": "call", "ref": "perps/prime-perps-state" },
        { "id": "setup-ensure-net", "action": "call", "ref": "perps/ensure-perps-network" },
        { "id": "setup-open-form", "action": "call", "ref": "perps/open-order-form", "params": { "symbol": "ETH", "side": "long" } },
        { "id": "setup-pause-intro", "action": "wait", "ms": 2000 }
      ],
      "entry": "ac1-size-autofocus-wait",
      "nodes": {

        "ac1-size-autofocus-wait": {
          "action": "wait_for",
          "expression": "!!document.activeElement && document.activeElement.closest('[data-testid=\"amount-input-field\"]') !== null",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 3000,
          "next": "ac1-size-screenshot"
        },
        "ac1-size-screenshot": {
          "action": "screenshot",
          "filename": "demo-01-size-autofocus",
          "note": "AC1 — size input is auto-focused on order-form mount (caret visible inside amount-input-field)",
          "next": "ac1-size-pause"
        },
        "ac1-size-pause": { "action": "wait", "ms": 1500, "next": "ac6-empty-button-eval" },

        "ac6-empty-button-eval": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text:(b?.textContent||'').trim(), disabled:!!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Order size must be at least $10" },
            { "field": "disabled", "operator": "eq", "value": true }
          ]},
          "next": "ac6-empty-screenshot"
        },
        "ac6-empty-screenshot": {
          "action": "screenshot",
          "filename": "demo-02-empty-min-order-copy",
          "note": "AC6 — empty form: submit reads 'Order size must be at least $10' and is disabled",
          "next": "ac6-empty-pause"
        },
        "ac6-empty-pause": { "action": "wait", "ms": 1500, "next": "ac5-below-type" },

        "ac5-below-type": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "5",
          "next": "ac5-below-eval"
        },
        "ac5-below-eval": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text:(b?.textContent||'').trim(), disabled:!!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Order size must be at least $10" },
            { "field": "disabled", "operator": "eq", "value": true }
          ]},
          "next": "ac5-below-screenshot"
        },
        "ac5-below-screenshot": {
          "action": "screenshot",
          "filename": "demo-03-below-min-disabled",
          "note": "AC5 — typing $5 keeps submit disabled with the same min-order copy (real-time validation)",
          "next": "ac5-below-pause"
        },
        "ac5-below-pause": { "action": "wait", "ms": 1500, "next": "ac5-valid-type" },

        "ac5-valid-type": {
          "action": "set_input",
          "test_id": "amount-input-field",
          "value": "15",
          "next": "ac5-valid-eval"
        },
        "ac5-valid-eval": {
          "action": "eval_sync",
          "expression": "(function(){var b=document.querySelector('[data-testid=\"submit-order-button\"]');return JSON.stringify({ text:(b?.textContent||'').trim(), disabled:!!b?.disabled });})()",
          "assert": { "all": [
            { "field": "text", "operator": "eq", "value": "Open long ETH" },
            { "field": "disabled", "operator": "eq", "value": false }
          ]},
          "next": "ac5-valid-screenshot"
        },
        "ac5-valid-screenshot": {
          "action": "screenshot",
          "filename": "demo-04-valid-amount-enabled",
          "note": "AC5/AC6 — typing $15 enables submit, copy swaps to 'Open long ETH'",
          "next": "ac5-valid-pause"
        },
        "ac5-valid-pause": { "action": "wait", "ms": 1500, "next": "ac2-blur" },

        "ac2-blur": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');i&&i.blur();return JSON.stringify({blurred:document.activeElement!==i});})()",
          "assert": { "field": "blurred", "operator": "eq", "value": true },
          "next": "ac2-blur-pause"
        },
        "ac2-blur-pause": { "action": "wait", "ms": 800, "next": "ac2-refocus" },
        "ac2-refocus": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i});})()",
          "assert": { "field": "focused", "operator": "eq", "value": true },
          "next": "ac2-assert-select"
        },
        "ac2-assert-select": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"amount-input-field\"] input');var len=(i&&i.value||'').length;return JSON.stringify({selected:!!i&&len>0&&i.selectionStart===0&&i.selectionEnd===len,len:len});})()",
          "assert": { "field": "selected", "operator": "eq", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "ac2-screenshot"
        },
        "ac2-screenshot": {
          "action": "screenshot",
          "filename": "demo-05-select-on-refocus",
          "note": "AC2 — blur & refocus selects the existing $15 in full so the next keystroke replaces it",
          "next": "ac2-pause"
        },
        "ac2-pause": { "action": "wait", "ms": 1500, "next": "ac1-limit-switch" },

        "ac1-limit-switch": {
          "action": "press",
          "test_id": "order-type-limit",
          "next": "ac1-limit-mount"
        },
        "ac1-limit-mount": {
          "action": "wait_for",
          "expression": "!!document.querySelector('[data-testid=\"limit-price-input\"]')",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 2000,
          "next": "ac1-limit-autofocus"
        },
        "ac1-limit-autofocus": {
          "action": "wait_for",
          "expression": "!!document.activeElement && document.activeElement.closest('[data-testid=\"limit-price-input\"]') !== null",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 3000,
          "next": "ac1-limit-screenshot"
        },
        "ac1-limit-screenshot": {
          "action": "screenshot",
          "filename": "demo-06-limit-autofocus",
          "note": "AC1 — switching to Limit moves focus to limit-price-input (no mouse needed)",
          "next": "ac1-limit-pause"
        },
        "ac1-limit-pause": { "action": "wait", "ms": 1500, "next": "ac1-market-switch" },

        "ac1-market-switch": {
          "action": "press",
          "test_id": "order-type-market",
          "next": "ac1-market-autofocus"
        },
        "ac1-market-autofocus": {
          "action": "wait_for",
          "expression": "!!document.activeElement && document.activeElement.closest('[data-testid=\"amount-input-field\"]') !== null",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 3000,
          "next": "ac1-market-screenshot"
        },
        "ac1-market-screenshot": {
          "action": "screenshot",
          "filename": "demo-07-market-refocus",
          "note": "AC1 — switching back to Market refocuses the size input automatically",
          "next": "ac1-market-pause"
        },
        "ac1-market-pause": { "action": "wait", "ms": 1500, "next": "lev-capture" },

        "lev-capture": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');return JSON.stringify({initial:i?parseInt(i.value,10):null});})()",
          "assert": { "field": "initial", "operator": "gt", "value": 0 },
          "save_as": "lev_initial",
          "next": "lev-focus"
        },
        "lev-focus": {
          "action": "eval_sync",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');if(!i)return JSON.stringify({focused:false});i.blur();i.focus();return JSON.stringify({focused:document.activeElement===i});})()",
          "assert": { "field": "focused", "operator": "eq", "value": true },
          "next": "lev-pause-1"
        },
        "lev-pause-1": { "action": "wait", "ms": 800, "next": "lev-arrow-up" },
        "lev-arrow-up": { "action": "key_press", "key": "ArrowUp", "next": "lev-after-up" },
        "lev-after-up": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');var cur=i?parseInt(i.value,10):NaN;return JSON.stringify({current:cur,matches:cur==={{vars.lev_initial.initial}}+1});})()",
          "assert": { "field": "matches", "operator": "eq", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "lev-pause-2"
        },
        "lev-pause-2": { "action": "wait", "ms": 800, "next": "lev-arrow-down" },
        "lev-arrow-down": { "action": "key_press", "key": "ArrowDown", "next": "lev-after-down" },
        "lev-after-down": {
          "action": "wait_for",
          "expression": "(function(){var i=document.querySelector('[data-testid=\"leverage-input\"] input');var cur=i?parseInt(i.value,10):NaN;return JSON.stringify({current:cur,matches:cur==={{vars.lev_initial.initial}}});})()",
          "assert": { "field": "matches", "operator": "eq", "value": true },
          "timeout_ms": 2000,
          "poll_ms": 100,
          "next": "lev-screenshot"
        },
        "lev-screenshot": {
          "action": "screenshot",
          "filename": "demo-08-leverage-keyboard",
          "note": "Leverage — select-on-focus + ArrowUp/ArrowDown step by 1",
          "next": "lev-pause-3"
        },
        "lev-pause-3": { "action": "wait", "ms": 1500, "next": "ac7-refocus-size" },

        "ac7-refocus-size": {
          "action": "eval_sync",
          "expression": "(function(){var w=document.querySelector('[data-testid=\"amount-input-field\"]');var i=w&&w.querySelector('input');if(!i)return JSON.stringify({focused:false});i.focus();return JSON.stringify({focused:document.activeElement===i,value:i.value});})()",
          "assert": { "field": "focused", "operator": "eq", "value": true },
          "next": "ac7-pause-pre-enter"
        },
        "ac7-pause-pre-enter": { "action": "wait", "ms": 1200, "next": "ac7-press-enter" },
        "ac7-press-enter": { "action": "key_press", "key": "Enter", "next": "ac7-wait-detail" },
        "ac7-wait-detail": {
          "action": "wait_for",
          "expression": "!!document.querySelector('[data-testid=\"perps-market-detail-page\"]') && window.location.hash==='#/perps/market/ETH'",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 15000,
          "poll_ms": 250,
          "next": "ac7-wait-position"
        },
        "ac7-wait-position": {
          "action": "wait_for",
          "expression": "(async()=>{var ps=await stateHooks.submitRequestToBackground('perpsGetPositions',[]);ps=Array.isArray(ps)?ps:[];return ps.some(function(p){return (p?.symbol||p?.coin||p?.asset||'').toString().toUpperCase()==='ETH'})})()",
          "assert": { "operator": "eq", "value": true },
          "timeout_ms": 15000,
          "poll_ms": 500,
          "next": "ac7-screenshot"
        },
        "ac7-screenshot": {
          "action": "screenshot",
          "filename": "demo-09-enter-submit-success",
          "note": "AC7 — pressing Enter from the focused size input submits the order; UI lands on the ETH market detail page with the new Long position visible",
          "next": "ac7-pause-final"
        },
        "ac7-pause-final": { "action": "wait", "ms": 2500, "next": "done" },

        "done": { "action": "end", "status": "pass" }
      },
      "teardown": [
        { "id": "teardown-close-eth", "action": "call", "ref": "perps/close-position", "params": { "symbol": "ETH" } }
      ]
    }
  }
}

Note

Medium Risk
Medium risk because it changes perps order-entry submission/navigate/toast flow and adds new client-side validation that can block submits if miscomputed; most other changes are localized UI/keyboard UX tweaks with good test coverage.

Overview
Adds a keyboard-first UX across perps order entry and modals: primary inputs now autoFocus, numeric fields select-all on focus, and key inputs (e.g., margin + TP/SL) can submit on Enter when valid; leverage input gains ArrowUp/ArrowDown stepping and swallows Enter to avoid accidental outer-form submits.

On PerpsOrderEntryPage, the page becomes a native <form> with type="submit" button, introduces real-time minimum market-order size gating (PERPS_MIN_MARKET_ORDER_USD) with localized submit-button copy (perpsMinOrderSize), and threads new autoFocusUsd/autoFocusLimitPrice + USD placeholder/ref props through OrderEntry/AmountInput (including refocus when toggling back to market).

Submission and navigation are adjusted to avoid stuck toasts: in-progress toasts are emitted from the page (not via route state), navigation back to market detail uses replace: true and happens only after successful background RPC completion; header back behavior now falls back to market detail/default route when history can’t pop. Tests are expanded/updated to cover the new focus/keyboard behaviors, placeholders, and navigation expectations.

Reviewed by Cursor Bugbot for commit 3ffa336. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 20, 2026

Builds ready [6bb62bc]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24668539596 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -57%
  • loadNewAccount/total: -57%
  • bridgeUserActions/bridge_load_page: -11%
  • bridgeUserActions/bridge_load_asset_picker: -24%
  • bridgeUserActions/bridge_search_token: -28%
  • bridgeUserActions/total: -25%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.6s
  • 🟡 confirmTx/FCP: p75 2.6s
  • 🟡 bridgeUserActions/FCP: p75 2.6s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -26%
  • startupStandardHome/load: -16%
  • startupStandardHome/domContentLoaded: -18%
  • startupStandardHome/firstReactRender: -10%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -21%
  • startupStandardHome/setupStore: +14%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -22%
  • startupStandardHome/load: -17%
  • startupStandardHome/domContentLoaded: -17%
  • startupStandardHome/backgroundConnect: -39%
  • startupStandardHome/firstReactRender: -23%
  • startupStandardHome/loadScripts: -17%
  • startupStandardHome/setupStore: -13%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -15%
  • startupStandardHome/domInteractive: -61%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/setupStore: -17%
  • startupStandardHome/numNetworkReqs: -34%
  • startupStandardHome/uiStartup: -32%
  • startupStandardHome/load: -28%
  • startupStandardHome/domContentLoaded: -28%
  • startupStandardHome/domInteractive: -56%
  • startupStandardHome/backgroundConnect: -26%
  • startupStandardHome/firstReactRender: -18%
  • startupStandardHome/initialActions: -43%
  • startupStandardHome/loadScripts: -28%
  • startupStandardHome/setupStore: -67%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -86%
  • onboardingImportWallet/pwFormToMetricsScreen: -12%
  • onboardingImportWallet/metricsToWalletReadyScreen: -39%
  • onboardingImportWallet/doneButtonToHomeScreen: -76%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +26%
  • onboardingImportWallet/total: -45%
  • onboardingNewWallet/srpButtonToPwForm: -79%
  • onboardingNewWallet/skipBackupToMetricsScreen: -66%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -24%
  • onboardingNewWallet/doneButtonToAssetList: -37%
  • onboardingNewWallet/total: -36%
  • assetDetails/assetClickToPriceChart: -55%
  • assetDetails/total: -55%
  • solanaAssetDetails/assetClickToPriceChart: -69%
  • solanaAssetDetails/total: -69%
  • importSrpHome/loginToHomeScreen: -13%
  • importSrpHome/openAccountMenuAfterLogin: -80%
  • importSrpHome/homeAfterImportWithNewWallet: -68%
  • importSrpHome/total: -62%
  • sendTransactions/openSendPageFromHome: -22%
  • sendTransactions/selectTokenToSendFormLoaded: -13%
  • sendTransactions/reviewTransactionToConfirmationPage: +35%
  • sendTransactions/total: +34%
  • swap/openSwapPageFromHome: -97%
  • swap/fetchAndDisplaySwapQuotes: +31%
  • swap/total: +11%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.6s
  • 🟡 solanaAssetDetails/LCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.4s
  • 🟡 sendTransactions/INP: p75 240ms
  • 🟡 sendTransactions/FCP: p75 2.5s
  • 🟡 swap/FCP: p75 2.5s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 13.78 KiB (0.22%)
  • ui: 30.85 KiB (0.36%)
  • common: 35.49 KiB (0.27%)

- Auto-focus primary input on every perps order screen (size on Trade,
  limit price on Limit switch, margin on Edit margin, TP trigger on
  TP/SL, Close button on Position close).
- Auto-select existing value on focus so typing replaces it.
- Contextual `min $10` placeholder and `Minimum order size $10` disabled
  submit copy until the market-order size clears PERPS_MIN_MARKET_ORDER_USD.
- Render the order-entry page as a native <form onSubmit> with the
  primary button set to type="submit" so the browser's Enter-to-submit
  behavior fires when the button is enabled and is naturally blocked when
  disabled. Edit-margin and TP/SL modals attach input-level Enter
  handlers guarded against Shift+Enter and IME composition.
- Unit coverage for focus, select-on-focus, placeholder, min-order gate,
  and form-submission semantics across all five screens.
…d-order-entry-ux

# Conflicts:
#	ui/pages/perps/perps-order-entry-page.tsx
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 21, 2026

✨ Files requiring CODEOWNER review ✨

👨‍🔧 @MetaMask/perps (18 files, +877 -88)
  • 📁 ui/
    • 📁 components/
      • 📁 app/
        • 📁 perps/
          • 📁 close-position/
            • 📄 close-position-modal.test.tsx +20 -0
            • 📄 close-position-modal.tsx +1 -0
          • 📁 edit-margin/
            • 📄 edit-margin-modal-content.test.tsx +68 -0
            • 📄 edit-margin-modal-content.tsx +23 -1
          • 📁 order-entry/
            • 📁 components/
              • 📁 amount-input/
                • 📄 amount-input.test.tsx +87 -0
                • 📄 amount-input.tsx +43 -6
              • 📁 direction-tabs/
                • 📄 direction-tabs.tsx +2 -0
              • 📁 leverage-slider/
                • 📄 leverage-slider.test.tsx +126 -1
                • 📄 leverage-slider.tsx +40 -4
              • 📁 limit-price-input/
                • 📄 limit-price-input.test.tsx +37 -0
                • 📄 limit-price-input.tsx +9 -0
              • 📄 order-entry.test.tsx +26 -0
              • 📄 order-entry.tsx +24 -0
              • 📄 order-entry.types.ts +14 -0
          • 📁 update-tpsl/
            • 📄 update-tpsl-modal-content.test.tsx +107 -0
            • 📄 update-tpsl-modal-content.tsx +54 -2
    • 📁 pages/
      • 📁 perps/
        • 📄 perps-order-entry-page.test.tsx +53 -14
        • 📄 perps-order-entry-page.tsx +143 -60

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 21, 2026

Builds ready [0850807]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24705145473 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -59%
  • loadNewAccount/total: -59%
  • bridgeUserActions/bridge_load_page: -24%
  • bridgeUserActions/bridge_load_asset_picker: -40%
  • bridgeUserActions/bridge_search_token: -25%
  • bridgeUserActions/total: -29%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.5s
  • 🟡 confirmTx/FCP: p75 2.5s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -22%
  • startupStandardHome/load: -11%
  • startupStandardHome/domContentLoaded: -13%
  • startupStandardHome/domInteractive: +16%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -15%
  • startupStandardHome/setupStore: +14%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -32%
  • startupStandardHome/load: -28%
  • startupStandardHome/domContentLoaded: -28%
  • startupStandardHome/domInteractive: -23%
  • startupStandardHome/backgroundConnect: -44%
  • startupStandardHome/firstReactRender: -38%
  • startupStandardHome/loadScripts: -28%
  • startupStandardHome/setupStore: -27%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -12%
  • startupStandardHome/domInteractive: -59%
  • startupStandardHome/backgroundConnect: +11%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/numNetworkReqs: -34%
  • startupStandardHome/domInteractive: -25%
  • startupStandardHome/initialActions: +14%
  • startupStandardHome/setupStore: -54%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -83%
  • onboardingImportWallet/metricsToWalletReadyScreen: -44%
  • onboardingImportWallet/doneButtonToHomeScreen: -76%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +27%
  • onboardingImportWallet/total: -43%
  • onboardingNewWallet/srpButtonToPwForm: -78%
  • onboardingNewWallet/skipBackupToMetricsScreen: -66%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -24%
  • onboardingNewWallet/doneButtonToAssetList: -25%
  • onboardingNewWallet/total: -26%
  • solanaAssetDetails/assetClickToPriceChart: -71%
  • solanaAssetDetails/total: -71%
  • importSrpHome/openAccountMenuAfterLogin: -49%
  • importSrpHome/homeAfterImportWithNewWallet: -68%
  • importSrpHome/total: -58%
  • sendTransactions/openSendPageFromHome: +26%
  • sendTransactions/selectTokenToSendFormLoaded: -23%
  • sendTransactions/reviewTransactionToConfirmationPage: +34%
  • sendTransactions/total: +32%
  • swap/openSwapPageFromHome: -97%
  • swap/fetchAndDisplaySwapQuotes: +31%
  • swap/total: +10%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.6s
  • 🟡 importSrpHome/FCP: p75 2.6s
  • 🟡 sendTransactions/INP: p75 208ms
  • 🟡 sendTransactions/FCP: p75 2.4s
  • 🟡 swap/FCP: p75 2.3s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 1.89 KiB (0.02%)
  • common: 347 Bytes (0%)

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 21, 2026

Builds ready [7f629d3]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24728665701 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -56%
  • loadNewAccount/total: -56%
  • bridgeUserActions/bridge_load_page: -17%
  • bridgeUserActions/bridge_load_asset_picker: -48%
  • bridgeUserActions/bridge_search_token: -27%
  • bridgeUserActions/total: -30%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.5s
  • 🟡 confirmTx/FCP: p75 2.5s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -25%
  • startupStandardHome/load: -14%
  • startupStandardHome/domContentLoaded: -16%
  • startupStandardHome/domInteractive: +10%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -19%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -21%
  • startupStandardHome/load: -16%
  • startupStandardHome/domContentLoaded: -15%
  • startupStandardHome/firstPaint: -25%
  • startupStandardHome/backgroundConnect: -40%
  • startupStandardHome/firstReactRender: -23%
  • startupStandardHome/loadScripts: -15%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -15%
  • startupStandardHome/domInteractive: -63%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/setupStore: -17%
  • startupStandardHome/numNetworkReqs: -34%
  • startupStandardHome/uiStartup: -19%
  • startupStandardHome/load: -11%
  • startupStandardHome/domContentLoaded: -11%
  • startupStandardHome/domInteractive: -65%
  • startupStandardHome/initialActions: +14%
  • startupStandardHome/loadScripts: -11%
  • startupStandardHome/setupStore: -60%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -84%
  • onboardingImportWallet/metricsToWalletReadyScreen: -34%
  • onboardingImportWallet/doneButtonToHomeScreen: -76%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +16%
  • onboardingImportWallet/total: -45%
  • onboardingNewWallet/srpButtonToPwForm: -76%
  • onboardingNewWallet/skipBackupToMetricsScreen: -69%
  • onboardingNewWallet/doneButtonToAssetList: -23%
  • onboardingNewWallet/total: -25%
  • solanaAssetDetails/assetClickToPriceChart: -65%
  • solanaAssetDetails/total: -65%
  • importSrpHome/loginToHomeScreen: -13%
  • importSrpHome/openAccountMenuAfterLogin: -74%
  • importSrpHome/homeAfterImportWithNewWallet: -71%
  • importSrpHome/total: -65%
  • sendTransactions/reviewTransactionToConfirmationPage: +36%
  • sendTransactions/total: +35%
  • swap/openSwapPageFromHome: -95%
  • swap/fetchAndDisplaySwapQuotes: +33%
  • swap/total: +12%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.4s
  • 🟡 sendTransactions/INP: p75 216ms
  • 🟡 sendTransactions/FCP: p75 2.7s
  • 🟡 sendTransactions/LCP: p75 2.7s
  • 🟡 swap/FCP: p75 2.8s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 97 Bytes (0%)
  • ui: 1.62 KiB (0.02%)
  • common: 118 Bytes (0%)

@abretonc7s
Copy link
Copy Markdown
Contributor Author

abretonc7s commented Apr 21, 2026

Run Duration Model Nudges Grade Cost
b80a7716 ? opus 0 ungraded $2.4885
Worker report

TAT-2802 — Keyboard-first order entry UX

Ticket: TAT-2802
Branch: feat/tat-2802-keyboard-order-entry-ux

Summary

Order entry now auto-focuses the primary input on every perps order screen, auto-selects the existing value on focus, gates submit with inline validation, and shows a contextual min $10 placeholder plus a Minimum order size $10 button copy until the size is valid. Pressing Enter from the primary input submits when the button is enabled. Applies uniformly to Trade, Limit, Add/Remove margin, TP/SL, and Position close screens. Leverage input is also keyboard-driven: select-on-focus plus ArrowUp/ArrowDown stepping (clamped to minLeverage..maxLeverage).

Changes

Production code:

  • ui/pages/perps/perps-order-entry-page.tsx — min-order-size gate on market orders; disabled submit + error-mode button copy when size is below PERPS_MIN_MARKET_ORDER_USD; placeholder wiring for size input; root rendered as <form onSubmit={handleFormSubmit}> with the primary button set to type="submit" so browsers trigger Enter-to-submit natively (disabled buttons block submission per HTML spec).
  • ui/components/app/perps/order-entry/order-entry.tsx — threads autoFocus + inputRef through to children; coordinates size-vs-limit focus on order-type switch.
  • ui/components/app/perps/order-entry/order-entry.types.ts — types for new focus/ref props.
  • ui/components/app/perps/order-entry/components/amount-input/amount-input.tsxautoFocus, select-all on focus, placeholder prop forwarded to TextField.
  • ui/components/app/perps/order-entry/components/limit-price-input/limit-price-input.tsxautoFocus + select-all on focus.
  • ui/components/app/perps/edit-margin/edit-margin-modal-content.tsx — auto-focus + select-all on margin input; Enter-to-save via inputProps.onKeyDown.
  • ui/components/app/perps/update-tpsl/update-tpsl-modal-content.tsx — auto-focus TP trigger input; select-all for TP and SL; Enter-to-save wired into all four TP/SL inputs, guarded by isSaving || hasInvalidTPSL.
  • ui/components/app/perps/close-position/close-position-modal.tsx — auto-focus the Close button on mount.
  • ui/components/app/perps/order-entry/components/leverage-slider/leverage-slider.tsx — select-on-focus plus ArrowUp/ArrowDown step handler (clamp to minLeverage..maxLeverage); wired onFocus and inputProps.onKeyDown on the existing TextField. Existing parseInt/isNaN usages migrated to Number.parseInt/Number.isNaN to clear sonar diagnostics.
  • app/_locales/en/messages.json, app/_locales/en_GB/messages.json — added perpsMinOrderSize and perpsSizePlaceholderMin with $1 substitution.

Tests:

  • Added focus / select-all / placeholder coverage to amount-input, limit-price-input, edit-margin, update-tpsl, close-position, and perps-order-entry-page test suites. Updated 3 pre-existing tests that asserted the full-width Open Long ETH copy with an empty amount (now needs a pre-typed value).
  • Added keyboard submission describe blocks to perps-order-entry-page, edit-margin, and update-tpsl test suites (Enter submits when valid; Enter is ignored when disabled / invalid / below-min; non-Enter keys ignored).

Test plan

  • yarn jest <file> --no-coverage on each changed test file — 230/230 pass (adds 8 Enter-to-submit cases across three suites).
  • node temp/.agent/coverage-analyze.js — VERDICT PASS (new code above 80%; existing edit-margin 48% is a pre-existing WARNING).
  • yarn lint:changed && yarn verify-locales --quiet && yarn circular-deps:check — passes.
  • Validation recipe bundle live-validated against CDP 6662 (16/16 main-workflow nodes pass, non-gating review). Included bundled sub-flows under temp/agentic/recipes/domains/tat-2802/flows/ covering AC-focused validation rather than expanding each file inline. Main orchestrator: temp/.task/feat/tat-2802-0420-210839/artifacts/recipe.json (18 nodes).

Evidence

  • evidence-min-order-empty.png — empty size → disabled button reading "Minimum order size $10", placeholder "min $10".
  • evidence-valid-amount.png — after entering $15, button enables and reads "Open Long ETH".
  • evidence-limit-autofocus.png — switching to limit auto-focuses the limit price input.
  • evidence-enter-submit-redirect.png — pressing Enter from the focused size input with a valid $15 submits the order and redirects to the ETH market detail page with the new Long position visible.
  • after.mp4 — not captured; capture-helper stream aborted with SCStreamErrorDomain Code=-3805 in this slot (see /tmp/farmslot-record-20608.log). Visual ACs fully covered by screenshots + recipe assertions.

Notes

  • PERPS_MIN_MARKET_ORDER_USD = 10 is the existing constant; no magic numbers introduced.
  • Reverse-position and remove-margin screens intentionally unchanged (ticket scope).

@abretonc7s abretonc7s marked this pull request as ready for review April 21, 2026 15:24
@abretonc7s abretonc7s requested a review from a team as a code owner April 21, 2026 15:24
Comment thread ui/components/app/perps/order-entry/order-entry.tsx Outdated
Comment thread ui/components/app/perps/order-entry/order-entry.tsx
Comment thread ui/pages/perps/perps-order-entry-page.tsx
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 22, 2026

Builds ready [e3f8e6e]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24773509461 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -63%
  • loadNewAccount/total: -63%
  • bridgeUserActions/bridge_load_page: -17%
  • bridgeUserActions/bridge_load_asset_picker: -25%
  • bridgeUserActions/bridge_search_token: -26%
  • bridgeUserActions/total: -26%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.4s
  • 🟡 confirmTx/FCP: p75 2.4s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -19%
  • startupStandardHome/backgroundConnect: +13%
  • startupStandardHome/initialActions: +33%
  • startupStandardHome/loadScripts: -13%
  • startupStandardHome/setupStore: +14%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -24%
  • startupStandardHome/load: -20%
  • startupStandardHome/domContentLoaded: -19%
  • startupStandardHome/backgroundConnect: -42%
  • startupStandardHome/firstReactRender: -27%
  • startupStandardHome/loadScripts: -19%
  • startupStandardHome/setupStore: -13%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -10%
  • startupStandardHome/domInteractive: -48%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/numNetworkReqs: -34%
  • startupStandardHome/uiStartup: -18%
  • startupStandardHome/domInteractive: -48%
  • startupStandardHome/backgroundConnect: -11%
  • startupStandardHome/initialActions: -43%
  • startupStandardHome/setupStore: -64%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -85%
  • onboardingImportWallet/metricsToWalletReadyScreen: -46%
  • onboardingImportWallet/doneButtonToHomeScreen: -73%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +18%
  • onboardingImportWallet/total: -44%
  • onboardingNewWallet/srpButtonToPwForm: -78%
  • onboardingNewWallet/skipBackupToMetricsScreen: -69%
  • onboardingNewWallet/doneButtonToAssetList: -32%
  • onboardingNewWallet/total: -32%
  • assetDetails/assetClickToPriceChart: -51%
  • assetDetails/total: -51%
  • solanaAssetDetails/assetClickToPriceChart: -72%
  • solanaAssetDetails/total: -72%
  • importSrpHome/loginToHomeScreen: -13%
  • importSrpHome/openAccountMenuAfterLogin: -73%
  • importSrpHome/homeAfterImportWithNewWallet: -67%
  • importSrpHome/total: -60%
  • sendTransactions/selectTokenToSendFormLoaded: -27%
  • sendTransactions/reviewTransactionToConfirmationPage: +35%
  • sendTransactions/total: +33%
  • swap/openSwapPageFromHome: -95%
  • swap/fetchAndDisplaySwapQuotes: +33%
  • swap/total: +12%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/LCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.5s
  • 🟡 sendTransactions/FCP: p75 2.6s
  • 🟡 swap/FCP: p75 2.6s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.3 KiB (0.02%)
  • ui: 6.23 KiB (0.07%)
  • common: 259 Bytes (0%)

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 29, 2026

Builds ready [d862730]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25096982948 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 2.85 KiB (0.03%)
  • common: 239 Bytes (0%)

The shared in-progress toast block was emitting SUBMIT_IN_PROGRESS for
new orders unconditionally. shouldShowOrderSubmissionToasts only gated
the success toast, so the "Submitting your trade" toast leaked while a
deposit was still pending — a regression vs main behavior.

Hoist the guard above the in-progress block and skip the new-order
toast emit when shouldShowOrderSubmissionToasts is false.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 426f037. Configure here.

Comment thread ui/pages/perps/perps-order-entry-page.tsx Outdated
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 30, 2026

Builds ready [426f037]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25139967428 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -487 Bytes (-0.01%)
  • ui: 9.36 KiB (0.11%)
  • common: 176.24 KiB (1.37%)

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 30, 2026

Builds ready [ce2cb1d]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25158896083 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 2.56 KiB (0.03%)
  • common: 239 Bytes (0%)

Copy link
Copy Markdown
Contributor

@michalconsensys michalconsensys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are there changes to app/images/icon-512.png?

Wire the contextual perps size placeholder through OrderEntry for market orders, apply token-input select-all after the editing rerender, cover the leverage new-code paths required by coverage analysis, and remove unrelated optimized PNG deltas from the PR diff.

Constraint: Review fixes must stay inside the existing PR surface and remove unrelated image diffs from the branch.\nRejected: Keep optimized PNG changes | unrelated to keyboard-first perps order entry and called out by the user.\nConfidence: high\nScope-risk: narrow\nTested: yarn lint:changed && yarn verify-locales --quiet && yarn circular-deps:check\nTested: affected jest command from task plus targeted OrderEntry and LeverageSlider suites\nTested: node temp/runtime/coverage-analyze.js\nNot-tested: Inherited recipe product path; blocked by missing shared flow perps/open-order-form before exercising behavior.
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 30, 2026

Builds ready [560621c]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25171456617 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -70 Bytes (0%)
  • ui: 7.25 KiB (0.08%)
  • common: 878 Bytes (0.01%)

Align the perps order-entry tests and formatted assets with the CI lint gate so the PR can pass the strict validation suite.\n\nConstraint: CI reported Test lint failure on PR #41949\nConfidence: high\nScope-risk: narrow\nTested: yarn lint && yarn verify-locales --quiet && yarn circular-deps:check\nTested: yarn jest ui/pages/perps/perps-order-entry-page.test.tsx --no-coverage
Remove image optimizer output that was accidentally included in the CI-fix commit. The perps test and formatting fixes remain intact.\n\nConstraint: PR fix must include only relevant CI changes\nConfidence: high\nScope-risk: narrow\nTested: git diff HEAD~1..HEAD --name-only
Merged origin/main into the PR branch and resolved the TP/SL TextField conflict by preserving the branch's keyboard/focus behavior while adopting main's testId prop usage.\n\nConstraint: Keep PR conflict-free without re-running lint because the image linter rewrites unrelated PNG assets\nRejected: Re-run full lint gate | it recreates unrelated image optimizer changes on this branch\nConfidence: high\nScope-risk: narrow\nTested: yarn jest ui/components/app/perps/update-tpsl/update-tpsl-modal-content.test.tsx --no-coverage\nNot-tested: Full lint gate intentionally not run
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 30, 2026

Builds ready [22a599f]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25174350699 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 3.18 KiB (0.04%)
  • common: 239 Bytes (0%)

gambinish
gambinish previously approved these changes Apr 30, 2026
geositta
geositta previously approved these changes Apr 30, 2026
Comment thread app/scripts/lib/metaRPCClientFactory.ts Outdated
@sonarqubecloud
Copy link
Copy Markdown

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 Bot commented Apr 30, 2026

Builds ready [3ffa336]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 25189275191 | Baseline logs

Interaction Benchmarks · Samples: 5

⚠️ Missing data: chrome/webpack/interactionUserActions, firefox/webpack/interactionUserActions

✅ No regressions detected

Startup Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/startupStandardHome, chrome/webpack/startupPowerUserHome, firefox/webpack/startupStandardHome, firefox/webpack/startupPowerUserHome

✅ No regressions detected

User Journey Benchmarks · Samples: 5 · mock API

⚠️ Missing data: chrome/webpack/userJourneyOnboardingImport, chrome/webpack/userJourneyOnboardingNew, chrome/webpack/userJourneyAssets, chrome/webpack/userJourneyAccountManagement, chrome/webpack/userJourneyTransactions, firefox/webpack/userJourneyOnboardingImport, firefox/webpack/userJourneyOnboardingNew, firefox/webpack/userJourneyAssets, firefox/webpack/userJourneyAccountManagement, firefox/webpack/userJourneyTransactions

✅ No regressions detected

Dapp Page Load Benchmarks · Samples: 100

⚠️ Missing data: chrome/webpack/pageLoadBenchmark

✅ No regressions detected

Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 926 Bytes (0.02%)
  • ui: 7.93 KiB (0.09%)
  • common: 234.98 KiB (1.8%)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

agentic release-13.31.0 Issue or pull request that will be included in release 13.31.0 size-L team-perps Perps team

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

7 participants