Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: [POM] Migrate token tests #29375

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c5367d0
migrate test to ts and POM
cmd-ob Dec 17, 2024
6e9112d
Merge branch 'main' into qq-mulitple-tokens-migration
cmd-ob Dec 17, 2024
20ba9c4
update typo, general tidy up
cmd-ob Dec 18, 2024
352dd40
Merge branch 'qq-mulitple-tokens-migration' of github.com:MetaMask/me…
cmd-ob Dec 18, 2024
886cf1a
add load check methods, make naming consistent
cmd-ob Dec 18, 2024
874c128
Merge branch 'main' into qq-mulitple-tokens-migration
cmd-ob Dec 18, 2024
d00f8e3
linting
cmd-ob Dec 18, 2024
5aa73cf
Merge branch 'qq-mulitple-tokens-migration' of github.com:MetaMask/me…
cmd-ob Dec 18, 2024
bab4345
migrate erc-20 to contract test
cmd-ob Dec 18, 2024
073579f
Merge branch 'main' of github.com:MetaMask/metamask-extension into qq…
cmd-ob Dec 19, 2024
b4d3fec
remove duplicate function
cmd-ob Dec 19, 2024
e8fbb7a
use seconadar name
cmd-ob Dec 19, 2024
f11c25d
get buttons by text to avoid flake
cmd-ob Dec 20, 2024
be104ee
Merge branch 'main' of github.com:MetaMask/metamask-extension into qq…
cmd-ob Dec 20, 2024
faffc73
update token list,sort and import
cmd-ob Dec 20, 2024
f8a0e0e
lint
cmd-ob Dec 20, 2024
10f2f7a
use largeDelay
cmd-ob Dec 20, 2024
37cbee3
Merge branch 'main' into qq-more-token-tests
cmd-ob Dec 20, 2024
7f54477
Merge branch 'main' into qq-more-token-tests
cmd-ob Dec 20, 2024
3c2f970
alphabetize token overview page, use better assertion for percentage
cmd-ob Dec 20, 2024
b0febde
Merge branch 'qq-more-token-tests' of github.com:MetaMask/metamask-ex…
cmd-ob Dec 20, 2024
86ae434
update warning assertion
cmd-ob Dec 20, 2024
8ea7007
linting
cmd-ob Dec 20, 2024
6be682d
Merge branch 'main' of github.com:MetaMask/metamask-extension into qq…
cmd-ob Dec 20, 2024
a8ab1ad
remove getAssetPercentageIncreaseDecrease method
cmd-ob Dec 20, 2024
c166dff
fix lint
chloeYue Dec 20, 2024
c09fd7d
Merge branch 'main' into qq-more-token-tests
chloeYue Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 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,42 @@ 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 getAssetPercentageIncreaseDecrease(
chloeYue marked this conversation as resolved.
Show resolved Hide resolved
assetAddress: string,
): Promise<string> {
console.log(
`Retrieving the percentage increase or decrease for ${assetAddress}`,
);
const percentageElement = await this.driver.findElement(
this.tokenPercentage(assetAddress),
);
const percentage = await percentageElement.getText();
Copy link
Contributor

@seaona seaona Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm this is an anti-pattern that opens the door to race conditions, where the element is rendered but it does not have yet the value we expect.
We should try to avoid finding an element and then getting its text where possible. I think in this case it's possible to avoid this. In the specs below, we should directly try to find the element by its expected inner value instead of this assert

const percentage =
  await assetListPage.getAssetPercentageIncreaseDecrease(tokenAddress);
assert.equal(percentage, '');

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I agree with you here. I have used isElementPresent to check for the existence of the element for a given token address

return percentage;
}

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 +186,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 +216,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 +336,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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of small things:

  • could we sort these selectors alphabetically maybe?
  • I think these buttons are the exact same buttons we have in the home screen. Maybe we could separate them in their own component, and re-use them in the homepage class and in the token overview class? Just a thought, it doesn't have to be in this PR. The advantage would be that if a button from this group changes, we'll only need to update it in one place. The disadvantage might be that it might complicate a bit the component? though not too much. What are your thoughts? In any case, it can be left outside this PR, if we decide to have this separate component

Screenshot from 2024-12-20 10-45-01

Screenshot from 2024-12-20 10-45-09

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also had the same thought. It could make sense (if we keep it text based as I have done here)
the only doubt that I have is the depending on the token type different buttons are displayed so that is still something to think about. I do agree though there is a way we could make this easier to deal with

};
Copy link
Contributor Author

@cmd-ob cmd-ob Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using text based selectors here, it's more accessible and means we don't have to create multiple selectors depending on the type of token screen we are looking at. i.e Is the token enabled for swap, bridge, send etc.

The other selector would be either coin-overview-* or token-overview-* or eth-overview-*
This would cause flakiness in the e2e test. I do think there is a bug on the correct selector/element being rendered. It is not affecting the user but they might need to cover it with unit tests so something else does not break


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
Loading