Skip to content

Commit cf5fdff

Browse files
authored
fix: e2e liquidation (#29993)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Fix perps e2e liquidation test <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to E2E test flows and selectors; low product risk but could affect CI stability if timing/mocks are still flaky. > > **Overview** > Refactors the perps liquidation smoke test to be deterministic and less flaky by extracting shared setup (fixtures/mocks), parameterizing mark prices by `long`/`short` direction, and adding explicit waits/retries to confirm the position closes after liquidation. > > Adds a reusable `openPosition(symbol, direction)` helper in `perps.flow.ts` (including optional “Explore crypto” handling) and improves tap logging for the perps long button to aid debugging. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit dd10775. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 119ae63 commit cf5fdff

3 files changed

Lines changed: 157 additions & 83 deletions

File tree

tests/flows/perps.flow.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import {
55
encapsulatedAction,
66
} from '../framework';
77
import PerpsMarketDetailsView from '../page-objects/Perps/PerpsMarketDetailsView';
8+
import PerpsHomeView from '../page-objects/Perps/PerpsHomeView';
9+
import PerpsMarketListView from '../page-objects/Perps/PerpsMarketListView';
810
import PerpsOrderView from '../page-objects/Perps/PerpsOrderView';
11+
import WalletView from '../page-objects/wallet/WalletView';
912

1013
/**
1114
* Checks if the position is open by checking if the close button is visible.
1215
* @returns {Promise<boolean>} True if the position is open, false otherwise.
1316
*/
1417
export const isPositionOpen = async (timeout = 5000): Promise<boolean> => {
15-
let isPositionOpen = false;
18+
let positionOpen = false;
1619
await encapsulatedAction({
1720
detox: async () => {
1821
try {
@@ -21,24 +24,24 @@ export const isPositionOpen = async (timeout = 5000): Promise<boolean> => {
2124
timeout,
2225
description: 'Close position button',
2326
});
24-
isPositionOpen = true;
27+
positionOpen = true;
2528
} catch {
26-
isPositionOpen = false;
29+
positionOpen = false;
2730
}
2831
},
2932
appium: async () => {
3033
try {
3134
const closeEl = await asPlaywrightElement(
3235
PerpsMarketDetailsView.closeButton,
3336
);
34-
isPositionOpen = await closeEl.isVisible();
37+
positionOpen = await closeEl.isVisible();
3538
} catch {
3639
// Element lookup timed out — position is not open
37-
isPositionOpen = false;
40+
positionOpen = false;
3841
}
3942
},
4043
});
41-
return isPositionOpen;
44+
return positionOpen;
4245
};
4346

4447
export const waitForPositionOpen = async (
@@ -103,3 +106,24 @@ export const waitForOrderScreenVisible = async (
103106
}
104107
throw new Error(`Order screen not visible after ${timeout}ms`);
105108
};
109+
110+
export type PerpsPositionDirection = 'long' | 'short';
111+
112+
export const openPosition = async (
113+
symbol: string,
114+
direction: PerpsPositionDirection,
115+
): Promise<void> => {
116+
await WalletView.scrollAndTapPerpsSection();
117+
await PerpsHomeView.tapExploreCryptoIfVisible();
118+
119+
await PerpsMarketListView.selectMarket(symbol);
120+
if (direction === 'long') {
121+
await PerpsMarketDetailsView.tapLongButton();
122+
} else {
123+
await PerpsMarketDetailsView.tapShortButton();
124+
}
125+
126+
await PerpsOrderView.tapPlaceOrderButton();
127+
await PerpsMarketDetailsView.waitForScreenReady();
128+
await PerpsMarketDetailsView.expectClosePositionButtonVisible();
129+
};

