Skip to content

Commit 41782bc

Browse files
davibrocclaude
andauthored
test: Adds gasless swap E2E tests (#41485)
<!-- 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** Add swap-gasless.spec.ts with three new E2E tests covering gas-included and gas-sponsored (7702) swap flows: ETH→USDC via Max, USDC→DAI with ERC20 approval, and ETH→USDC via 7702 relay ## **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** 1. Go to this page... 2. 3. ## **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 - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are confined to E2E test code, fixtures, and mock quote payloads, with no production logic modified. > > **Overview** > Adds a new `swap-gasless.spec.ts` E2E suite that validates **gas-included** and **gas-sponsored (7702 relay)** swap paths, including a Max-button swap and an ERC20 approval + swap sequence. > > Extends the bridge E2E page object and fixture utilities to support these scenarios by adding selectors/helpers for the gas-fee indicators and Max button, plus new mock SSE quote streams and sentinel/feature-flag configurations to drive the gasless behaviors under test. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ac23f85. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8566ecc commit 41782bc

6 files changed

Lines changed: 679 additions & 0 deletions

File tree

test/e2e/page-objects/pages/bridge/quote-page.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class BridgeQuotePage {
4747

4848
private backButton = '[aria-label="Back"]';
4949

50+
private gasIncludedIndicator = '[data-testid="network-fees-included"]';
51+
52+
private gasSponsoredIndicator = '[data-testid="network-fees-sponsored"]';
53+
54+
private maxButton = { text: 'Max' };
55+
5056
private networkSelector = '[data-testid="multichain-asset-picker__network"]';
5157

5258
private networkFees = '[data-testid="network-fees"]';
@@ -305,6 +311,36 @@ class BridgeQuotePage {
305311
console.log('Price matches expected format');
306312
}
307313

314+
async checkGasIncludedIsDisplayed(): Promise<void> {
315+
try {
316+
await this.driver.waitForSelector(this.gasIncludedIndicator, {
317+
timeout: 30000,
318+
});
319+
} catch (e) {
320+
console.log('Expected "Gas fees included" indicator is not present');
321+
throw e;
322+
}
323+
console.log('Gas fees included indicator is displayed');
324+
}
325+
326+
async checkGasSponsoredIsDisplayed(): Promise<void> {
327+
try {
328+
await this.driver.waitForSelector(this.gasSponsoredIndicator, {
329+
timeout: 30000,
330+
});
331+
} catch (e) {
332+
console.log('Expected "Gas fees sponsored" indicator is not present');
333+
throw e;
334+
}
335+
console.log('Gas fees sponsored indicator is displayed');
336+
}
337+
338+
async clickMaxButton(): Promise<void> {
339+
await this.driver.waitForSelector(this.maxButton, { timeout: 30000 });
340+
await this.driver.clickElement(this.maxButton);
341+
console.log('Clicked Max button');
342+
}
343+
308344
checkDestAmount = async (amount: string) => {
309345
const destAmount = await this.driver.findElement(this.destinationAmount);
310346
assert.equal(await destAmount.getAttribute('value'), amount);
@@ -336,6 +372,26 @@ class BridgeQuotePage {
336372
`);
337373
}
338374

375+
async selectSrcToken(token: string): Promise<void> {
376+
await this.driver.waitForSelector(this.sourceAssetPickerButton);
377+
await this.driver.clickElement(this.sourceAssetPickerButton);
378+
await this.driver.fill(this.assetPrickerSearchInput, token);
379+
await this.driver.clickElementAndWaitToDisappear({
380+
text: token,
381+
css: this.tokenButton,
382+
});
383+
}
384+
385+
async selectDestToken(token: string): Promise<void> {
386+
await this.driver.waitForSelector(this.destinationAssetPickerButton);
387+
await this.driver.clickElement(this.destinationAssetPickerButton);
388+
await this.driver.fill(this.assetPrickerSearchInput, token);
389+
await this.driver.clickElementAndWaitToDisappear({
390+
text: token,
391+
css: this.tokenButton,
392+
});
393+
}
394+
339395
async selectNetwork(network: string): Promise<void> {
340396
await this.driver.clickElement(this.networkSelector);
341397
await this.driver.clickElement(this.networkNameSelector(network));

test/e2e/tests/bridge/bridge-test-utils.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ import {
4040
SSE_RESPONSE_HEADER,
4141
EXPECTED_INPUT_CHANGES,
4242
BRIDGE_REFRESH_RATE,
43+
BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED,
4344
} from './constants';
4445
import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json';
46+
import MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED from './mocks/swap-quotes-eth-usdc-gas-included.json';
47+
import MOCK_SWAP_QUOTES_USDC_DAI_GAS_INCLUDED from './mocks/swap-quotes-usdc-dai-gas-included.json';
48+
import MOCK_SWAP_QUOTES_ETH_USDC_GAS_SPONSORED from './mocks/swap-quotes-eth-usdc-gas-sponsored.json';
4549

4650
export class BridgePage {
4751
driver: Driver;
@@ -1773,6 +1777,239 @@ export const checkInputChangedEvents = async (
17731777
return expectedInputChanges.length;
17741778
};
17751779

1780+
async function mockSentinelNetworks(
1781+
mockServer: Mockttp,
1782+
sendBundle: boolean = true,
1783+
) {
1784+
return await mockServer
1785+
.forGet('https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks')
1786+
.always()
1787+
.thenCallback(() => ({
1788+
statusCode: 200,
1789+
json: {
1790+
'1': {
1791+
network: 'ethereum-mainnet',
1792+
explorer: 'https://etherscan.io',
1793+
confirmations: true,
1794+
smartTransactions: true,
1795+
relayTransactions: true,
1796+
hidden: false,
1797+
sendBundle,
1798+
},
1799+
},
1800+
}));
1801+
}
1802+
1803+
async function mockGasIncludedSwapETHtoUSDC(mockServer: Mockttp) {
1804+
return await mockServer
1805+
.forGet(/getQuoteStream/u)
1806+
.once()
1807+
.withQuery({
1808+
srcTokenAddress: '0x0000000000000000000000000000000000000000',
1809+
destTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
1810+
})
1811+
.thenStream(
1812+
200,
1813+
mockSseEventSource(MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED),
1814+
SSE_RESPONSE_HEADER,
1815+
);
1816+
}
1817+
1818+
async function mockGasIncludedSwapUSDCtoDAI(mockServer: Mockttp) {
1819+
return await mockServer
1820+
.forGet(/getQuoteStream/u)
1821+
.once()
1822+
.withQuery({
1823+
srcTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
1824+
destTokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
1825+
})
1826+
.thenStream(
1827+
200,
1828+
mockSseEventSource(MOCK_SWAP_QUOTES_USDC_DAI_GAS_INCLUDED),
1829+
SSE_RESPONSE_HEADER,
1830+
);
1831+
}
1832+
1833+
export const getGasIncludedSwapFixtures = (title?: string) => {
1834+
const fixtureBuilder = new FixtureBuilder({
1835+
inputChainId: CHAIN_IDS.MAINNET,
1836+
})
1837+
.withCurrencyController(MOCK_CURRENCY_RATES)
1838+
.withBridgeControllerDefaultState()
1839+
.withTokensControllerERC20({ chainId: 1 })
1840+
.withEnabledNetworks({
1841+
eip155: {
1842+
'0x1': true,
1843+
'0xe708': true,
1844+
'0xa4b1': true,
1845+
},
1846+
});
1847+
1848+
return {
1849+
forceBip44Version: false,
1850+
fixtures: fixtureBuilder.build(),
1851+
testSpecificMock: async (mockServer: Mockttp) => {
1852+
const mocks = [
1853+
await mockPortfolioPage(mockServer),
1854+
await mockGetTxStatus(mockServer),
1855+
await mockTopAssetsLinea(mockServer),
1856+
await mockTopAssetsArbitrum(mockServer),
1857+
await mockTokensEthereum(mockServer),
1858+
await mockTokensLinea(mockServer),
1859+
await mockGetTokenArbitrum(mockServer),
1860+
await mockGetPopularTokens(mockServer),
1861+
await mockGasIncludedSwapETHtoUSDC(mockServer),
1862+
await mockGasIncludedSwapUSDCtoDAI(mockServer),
1863+
await mockFeatureFlags(
1864+
mockServer,
1865+
BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED,
1866+
STX_MAINNET_NETWORK_CONFIG,
1867+
),
1868+
await mockAccountsTransactions(mockServer),
1869+
await mockAccountsBalances(mockServer),
1870+
await mockPriceSpotPrices(mockServer),
1871+
await mockPriceSpotPricesV3(mockServer),
1872+
await mockGasPricesMainnet(mockServer),
1873+
await mockHistoricalPrices(mockServer),
1874+
await mockSentinelNetworks(mockServer),
1875+
...(await mockSearchTokens(mockServer)),
1876+
];
1877+
1878+
await mockSmartTransactionsForBridge(mockServer);
1879+
1880+
return mocks;
1881+
},
1882+
manifestFlags: {
1883+
remoteFeatureFlags: {
1884+
bridgeConfig: BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED,
1885+
...STX_MAINNET_NETWORK_CONFIG,
1886+
},
1887+
},
1888+
ethConversionInUsd: ETH_CONVERSION_RATE_USD,
1889+
smartContract: SMART_CONTRACTS.HST,
1890+
localNodeOptions: [
1891+
{
1892+
type: 'anvil' as const,
1893+
options: {
1894+
chainId: 1,
1895+
hardfork: 'london',
1896+
loadState:
1897+
'./test/e2e/seeder/network-states/with100Usdc100Usdt50Dai.json',
1898+
},
1899+
},
1900+
],
1901+
title,
1902+
};
1903+
};
1904+
1905+
async function mockGasSponsoredSwapETHtoUSDC(mockServer: Mockttp) {
1906+
return await mockServer
1907+
.forGet(/getQuoteStream/u)
1908+
.once()
1909+
.withQuery({
1910+
srcTokenAddress: '0x0000000000000000000000000000000000000000',
1911+
destTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
1912+
})
1913+
.thenStream(
1914+
200,
1915+
mockSseEventSource(MOCK_SWAP_QUOTES_ETH_USDC_GAS_SPONSORED),
1916+
SSE_RESPONSE_HEADER,
1917+
);
1918+
}
1919+
1920+
async function mockSentinelNetworksRelayOnly(mockServer: Mockttp) {
1921+
return mockSentinelNetworks(mockServer, false);
1922+
}
1923+
1924+
export const getGasless7702SwapFixtures = (title?: string) => {
1925+
const fixtureBuilder = new FixtureBuilder({
1926+
inputChainId: CHAIN_IDS.MAINNET,
1927+
})
1928+
.withCurrencyController(MOCK_CURRENCY_RATES)
1929+
.withBridgeControllerDefaultState()
1930+
.withTokensControllerERC20({ chainId: 1 })
1931+
.withEnabledNetworks({
1932+
eip155: {
1933+
'0x1': true,
1934+
'0xe708': true,
1935+
'0xa4b1': true,
1936+
},
1937+
});
1938+
1939+
return {
1940+
forceBip44Version: false,
1941+
fixtures: fixtureBuilder.build(),
1942+
testSpecificMock: async (mockServer: Mockttp) => {
1943+
const mocks = [
1944+
await mockPortfolioPage(mockServer),
1945+
await mockGetTxStatus(mockServer),
1946+
await mockTopAssetsLinea(mockServer),
1947+
await mockTopAssetsArbitrum(mockServer),
1948+
await mockTokensEthereum(mockServer),
1949+
await mockTokensLinea(mockServer),
1950+
await mockGetTokenArbitrum(mockServer),
1951+
await mockGetPopularTokens(mockServer),
1952+
await mockGasSponsoredSwapETHtoUSDC(mockServer),
1953+
await mockFeatureFlags(
1954+
mockServer,
1955+
BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED,
1956+
{
1957+
smartTransactionsNetworks: {
1958+
'0x1': {
1959+
maxDeadline: 160,
1960+
sentinelUrl: STX_MAINNET_SENTINEL_URL,
1961+
expectedDeadline: 45,
1962+
extensionActive: true,
1963+
gaslessBridgeWith7702Enabled: true,
1964+
},
1965+
},
1966+
},
1967+
),
1968+
await mockAccountsTransactions(mockServer),
1969+
await mockAccountsBalances(mockServer),
1970+
await mockPriceSpotPrices(mockServer),
1971+
await mockPriceSpotPricesV3(mockServer),
1972+
await mockGasPricesMainnet(mockServer),
1973+
await mockHistoricalPrices(mockServer),
1974+
await mockSentinelNetworksRelayOnly(mockServer),
1975+
...(await mockSearchTokens(mockServer)),
1976+
];
1977+
1978+
await mockSmartTransactionsForBridge(mockServer);
1979+
1980+
return mocks;
1981+
},
1982+
manifestFlags: {
1983+
remoteFeatureFlags: {
1984+
bridgeConfig: BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED,
1985+
smartTransactionsNetworks: {
1986+
'0x1': {
1987+
maxDeadline: 160,
1988+
sentinelUrl: STX_MAINNET_SENTINEL_URL,
1989+
expectedDeadline: 45,
1990+
extensionActive: true,
1991+
gaslessBridgeWith7702Enabled: true,
1992+
},
1993+
},
1994+
},
1995+
},
1996+
ethConversionInUsd: ETH_CONVERSION_RATE_USD,
1997+
smartContract: SMART_CONTRACTS.HST,
1998+
localNodeOptions: [
1999+
{
2000+
type: 'anvil' as const,
2001+
options: {
2002+
chainId: 1,
2003+
hardfork: 'london',
2004+
loadState:
2005+
'./test/e2e/seeder/network-states/with100Usdc100Usdt50Dai.json',
2006+
},
2007+
},
2008+
],
2009+
title,
2010+
};
2011+
};
2012+
17762013
export const checkQuoteRequestsAreNotMadeAfterTimestamp = async (
17772014
driver: Driver,
17782015
timestamp: number,

0 commit comments

Comments
 (0)