Skip to content

Commit bbf465f

Browse files
authored
test: add swaps trending e2e tests (#27118)
## **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** > Primarily adds/adjusts test code and refactors `testID` constants; production behavior is unchanged aside from `testID` wiring, so risk is low and limited to potential selector breakage in tests. > > **Overview** > Adds a new SmokeTrade Detox spec validating Bridge *zero-state* trending tokens behavior end-to-end: feature-flagged enablement, filter bottom sheets (price/time/network), token-row navigation to asset details, and trending section hiding once a quote amount is entered. > > Refactors trending token `testID`s out of `BridgeViewSelectorsIDs` into a dedicated `BridgeTrendingTokensSectionTestIds` module, updates `BridgeTrendingTokensSection` and related unit tests/mocks accordingly, and introduces a `SwapTrendingTokensView` page object to drive the new E2E flow. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ffebb00. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent f10a65b commit bbf465f

7 files changed

Lines changed: 441 additions & 26 deletions

File tree

app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { BridgeViewSelectorsIDs } from './BridgeView.testIds';
2929
import { MOCK_ENTROPY_SOURCE as mockEntropySource } from '../../../../../util/test/keyringControllerTestUtils';
3030
import { RootState } from '../../../../../reducers';
3131
import { mockQuoteWithMetadata } from '../../_mocks_/bridgeQuoteWithMetadata';
32+
import { BridgeTrendingTokensSectionTestIds } from '../../components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.testIds';
3233

3334
// Mock the account-tree-controller file that imports the problematic module
3435
jest.mock(
@@ -291,15 +292,15 @@ jest.mock(
291292
() => {
292293
const React = jest.requireActual('react');
293294
const { View } = jest.requireActual('react-native');
294-
const { BridgeViewSelectorsIDs: BridgeViewTestIds } = jest.requireActual(
295-
'./BridgeView.testIds',
296-
);
295+
const TrendingTokensSectionTestIds = jest.requireActual(
296+
'../../components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.testIds',
297+
).BridgeTrendingTokensSectionTestIds;
297298

298299
return {
299300
__esModule: true,
300301
default: () =>
301302
React.createElement(View, {
302-
testID: BridgeViewTestIds.TRENDING_TOKENS_SECTION,
303+
testID: TrendingTokensSectionTestIds.SECTION,
303304
}),
304305
};
305306
},
@@ -853,7 +854,7 @@ describe('BridgeView', () => {
853854
expect(queryByTestId('banneralert')).toBeNull();
854855
expect(queryByTestId('edit-slippage-button')).toBeNull();
855856
expect(
856-
queryByTestId(BridgeViewSelectorsIDs.TRENDING_TOKENS_SECTION),
857+
queryByTestId(BridgeTrendingTokensSectionTestIds.SECTION),
857858
).toBeNull();
858859
expect(queryByText('Fetching quote')).toBeNull();
859860
});
@@ -929,7 +930,7 @@ describe('BridgeView', () => {
929930
});
930931
expect(queryByTestId('edit-slippage-button')).toBeNull();
931932
expect(
932-
queryByTestId(BridgeViewSelectorsIDs.TRENDING_TOKENS_SECTION),
933+
queryByTestId(BridgeTrendingTokensSectionTestIds.SECTION),
933934
).toBeNull();
934935
});
935936

@@ -967,7 +968,7 @@ describe('BridgeView', () => {
967968
});
968969
expect(getByTestId(BridgeViewSelectorsIDs.CONFIRM_BUTTON)).toBeTruthy();
969970
expect(
970-
queryByTestId(BridgeViewSelectorsIDs.TRENDING_TOKENS_SECTION),
971+
queryByTestId(BridgeTrendingTokensSectionTestIds.SECTION),
971972
).toBeNull();
972973
});
973974

