Skip to content

Commit 18be11f

Browse files
authored
test: add smoke swap trending tokens e2e coverage (#26910)
## **Description** Adds minimal SmokeTrade E2E coverage for Bridge Swap Trending Tokens zero-state behavior as a follow-up to the feature PR to keep implementation and test review separated. Scope is intentionally narrow: - Verifies zero-state trending section visibility and filter interaction flow. - Verifies row navigation behavior from trending list. - Uses existing smoke framework/page-object patterns. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: Follow-up coverage for #26620 (SWAPS-4038) ## **Manual testing steps** ```gherkin Feature: Swap trending tokens smoke coverage Scenario: user validates bridge zero-state trending interactions Given the app is running with swap trending tokens enabled And the user is on the Swap screen in Bridge zero state When the user opens and applies trending filters Then the trending list reflects the selected filters When the user taps a trending token row Then the user is navigated to that token's asset details ``` ## **Screenshots/Recordings** ### **Before** N/A (test-only follow-up PR) ### **After** N/A (test-only follow-up PR) ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] 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. ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Test-only additions; primary risk is potential flakiness due to new Detox selectors, scrolling, and network mocking for trending tokens/feature flags. > > **Overview** > Adds SmokeTrade E2E coverage for **Swap Trending Tokens (Bridge zero-state)**, including a new `SwapTrendingTokensView` page object for interacting with the trending section, filters, bottom sheets, and token rows. > > Introduces a new smoke spec that enables the `swapsTrendingTokens` remote flag, mocks the `/v3/tokens/trending` proxy responses (all networks vs Base-only), verifies filter behavior and default sort text, confirms tapping a row opens token details, and asserts the trending section hides once a swap amount is entered. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7e90d12. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
2 parents 39cb2ce + 7e90d12 commit 18be11f

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Assertions, Gestures, Matchers } from '../../framework';
2+
3+
const SwapTrendingTokensSelectorIDs = {
4+
SECTION: 'bridge-trending-tokens-section',
5+
PRICE_FILTER: 'bridge-trending-price-filter',
6+
NETWORK_FILTER: 'bridge-trending-network-filter',
7+
TIME_FILTER: 'bridge-trending-time-filter',
8+
PRICE_BOTTOM_SHEET: 'trending-token-price-change-bottom-sheet',
9+
NETWORK_BOTTOM_SHEET: 'trending-token-network-bottom-sheet',
10+
TIME_BOTTOM_SHEET: 'trending-token-time-bottom-sheet',
11+
INNER_LIST: 'trending-tokens-list',
12+
CLOSE_BUTTON: 'close-button',
13+
TIME_SELECT_6H: 'time-select-6h',
14+
BRIDGE_VIEW_SCROLL: 'bridge-view-scroll',
15+
} as const;
16+
17+
class SwapTrendingTokensView {
18+
private el(id: string): DetoxElement {
19+
return Matchers.getElementByID(id);
20+
}
21+
22+
async expectSectionVisible(timeout = 10000): Promise<void> {
23+
await Assertions.expectElementToBeVisible(
24+
this.el(SwapTrendingTokensSelectorIDs.SECTION),
25+
{
26+
timeout,
27+
description: 'Trending section should be visible',
28+
},
29+
);
30+
}
31+
32+
async expectSectionNotVisible(timeout = 10000): Promise<void> {
33+
await Assertions.expectElementToNotBeVisible(
34+
this.el(SwapTrendingTokensSelectorIDs.SECTION),
35+
{
36+
timeout,
37+
description: 'Trending section should not be visible',
38+
},
39+
);
40+
}
41+
42+
async expectNoInnerList(): Promise<void> {
43+
await Assertions.expectElementToNotBeVisible(
44+
this.el(SwapTrendingTokensSelectorIDs.INNER_LIST),
45+
{
46+
description:
47+
'Bridge trending should not render an inner list scroll container',
48+
},
49+
);
50+
}
51+
52+
async expectPriceBottomSheetVisible(): Promise<void> {
53+
await Assertions.expectElementToBeVisible(
54+
this.el(SwapTrendingTokensSelectorIDs.PRICE_BOTTOM_SHEET),
55+
{
56+
description: 'Price sort bottom sheet should be visible',
57+
},
58+
);
59+
}
60+
61+
async expectTimeBottomSheetVisible(): Promise<void> {
62+
await Assertions.expectElementToBeVisible(
63+
this.el(SwapTrendingTokensSelectorIDs.TIME_BOTTOM_SHEET),
64+
{
65+
description: 'Time bottom sheet should be visible',
66+
},
67+
);
68+
}
69+
70+
async expectNetworkBottomSheetVisible(): Promise<void> {
71+
await Assertions.expectElementToBeVisible(
72+
this.el(SwapTrendingTokensSelectorIDs.NETWORK_BOTTOM_SHEET),
73+
{
74+
description: 'Network bottom sheet should be visible',
75+
},
76+
);
77+
}
78+
79+
tokenRow(assetId: string): DetoxElement {
80+
return Matchers.getElementByID(`trending-token-row-item-${assetId}`);
81+
}
82+
83+
async scrollToFilters(): Promise<void> {
84+
await Gestures.scrollToElement(
85+
this.el(SwapTrendingTokensSelectorIDs.PRICE_FILTER),
86+
Matchers.getIdentifier(SwapTrendingTokensSelectorIDs.BRIDGE_VIEW_SCROLL),
87+
{
88+
direction: 'down',
89+
scrollAmount: 250,
90+
elemDescription: 'Scroll bridge view to trending filters',
91+
},
92+
);
93+
}
94+
95+
async openPriceFilter(): Promise<void> {
96+
await Gestures.waitAndTap(
97+
this.el(SwapTrendingTokensSelectorIDs.PRICE_FILTER),
98+
{
99+
elemDescription: 'Tap trending price filter',
100+
},
101+
);
102+
}
103+
104+
async openTimeFilter(): Promise<void> {
105+
await Gestures.waitAndTap(
106+
this.el(SwapTrendingTokensSelectorIDs.TIME_FILTER),
107+
{
108+
elemDescription: 'Tap trending time filter',
109+
},
110+
);
111+
}
112+
113+
async openNetworkFilter(): Promise<void> {
114+
await Gestures.waitAndTap(
115+
this.el(SwapTrendingTokensSelectorIDs.NETWORK_FILTER),
116+
{
117+
elemDescription: 'Tap trending network filter',
118+
},
119+
);
120+
}
121+
122+
async closeBottomSheet(): Promise<void> {
123+
await Gestures.waitAndTap(
124+
this.el(SwapTrendingTokensSelectorIDs.CLOSE_BUTTON),
125+
{
126+
elemDescription: 'Close trending bottom sheet',
127+
},
128+
);
129+
}
130+
131+
async selectTimeSixHours(): Promise<void> {
132+
await Gestures.waitAndTap(
133+
this.el(SwapTrendingTokensSelectorIDs.TIME_SELECT_6H),
134+
{
135+
elemDescription: 'Select 6h time filter',
136+
},
137+
);
138+
}
139+
140+
async selectNetworkByName(networkName: string): Promise<void> {
141+
await Gestures.waitAndTap(Matchers.getElementByText(networkName), {
142+
elemDescription: `Select trending network ${networkName}`,
143+
});
144+
}
145+
146+
async tapTokenRow(assetId: string): Promise<void> {
147+
await Gestures.waitAndTap(this.tokenRow(assetId), {
148+
elemDescription: `Tap trending token row ${assetId}`,
149+
});
150+
}
151+
152+
async expectTokenRowVisible(assetId: string): Promise<void> {
153+
await Assertions.expectElementToBeVisible(this.tokenRow(assetId), {
154+
timeout: 10000,
155+
description: `Trending token row ${assetId} should be visible`,
156+
});
157+
}
158+
159+
async expectTokenRowNotVisible(assetId: string): Promise<void> {
160+
await Assertions.expectElementToNotBeVisible(this.tokenRow(assetId), {
161+
timeout: 10000,
162+
description: `Trending token row ${assetId} should not be visible`,
163+
});
164+
}
165+
}
166+
167+
export default new SwapTrendingTokensView();
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { Mockttp } from 'mockttp';
2+
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
3+
import { LocalNode, LocalNodeType } from '../../framework/types';
4+
import { loginToApp } from '../../flows/wallet.flow';
5+
import WalletView from '../../page-objects/wallet/WalletView';
6+
import QuoteView from '../../page-objects/swaps/QuoteView';
7+
import SwapTrendingTokensView from '../../page-objects/swaps/SwapTrendingTokensView';
8+
import { Assertions } from '../../framework';
9+
import CommonView from '../../page-objects/CommonView';
10+
import TokenOverview from '../../page-objects/wallet/TokenOverview';
11+
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
12+
import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTestEnvironment';
13+
import { testSpecificMock } from '../../helpers/swap/bridge-mocks';
14+
import { GET_QUOTE_ETH_USDC_RESPONSE } from '../../helpers/swap/constants';
15+
import { getDecodedProxiedURL } from '../notifications/utils/helpers';
16+
import { SmokeTrade } from '../../tags';
17+
import { AnvilPort } from '../../framework/fixtures/FixtureUtils';
18+
import { AnvilManager } from '../../seeder/anvil-manager';
19+
import enContent from '../../../locales/languages/en.json';
20+
import { createRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper';
21+
import { setupMockRequest } from '../../api-mocking/helpers/mockHelpers';
22+
23+
const BASE_CHAIN_ID_DECIMAL = '8453';
24+
25+
const ETHEREUM_TRENDING_ASSET_ID =
26+
'eip155:1/erc20:0x1111111111111111111111111111111111111111';
27+
const BASE_TRENDING_ASSET_ID =
28+
'eip155:8453/erc20:0x2222222222222222222222222222222222222222';
29+
30+
const TRENDING_ALL_NETWORKS_RESPONSE = [
31+
{
32+
assetId: ETHEREUM_TRENDING_ASSET_ID,
33+
symbol: 'ETHX',
34+
name: 'Ethereum Trending',
35+
decimals: 18,
36+
price: '10',
37+
aggregatedUsdVolume: 2000000,
38+
marketCap: 100000000,
39+
priceChangePct: {
40+
h24: '0.2',
41+
h6: '0.1',
42+
h1: '0.01',
43+
m5: '0.001',
44+
},
45+
},
46+
{
47+
assetId: BASE_TRENDING_ASSET_ID,
48+
symbol: 'BASEX',
49+
name: 'Base Trending',
50+
decimals: 18,
51+
price: '8',
52+
aggregatedUsdVolume: 3000000,
53+
marketCap: 200000000,
54+
priceChangePct: {
55+
h24: '0.3',
56+
h6: '0.15',
57+
h1: '0.02',
58+
m5: '0.002',
59+
},
60+
},
61+
];
62+
63+
const TRENDING_BASE_ONLY_RESPONSE = [TRENDING_ALL_NETWORKS_RESPONSE[1]];
64+
65+
const setupSwapsTrendingTokensMock = async (mockServer: Mockttp) => {
66+
const { response } = createRemoteFeatureFlagsMock({
67+
swapsTrendingTokens: true,
68+
});
69+
70+
await setupMockRequest(
71+
mockServer,
72+
{
73+
requestMethod: 'GET',
74+
url: /client-config\.api\.cx\.metamask\.io\/v1\/flags/i,
75+
response,
76+
responseCode: 200,
77+
},
78+
1001,
79+
);
80+
};
81+
82+
const setupTrendingTokensMock = async (mockServer: Mockttp) => {
83+
await mockServer
84+
.forGet('/proxy')
85+
.matching((request) => {
86+
const decodedUrl = getDecodedProxiedURL(request.url);
87+
return /\/v3\/tokens\/trending/.test(decodedUrl);
88+
})
89+
.asPriority(1001)
90+
.thenCallback((request) => {
91+
const decodedUrl = getDecodedProxiedURL(request.url);
92+
const isBaseOnlyRequest = decodedUrl.includes(
93+
`chainIds=eip155:${BASE_CHAIN_ID_DECIMAL}`,
94+
);
95+
96+
return {
97+
statusCode: 200,
98+
json: isBaseOnlyRequest
99+
? TRENDING_BASE_ONLY_RESPONSE
100+
: TRENDING_ALL_NETWORKS_RESPONSE,
101+
};
102+
});
103+
};
104+
105+
const setupQuoteFallbackMock = async (mockServer: Mockttp) => {
106+
await setupMockRequest(
107+
mockServer,
108+
{
109+
requestMethod: 'GET',
110+
url: /getQuote/i,
111+
response: GET_QUOTE_ETH_USDC_RESPONSE,
112+
responseCode: 200,
113+
},
114+
1000,
115+
);
116+
};
117+
118+
const openSwapFromWalletActions = async () => {
119+
await loginToApp();
120+
await prepareSwapsTestEnvironment();
121+
await WalletView.tapWalletSwapButton();
122+
};
123+
124+
const withBridgeFixtures = async (run: () => Promise<void>) => {
125+
await withFixtures(
126+
{
127+
fixture: ({ localNodes }: { localNodes?: LocalNode[] }) => {
128+
const node = localNodes?.[0] as unknown as AnvilManager;
129+
const rpcPort =
130+
node instanceof AnvilManager
131+
? (node.getPort() ?? AnvilPort())
132+
: undefined;
133+
134+
return new FixtureBuilder()
135+
.withNetworkController({
136+
providerConfig: {
137+
chainId: '0x1',
138+
rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`,
139+
type: 'custom',
140+
nickname: 'Localhost',
141+
ticker: 'ETH',
142+
},
143+
})
144+
.withDisabledSmartTransactions()
145+
.build();
146+
},
147+
localNodeOptions: [
148+
{
149+
type: LocalNodeType.anvil,
150+
options: {
151+
chainId: 1,
152+
},
153+
},
154+
],
155+
restartDevice: true,
156+
testSpecificMock: async (mockServer: Mockttp) => {
157+
await testSpecificMock(mockServer);
158+
await setupSwapsTrendingTokensMock(mockServer);
159+
await setupTrendingTokensMock(mockServer);
160+
await setupQuoteFallbackMock(mockServer);
161+
},
162+
},
163+
run,
164+
);
165+
};
166+
167+
describe(SmokeTrade('Swap Trending Tokens (Bridge zero-state)'), () => {
168+
beforeEach(() => {
169+
jest.setTimeout(180000);
170+
});
171+
172+
it('zero-state trending supports filters then row navigation', async () => {
173+
await withBridgeFixtures(async () => {
174+
await openSwapFromWalletActions();
175+
176+
await SwapTrendingTokensView.expectSectionVisible();
177+
await SwapTrendingTokensView.expectNoInnerList();
178+
179+
await SwapTrendingTokensView.scrollToFilters();
180+
181+
await SwapTrendingTokensView.openPriceFilter();
182+
await SwapTrendingTokensView.expectPriceBottomSheetVisible();
183+
await Assertions.expectTextDisplayed(enContent.trending.high_to_low, {
184+
description: 'Default price change sort should be high to low',
185+
});
186+
await SwapTrendingTokensView.closeBottomSheet();
187+
188+
await SwapTrendingTokensView.openTimeFilter();
189+
await SwapTrendingTokensView.expectTimeBottomSheetVisible();
190+
await SwapTrendingTokensView.selectTimeSixHours();
191+
192+
await SwapTrendingTokensView.openNetworkFilter();
193+
await SwapTrendingTokensView.expectNetworkBottomSheetVisible();
194+
await SwapTrendingTokensView.selectNetworkByName('Base');
195+
196+
await SwapTrendingTokensView.expectTokenRowVisible(
197+
BASE_TRENDING_ASSET_ID,
198+
);
199+
await SwapTrendingTokensView.expectTokenRowNotVisible(
200+
ETHEREUM_TRENDING_ASSET_ID,
201+
);
202+
203+
await SwapTrendingTokensView.tapTokenRow(BASE_TRENDING_ASSET_ID);
204+
await Assertions.expectElementToBeVisible(TokenOverview.tokenPrice, {
205+
timeout: 10000,
206+
description: 'Token details should open from trending token row tap',
207+
});
208+
209+
await CommonView.tapBackButton();
210+
await SwapTrendingTokensView.expectSectionVisible();
211+
212+
await QuoteView.tapSourceAmountInput();
213+
await QuoteView.enterAmount('1');
214+
215+
await SwapTrendingTokensView.expectSectionNotVisible();
216+
});
217+
});
218+
});

0 commit comments

Comments
 (0)