fix(perps): [Bug]: Perps - Available to trade percentage shows truncated decimal on initial load (e.g. "22...") - #45561
Conversation
|
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. |
✨ Files requiring CODEOWNER review ✨👨🔧 @MetaMask/perps (2 files, +53 -6)
|
…mber The order form seeded balancePercent rounded to two decimals, so on initial load the percentage pill next to the size slider rendered a value like 0.44. That text overflows the pill's 4.5rem field and the browser clipped it, which is the reported "22...". Every other writer of balancePercent already yields a whole number, so the value corrected itself as soon as the user touched the slider. Round the seed to a whole percent so initial load matches the slider's step=1 grid and every user-driven write path.
Builds ready [91f094d]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 11 warn · 🔴 3 fail)
Bundle size diffs
|
91f094d to
edba250
Compare
Worker reportPR #45561 — Comments reportPR: Context gathered
Comment triage
No review-fix commit was created. CI status
All three red checks stem from a single pre-existing flake in Resolution: force-push triggers a fresh CI run that re-runs the flaked unit-test shards. Integration statusRebased cleanly onto Validation
Summary
|
|
Builds ready [edba250] [reused from 91f094d]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 11 warn · 🔴 3 fail)
Bundle size diffs
|



Description
The Perps order form seeded its initial
balancePercentrounded to two decimals, so on initial load the percentage pill next to the size slider rendered a value like0.44. That text overflows the pill's fixed4.5remfield (measured live:scrollWidth50px vsclientWidth45px), so the browser clipped it — the reported22.... Every other writer ofbalancePercent(sliderstep={1}, size input, percent field, leverage) already rounds to a whole number, which is why the value corrected itself as soon as the user touched the slider.Rounding the seed to a whole percent at
ui/hooks/perps/usePerpsOrderForm.tsmakes initial load agree with the slider's step grid and with every user-driven write path, and keeps the single source of truth consistent for all three consumers: the pill, the slider thumb, and theORDER_SIZE_PERCENTanalytics property.Mobile carries the identical expression but never renders
balancePercent, so the visible pill is Extension-only — this fixes the Extension renderer contract without diverging from mobile's internal state. It is a percentage rather than a fiat value, so notoFixedor currency formatter was introduced.Changelog
CHANGELOG entry: Fixed the Perps available-to-trade percentage showing a truncated decimal on initial load
Related issues
Fixes: TAT-3763
Manual testing steps
#/perps/trade/ETH).0 %), fully visible and not clipped.Screenshots/Recordings
Post-rebase re-validation: the Perps available-to-trade percentage pill renders a clean whole number on initial load.
Pre-merge author checklist
Pre-merge reviewer checklist
Validation Recipe
Runs against the live extension over CDP. Both AC assertion groups fail if the fix is reverted; a sibling baseline recipe asserting the buggy values passes on the unfixed tree, which is what makes the before-evidence captures honest.
recipe.json
{ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json", "title": "TAT-3763 — Perps available-to-trade percentage is a clean rounded number on initial load", "description": "Opens the live Perps order-entry form and proves the balance-percent pill renders a whole, unclipped percentage on initial load (AC1), then drives the slider through trusted UI input to prove the already-working interaction path still renders a whole, unclipped percentage (AC2). Data prerequisites, enforced by the gate-cdp and setup-unlock nodes: the Extension runtime must be reachable over CDP on the configured port, and the fixture wallet's active account must have a non-zero Perps tradeable balance — a zero balance makes the default balancePercent 0 and leaves AC1 unobservable.", "workflow": { "entry": "setup-status", "nodes": { "setup-status": { "action": "app.status", "next": "gate-cdp", "intent": "Resolve the Extension checkout and adapter status before proving the fix" }, "gate-cdp": { "action": "cdp.target", "required": true, "timeout_ms": 15000, "next": "setup-unlock", "intent": "Fail fast unless the Extension CDP runtime is reachable" }, "setup-unlock": { "action": "metamask.wallet.ensure_unlocked", "timeout_ms": 45000, "next": "setup-open-home", "intent": "Ensure the wallet is unlocked before opening the Perps order form" }, "setup-open-home": { "action": "ui.navigate", "page": "home", "timeout_ms": 30000, "next": "setup-open-order-entry", "intent": "Leave any previous Perps route so the order form mounts fresh and the initial-load state is genuine" }, "setup-open-order-entry": { "action": "ui.navigate", "hash": "#/perps/trade/ETH", "timeout_ms": 30000, "next": "ac1-wait-percent-pill", "intent": "Open the ETH Perps order-entry form where the available-to-trade percentage pill renders" }, "ac1-wait-percent-pill": { "action": "ui.wait_for", "selector": "[data-testid=\"balance-percent-input\"] input", "visible": true, "timeout_ms": 30000, "next": "ac1-probe-initial-load", "intent": "AC1: confirm the balance-percent pill is visible on initial load before asserting and capturing it" }, "ac1-probe-initial-load": { "action": "command", "timeout_ms": 30000, "next": "ac1-assert-percent-is-whole-number", "intent": "AC1: read the initial-load percentage value and its clipping geometry without mutating any state" }, "ac1-assert-percent-is-whole-number": { "action": "assert_json", "assert": { "path": "$.percentIsInteger", "operator": "eq", "value": true }, "next": "ac1-assert-percent-not-clipped", "intent": "AC1: the initial-load percentage is a clean rounded whole number, not a long decimal" }, "ac1-assert-percent-not-clipped": { "action": "assert_json", "assert": { "path": "$.percentIsClipped", "operator": "eq", "value": false }, "next": "ac1-assert-slider-on-step-grid", "intent": "AC1: the pill's text fits its field, so the user never sees the reported truncated value" }, "ac1-assert-slider-on-step-grid": { "action": "assert_json", "assert": { "path": "$.sliderIsOnStepGrid", "operator": "eq", "value": true }, "next": "ac1-clear-capture-orphans", "intent": "AC1: the slider's aria-valuenow sits on its declared step=1 grid instead of an off-step decimal" }, "ac1-clear-capture-orphans": { "action": "command", "cmd": "pkill -9 -f capture-helper || true", "allow_failure": true, "next": "ac1-screenshot-initial-load-percent", "intent": "AC1: clear wedged capture-helper sessions so the screenshot comes from capture-helper, not the raster fallback" }, "ac1-screenshot-initial-load-percent": { "action": "ui.screenshot", "label": "AC1: initial load shows a clean rounded percentage in the pill, not a truncated decimal", "timeout_ms": 60000, "next": "ac2-press-slider", "intent": "AC1: capture the visible pill on initial load, the exact surface the ticket reports as truncated" }, "ac2-press-slider": { "action": "ui.press", "selector": "[data-testid=\"amount-slider\"] input[type=\"range\"]", "timeout_ms": 20000, "next": "ac2-key-increment-1", "intent": "AC2: move the slider through a real click, as the ticket's reproduction step 5 describes" }, "ac2-key-increment-1": { "action": "ui.key_press", "key": "ArrowRight", "timeout_ms": 10000, "next": "ac2-key-increment-2", "intent": "AC2: nudge the focused slider with trusted keyboard input to a non-zero percentage" }, "ac2-key-increment-2": { "action": "ui.key_press", "key": "ArrowRight", "timeout_ms": 10000, "next": "ac2-key-increment-3", "intent": "AC2: continue moving the slider through trusted keyboard input" }, "ac2-key-increment-3": { "action": "ui.key_press", "key": "ArrowRight", "timeout_ms": 10000, "next": "ac2-wait-percent-pill", "intent": "AC2: settle the slider on a non-zero percentage so the assertion is not trivially satisfied by 0" }, "ac2-wait-percent-pill": { "action": "ui.wait_for", "selector": "[data-testid=\"amount-slider\"] input[type=\"range\"][aria-valuenow=\"3\"]", "visible": true, "timeout_ms": 20000, "next": "ac2-probe-after-interaction", "intent": "AC2: confirm the three trusted key presses actually moved the slider to 3 before asserting" }, "ac2-probe-after-interaction": { "action": "command", "timeout_ms": 30000, "next": "ac2-assert-percent-value", "intent": "AC2: read the post-interaction percentage value and its clipping geometry" }, "ac2-assert-percent-value": { "action": "assert_json", "assert": { "path": "$.percentValue", "operator": "eq", "value": "3" }, "next": "ac2-assert-percent-is-whole-number", "intent": "AC2: the pill reflects the exact percentage the user's slider interaction produced" }, "ac2-assert-percent-is-whole-number": { "action": "assert_json", "assert": { "path": "$.percentIsInteger", "operator": "eq", "value": true }, "next": "ac2-assert-percent-not-clipped", "intent": "AC2: the already-working interaction path still renders a whole number after the fix" }, "ac2-assert-percent-not-clipped": { "action": "assert_json", "assert": { "path": "$.percentIsClipped", "operator": "eq", "value": false }, "next": "ac2-clear-capture-orphans", "intent": "AC2: the post-interaction pill still fits its field, confirming no regression" }, "ac2-clear-capture-orphans": { "action": "command", "cmd": "pkill -9 -f capture-helper || true", "allow_failure": true, "next": "ac2-screenshot-after-interaction", "intent": "AC2: clear wedged capture-helper sessions before the second capture" }, "ac2-screenshot-after-interaction": { "action": "ui.screenshot", "label": "AC2: after moving the slider the pill shows 3, a whole unclipped percentage", "timeout_ms": 60000, "next": "done", "intent": "AC2: capture the moved-slider state so reviewers can see the interaction path is unchanged" }, "done": { "action": "end", "status": "pass" } } } }Recipe Workflow
workflow.mmd
flowchart TD setup-status["setup-status<br/>app.status"] --> gate-cdp gate-cdp["gate-cdp<br/>cdp.target"] --> setup-unlock setup-unlock["setup-unlock<br/>metamask.wallet.ensure_unlocked"] --> setup-open-home setup-open-home["setup-open-home<br/>ui.navigate page=home"] --> setup-open-order-entry setup-open-order-entry["setup-open-order-entry<br/>ui.navigate #/perps/trade/ETH"] --> ac1-wait-percent-pill subgraph AC1["AC1 — initial load shows a clean rounded percentage (mixed)"] ac1-wait-percent-pill["ac1-wait-percent-pill<br/>ui.wait_for balance-percent-input"] --> ac1-probe-initial-load ac1-probe-initial-load["ac1-probe-initial-load<br/>command: read-only DOM probe"] --> ac1-assert-percent-is-whole-number ac1-assert-percent-is-whole-number["ac1-assert-percent-is-whole-number<br/>assert_json percentIsInteger eq true"] --> ac1-assert-percent-not-clipped ac1-assert-percent-not-clipped["ac1-assert-percent-not-clipped<br/>assert_json percentIsClipped eq false"] --> ac1-assert-slider-on-step-grid ac1-assert-slider-on-step-grid["ac1-assert-slider-on-step-grid<br/>assert_json sliderIsOnStepGrid eq true"] --> ac1-clear-capture-orphans ac1-clear-capture-orphans["ac1-clear-capture-orphans<br/>command: pkill -9 capture-helper"] --> ac1-screenshot-initial-load-percent ac1-screenshot-initial-load-percent["ac1-screenshot-initial-load-percent<br/>ui.screenshot evidence-ac1"] end ac1-screenshot-initial-load-percent --> ac2-press-slider subgraph AC2["AC2 — moving the slider still shows the correct value (state)"] ac2-press-slider["ac2-press-slider<br/>ui.press slider range input"] --> ac2-key-increment-1 ac2-key-increment-1["ac2-key-increment-1<br/>ui.key_press ArrowRight"] --> ac2-key-increment-2 ac2-key-increment-2["ac2-key-increment-2<br/>ui.key_press ArrowRight"] --> ac2-key-increment-3 ac2-key-increment-3["ac2-key-increment-3<br/>ui.key_press ArrowRight"] --> ac2-wait-percent-pill ac2-wait-percent-pill["ac2-wait-percent-pill<br/>ui.wait_for aria-valuenow=3"] --> ac2-probe-after-interaction ac2-probe-after-interaction["ac2-probe-after-interaction<br/>command: read-only DOM probe"] --> ac2-assert-percent-value ac2-assert-percent-value["ac2-assert-percent-value<br/>assert_json percentValue eq 3"] --> ac2-assert-percent-is-whole-number ac2-assert-percent-is-whole-number["ac2-assert-percent-is-whole-number<br/>assert_json percentIsInteger eq true"] --> ac2-assert-percent-not-clipped ac2-assert-percent-not-clipped["ac2-assert-percent-not-clipped<br/>assert_json percentIsClipped eq false"] --> ac2-clear-capture-orphans ac2-clear-capture-orphans["ac2-clear-capture-orphans<br/>command: pkill -9 capture-helper"] --> ac2-screenshot-after-interaction ac2-screenshot-after-interaction["ac2-screenshot-after-interaction<br/>ui.screenshot evidence-ac2"] end ac2-screenshot-after-interaction --> done done(["done — end status=pass"])Note
Low Risk
Small display/state seeding change in the order form hook with targeted unit tests; no auth, payments, or trading execution paths.
Overview
Fixes TAT-3763 by changing how the Perps order form seeds
balancePercenton first load inusePerpsOrderForm: it now usesMath.round(initialBalancePercent)instead of rounding to two decimals.That aligns the initial value with the size slider’s
step={1}and with slider, leverage, and percent-field updates, so the narrow available-to-trade % pill no longer shows clipped decimals like 0.44 or 22... before the user interacts.Adds regression tests for whole-number initial percents (including zero and non-zero cases) and adjusts an existing leverage/balance recap test to expect whole-percent math.
Reviewed by Cursor Bugbot for commit edba250. Bugbot is set up for automated code reviews on this repo. Configure here.