@@ -1020,7 +1021,7 @@ describe('BridgeView', () => {
10201021
);
10211022

10221023
expect(
1023-
getByTestId(BridgeViewSelectorsIDs.TRENDING_TOKENS_SECTION),
1024+
getByTestId(BridgeTrendingTokensSectionTestIds.SECTION),
10241025
).toBeTruthy();
10251026
expect(queryByTestId('edit-slippage-button')).toBeNull();
10261027
});

app/components/UI/Bridge/Views/BridgeView/BridgeView.testIds.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@ export const BridgeViewSelectorsIDs = {
66
CONFIRM_BUTTON: 'bridge-confirm-button',
77
CONFIRM_BUTTON_KEYPAD: 'bridge-confirm-button-keypad',
88
BRIDGE_VIEW_SCROLL: 'bridge-view-scroll',
9-
TRENDING_TOKENS_SECTION: 'bridge-trending-tokens-section',
10-
TRENDING_PRICE_FILTER: 'bridge-trending-price-filter',
11-
TRENDING_NETWORK_FILTER: 'bridge-trending-network-filter',
12-
TRENDING_TIME_FILTER: 'bridge-trending-time-filter',
13-
TRENDING_SHOW_MORE: 'bridge-trending-show-more',
149
QUOTE_DETAILS_SKELETON: 'bridge-quote-details-skeleton',
1510
} as const;
1611

app/components/UI/Bridge/components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.test.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import BridgeTrendingTokensSection from './BridgeTrendingTokensSection';
55
import { useTokenListFilters } from '../../../Trending/hooks/useTokenListFilters/useTokenListFilters';
66
import { useTrendingRequest } from '../../../Trending/hooks/useTrendingRequest/useTrendingRequest';
7+
import { BridgeTrendingTokensSectionTestIds } from './BridgeTrendingTokensSection.testIds';
78

