Skip to content

Commit fed7efe

Browse files
authored
chore: adding e2e for new send implementation (#36626)
<!-- 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** Adding e2e for new send implementation ## **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: #36527 ## **Manual testing steps** NA ## **Screenshots/Recordings** NA ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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 - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/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] > Adds E2E tests for the new Send flow (ETH, Max, address book, ENS, Solana) and changes token asset data-testid to include chainId and symbol. > > - **E2E (Send flow)**: > - Add `test/e2e/page-objects/pages/send/send-page.ts` with helpers to select token, fill recipient/amount, use Max, and continue. > - Add tests in `test/e2e/tests/send/`: > - `send-eth.spec.ts`: ETH send, Max send, address book recipient, and ENS/name-lookup resolution (with snap mock). > - `send-solana.spec.ts`: SOL send flow using Solana snap confirmation. > - `common.ts`: mock feature flag `sendRedesign` via `FEATURE_FLAGS_URL`. > - **UI**: > - `ui/pages/confirmations/components/UI/asset/asset.tsx`: change token asset `data-testid` from `token-asset` to ``token-asset-${chainId}-${symbol}``. > - Update tests in `asset.test.tsx` to use the new `data-testid` and variants when `chainId` is undefined. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a1bae19. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7d628e6 commit fed7efe

File tree

7 files changed

+334
-5
lines changed

7 files changed

+334
-5
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Driver } from '../../../webdriver/driver';
2+
3+
class SendPage {
4+
private driver: Driver;
5+
6+
private readonly tokenAsset = (chainId: string, symbol: string) =>
7+
`[data-testid="token-asset-${chainId}-${symbol}"]`;
8+
9+
private readonly inputRecipient =
10+
'input[placeholder="Enter or paste a valid address"]';
11+
12+
private readonly recipientModalButton =
13+
'[data-testid="open-recipient-modal-btn"]';
14+
15+
private readonly amountInput = 'input[placeholder="0"]';
16+
17+
private readonly continueButton = {
18+
text: 'Continue',
19+
tag: 'button',
20+
};
21+
22+
private readonly maxButton = {
23+
text: 'Max',
24+
tag: 'button',
25+
};
26+
27+
private readonly insufficientFundsError = {
28+
text: 'Insufficient funds',
29+
};
30+
31+
constructor(driver: Driver) {
32+
this.driver = driver;
33+
}
34+
35+
async selectToken(chainId: string, symbol: string): Promise<void> {
36+
await this.driver.clickElement(this.tokenAsset(chainId, symbol));
37+
}
38+
39+
async fillRecipient(recipientAddress: string): Promise<void> {
40+
await this.driver.pasteIntoField(this.inputRecipient, recipientAddress);
41+
}
42+
43+
async fillAmount(amount: string): Promise<void> {
44+
await this.driver.pasteIntoField(this.amountInput, amount);
45+
}
46+
47+
async pressOnAmountInput(key: string): Promise<void> {
48+
await this.driver.press(
49+
'input[placeholder="0"]',
50+
this.driver.Key[key as keyof typeof this.driver.Key],
51+
);
52+
}
53+
54+
async pressMaxButton(): Promise<void> {
55+
await this.driver.clickElement(this.maxButton);
56+
}
57+
58+
async pressContinueButton(): Promise<void> {
59+
await this.driver.clickElement(this.continueButton);
60+
}
61+
62+
async checkInsufficientFundsError(): Promise<void> {
63+
await this.driver.findElement(this.insufficientFundsError);
64+
}
65+
66+
async selectAccountFromRecipientModal(accountName: string): Promise<void> {
67+
await this.driver.clickElement(this.recipientModalButton);
68+
await this.driver.clickElement({ text: accountName });
69+
}
70+
71+
async createSendRequest({
72+
chainId,
73+
symbol,
74+
recipientAddress,
75+
amount,
76+
}: {
77+
chainId: string;
78+
symbol: string;
79+
recipientAddress: string;
80+
amount: string;
81+
}): Promise<void> {
82+
await this.selectToken(chainId, symbol);
83+
await this.fillRecipient(recipientAddress);
84+
await this.fillAmount(amount);
85+
await this.pressContinueButton();
86+
}
87+
88+
async createMaxSendRequest({
89+
chainId,
90+
symbol,
91+
recipientAddress,
92+
}: {
93+
chainId: string;
94+
symbol: string;
95+
recipientAddress: string;
96+
}): Promise<void> {
97+
await this.selectToken(chainId, symbol);
98+
await this.fillRecipient(recipientAddress);
99+
await this.pressMaxButton();
100+
await this.pressContinueButton();
101+
}
102+
}
103+
104+
export default SendPage;

