Skip to content

Commit

Permalink
test: [POM] Migrate token tests (#29375)
Browse files Browse the repository at this point in the history
<!--
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**

* Updates `asset-list` Page Object - Adds methods for interacting with
token list (i.e sorting, getting list, assertion on increase/decrease
price and percentage)
* Adds new method for importing a custom token using contract address
* Adds new method for adding multiple tokens by search in one step
* Minor update to `send-token` page for warning message
* New page object for `token-overview`
* Tests Updated - `import-tokens`, `send-erc20-to-contract`,
`token-list` and `token-sort`


## **Related issues**

Fixes:

## **Manual testing steps**

* All tests must pass on CI

## **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**

- [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**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] 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.

---------

Co-authored-by: Chloe Gao <[email protected]>
Co-authored-by: chloeYue <[email protected]>
Co-authored-by: MetaMask Bot <[email protected]>
  • Loading branch information
4 people authored Jan 8, 2025
1 parent e5ff471 commit 7fe00dc
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 300 deletions.
153 changes: 153 additions & 0 deletions test/e2e/page-objects/pages/home/asset-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class AssetListPage {

private readonly currentNetworksTotal = `${this.currentNetworkOption} [data-testid="account-value-and-suffix"]`;

private readonly customTokenModalOption = {
text: 'Custom token',
tag: 'button',
};

private readonly hideTokenButton = '[data-testid="asset-options__hide"]';

private readonly hideTokenConfirmationButton =
Expand All @@ -43,16 +48,42 @@ class AssetListPage {

private readonly networksToggle = '[data-testid="sort-by-networks"]';

private sortByAlphabetically = '[data-testid="sortByAlphabetically"]';

private sortByDecliningBalance = '[data-testid="sortByDecliningBalance"]';

private sortByPopoverToggle = '[data-testid="sort-by-popover-toggle"]';

private readonly tokenAddressInput =
'[data-testid="import-tokens-modal-custom-address"]';

private readonly tokenAmountValue =
'[data-testid="multichain-token-list-item-value"]';

private readonly tokenImportedSuccessMessage = {
text: 'Token imported',
tag: 'h6',
};

private readonly tokenListItem =
'[data-testid="multichain-token-list-button"]';

private readonly tokenOptionsButton = '[data-testid="import-token-button"]';

private tokenPercentage(address: string): string {
return `[data-testid="token-increase-decrease-percentage-${address}"]`;
}

private readonly tokenSearchInput = 'input[placeholder="Search tokens"]';

private readonly tokenSymbolInput =
'[data-testid="import-tokens-modal-custom-symbol"]';

private readonly modalWarningBanner = 'div.mm-banner-alert--severity-warning';

private readonly tokenIncreaseDecreaseValue =
'[data-testid="token-increase-decrease-value"]';

constructor(driver: Driver) {
this.driver = driver;
}
Expand Down Expand Up @@ -103,6 +134,29 @@ class AssetListPage {
return assets.length;
}

async getTokenListNames(): Promise<string[]> {
console.log(`Retrieving the list of token names`);
const tokenElements = await this.driver.findElements(this.tokenListItem);
const tokenNames = await Promise.all(
tokenElements.map(async (element) => {
return await element.getText();
}),
);
return tokenNames;
}

async sortTokenList(
sortBy: 'alphabetically' | 'decliningBalance',
): Promise<void> {
console.log(`Sorting the token list by ${sortBy}`);
await this.driver.clickElement(this.sortByPopoverToggle);
if (sortBy === 'alphabetically') {
await this.driver.clickElement(this.sortByAlphabetically);
} else if (sortBy === 'decliningBalance') {
await this.driver.clickElement(this.sortByDecliningBalance);
}
}

/**
* Hides a token by clicking on the token name, and confirming the hide modal.
*
Expand All @@ -119,6 +173,22 @@ class AssetListPage {
);
}

async importCustomToken(tokenAddress: string, symbol: string): Promise<void> {
console.log(`Creating custom token ${symbol} on homepage`);
await this.driver.clickElement(this.tokenOptionsButton);
await this.driver.clickElement(this.importTokensButton);
await this.driver.waitForSelector(this.importTokenModalTitle);
await this.driver.clickElement(this.customTokenModalOption);
await this.driver.waitForSelector(this.modalWarningBanner);
await this.driver.fill(this.tokenAddressInput, tokenAddress);
await this.driver.fill(this.tokenSymbolInput, symbol);
await this.driver.clickElement(this.importTokensNextButton);
await this.driver.clickElementAndWaitToDisappear(
this.confirmImportTokenButton,
);
await this.driver.waitForSelector(this.tokenImportedSuccessMessage);
}

async importTokenBySearch(tokenName: string) {
console.log(`Import token ${tokenName} on homepage by search`);
await this.driver.clickElement(this.tokenOptionsButton);
Expand All @@ -133,6 +203,24 @@ class AssetListPage {
);
}

async importMultipleTokensBySearch(tokenNames: string[]) {
console.log(
`Importing tokens ${tokenNames.join(', ')} on homepage by search`,
);
await this.driver.clickElement(this.tokenOptionsButton);
await this.driver.clickElement(this.importTokensButton);
await this.driver.waitForSelector(this.importTokenModalTitle);

for (const name of tokenNames) {
await this.driver.fill(this.tokenSearchInput, name);
await this.driver.clickElement({ text: name, tag: 'p' });
}
await this.driver.clickElement(this.importTokensNextButton);
await this.driver.clickElementAndWaitToDisappear(
this.confirmImportTokenButton,
);
}

async openNetworksFilter(): Promise<void> {
console.log(`Opening the network filter`);
await this.driver.clickElement(this.networksToggle);
Expand Down Expand Up @@ -235,6 +323,71 @@ class AssetListPage {
`Expected number of token items ${expectedNumber} is displayed.`,
);
}

/**
* Checks if the token's general increase or decrease percentage is displayed correctly
*
* @param address - The token address to check
* @param expectedChange - The expected change percentage value (e.g. '+0.02%' or '-0.03%')
*/
async check_tokenGeneralChangePercentage(
address: string,
expectedChange: string,
): Promise<void> {
console.log(
`Checking token general change percentage for address ${address}`,
);
const isPresent = await this.driver.isElementPresentAndVisible({
css: this.tokenPercentage(address),
text: expectedChange,
});
if (!isPresent) {
throw new Error(
`Token general change percentage ${expectedChange} not found for address ${address}`,
);
}
}

/**
* Checks if the token's percentage change element does not exist
*
* @param address - The token address to check
*/
async check_tokenGeneralChangePercentageNotPresent(
address: string,
): Promise<void> {
console.log(
`Checking token general change percentage is not present for address ${address}`,
);
const isPresent = await this.driver.isElementPresent({
css: this.tokenPercentage(address),
});
if (isPresent) {
throw new Error(
`Token general change percentage element should not exist for address ${address}`,
);
}
}

/**
* Checks if the token's general increase or decrease value is displayed correctly
*
* @param expectedChangeValue - The expected change value (e.g. '+$50.00' or '-$30.00')
*/
async check_tokenGeneralChangeValue(
expectedChangeValue: string,
): Promise<void> {
console.log(`Checking token general change value ${expectedChangeValue}`);
const isPresent = await this.driver.isElementPresentAndVisible({
css: this.tokenIncreaseDecreaseValue,
text: expectedChangeValue,
});
if (!isPresent) {
throw new Error(
`Token general change value ${expectedChangeValue} not found`,
);
}
}
}

export default AssetListPage;
19 changes: 19 additions & 0 deletions test/e2e/page-objects/pages/send/send-token-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class SendTokenPage {

private readonly toastText = '.toast-text';

private readonly warning =
'[data-testid="send-warning"] .mm-box--min-width-0 span';

constructor(driver: Driver) {
this.driver = driver;
}
Expand Down Expand Up @@ -196,6 +199,22 @@ class SendTokenPage {
text: address,
});
}

/**
* Verifies that a specific warning message is displayed on the send token screen.
*
* @param warningText - The expected warning text to validate against.
* @returns A promise that resolves if the warning message matches the expected text.
* @throws Assertion error if the warning message does not match the expected text.
*/
async check_warningMessage(warningText: string): Promise<void> {
console.log(`Checking if warning message "${warningText}" is displayed`);
await this.driver.waitForSelector({
css: this.warning,
text: warningText,
});
console.log('Warning message validation successful');
}
}