89
jest.mock('react-redux', () => ({
910
useSelector: jest.fn(() => ({})),
@@ -30,24 +31,24 @@ jest.mock('../../../Trending/utils/sortTrendingTokens', () => ({
3031
jest.mock(
3132
'../../../Trending/components/TrendingTokenRowItem/TrendingTokenRowItem',
3233
() => {
33-
const React = jest.requireActual('react');
34+
const ReactLib = jest.requireActual('react');
3435
const { View } = jest.requireActual('react-native');
3536
return {
3637
__esModule: true,
3738
default: ({ token }: { token: { assetId: string } }) =>
38-
React.createElement(View, { testID: `row-${token.assetId}` }),
39+
ReactLib.createElement(View, { testID: `row-${token.assetId}` }),
3940
};
4041
},
4142
);
4243

4344
jest.mock(
4445
'../../../Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton',
4546
() => {
46-
const React = jest.requireActual('react');
47+
const ReactLib = jest.requireActual('react');
4748
const { View } = jest.requireActual('react-native');
4849
return {
4950
__esModule: true,
50-
default: () => React.createElement(View, { testID: 'skeleton-row' }),
51+
default: () => ReactLib.createElement(View, { testID: 'skeleton-row' }),
5152
};
5253
},
5354
);
@@ -118,7 +119,9 @@ describe('BridgeTrendingTokensSection', () => {
118119

119120
const rows = getAllByTestId(/^row-/);
120121
expect(rows).toHaveLength(12);
121-
expect(getByTestId('bridge-trending-show-more')).toBeTruthy();
122+
expect(
123+
getByTestId(BridgeTrendingTokensSectionTestIds.SHOW_MORE),
124+
).toBeTruthy();
122125
});
123126

124127
it('appends one chunk when isNearBottom becomes true', () => {
@@ -143,15 +146,19 @@ describe('BridgeTrendingTokensSection', () => {
143146
rerender(<BridgeTrendingTokensSection />);
144147

145148
expect(getAllByTestId(/^row-/)).toHaveLength(8);
146-
expect(queryByTestId('bridge-trending-show-more')).toBeNull();
149+
expect(
150+
queryByTestId(BridgeTrendingTokensSectionTestIds.SHOW_MORE),
151+
).toBeNull();
147152
});
148153

149154
it('does not append chunk while a bottom sheet is open', () => {
150155
const { getAllByTestId, getByTestId, rerender } = render(
151156
<BridgeTrendingTokensSection />,
152157
);
153158

154-
fireEvent.press(getByTestId('bridge-trending-price-filter'));
159+
fireEvent.press(
160+
getByTestId(BridgeTrendingTokensSectionTestIds.PRICE_FILTER),
161+
);
155162

156163
rerender(<BridgeTrendingTokensSection isNearBottom />);
157164

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const BridgeTrendingTokensSectionTestIds = {
2+
SECTION: 'bridge-trending-tokens-section',
3+
PRICE_FILTER: 'bridge-trending-price-filter',
4+
NETWORK_FILTER: 'bridge-trending-network-filter',
5+
TIME_FILTER: 'bridge-trending-time-filter',
6+
SHOW_MORE: 'bridge-trending-show-more',
7+
} as const;
8+
9+
export type BridgeTrendingTokensSectionTestIdsType =
10+
typeof BridgeTrendingTokensSectionTestIds;

app/components/UI/Bridge/components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import { useTokenListFilters } from '../../../Trending/hooks/useTokenListFilters
2727
import { useTrendingRequest } from '../../../Trending/hooks/useTrendingRequest/useTrendingRequest';
2828
import { sortTrendingTokens } from '../../../Trending/utils/sortTrendingTokens';
2929
import { strings } from '../../../../../../locales/i18n';
30-
import { BridgeViewSelectorsIDs } from '../../Views/BridgeView/BridgeView.testIds';
3130
import { getNetworkImageSource } from '../../../../../util/networks';
3231
import { NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../../../constants/bridge';
3332
import type { ProcessedNetwork } from '../../../../hooks/useNetworksByNamespace/useNetworksByNamespace';
3433
import type { CaipChainId } from '@metamask/utils';
3534
import { FilterButton } from '../../../Trending/components/FilterBar/FilterBar';
35+
import { BridgeTrendingTokensSectionTestIds } from './BridgeTrendingTokensSection.testIds';
3636

3737
const TOKEN_CHUNK_SIZE = 12;
3838

@@ -142,7 +142,7 @@ const BridgeTrendingTokensSection = ({
142142
<>
143143
<Box
144144
twClassName="mt-4 px-4 pb-4"
145-
testID={BridgeViewSelectorsIDs.TRENDING_TOKENS_SECTION}
145+
testID={BridgeTrendingTokensSectionTestIds.SECTION}
146146
>
147147
<Text
148148
variant={TextVariant.HeadingLg}
@@ -156,19 +156,19 @@ const BridgeTrendingTokensSection = ({
156156
twClassName="gap-2 mb-3 w-full"
157157
>
158158
<FilterButton
159-
testID={BridgeViewSelectorsIDs.TRENDING_PRICE_FILTER}
159+
testID={BridgeTrendingTokensSectionTestIds.PRICE_FILTER}
160160
onPress={() => setActiveBottomSheet('price_change')}
161161
label={priceChangeButtonText}
162162
twClassName="flex-1"
163163
/>
164164
<FilterButton
165-
testID={BridgeViewSelectorsIDs.TRENDING_NETWORK_FILTER}
165+
testID={BridgeTrendingTokensSectionTestIds.NETWORK_FILTER}
166166
onPress={() => setActiveBottomSheet('network')}
167167
label={selectedNetworkName}
168168
twClassName="flex-1"
169169
/>
170170
<FilterButton
171-
testID={BridgeViewSelectorsIDs.TRENDING_TIME_FILTER}
171+
testID={BridgeTrendingTokensSectionTestIds.TIME_FILTER}
172172
onPress={() => setActiveBottomSheet('time')}
173173
label={selectedTimeOption}
174174
twClassName="w-[72px] shrink-0"
@@ -192,7 +192,7 @@ const BridgeTrendingTokensSection = ({
192192
))}
193193
{!isLoading && hasMore ? (
194194
<Pressable
195-
testID={BridgeViewSelectorsIDs.TRENDING_SHOW_MORE}
195+
testID={BridgeTrendingTokensSectionTestIds.SHOW_MORE}
196196
onPress={loadNextChunk}
197197
style={({ pressed }) =>
198198
tw.style('mt-3 py-2 self-center', pressed && 'opacity-70')
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Assertions, Gestures, Matchers } from '../../framework';
2+
import { BridgeViewSelectorsIDs } from '../../../app/components/UI/Bridge/Views/BridgeView/BridgeView.testIds';
3+
import { BridgeTrendingTokensSectionTestIds } from '../../../app/components/UI/Bridge/components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.testIds';
4+
5+
const SwapTrendingTokensViewTestIds = {
6+
PRICE_BOTTOM_SHEET: 'trending-token-price-change-bottom-sheet',
7+
NETWORK_BOTTOM_SHEET: 'trending-token-network-bottom-sheet',
8+
TIME_BOTTOM_SHEET: 'trending-token-time-bottom-sheet',
9+
INNER_LIST: 'trending-tokens-list',
10+
CLOSE_BUTTON: 'close-button',
11+
TIME_SELECT_6H: 'time-select-6h',
12+
TOKEN_ROW_PREFIX: 'trending-token-row-item-',
13+
} as const;
14+
15+
class SwapTrendingTokensView {
16+
public get section(): DetoxElement {
17+
return Matchers.getElementByID(BridgeTrendingTokensSectionTestIds.SECTION);
18+
}
19+
20+
public get priceFilter(): DetoxElement {
21+
return Matchers.getElementByID(
22+
BridgeTrendingTokensSectionTestIds.PRICE_FILTER,
23+
);
24+
}
25+
26+
public get networkFilter(): DetoxElement {
27+
return Matchers.getElementByID(
28+
BridgeTrendingTokensSectionTestIds.NETWORK_FILTER,
29+
);
30+
}
31+
32+
public get timeFilter(): DetoxElement {
33+
return Matchers.getElementByID(
34+
BridgeTrendingTokensSectionTestIds.TIME_FILTER,
35+
);
36+
}
37+
38+
public get priceBottomSheet(): DetoxElement {
39+
return Matchers.getElementByID(
40+
SwapTrendingTokensViewTestIds.PRICE_BOTTOM_SHEET,
41+
);
42+
}
43+
44+
public get networkBottomSheet(): DetoxElement {
45+
return Matchers.getElementByID(
46+
SwapTrendingTokensViewTestIds.NETWORK_BOTTOM_SHEET,
47+
);
48+
}
49+
50+
public get timeBottomSheet(): DetoxElement {
51+
return Matchers.getElementByID(
52+
SwapTrendingTokensViewTestIds.TIME_BOTTOM_SHEET,
53+
);
54+
}
55+
56+
public get innerList(): DetoxElement {
57+
return Matchers.getElementByID(SwapTrendingTokensViewTestIds.INNER_LIST);
58+
}
59+
60+
public get closeButton(): DetoxElement {
61+
return Matchers.getElementByID(SwapTrendingTokensViewTestIds.CLOSE_BUTTON);
62+
}
63+
64+
public get timeSelectSixHours(): DetoxElement {
65+
return Matchers.getElementByID(
66+
SwapTrendingTokensViewTestIds.TIME_SELECT_6H,
67+
);
68+
}
69+
70+
async expectSectionVisible(timeout = 10000): Promise<void> {
71+
await Assertions.expectElementToBeVisible(this.section, {
72+
timeout,
73+
description: 'Trending section should be visible',
74+
});
75+
}
76+
77+
async expectSectionNotVisible(timeout = 10000): Promise<void> {
78+
await Assertions.expectElementToNotBeVisible(this.section, {
79+
timeout,
80+
description: 'Trending section should not be visible',
81+
});
82+
}
83+
84+
async expectNoInnerList(): Promise<void> {
85+
await Assertions.expectElementToNotBeVisible(this.innerList, {
86+
description:
87+
'Bridge trending should not render an inner list scroll container',
88+
});
89+
}
90+
91+
async expectPriceBottomSheetVisible(): Promise<void> {
92+
await Assertions.expectElementToBeVisible(this.priceBottomSheet, {
93+
description: 'Price sort bottom sheet should be visible',
94+
});
95+
}
96+
97+
async expectTimeBottomSheetVisible(): Promise<void> {
98+
await Assertions.expectElementToBeVisible(this.timeBottomSheet, {
99+
description: 'Time bottom sheet should be visible',
100+
});
101+
}
102+
103+
async expectNetworkBottomSheetVisible(): Promise<void> {
104+
await Assertions.expectElementToBeVisible(this.networkBottomSheet, {
105+
description: 'Network bottom sheet should be visible',
106+
});
107+
}
108+
109+
tokenRow(assetId: string): DetoxElement {
110+
return Matchers.getElementByID(
111+
`${SwapTrendingTokensViewTestIds.TOKEN_ROW_PREFIX}${assetId}`,
112+
);
113+
}
114+
115+
async scrollToFilters(): Promise<void> {
116+
await Gestures.scrollToElement(
117+
this.priceFilter,
118+
Matchers.getIdentifier(BridgeViewSelectorsIDs.BRIDGE_VIEW_SCROLL),
119+
{
120+
direction: 'down',
121+
scrollAmount: 250,
122+
elemDescription: 'Scroll bridge view to trending filters',
123+
},
124+
);
125+
}
126+
127+
async openPriceFilter(): Promise<void> {
128+
await Gestures.waitAndTap(this.priceFilter, {
129+
elemDescription: 'Tap trending price filter',
130+
});
131+
}
132+
133+
async openTimeFilter(): Promise<void> {
134+
await Gestures.waitAndTap(this.timeFilter, {
135+
elemDescription: 'Tap trending time filter',
136+
});
137+
}
138+
139+
async openNetworkFilter(): Promise<void> {
140+
await Gestures.waitAndTap(this.networkFilter, {
141+
elemDescription: 'Tap trending network filter',
142+
});
143+
}
144+
145+
async closeBottomSheet(): Promise<void> {
146+
await Gestures.waitAndTap(this.closeButton, {
147+
elemDescription: 'Close trending bottom sheet',
148+
});
149+
}
150+
151+
async selectTimeSixHours(): Promise<void> {
152+
await Gestures.waitAndTap(this.timeSelectSixHours, {
153+
elemDescription: 'Select 6h time filter',
154+
});
155+
}
156+
157+
async selectNetworkByName(networkName: string): Promise<void> {
158+
await Gestures.waitAndTap(Matchers.getElementByText(networkName), {
159+
elemDescription: `Select trending network ${networkName}`,
160+
});
161+
}
162+
163+
async tapTokenRow(assetId: string): Promise<void> {
164+
await Gestures.waitAndTap(this.tokenRow(assetId), {
165+
elemDescription: `Tap trending token row ${assetId}`,
166+
});
167+
}
168+
169+
async expectTokenRowVisible(assetId: string): Promise<void> {
170+
await Assertions.expectElementToBeVisible(this.tokenRow(assetId), {
171+
timeout: 10000,
172+
description: `Trending token row ${assetId} should be visible`,
173+
});
174+
}
175+
176+
async expectTokenRowNotVisible(assetId: string): Promise<void> {
177+
await Assertions.expectElementToNotBeVisible(this.tokenRow(assetId), {
178+
timeout: 10000,
179+
description: `Trending token row ${assetId} should not be visible`,
180+
});
181+
}
182+
}
183+
184+
export default new SwapTrendingTokensView();

0 commit comments

Comments
 (0)