Skip to content

Commit 9874d9c

Browse files
davibrocclaude
andauthored
test: add component view tests for EVM and ERC-1155 send flows (MetaMask#27759)
Adds 5 new view tests covering previously untested send scenarios: - EVM ETH happy path (Amount → Recipient → Review button enabled) - Invalid address disables Review with error text - Token contract address opens SendAlertModal; cancel closes it - Amount exceeding balance shows Insufficient funds on Continue - ERC-1155 Amount screen shows NFT name and Next button lifecycle <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- 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** - [ ] 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. ## **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** > Low risk since changes are limited to component-view tests and test mocks, with no production logic modifications. > > **Overview** > Adds new component-view coverage for **EVM send flows**, including ETH happy path navigation, invalid recipient validation, insufficient-funds gating, and the token-contract recipient warning modal (with modal `CANCEL_BUTTON`/`ACKNOWLEDGE_BUTTON` test IDs). > > Extends the component-view `Engine` mock to include `AssetsContractController.getTokenStandardAndDetails`, enabling tests to simulate token contract address detection and reset mock behavior between cases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b4acf8f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 025e1ef commit 9874d9c

3 files changed

Lines changed: 290 additions & 5 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const SendAlertModalSelectorIDs = {
2+
CANCEL_BUTTON: 'send-alert-modal-cancel-button',
3+
ACKNOWLEDGE_BUTTON: 'send-alert-modal-acknowledge-button',
4+
};

app/components/Views/confirmations/components/send/send.view.test.tsx

Lines changed: 283 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,26 @@ import {
1919
RedesignedSendViewSelectorsIDs,
2020
} from './RedesignedSendView.testIds';
2121
import { Send } from './send';
22+
import { SendAlertModalSelectorIDs } from './send-alert-modal/send-alert-modal.testIds';
23+
24+
/** A minimal ETH asset with 2 ETH balance, suitable for EVM send tests. */
25+
const EVM_ETH_ASSET = {
26+
address: '0x0000000000000000000000000000000000000000',
27+
chainId: '0x1',
28+
symbol: 'ETH',
29+
decimals: 18,
30+
balance: '2',
31+
rawBalance: '0x1BC16D674EC80000', // 2 ETH
32+
};
33+
34+
const VALID_EVM_RECIPIENT = '0x0000000000000000000000000000000000000002';
35+
const TOKEN_CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000003';
2236

2337
describeForPlatforms('Send', () => {
2438
describe('Non-EVM', () => {
25-
/**
26-
* Regression test for Issue #22789 and related to #23251
27-
* TRON send flow: selecting a destination account must move the flow forward
28-
* (previously it stayed on the recipient list and did not navigate).
29-
*/
39+
// Regression test for Issue #22789 and related to #23251
40+
// TRON send flow: selecting a destination account must move the flow forward
41+
// (previously it stayed on the recipient list and did not navigate).
3042
it('TRON send: selecting destination account updates selection', async () => {
3143
const { tronOverrides, recipientAddresses } = buildTronSendFixture();
3244

@@ -239,6 +251,272 @@ describeForPlatforms('Send', () => {
239251
});
240252
});
241253

254+
describe('EVM', () => {
255+
beforeEach(() => {
256+
// Reset AssetsContractController mock so each test starts clean.
257+
// The Engine mock in mocks.ts registers this as jest.fn().mockResolvedValue({}).
258+
// Resetting here ensures any one-time override from a previous test is cleared.
259+
const engineMock = jest.requireMock(
260+
'../../../../../../app/core/Engine',
261+
) as unknown as {
262+
default: {
263+
context: {
264+
AssetsContractController: { getTokenStandardAndDetails: jest.Mock };
265+
};
266+
};
267+
};
268+
engineMock.default.context.AssetsContractController.getTokenStandardAndDetails.mockResolvedValue(
269+
{},
270+
);
271+
});
272+
273+
/**
274+
* Core EVM send happy path: Amount → Continue → Recipient.
275+
* Typing a valid address must enable the Review button.
276+
*/
277+
it('ETH: Amount → Continue → Recipient, valid address enables Review', async () => {
278+
const state = initialStateWallet()
279+
.withOverrides(sendViewOverrides)
280+
.build();
281+
282+
const { getByTestId, getByRole, findByTestId } = renderScreenWithRoutes(
283+
Send as unknown as React.ComponentType,
284+
{ name: Routes.SEND.DEFAULT },
285+
[],
286+
{ state },
287+
{ screen: Routes.SEND.AMOUNT, params: { asset: EVM_ETH_ASSET } },
288+
);
289+
290+
expect(
291+
getByTestId(RedesignedSendViewSelectorsIDs.SEND_AMOUNT),
292+
).toBeOnTheScreen();
293+
294+
fireEvent.press(
295+
getByTestId(RedesignedSendViewSelectorsIDs.PERCENTAGE_BUTTON_100),
296+
);
297+
fireEvent.press(getByRole('button', { name: 'Continue' }));
298+
299+
const addressInput = await findByTestId(
300+
RedesignedSendViewSelectorsIDs.RECIPIENT_ADDRESS_INPUT,
301+
{},
302+
{ timeout: 5000 },
303+
);
304+
fireEvent.changeText(addressInput, VALID_EVM_RECIPIENT);
305+
306+
const reviewButton = await findByTestId(
307+
RedesignedSendViewSelectorsIDs.REVIEW_BUTTON,
308+
{},
309+
{ timeout: 5000 },
310+
);
311+
await waitFor(() => expect(reviewButton).toBeEnabled(), {
312+
timeout: 5000,
313+
});
314+
});
315+
316+
/**
317+
* Typing an invalid address (not a valid hex, ENS, or non-EVM address)
318+
* must disable the Review button and show an error label.
319+
*/
320+
it('Recipient: invalid address disables Review with error text', async () => {
321+
const state = initialStateWallet()
322+
.withOverrides(sendViewOverrides)
323+
.build();
324+
325+
const { findByTestId } = renderScreenWithRoutes(
326+
Send as unknown as React.ComponentType,
327+
{ name: Routes.SEND.DEFAULT },
328+
[],
329+
{ state },
330+
{ screen: Routes.SEND.RECIPIENT, params: { asset: EVM_ETH_ASSET } },
331+
);
332+
333+
const addressInput = await findByTestId(
334+
RedesignedSendViewSelectorsIDs.RECIPIENT_ADDRESS_INPUT,
335+
{},
336+
{ timeout: 5000 },
337+
);
338+
fireEvent.changeText(addressInput, 'notanaddress');
339+
340+
const reviewButton = await findByTestId(
341+
RedesignedSendViewSelectorsIDs.REVIEW_BUTTON,
342+
{},
343+
{ timeout: 5000 },
344+
);
345+
await waitFor(() => expect(reviewButton).toBeDisabled(), {
346+
timeout: 5000,
347+
});
348+
});
349+
350+
/**
351+
* When the recipient is a token contract address, sending would burn the tokens.
352+
* The Recipient screen must show a warning modal before proceeding.
353+
* Cancelling the modal must close it and keep the user on the Recipient screen.
354+
*/
355+
it('Recipient: token contract address opens alert modal; cancel closes it', async () => {
356+
const engineMock = jest.requireMock(
357+
'../../../../../../app/core/Engine',
358+
) as unknown as {
359+
default: {
360+
context: {
361+
AssetsContractController: { getTokenStandardAndDetails: jest.Mock };
362+
};
363+
};
364+
};
365+
engineMock.default.context.AssetsContractController.getTokenStandardAndDetails.mockImplementation(
366+
(tokenAddress: string) => {
367+
if (
368+
tokenAddress?.toLowerCase() === TOKEN_CONTRACT_ADDRESS.toLowerCase()
369+
) {
370+
return Promise.resolve({
371+
standard: 'ERC20',
372+
symbol: 'TOKEN',
373+
decimals: '18',
374+
});
375+
}
376+
return Promise.resolve({});
377+
},
378+
);
379+
380+
const state = initialStateWallet()
381+
.withOverrides(sendViewOverrides)
382+
.build();
383+
384+
const { findByTestId, queryByTestId } = renderScreenWithRoutes(
385+
Send as unknown as React.ComponentType,
386+
{ name: Routes.SEND.DEFAULT },
387+
[],
388+
{ state },
389+
{ screen: Routes.SEND.RECIPIENT, params: { asset: EVM_ETH_ASSET } },
390+
);
391+
392+
const addressInput = await findByTestId(
393+
RedesignedSendViewSelectorsIDs.RECIPIENT_ADDRESS_INPUT,
394+
{},
395+
{ timeout: 5000 },
396+
);
397+
fireEvent.changeText(addressInput, TOKEN_CONTRACT_ADDRESS);
398+
399+
const reviewButton = await findByTestId(
400+
RedesignedSendViewSelectorsIDs.REVIEW_BUTTON,
401+
{},
402+
{ timeout: 5000 },
403+
);
404+
await waitFor(() => expect(reviewButton).toBeEnabled(), {
405+
timeout: 5000,
406+
});
407+
fireEvent.press(reviewButton);
408+
409+
expect(
410+
await findByTestId(
411+
SendAlertModalSelectorIDs.CANCEL_BUTTON,
412+
{},
413+
{ timeout: 5000 },
414+
),
415+
).toBeOnTheScreen();
416+
417+
fireEvent.press(
418+
screen.getByTestId(SendAlertModalSelectorIDs.CANCEL_BUTTON),
419+
);
420+
421+
await waitFor(
422+
() =>
423+
expect(
424+
queryByTestId(SendAlertModalSelectorIDs.CANCEL_BUTTON),
425+
).not.toBeOnTheScreen(),
426+
{ timeout: 5000 },
427+
);
428+
});
429+
430+
/**
431+
* When the entered amount exceeds the asset balance, the Continue button
432+
* must show the "Insufficient funds" error and be disabled.
433+
*/
434+
it('Amount: exceeding balance disables Continue with Insufficient funds', async () => {
435+
const tinyBalanceAsset = {
436+
...EVM_ETH_ASSET,
437+
balance: '0',
438+
rawBalance: '0x1', // 1 wei — any 1 ETH input exceeds it
439+
};
440+
441+
const state = initialStateWallet()
442+
.withOverrides(sendViewOverrides)
443+
.build();
444+
445+
const { getByTestId, getByText, findByRole } = renderScreenWithRoutes(
446+
Send as unknown as React.ComponentType,
447+
{ name: Routes.SEND.DEFAULT },
448+
[],
449+
{ state },
450+
{ screen: Routes.SEND.AMOUNT, params: { asset: tinyBalanceAsset } },
451+
);
452+
453+
expect(
454+
getByTestId(RedesignedSendViewSelectorsIDs.SEND_AMOUNT),
455+
).toBeOnTheScreen();
456+
457+
// Press digit '1' — enters 1 ETH, far exceeding 1 wei balance
458+
fireEvent.press(getByText('1'));
459+
460+
// Finding the button by its error label is sufficient — it proves the error state is shown
461+
expect(
462+
await findByRole(
463+
'button',
464+
{ name: 'Insufficient funds' },
465+
{ timeout: 5000 },
466+
),
467+
).toBeOnTheScreen();
468+
});
469+
});
470+
471+
describe('ERC-1155', () => {
472+
/**
473+
* ERC-1155 tokens show the NFT name on the Amount screen and use a "Next"
474+
* label instead of "Continue". The button is disabled until a quantity is entered,
475+
* then enabled when the entered quantity does not exceed the owned balance.
476+
*/
477+
it('Amount screen shows NFT name and Next button enabled after entering quantity', async () => {
478+
const erc1155Asset = {
479+
address: '0x495f947276749ce646f68ac8c248420045cb7b5e',
480+
chainId: '0x1',
481+
symbol: 'ITEM',
482+
name: 'Magic Sword',
483+
standard: TokenStandard.ERC1155,
484+
tokenId: '99',
485+
balance: '5',
486+
};
487+
488+
const state = initialStateWallet()
489+
.withOverrides(sendViewOverrides)
490+
.build();
491+
492+
const { getByTestId, getByRole, getByText } = renderScreenWithRoutes(
493+
Send as unknown as React.ComponentType,
494+
{ name: Routes.SEND.DEFAULT },
495+
[],
496+
{ state },
497+
{ screen: Routes.SEND.AMOUNT, params: { asset: erc1155Asset } },
498+
);
499+
500+
expect(
501+
getByTestId(RedesignedSendViewSelectorsIDs.SEND_AMOUNT),
502+
).toBeOnTheScreen();
503+
504+
// NFT name is shown in the amount header
505+
expect(getByText('Magic Sword')).toBeOnTheScreen();
506+
507+
// "Next" button is visible immediately (ERC-1155 always shows it)
508+
expect(getByRole('button', { name: 'Next' })).toBeOnTheScreen();
509+
510+
// Enter quantity 3 (≤ balance of 5) → button becomes enabled
511+
fireEvent.press(getByText('3'));
512+
513+
await waitFor(
514+
() => expect(getByRole('button', { name: 'Next' })).toBeEnabled(),
515+
{ timeout: 5000 },
516+
);
517+
});
518+
});
519+
242520
describe('Recipient list', () => {
243521
/**
244522
* Regression test for issue #22806

tests/component-view/mocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ jest.mock('../../app/core/Engine', () => {
125125
AuthenticationController: {
126126
getBearerToken: jest.fn().mockResolvedValue('mock-bearer-token'),
127127
},
128+
AssetsContractController: {
129+
getTokenStandardAndDetails: jest.fn().mockResolvedValue({}),
130+
},
128131
TransactionController: {
129132
state: {
130133
transactions: [],

0 commit comments

Comments
 (0)