test/e2e/tests/send/common.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Mockttp } from 'mockttp';
2+
3+
export const FEATURE_FLAGS_URL =
4+
'https://client-config.api.cx.metamask.io/v1/flags';
5+
6+
export const mockSendRedesignFeatureFlag = (mockServer: Mockttp) =>
7+
mockServer
8+
.forGet(FEATURE_FLAGS_URL)
9+
.withQuery({
10+
client: 'extension',
11+
distribution: 'main',
12+
environment: 'dev',
13+
})
14+
.thenCallback(() => {
15+
return {
16+
ok: true,
17+
statusCode: 200,
18+
json: [
19+
{
20+
sendRedesign: {
21+
enabled: true,
22+
},
23+
},
24+
],
25+
};
26+
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Mockttp } from 'mockttp';
2+
3+
import { CHAIN_IDS } from '../../../../shared/constants/network';
4+
import ActivityListPage from '../../page-objects/pages/home/activity-list';
5+
import FixtureBuilder from '../../fixture-builder';
6+
import HomePage from '../../page-objects/pages/home/homepage';
7+
import SendPage from '../../page-objects/pages/send/send-page';
8+
import SendTokenConfirmPage from '../../page-objects/pages/send/send-token-confirmation-page';
9+
import { Driver } from '../../webdriver/driver';
10+
import { WINDOW_TITLES, withFixtures } from '../../helpers';
11+
import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow';
12+
import { mockLookupSnap } from '../../mock-response-data/snaps/snap-binary-mocks';
13+
import { openTestSnapClickButtonAndInstall } from '../../page-objects/flows/install-test-snap.flow';
14+
import { mockSendRedesignFeatureFlag } from './common';
15+
16+
describe('Send ETH', function () {
17+
it('it should be possible to send ETH', async function () {
18+
await withFixtures(
19+
{
20+
fixtures: new FixtureBuilder().build(),
21+
title: this.test?.fullTitle(),
22+
testSpecificMock: mockSendRedesignFeatureFlag,
23+
},
24+
async ({ driver }: { driver: Driver }) => {
25+
await loginWithBalanceValidation(driver);
26+
27+
const homePage = new HomePage(driver);
28+
const sendPage = new SendPage(driver);
29+
const sendTokenConfirmationPage = new SendTokenConfirmPage(driver);
30+
const activityListPage = new ActivityListPage(driver);
31+
32+
await homePage.startSendFlow();
33+
34+
await sendPage.selectToken('0x539', 'ETH');
35+
await sendPage.fillRecipient(
36+
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
37+
);
38+
await sendPage.fillAmount('1000');
39+
await sendPage.checkInsufficientFundsError();
40+
await sendPage.pressOnAmountInput('BACK_SPACE');
41+
await sendPage.pressOnAmountInput('BACK_SPACE');
42+
await sendPage.pressOnAmountInput('BACK_SPACE');
43+
await sendPage.pressContinueButton();
44+
45+
await sendTokenConfirmationPage.clickOnConfirm();
46+
await activityListPage.checkTransactionActivityByText('Sent');
47+
await activityListPage.checkCompletedTxNumberDisplayedInActivity(1);
48+
await activityListPage.checkTxAmountInActivity('-1 ETH');
49+
},
50+
);
51+
});
52+
53+
it('it should be possible to send Max ETH', async function () {
54+
await withFixtures(
55+
{
56+
fixtures: new FixtureBuilder().build(),
57+
title: this.test?.fullTitle(),
58+
testSpecificMock: mockSendRedesignFeatureFlag,
59+
},
60+
async ({ driver }: { driver: Driver }) => {
61+
await loginWithBalanceValidation(driver);
62+
63+
const homePage = new HomePage(driver);
64+
const sendPage = new SendPage(driver);
65+
const sendTokenConfirmationPage = new SendTokenConfirmPage(driver);
66+
const activityListPage = new ActivityListPage(driver);
67+
68+
await homePage.startSendFlow();
69+
70+
await sendPage.createMaxSendRequest({
71+
chainId: '0x539',
72+
symbol: 'ETH',
73+
recipientAddress: '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
74+
});
75+
76+
await sendTokenConfirmationPage.clickOnConfirm();
77+
await activityListPage.checkTransactionActivityByText('Sent');
78+
await activityListPage.checkCompletedTxNumberDisplayedInActivity(1);
79+
},
80+
);
81+
});
82+
83+
it('it should be possible to send to address book entry', async function () {
84+
await withFixtures(
85+
{
86+
fixtures: new FixtureBuilder()
87+
.withAddressBookController({
88+
addressBook: {
89+
'0x539': {
90+
'0x2f318C334780961FB129D2a6c30D0763d9a5C970': {
91+
address: '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
92+
chainId: '0x539',
93+
isEns: false,
94+
memo: '',
95+
name: 'Test Name 1',
96+
},
97+
},
98+
},
99+
})
100+
.build(),
101+
title: this.test?.fullTitle(),
102+
testSpecificMock: mockSendRedesignFeatureFlag,
103+
},
104+
async ({ driver }) => {
105+
await loginWithBalanceValidation(driver);
106+
107+
const homePage = new HomePage(driver);
108+
const sendPage = new SendPage(driver);
109+
const sendTokenConfirmationPage = new SendTokenConfirmPage(driver);
110+
const activityListPage = new ActivityListPage(driver);
111+
112+
await homePage.startSendFlow();
113+
114+
await sendPage.selectToken('0x539', 'ETH');
115+
await sendPage.selectAccountFromRecipientModal('Test Name 1');
116+
await sendPage.fillAmount('1');
117+
await sendPage.pressContinueButton();
118+
119+
await sendTokenConfirmationPage.clickOnConfirm();
120+
await activityListPage.checkTransactionActivityByText('Sent');
121+
await activityListPage.checkCompletedTxNumberDisplayedInActivity(1);
122+
await activityListPage.checkTxAmountInActivity('-1 ETH');
123+
},
124+
);
125+
});
126+
127+
it('it should be possible to resolve name lookup address', async function () {
128+
await withFixtures(
129+
{
130+
fixtures: new FixtureBuilder({
131+
inputChainId: CHAIN_IDS.MAINNET,
132+
}).build(),
133+
title: this.test?.fullTitle(),
134+
testSpecificMock: (mockServer: Mockttp) => {
135+
mockSendRedesignFeatureFlag(mockServer);
136+
mockLookupSnap(mockServer);
137+
},
138+
},
139+
async ({ driver }) => {
140+
await loginWithBalanceValidation(driver);
141+
142+
await openTestSnapClickButtonAndInstall(
143+
driver,
144+
'connectNameLookUpButton',
145+
);
146+
await driver.switchToWindowWithTitle(
147+
WINDOW_TITLES.ExtensionInFullScreenView,
148+
);
149+
150+
const homePage = new HomePage(driver);
151+
const sendPage = new SendPage(driver);
152+
153+
await homePage.startSendFlow();
154+
155+
await sendPage.selectToken('0x1', 'ETH');
156+
await sendPage.fillRecipient('test.eth');
157+
158+
await driver.findElement({ text: '0xc0ffe...54979' });
159+
},
160+
);
161+
});
162+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import NonEvmHomepage from '../../page-objects/pages/home/non-evm-homepage';
2+
import SendPage from '../../page-objects/pages/send/send-page';
3+
import SnapTransactionConfirmation from '../../page-objects/pages/confirmations/redesign/snap-transaction-confirmation';
4+
import { DEFAULT_SOLANA_TEST_DAPP_FIXTURE_OPTIONS } from '../../flask/solana-wallet-standard/testHelpers';
5+
import { withSolanaAccountSnap } from '../solana/common-solana';
6+
import { mockSendRedesignFeatureFlag } from './common';
7+
8+
describe('Send Solana', function () {
9+
it('it should be possible to send SOL', async function () {
10+
await withSolanaAccountSnap(
11+
{
12+
...DEFAULT_SOLANA_TEST_DAPP_FIXTURE_OPTIONS,
13+
title: this.test?.fullTitle(),
14+
mockGetTransactionSuccess: true,
15+
withCustomMocks: mockSendRedesignFeatureFlag,
16+
},
17+
async (driver) => {
18+
const sendPage = new SendPage(driver);
19+
const nonEvmHomepage = new NonEvmHomepage(driver);
20+
const snapTransactionConfirmation = new SnapTransactionConfirmation(
21+
driver,
22+
);
23+
24+
await nonEvmHomepage.clickOnSendButton();
25+
26+
await sendPage.createSendRequest({
27+
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
28+
symbol: 'SOL',
29+
recipientAddress: '7bYxDqvLQ4P8p6Vq3J6t1wczVwLk9h4Q9M5rjqvN1sVg',
30+
amount: '1',
31+
});
32+
33+
await snapTransactionConfirmation.clickFooterCancelButton();
34+
},
35+
);
36+
});
37+
});

ui/pages/confirmations/components/UI/asset/asset.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('TokenAsset', () => {
6565
it('renders token asset with correct information', () => {
6666
const { getByText, getByTestId } = render(<Asset asset={mockTokenAsset} />);
6767

68-
expect(getByTestId('token-asset')).toBeInTheDocument();
68+
expect(getByTestId('token-asset-0x1-TEST')).toBeInTheDocument();
6969
expect(getByText('Test Token')).toBeInTheDocument();
7070
expect(getByText('TEST')).toBeInTheDocument();
7171
expect(getByText('$100.00')).toBeInTheDocument();
@@ -78,7 +78,7 @@ describe('TokenAsset', () => {
7878
<Asset asset={mockTokenAsset} onClick={mockOnClick} />,
7979
);
8080

81-
fireEvent.click(getByTestId('token-asset'));
81+
fireEvent.click(getByTestId('token-asset-0x1-TEST'));
8282
expect(mockOnClick).toHaveBeenCalled();
8383
});
8484

@@ -87,7 +87,7 @@ describe('TokenAsset', () => {
8787
<Asset asset={mockTokenAsset} isSelected={true} />,
8888
);
8989

90-
const tokenAsset = getByTestId('token-asset');
90+
const tokenAsset = getByTestId('token-asset-0x1-TEST');
9191
expect(tokenAsset).toHaveStyle(
9292
'background-color: var(--color-background-hover)',
9393
);
@@ -99,7 +99,7 @@ describe('TokenAsset', () => {
9999
<Asset asset={assetWithoutChainId} />,
100100
);
101101

102-
expect(getByTestId('token-asset')).toBeInTheDocument();
102+
expect(getByTestId('token-asset-undefined-TEST')).toBeInTheDocument();
103103
expect(queryByRole('img', { name: 'Ethereum' })).not.toBeInTheDocument();
104104
});
105105
});

ui/pages/confirmations/components/UI/asset/asset.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ const TokenAsset = ({ asset, onClick, isSelected }: AssetProps) => {
128128
: BackgroundColor.transparent
129129
}
130130
className="send-asset"
131-
data-testid="token-asset"
131+
data-testid={`token-asset-${chainId}-${symbol}`}
132132
display={Display.Flex}
133133
onClick={onClick}
134134
paddingTop={3}

0 commit comments

Comments
 (0)