export default SendTokenPage;
54 changes: 54 additions & 0 deletions test/e2e/page-objects/pages/token-overview-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Driver } from '../../webdriver/driver';

class TokenOverviewPage {
private driver: Driver;

private readonly receiveButton = {
text: 'Receive',
css: '.icon-button',
};

private readonly sendButton = {
text: 'Send',
css: '.icon-button',
};

private readonly swapButton = {
text: 'Swap',
css: '.icon-button',
};

constructor(driver: Driver) {
this.driver = driver;
}

async check_pageIsLoaded(): Promise<void> {
try {
await this.driver.waitForMultipleSelectors([
this.sendButton,
this.swapButton,
]);
} catch (e) {
console.log(
'Timeout while waiting for Token overview page to be loaded',
e,
);
throw e;
}
console.log('Token overview page is loaded');
}

async clickReceive(): Promise<void> {
await this.driver.clickElement(this.receiveButton);
}

async clickSend(): Promise<void> {
await this.driver.clickElement(this.sendButton);
}

async clickSwap(): Promise<void> {
await this.driver.clickElement(this.swapButton);
}
}

export default TokenOverviewPage;
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const { strict: assert } = require('assert');
const {
import AssetListPage from '../../page-objects/pages/home/asset-list';
import HomePage from '../../page-objects/pages/home/homepage';

import {
defaultGanacheOptions,
withFixtures,
unlockWallet,
} = require('../../helpers');
const FixtureBuilder = require('../../fixture-builder');
} from '../../helpers';
import FixtureBuilder from '../../fixture-builder';
import { Mockttp } from '../../mock-e2e';