tests/page-objects/Perps/PerpsMarketDetailsView.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ class PerpsMarketDetailsView {
231231
await Utilities.waitForElementToBeEnabled(
232232
this.longButton as DetoxElement,
233233
);
234-
await Gestures.waitAndTap(this.longButton);
234+
await Gestures.waitAndTap(this.longButton, {
235+
elemDescription: 'Perps Long button',
236+
});
235237
},
236238
appium: async () => {
237239
await PlaywrightGestures.waitAndTap(

tests/smoke/perps/perps-position-liquidation.spec.ts

Lines changed: 124 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,123 +2,171 @@ import { loginToApp } from '../../flows/wallet.flow';
22
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
33
import { SmokePerps } from '../../tags';
44
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
5-
import WalletView from '../../page-objects/wallet/WalletView';
6-
import PerpsMarketListView from '../../page-objects/Perps/PerpsMarketListView';
75
import {
86
PERPS_ARBITRUM_MOCKS,
97
mockPerpsGeolocation,
108
} from '../../api-mocking/mock-responses/perps-arbitrum-mocks';
119
import PerpsMarketDetailsView from '../../page-objects/Perps/PerpsMarketDetailsView';
12-
import PerpsView from '../../page-objects/Perps/PerpsView';
13-
import PerpsOrderView from '../../page-objects/Perps/PerpsOrderView';
1410
import PerpsE2EModifiers from '../../helpers/perps/perps-modifiers';
15-
import { createLogger, LogLevel, type TestSuiteParams } from '../../framework';
11+
import {
12+
createLogger,
13+
LogLevel,
14+
Utilities,
15+
type TestSuiteParams,
16+
} from '../../framework';
1617
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
1718
import { Mockttp } from 'mockttp';
1819
import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper';
1920
import { remoteFeatureFlagHomepageSectionsV1Enabled } from '../../api-mocking/mock-responses/feature-flags-mocks';
21+
import CommandQueueServer from '../../framework/fixtures/CommandQueueServer';
22+
import {
23+
openPosition,
24+
type PerpsPositionDirection,
25+
} from '../../flows/perps.flow';
2026

2127
const logger = createLogger({
2228
name: 'PerpsPositionLiquidationSpec',
2329
level: LogLevel.INFO,
2430
});
2531

26-
// Skipped until liquidation mock + assertions are verified stable on iOS and Android in CI.
27-
describe.skip(SmokePerps('Perps Position Liquidation'), () => {
28-
it('opens a long position and gets liquidated', async () => {
32+
const MARKET_SYMBOL = 'ETH';
33+
const POSITION_DIRECTION: PerpsPositionDirection = 'long';
34+
const MARK_PRICE_BY_DIRECTION: Record<
35+
PerpsPositionDirection,
36+
{
37+
nonLiquidating: string;
38+
liquidating: string;
39+
liquidatingPriceDirection: 'below' | 'above';
40+
}
41+
> = {
42+
long: {
43+
nonLiquidating: '2400.00',
44+
liquidating: '1.00',
45+
liquidatingPriceDirection: 'below',
46+
},
47+
short: {
48+
nonLiquidating: '2600.00',
49+
liquidating: '100000.00',
50+
liquidatingPriceDirection: 'above',
51+
},
52+
};
53+
54+
const setupPerpsMocks = async (mockServer: Mockttp) => {
55+
await setupRemoteFeatureFlagsMock(mockServer, {
56+
...remoteFeatureFlagHomepageSectionsV1Enabled(),
57+
});
58+
await PERPS_ARBITRUM_MOCKS(mockServer);
59+
await mockPerpsGeolocation(mockServer, RampsRegions[RampsRegionsEnum.SPAIN]);
60+
};
61+
62+
const buildPerpsFixture = () =>
63+
new FixtureBuilder()
64+
.withPerpsProfile('no-positions')
65+
.withPerpsFirstTimeUser(false)
66+
.withNetworkController({
67+
type: 'rpc',
68+
chainId: '0xa4b1',
69+
rpcUrl: 'https://arb1.arbitrum.io/rpc',
70+
nickname: 'Arbitrum One',
71+
ticker: 'ETH',
72+
})
73+
.withTokensForAllPopularNetworks([
74+
{
75+
address: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8',
76+
symbol: 'USDC',
77+
decimals: 6,
78+
name: 'USD Coin',
79+
type: 'erc20',
80+
},
81+
])
82+
.withPopularNetworks()
83+
.build();
84+
85+
const expectPositionClosedAfterLiquidation = async () => {
86+
await Utilities.executeWithRetry(
87+
async () => {
88+
await PerpsMarketDetailsView.expectClosePositionButtonNotVisible();
89+
},
90+
{
91+
interval: 1000,
92+
timeout: 30000,
93+
description: 'wait for Close position to disappear after liquidation',
94+
},
95+
);
96+
};
97+
98+
const queueLiquidationCheckAtPrice = async (
99+
commandQueueServer: CommandQueueServer,
100+
symbol: string,
101+
price: string,
102+
) => {
103+
await PerpsE2EModifiers.updateMarketPriceServer(
104+
commandQueueServer,
105+
symbol,
106+
price,
107+
);
108+
await PerpsE2EModifiers.triggerLiquidationServer(commandQueueServer, symbol);
109+
};
110+
111+
const waitForCommandQueueToProcess = async (
112+
commandQueueServer: CommandQueueServer,
113+
) => {
114+
commandQueueServer.requestStateExport();
115+
await commandQueueServer.getExportedState();
116+
};
117+
118+
describe(SmokePerps('Perps Position Liquidation'), () => {
119+
it(`liquidates a ${POSITION_DIRECTION} position when mark price moves ${MARK_PRICE_BY_DIRECTION[POSITION_DIRECTION].liquidatingPriceDirection} liquidation price`, async () => {
29120
await withFixtures(
30121
{
31-
fixture: new FixtureBuilder()
32-
.withPerpsProfile('no-positions')
33-
.withPerpsFirstTimeUser(false)
34-
.withNetworkController({
35-
type: 'rpc',
36-
chainId: '0xa4b1',
37-
rpcUrl: 'https://arb1.arbitrum.io/rpc',
38-
nickname: 'Arbitrum One',
39-
ticker: 'ETH',
40-
})
41-
.withTokensForAllPopularNetworks([
42-
{
43-
address: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8',
44-
symbol: 'USDC',
45-
decimals: 6,
46-
name: 'USD Coin',
47-
type: 'erc20',
48-
},
49-
])
50-
.withPopularNetworks()
51-
.build(),
122+
fixture: buildPerpsFixture(),
52123
restartDevice: true,
53-
testSpecificMock: async (mockServer: Mockttp) => {
54-
await setupRemoteFeatureFlagsMock(mockServer, {
55-
...remoteFeatureFlagHomepageSectionsV1Enabled(),
56-
});
57-
await PERPS_ARBITRUM_MOCKS(mockServer);
58-
await mockPerpsGeolocation(
59-
mockServer,
60-
RampsRegions[RampsRegionsEnum.SPAIN],
61-
);
62-
},
124+
testSpecificMock: setupPerpsMocks,
63125
useCommandQueueServer: true,
64126
},
65127
async ({ commandQueueServer }: TestSuiteParams) => {
66128
if (!commandQueueServer) {
67129
throw new Error('Command queue server not found');
68130
}
69-
logger.info('💰 Using E2E mock balance - no wallet import needed');
70-
logger.info('🎯 Mock account: $10,000 total, $8,000 available');
71-
await loginToApp();
72-
await device.disableSynchronization();
73131

74-
// Navigate to Perps via homepage section (same click path as smoke perps tests)
75-
await WalletView.scrollAndTapPerpsSection();
76-
await PerpsMarketListView.selectMarket('ETH');
77-
await PerpsMarketDetailsView.tapLongButton();
78-
await PerpsOrderView.tapPlaceOrderButton();
132+
logger.info('Using E2E mock balance - no wallet import needed');
133+
logger.info(`Opening ${MARKET_SYMBOL} ${POSITION_DIRECTION} position`);
79134

80-
// Wait for market details like perps-position.spec: a price push before the
81-
// sheet finishes closing can redraw the chart and leave the scroll view
82-
// under the 75% visible / not-obscured threshold on Android.
83-
await PerpsMarketDetailsView.waitForScreenReady();
84-
await PerpsMarketDetailsView.expectClosePositionButtonVisible();
135+
await loginToApp();
136+
await device.disableSynchronization();
85137

86-
logger.info('📈 E2E Mock: Order placed successfully');
87-
logger.info('💎 E2E Mock: Position created with mock data');
138+
await openPosition(MARKET_SYMBOL, POSITION_DIRECTION);
88139

89-
await PerpsE2EModifiers.updateMarketPriceServer(
90-
commandQueueServer,
91-
'ETH',
92-
'2125.00',
140+
logger.info(
141+
`Pushing ${MARKET_SYMBOL} mark price that should keep ${POSITION_DIRECTION} open`,
93142
);
94-
await PerpsE2EModifiers.triggerLiquidationServer(
143+
144+
await queueLiquidationCheckAtPrice(
95145
commandQueueServer,
96-
'ETH',
146+
MARKET_SYMBOL,
147+
MARK_PRICE_BY_DIRECTION[POSITION_DIRECTION].nonLiquidating,
97148
);
149+
await waitForCommandQueueToProcess(commandQueueServer);
150+
98151
logger.info(
99-
'E2E Mock: First liquidation attempt at 2125 — position expected to stay open until a lower mark',
152+
`Verifying ${MARKET_SYMBOL} ${POSITION_DIRECTION} remains open after non-liquidating price change`,
100153
);
154+
await PerpsMarketDetailsView.expectClosePositionButtonVisible();
101155

102-
await PerpsView.tapBackButtonPositionSheet();
103-
await PerpsE2EModifiers.updateMarketPriceServer(
104-
commandQueueServer,
105-
'ETH',
106-
'1200.00',
156+
logger.info(
157+
`Pushing ${MARKET_SYMBOL} mark price ${MARK_PRICE_BY_DIRECTION[POSITION_DIRECTION].liquidatingPriceDirection} liquidation price to liquidate ${POSITION_DIRECTION}`,
107158
);
108-
await PerpsE2EModifiers.triggerLiquidationServer(
159+
160+
await queueLiquidationCheckAtPrice(
109161
commandQueueServer,
110-
'ETH',
162+
MARKET_SYMBOL,
163+
MARK_PRICE_BY_DIRECTION[POSITION_DIRECTION].liquidating,
111164
);
112165

113166
logger.info(
114-
'E2E Mock: Second liquidation attempt at 1200 — position expected to clear',
115-
);
116-
117-
await PerpsView.expectPositionRowNotVisibleAnyLeverage(
118-
'ETH',
119-
'long',
120-
0,
167+
`Verifying ${MARKET_SYMBOL} ${POSITION_DIRECTION} is closed after liquidation`,
121168
);
169+
await expectPositionClosedAfterLiquidation();
122170
},
123171
);
124172
});

0 commit comments

Comments
 (0)