Skip to content

Commit bddeb61

Browse files
chore(runway): cherry-pick fix: explore search v2 issues cp-7.79.0 (#30688)
- fix: explore search v2 issues cp-7.79.0 (#30677) <!-- 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 explore search v2 issues: - Cryptos -> Crypto - "View all" wont show in the loading state - "View all" stays on the no_query state - When there is a query: - "View X more" will be shown only if there are more than 3 items - Every section with less than 3 items does not show "View all" - When there are no results we should show the correct empty state with the pills - If there are no results for all sections we do not show "We found these results for "Btcx"" <!-- 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: fix explore search v2 issues ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3272 ## **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** > UI and label logic in Explore search with broad test updates; no auth, payments, or security-sensitive paths. > > **Overview** > Explore Search v2 UX fixes: **Crypto** tab copy (was “Cryptos”), smarter **View all / View more** behavior, richer **empty states**, and a **custom search placeholder** overlay. > > **Section actions:** `getViewMoreLabel` now returns `null` when there’s nothing beyond the 3-item cap (with an active query), so headers hide the control instead of showing misleading “View all”. Loading sections skip the button entirely. With **no** query, labels still use **View all**. With a query, **View X more** appears only when counts justify it; tokens without a server total can still fall back to **View all**. > > **Empty results:** When every section is empty, the list shows a global “no results” message, optional **BTC/ETH/SOL** quick pills, and only shows the “other results” line when some sections still have hits (with a `count` in copy). Per-feed empty states keep the feed-specific message. > > **Search bar:** Interactive mode uses an empty native placeholder plus a non-interactive overlay for alignment; tests target `EXPLORE_VIEW_SEARCH_TEXT_INPUT` instead of placeholder text. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 906285a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> [596aadf](596aadf) Co-authored-by: Juanmi <95381763+juanmigdr@users.noreply.github.com>
1 parent fe2ba2e commit bddeb61

10 files changed

Lines changed: 243 additions & 155 deletions

File tree

app/components/Views/TrendingView/TrendingView.view.test.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,7 @@ describeForPlatforms('ExploreFeed - Component Tests', () => {
185185
});
186186

187187
it('user can search for a trending token from the explore feed', async () => {
188-
const { findByPlaceholderText, findByTestId, getByTestId } =
189-
renderTrendingViewWithRoutes();
188+
const { findByTestId, getByTestId } = renderTrendingViewWithRoutes();
190189

191190
await waitFor(() => {
192191
expect(
@@ -199,8 +198,8 @@ describeForPlatforms('ExploreFeed - Component Tests', () => {
199198
);
200199
await actButtonPress(searchButton);
201200

202-
const searchInput = await findByPlaceholderText(
203-
strings('trending.search_placeholder'),
201+
const searchInput = await findByTestId(
202+
TrendingViewSelectorsIDs.EXPLORE_VIEW_SEARCH_TEXT_INPUT,
204203
);
205204
expect(searchInput).toBeOnTheScreen();
206205

@@ -301,7 +300,7 @@ describeForPlatforms('TrendingTokensFullView - Component Tests', () => {
301300
});
302301

303302
it('user can search on trending tokens full view', async () => {
304-
const { findByPlaceholderText, getByTestId, queryByTestId } =
303+
const { findByPlaceholderText, findByTestId, getByTestId, queryByTestId } =
305304
renderTrendingViewWithRoutes();
306305

307306
await waitFor(() => {

app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.testIds.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const ExploreSearchScreenSelectorsIDs = {
1212
PILL_ROW: 'explore-search-pills',
1313
/** "All" pill */
1414
PILL_ALL: 'explore-search-pill-all',
15-
/** Cryptos / tokens feed pill */
15+
/** Crypto / tokens feed pill */
1616
PILL_CRYPTOS: 'explore-search-pill-tokens',
1717
/** Perps feed pill */
1818
PILL_PERPS: 'explore-search-pill-perps',

app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.view.test.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describeForPlatforms('ExploreSearchScreen V2 - Component Tests', () => {
8383
).toBeOnTheScreen();
8484
});
8585

86-
// Tap the Cryptos pill to switch to the single-feed view
86+
// Tap the Crypto pill to switch to the single-feed view
8787
await actButtonPress(cryptosPill);
8888

8989
// The aggregated results list disappears (replaced by the single-feed FlashList)
@@ -104,8 +104,10 @@ describeForPlatforms('ExploreSearchScreen V2 - Component Tests', () => {
104104
});
105105
});
106106

107-
it('Cryptos section header shows "View all" label (remote search — no exact count)', async () => {
108-
const { findByTestId, getByTestId, getAllByText } =
107+
it('Crypto section header hides "View all" when results fit within the cap', async () => {
108+
// The mock returns 3 tokens (= MAX_ITEMS_PER_SECTION), so getViewMoreLabel
109+
// returns null and the "View all" button should not be rendered.
110+
const { findByTestId, getByTestId, queryByText } =
109111
renderExploreSearchScreenWithRoutes();
110112

111113
const searchInput = getByTestId(
@@ -116,25 +118,18 @@ describeForPlatforms('ExploreSearchScreen V2 - Component Tests', () => {
116118
// Wait for the aggregated results list to appear
117119
await findByTestId(ExploreSearchScreenSelectorsIDs.SEARCH_RESULTS_LIST);
118120

119-
// The Cryptos section header button label should be "View all" because
120-
// tokens use remote pagination and we never have a precise count of
121-
// remaining items.
122-
await waitFor(() => {
123-
const viewAllLabels = getAllByText(strings('trending.view_all'));
124-
expect(viewAllLabels.length).toBeGreaterThanOrEqual(1);
125-
});
126-
127-
// The Pressable wrapping that label has accessibilityLabel = "{label} {title}".
128-
// Verify the Cryptos section specifically by checking the accessibility label.
129-
const viewAllCryptosLabel = `${strings('trending.view_all')} ${strings('trending.search_tabs.crypto')}`;
121+
// With only 3 results (≤ MAX_ITEMS_PER_SECTION) the "View all" button must
122+
// not be rendered — there is nothing more to reveal.
130123
await waitFor(() => {
124+
const viewAllCryptosLabel = `${strings('trending.view_all')} ${strings('trending.search_tabs.crypto')}`;
131125
const resultsListEl = getByTestId(
132126
ExploreSearchScreenSelectorsIDs.SEARCH_RESULTS_LIST,
133127
);
134128
const pressables = resultsListEl.findAll(
135129
(node) => node.props.accessibilityLabel === viewAllCryptosLabel,
136130
);
137-
expect(pressables.length).toBeGreaterThan(0);
131+
expect(pressables.length).toBe(0);
132+
expect(queryByText(strings('trending.view_all'))).toBeNull();
138133
});
139134
});
140135

@@ -146,13 +141,13 @@ describeForPlatforms('ExploreSearchScreen V2 - Component Tests', () => {
146141
);
147142
await userEvent.type(searchInput, 'btc');
148143

149-
// Tap Cryptos pill to activate it
144+
// Tap Crypto pill to activate it
150145
const cryptosPill = await findByTestId(
151146
ExploreSearchScreenSelectorsIDs.PILL_CRYPTOS,
152147
);
153148
await actButtonPress(cryptosPill);
154149

155-
// Cryptos pill should now be selected (active): accessibilityState.selected === true
150+
// Crypto pill should now be selected (active): accessibilityState.selected === true
156151
await waitFor(() => {
157152
expect(cryptosPill.props.accessibilityState?.selected).toBe(true);
158153
});
@@ -161,7 +156,7 @@ describeForPlatforms('ExploreSearchScreen V2 - Component Tests', () => {
161156
const clearButton = getByTestId('explore-search-clear-button');
162157
await actButtonPress(clearButton);
163158

164-
// After clearing, the Cryptos pill should remain selected — clearing the
159+
// After clearing, the Crypto pill should remain selected — clearing the
165160
// query must not auto-navigate back to "All".
166161
await waitFor(() => {
167162
expect(cryptosPill.props.accessibilityState?.selected).toBe(true);

app/components/Views/TrendingView/components/ExploreSearchBar/ExploreSearchBar.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render, fireEvent } from '@testing-library/react-native';
33
import ExploreSearchBar from './ExploreSearchBar';
44
import { useSelector } from 'react-redux';
55
import { selectBasicFunctionalityEnabled } from '../../../../../selectors/settings';
6-
import { strings } from '../../../../../../locales/i18n';
6+
import { TrendingViewSelectorsIDs } from '../../TrendingView.testIds';
77

88
jest.mock('react-redux', () => ({
99
...jest.requireActual('react-redux'),
@@ -82,7 +82,7 @@ describe('ExploreSearchBar', () => {
8282
const mockOnSearchChange = jest.fn();
8383
const mockOnCancel = jest.fn();
8484

85-
const { getByPlaceholderText } = render(
85+
const { getByTestId } = render(
8686
<ExploreSearchBar
8787
type="interactive"
8888
searchQuery=""
@@ -91,8 +91,8 @@ describe('ExploreSearchBar', () => {
9191
/>,
9292
);
9393

94-
const input = getByPlaceholderText(
95-
strings('trending.search_placeholder'),
94+
const input = getByTestId(
95+
TrendingViewSelectorsIDs.EXPLORE_VIEW_SEARCH_TEXT_INPUT,
9696
);
9797

9898
fireEvent.changeText(input, 'ethereum');
@@ -193,7 +193,7 @@ describe('ExploreSearchBar', () => {
193193
const mockOnSearchChange = jest.fn();
194194
const mockOnCancel = jest.fn();
195195

196-
const { getByPlaceholderText } = render(
196+
const { getByTestId } = render(
197197
<ExploreSearchBar
198198
type="interactive"
199199
searchQuery=""
@@ -202,8 +202,8 @@ describe('ExploreSearchBar', () => {
202202
/>,
203203
);
204204

205-
const input = getByPlaceholderText(
206-
strings('trending.search_placeholder'),
205+
const input = getByTestId(
206+
TrendingViewSelectorsIDs.EXPLORE_VIEW_SEARCH_TEXT_INPUT,
207207
);
208208

209209
expect(input.props.autoFocus).toBe(true);

app/components/Views/TrendingView/components/ExploreSearchBar/ExploreSearchBar.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { TouchableOpacity } from 'react-native';
2+
import { StyleSheet, TouchableOpacity, View } from 'react-native';
33
import {
44
Box,
55
BoxFlexDirection,
@@ -19,6 +19,11 @@ import { strings } from '../../../../../../locales/i18n';
1919
import { selectBasicFunctionalityEnabled } from '../../../../../selectors/settings';
2020
import { TrendingViewSelectorsIDs } from '../../TrendingView.testIds';
2121

22+
// px-4 (16) + icon Md (20) + gap-3 (12) = 48 — aligns overlay text with the input cursor
23+
const styles = StyleSheet.create({
24+
placeholderOverlay: { paddingLeft: 48 },
25+
});
26+
2227
interface ExploreSearchBarButtonProps {
2328
type: 'button';
2429
onPress: () => void;
@@ -61,7 +66,12 @@ const ExploreSearchBar: React.FC<ExploreSearchBarProps> = (props) => {
6166
size={IconSize.Md}
6267
color={IconColor.IconAlternative}
6368
/>
64-
<Text variant={TextVariant.BodyMd} color={TextColor.TextAlternative}>
69+
<Text
70+
variant={TextVariant.BodyMd}
71+
color={TextColor.TextAlternative}
72+
numberOfLines={1}
73+
twClassName="flex-1"
74+
>
6575
{placeholder}
6676
</Text>
6777
</Box>
@@ -91,7 +101,7 @@ const ExploreSearchBar: React.FC<ExploreSearchBarProps> = (props) => {
91101
<TextFieldSearch
92102
value={props.searchQuery}
93103
onChangeText={props.onSearchChange}
94-
placeholder={placeholder}
104+
placeholder=""
95105
autoFocus={props.type === 'interactive'}
96106
onPressClearButton={() => {
97107
props.onSearchChange('');
@@ -102,6 +112,23 @@ const ExploreSearchBar: React.FC<ExploreSearchBarProps> = (props) => {
102112
testID: TrendingViewSelectorsIDs.EXPLORE_VIEW_SEARCH_TEXT_INPUT,
103113
}}
104114
/>
115+
{!props.searchQuery && (
116+
<View
117+
style={[
118+
tw.style('absolute inset-0 justify-center pr-4'),
119+
styles.placeholderOverlay,
120+
]}
121+
pointerEvents="none"
122+
>
123+
<Text
124+
variant={TextVariant.BodyMd}
125+
color={TextColor.TextAlternative}
126+
numberOfLines={1}
127+
>
128+
{placeholder}
129+
</Text>
130+
</View>
131+
)}
105132
</Box>
106133
<TouchableOpacity
107134
onPress={() => {

app/components/Views/TrendingView/search/ExploreSearchResultsV2.test.ts

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* ExploreSearchResultsV2 — unit tests for getViewMoreLabel and LOCAL_SEARCH_FEEDS
33
*
44
* Tests the pure label-derivation logic that determines what text the
5-
* "View all / View X more" button shows for each feed section.
5+
* "View X more" button shows for each feed section, or null when the button
6+
* should not be shown.
67
*/
78

89
import { getViewMoreLabel, LOCAL_SEARCH_FEEDS } from './viewMoreLabel';
@@ -30,17 +31,39 @@ describe('LOCAL_SEARCH_FEEDS', () => {
3031
});
3132

3233
describe('getViewMoreLabel', () => {
33-
describe('tokens — falls back to "View all" without a server total', () => {
34-
it('returns "View all" even with many results and an active query', () => {
35-
expect(getViewMoreLabel('tokens', 10, 'eth')).toBe('View all');
36-
});
34+
describe('no active query — always "View all" regardless of item count', () => {
35+
it.each([
36+
['perps', 10, ''],
37+
['stocks', 10, ' '],
38+
['sites', 10, ''],
39+
['predictions', 10, ''],
40+
['tokens', 10, ''],
41+
['tokens', 1, ''],
42+
] as [SearchFeedId, number, string][])(
43+
'%s: %d items, query "%s" → "View all"',
44+
(feedId, totalItems, query) => {
45+
expect(getViewMoreLabel(feedId, totalItems, query)).toBe('View all');
46+
},
47+
);
48+
});
3749

38-
it('returns "View all" with no query', () => {
39-
expect(getViewMoreLabel('tokens', 10, '')).toBe('View all');
40-
});
50+
describe('active query — returns null when results fit within the cap (≤ 3)', () => {
51+
it.each([
52+
['perps', 0, 'eth'],
53+
['perps', 1, 'eth'],
54+
['perps', 3, 'eth'],
55+
['stocks', 2, 'bit'],
56+
['sites', 1, 'meta'],
57+
['tokens', 3, 'eth'],
58+
] as [SearchFeedId, number, string][])(
59+
'%s: %d items → null',
60+
(feedId, totalItems, query) => {
61+
expect(getViewMoreLabel(feedId, totalItems, query)).toBeNull();
62+
},
63+
);
4164
});
4265

43-
describe('local search feeds with active query and extra items', () => {
66+
describe('active query — local feeds return "View X more" when items exceed cap', () => {
4467
it.each([
4568
['perps', 5, 'eth', 'View 2 more'],
4669
['stocks', 7, 'bit', 'View 4 more'],
@@ -53,52 +76,28 @@ describe('getViewMoreLabel', () => {
5376
);
5477
});
5578

56-
describe('local search feeds with active query but no extra items', () => {
57-
it.each([
58-
['perps', 3, 'eth'],
59-
['stocks', 2, 'bit'],
60-
['sites', 1, 'meta'],
61-
['perps', 0, 'eth'],
62-
] as [SearchFeedId, number, string][])(
63-
'%s: %d items → "View all" (no hidden items)',
64-
(feedId, totalItems, query) => {
65-
expect(getViewMoreLabel(feedId, totalItems, query)).toBe('View all');
66-
},
67-
);
79+
describe('active query — non-local feeds (tokens) without server total fall back to "View all"', () => {
80+
it('returns "View all" for tokens with many results but no server total', () => {
81+
expect(getViewMoreLabel('tokens', 10, 'eth')).toBe('View all');
82+
});
6883
});
6984

70-
describe('server total provided (tokens and predictions)', () => {
71-
it('returns "View X more" for predictions when server total exceeds visible', () => {
85+
describe('active query — server total provided (tokens and predictions)', () => {
86+
it('returns null when server total fits within the cap (≤ 3)', () => {
87+
expect(getViewMoreLabel('predictions', 3, 'eth', 3)).toBeNull();
88+
expect(getViewMoreLabel('tokens', 3, 'eth', 2)).toBeNull();
89+
});
90+
91+
it('returns "View X more" when server total exceeds the cap', () => {
7292
expect(getViewMoreLabel('predictions', 3, 'eth', 50)).toBe(
7393
'View 47 more',
7494
);
75-
});
76-
77-
it('returns "View X more" for tokens when server total exceeds visible', () => {
7895
expect(getViewMoreLabel('tokens', 3, 'eth', 2101)).toBe('View 2098 more');
7996
});
8097

81-
it('returns "View all" when server total equals visible items', () => {
82-
expect(getViewMoreLabel('predictions', 3, 'nba', 3)).toBe('View all');
98+
it('returns "View X more" when server total only slightly exceeds cap', () => {
99+
expect(getViewMoreLabel('predictions', 3, 'nba', 4)).toBe('View 1 more');
100+
expect(getViewMoreLabel('tokens', 3, 'eth', 4)).toBe('View 1 more');
83101
});
84-
85-
it('returns "View all" when server total is less than visible items', () => {
86-
expect(getViewMoreLabel('tokens', 3, 'eth', 2)).toBe('View all');
87-
});
88-
});
89-
90-
describe('no active query — always "View all" regardless of item count', () => {
91-
it.each([
92-
['perps', 10, ''],
93-
['stocks', 10, ' '],
94-
['sites', 10, ''],
95-
['predictions', 10, ''],
96-
['tokens', 10, ''],
97-
] as [SearchFeedId, number, string][])(
98-
'%s: %d items, query "%s" → "View all"',
99-
(feedId, totalItems, query) => {
100-
expect(getViewMoreLabel(feedId, totalItems, query)).toBe('View all');
101-
},
102-
);
103102
});
104103
});

0 commit comments

Comments
 (0)