describe('Import flow', function () {
async function mockPriceFetch(mockServer) {
async function mockPriceFetch(mockServer: Mockttp) {
return [
await mockServer
.forGet('https://price.api.cx.metamask.io/v2/chains/1/spot-prices')
Expand Down Expand Up @@ -60,47 +63,27 @@ describe('Import flow', function () {
})
.build(),
ganacheOptions: defaultGanacheOptions,
title: this.test.fullTitle(),
title: this.test?.fullTitle(),
testSpecificMock: mockPriceFetch,
},
async ({ driver }) => {
await unlockWallet(driver);

await driver.assertElementNotPresent('.loading-overlay');

await driver.clickElement('[data-testid="import-token-button"]');
await driver.clickElement('[data-testid="importTokens"]');

await driver.fill('input[placeholder="Search tokens"]', 'cha');

await driver.clickElement('.token-list__token_component');
await driver.clickElement(
'.token-list__token_component:nth-of-type(2)',
);
await driver.clickElement(
'.token-list__token_component:nth-of-type(3)',
);

await driver.clickElement('[data-testid="import-tokens-button-next"]');
await driver.clickElement(
'[data-testid="import-tokens-modal-import-button"]',
);

// Wait for "loading tokens" to be gone
await driver.assertElementNotPresent(
'[data-testid="token-list-loading-message"]',
);

await driver.assertElementNotPresent(
'[data-testid="token-list-loading-message"]',
);

await driver.clickElement('[data-testid="sort-by-networks"]');
await driver.clickElement('[data-testid="network-filter-current"]');
const homePage = new HomePage(driver);
const assetListPage = new AssetListPage(driver);
await homePage.check_pageIsLoaded();
await assetListPage.importMultipleTokensBySearch([
'CHAIN',
'CHANGE',
'CHAI',
]);

const expectedTokenListElementsAreFound =
await driver.elementCountBecomesN('.multichain-token-list-item', 4);
assert.equal(expectedTokenListElementsAreFound, true);
const tokenList = new AssetListPage(driver);
await tokenList.check_tokenItemNumber(5); // Linea & Mainnet Eth
await tokenList.check_tokenIsDisplayed('Ethereum');
await tokenList.check_tokenIsDisplayed('Chain Games');
await tokenList.check_tokenIsDisplayed('Changex');
await tokenList.check_tokenIsDisplayed('Chai');
},
);
});
Expand Down
Loading

0 comments on commit 7fe00dc

Please sign in to comment.