From 399f2ffbbeb616916443e9b5556dd054fbad6b1e Mon Sep 17 00:00:00 2001 From: kyzooghost <73516204+kyzooghost@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:34:38 +1000 Subject: [PATCH] [Fix] Bridge UI test method approveTokenPermission (#840) * new describe * fix * hi * fix * new approve impl * try * try fix * try * try * rewrite * rewrite * different flake test * different flake test * different flake test * different flake test * different flake test * different flake test * do flakiness test * Revert "do flakiness test" This reverts commit e06e5dc0d55b6fef08e8b24243940c1044eaaf96. --- bridge-ui/playwright.config.ts | 10 +- bridge-ui/test/advancedFixtures.ts | 56 ++- bridge-ui/test/constants.ts | 2 + bridge-ui/test/e2e/bridge-l1-l2.spec.ts | 341 +++++++++--------- .../utils/selectTokenAndWaitForBalance.ts | 5 +- 5 files changed, 229 insertions(+), 185 deletions(-) diff --git a/bridge-ui/playwright.config.ts b/bridge-ui/playwright.config.ts index 93530d0f..c08756e9 100644 --- a/bridge-ui/playwright.config.ts +++ b/bridge-ui/playwright.config.ts @@ -3,7 +3,7 @@ import "dotenv/config"; export default defineConfig({ testDir: ".", - testMatch: '**/*.spec.ts', + testMatch: "**/*.spec.ts", // Timeout for tests that don't involve blockchain transactions timeout: 40_000, fullyParallel: true, @@ -12,10 +12,10 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? [ - ["html", { open: "never", outputFolder: `playwright-report-${process.env.HEADLESS ? "headless" : "headful"}` }], - ["list"] - ] - : [["html"],["list"]], + ["html", { open: "never", outputFolder: `playwright-report-${process.env.HEADLESS ? "headless" : "headful"}` }], + ["list"], + ] + : [["html"], ["list"]], use: { baseURL: "http://localhost:3000", trace: process.env.CI ? "on" : "retain-on-failure", diff --git a/bridge-ui/test/advancedFixtures.ts b/bridge-ui/test/advancedFixtures.ts index 5e9ad4ab..9b648d7f 100644 --- a/bridge-ui/test/advancedFixtures.ts +++ b/bridge-ui/test/advancedFixtures.ts @@ -1,8 +1,9 @@ -import { metaMaskFixtures } from "@synthetixio/synpress/playwright"; +import { metaMaskFixtures, getExtensionId } from "@synthetixio/synpress/playwright"; import setup from "./wallet-setup/metamask.setup"; -import { Locator } from "@playwright/test"; +import { Locator, Page } from "@playwright/test"; import { getNativeBridgeTransactionsCountImpl, selectTokenAndWaitForBalance } from "./utils"; -import { LINEA_SEPOLIA_NETWORK, POLLING_INTERVAL } from "./constants"; +import { LINEA_SEPOLIA_NETWORK, PAGE_TIMEOUT, POLLING_INTERVAL } from "./constants"; +import next from "next"; /** * NB: There is an issue with Synpress `metaMaskFixtures` extension functions wherein extension functions * may not be able to reuse other extension functions. This is especially the case when advanced operations @@ -25,6 +26,8 @@ export const test = metaMaskFixtures(setup).extend<{ // Metamask Actions - Should be ok to reuse within other fixture functions connectMetamaskToDapp: () => Promise; + openMetamaskActivityPage: () => Promise; + submitERC20ApprovalTx: () => Promise; waitForTransactionToConfirm: () => Promise; confirmTransactionAndWaitForInclusion: () => Promise; switchToLineaSepolia: () => Promise; @@ -134,7 +137,7 @@ export const test = metaMaskFixtures(setup).extend<{ await page.bringToFront(); }); }, - waitForTransactionToConfirm: async ({ metamask }, use) => { + openMetamaskActivityPage: async ({ metamask }, use) => { await use(async () => { await metamask.page.bringToFront(); await metamask.page.reload(); @@ -147,7 +150,45 @@ export const test = metaMaskFixtures(setup).extend<{ if (await gotItButton.isVisible()) await gotItButton.click(); // Click Activity button await activityButton.click(); - + }); + }, + // We use this instead of metamask.approveTokenPermission because we found the original method flaky + submitERC20ApprovalTx: async ({ context, page, metamask }, use) => { + await use(async () => { + // Need to wait for Metamask Notification page to exist, does not exist immediately after clicking 'Approve' button. + // In Synpress source code, they use this logic in every method interacting with the Metamask notification page. + const extensionId = await getExtensionId(context, "MetaMask"); + const notificationPageUrl = `chrome-extension://${extensionId}/notification.html`; + while ( + metamask.page + .context() + .pages() + .find((page) => page.url().includes(notificationPageUrl)) === undefined + ) { + await page.waitForTimeout(POLLING_INTERVAL); + } + const notificationPage = metamask.page + .context() + .pages() + .find((page) => page.url().includes(notificationPageUrl)) as Page; + await notificationPage.waitForLoadState("domcontentloaded", { timeout: PAGE_TIMEOUT }); + await notificationPage.waitForLoadState("networkidle", { timeout: PAGE_TIMEOUT }); + await metamask.page.reload(); + // Unsure if commented out below are required to mitigate flakiness + // await metamask.page.waitForLoadState("domcontentloaded", { timeout: PAGE_TIMEOUT }); + // await metamask.page.waitForLoadState("networkidle", { timeout: PAGE_TIMEOUT }); + const nextBtn = metamask.page.getByRole("button", { name: "Next", exact: true }); + // Unsure if commented out below are required to mitigate flakiness + // await expect(nextBtn).toBeVisible(); + // await expect(nextBtn).toBeEnabled(); + await nextBtn.click(); + const approveMMBtn = metamask.page.getByRole("button", { name: "Approve", exact: true }); + await approveMMBtn.click(); + }); + }, + waitForTransactionToConfirm: async ({ metamask, openMetamaskActivityPage }, use) => { + await use(async () => { + await openMetamaskActivityPage(); let txCount = await metamask.page .locator(metamask.homePage.selectors.activityTab.pendingApprovedTransactions) .count(); @@ -177,7 +218,7 @@ export const test = metaMaskFixtures(setup).extend<{ }, // Composite Bridge UI + Metamask Actions - doTokenApprovalIfNeeded: async ({ page, metamask, waitForTransactionToConfirm }, use) => { + doTokenApprovalIfNeeded: async ({ page, submitERC20ApprovalTx, waitForTransactionToConfirm }, use) => { await use(async () => { // Check if approval required const approvalButton = page.getByRole("button", { name: "Approve Token", exact: true }); @@ -185,8 +226,7 @@ export const test = metaMaskFixtures(setup).extend<{ await approvalButton.click(); // Handle Metamask approval UI - // bridge-ui-known-flaky-line - Once seen Metamask stuck here on approval screen in CI - await metamask.approveTokenPermission(); + await submitERC20ApprovalTx(); await waitForTransactionToConfirm(); // Close 'Transaction successful' modal diff --git a/bridge-ui/test/constants.ts b/bridge-ui/test/constants.ts index 4f23041e..f96c61c7 100644 --- a/bridge-ui/test/constants.ts +++ b/bridge-ui/test/constants.ts @@ -1,4 +1,5 @@ import { formatEther, formatUnits } from "viem"; +import "dotenv/config"; export const METAMASK_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE; export const METAMASK_PASSWORD = process.env.E2E_TEST_WALLET_PASSWORD; @@ -21,3 +22,4 @@ export const ETH_SYMBOL = "ETH"; export const USDC_SYMBOL = "USDC"; export const POLLING_INTERVAL = 250; +export const PAGE_TIMEOUT = 10000; diff --git a/bridge-ui/test/e2e/bridge-l1-l2.spec.ts b/bridge-ui/test/e2e/bridge-l1-l2.spec.ts index 258eaa62..01a4bceb 100644 --- a/bridge-ui/test/e2e/bridge-l1-l2.spec.ts +++ b/bridge-ui/test/e2e/bridge-l1-l2.spec.ts @@ -8,190 +8,193 @@ const { expect, describe } = test; // There are known lines causing flaky E2E tests in this test suite, these are annotated by 'bridge-ui-known-flaky-line' describe("L1 > L2 via Native Bridge", () => { - test("should successfully go to the bridge UI page", async ({ page }) => { - const pageUrl = page.url(); - expect(pageUrl).toEqual(TEST_URL); + describe("No blockchain tx cases", () => { + test.describe.configure({ mode: "parallel" }); + + test("should successfully go to the bridge UI page", async ({ page }) => { + const pageUrl = page.url(); + expect(pageUrl).toEqual(TEST_URL); + }); + + test("should have 'Native Bridge' button link on homepage", async ({ clickNativeBridgeButton }) => { + const nativeBridgeBtn = await clickNativeBridgeButton(); + await expect(nativeBridgeBtn).toBeVisible(); + }); + + test("should connect MetaMask to dapp correctly", async ({ connectMetamaskToDapp, clickNativeBridgeButton }) => { + await clickNativeBridgeButton(); + await connectMetamaskToDapp(); + }); + + test("should be able to load the transaction history", async ({ + page, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeTransactionHistory, + }) => { + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeTransactionHistory(); + + const txHistoryHeading = page.getByRole("heading").filter({ hasText: "Transaction History" }); + await expect(txHistoryHeading).toBeVisible(); + }); + + test("should be able to switch to test networks", async ({ + page, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeFormSettings, + toggleShowTestNetworksInNativeBridgeForm, + }) => { + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeFormSettings(); + await toggleShowTestNetworksInNativeBridgeForm(); + + // Should have Sepolia text visible + const sepoliaText = page.getByText("Sepolia").first(); + await expect(sepoliaText).toBeVisible(); + }); + + test("should not be able to approve on the wrong network", async ({ + page, + metamask, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeFormSettings, + toggleShowTestNetworksInNativeBridgeForm, + selectTokenAndInputAmount, + switchToEthereumMainnet, + }) => { + test.setTimeout(60_000); + + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeFormSettings(); + await toggleShowTestNetworksInNativeBridgeForm(); + + await switchToEthereumMainnet(); + await selectTokenAndInputAmount(USDC_SYMBOL, USDC_AMOUNT); + + // Should have 'Switch to Sepolia' network button visible and enabled + const switchBtn = page.getByRole("button", { name: "Switch to Sepolia", exact: true }); + await expect(switchBtn).toBeVisible(); + await expect(switchBtn).toBeEnabled(); + + // Do network switch + await switchBtn.click(); + await metamask.approveSwitchNetwork(); + + // After network switch, should have 'Approve Token' button visible and enabled + const approvalButton = page.getByRole("button", { name: "Approve Token", exact: true }); + await expect(approvalButton).toBeVisible(); + await expect(approvalButton).toBeEnabled(); + }); }); - test("should have 'Native Bridge' button link on homepage", async ({ clickNativeBridgeButton }) => { - const nativeBridgeBtn = await clickNativeBridgeButton(); - await expect(nativeBridgeBtn).toBeVisible(); - }); + describe("Blockchain tx cases", () => { + // If not serial risk colliding nonces -> transactions cancelling each other out + test.describe.configure({ retries: 1, timeout: 120_000, mode: "serial" }); - test("should connect MetaMask to dapp correctly", async ({ connectMetamaskToDapp, clickNativeBridgeButton }) => { - await clickNativeBridgeButton(); - await connectMetamaskToDapp(); - }); + test("should be able to initiate bridging ETH from L1 to L2 in testnet", async ({ + getNativeBridgeTransactionsCount, + waitForNewTxAdditionToTxList, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeFormSettings, + toggleShowTestNetworksInNativeBridgeForm, + selectTokenAndInputAmount, + doInitiateBridgeTransaction, + openNativeBridgeTransactionHistory, + closeNativeBridgeTransactionHistory, + }) => { + // Setup testnet UI + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeFormSettings(); + await toggleShowTestNetworksInNativeBridgeForm(); - test("should be able to load the transaction history", async ({ - page, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeTransactionHistory, - }) => { - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeTransactionHistory(); + // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory. + await openNativeBridgeTransactionHistory(); + const txnsLengthBefore = await getNativeBridgeTransactionsCount(); + await closeNativeBridgeTransactionHistory(); - const txHistoryHeading = page.getByRole("heading").filter({ hasText: "Transaction History" }); - await expect(txHistoryHeading).toBeVisible(); - }); + // // Actual bridging actions + await selectTokenAndInputAmount(ETH_SYMBOL, WEI_AMOUNT); + await doInitiateBridgeTransaction(); - test("should be able to switch to test networks", async ({ - page, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeFormSettings, - toggleShowTestNetworksInNativeBridgeForm, - }) => { - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeFormSettings(); - await toggleShowTestNetworksInNativeBridgeForm(); + // Check that our bridge tx shows up in the tx history + await waitForNewTxAdditionToTxList(txnsLengthBefore); + }); - // Should have Sepolia text visible - const sepoliaText = page.getByText("Sepolia").first(); - await expect(sepoliaText).toBeVisible(); - }); + test("should be able to initiate bridging USDC from L1 to L2 in testnet", async ({ + getNativeBridgeTransactionsCount, + waitForNewTxAdditionToTxList, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeFormSettings, + toggleShowTestNetworksInNativeBridgeForm, + selectTokenAndInputAmount, + doInitiateBridgeTransaction, + openNativeBridgeTransactionHistory, + closeNativeBridgeTransactionHistory, + doTokenApprovalIfNeeded, + }) => { + // Setup testnet UI + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeFormSettings(); + await toggleShowTestNetworksInNativeBridgeForm(); - test("should not be able to approve on the wrong network", async ({ - page, - metamask, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeFormSettings, - toggleShowTestNetworksInNativeBridgeForm, - selectTokenAndInputAmount, - switchToEthereumMainnet, - }) => { - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeFormSettings(); - await toggleShowTestNetworksInNativeBridgeForm(); + // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory. + await openNativeBridgeTransactionHistory(); + const txnsLengthBefore = await getNativeBridgeTransactionsCount(); + await closeNativeBridgeTransactionHistory(); - await switchToEthereumMainnet(); - await selectTokenAndInputAmount(USDC_SYMBOL, USDC_AMOUNT); + // Actual bridging actions + await selectTokenAndInputAmount(USDC_SYMBOL, USDC_AMOUNT); + await doTokenApprovalIfNeeded(); + await doInitiateBridgeTransaction(); - // Should have 'Switch to Sepolia' network button visible and enabled - const switchBtn = page.getByRole("button", {name: "Switch to Sepolia", exact: true}); - await expect(switchBtn).toBeVisible(); - await expect(switchBtn).toBeEnabled(); + // Check that our bridge tx shows up in the tx history + await waitForNewTxAdditionToTxList(txnsLengthBefore); + }); - // Do network switch - await switchBtn.click(); - await metamask.approveSwitchNetwork(); + test("should be able to claim if available READY_TO_CLAIM transactions", async ({ + page, + connectMetamaskToDapp, + clickNativeBridgeButton, + openNativeBridgeFormSettings, + toggleShowTestNetworksInNativeBridgeForm, + openNativeBridgeTransactionHistory, + getNativeBridgeTransactionsCount, + switchToLineaSepolia, + doClaimTransaction, + waitForTxListUpdateForClaimTx, + }) => { + await connectMetamaskToDapp(); + await clickNativeBridgeButton(); + await openNativeBridgeFormSettings(); + await toggleShowTestNetworksInNativeBridgeForm(); - // After network switch, should have 'Approve Token' button visible and enabled - const approvalButton = page.getByRole("button", { name: "Approve Token", exact: true }); - await expect(approvalButton).toBeVisible(); - await expect(approvalButton).toBeEnabled(); - }); + // Switch to L2 network + await switchToLineaSepolia(); - test("should be able to initiate bridging ETH from L1 to L2 in testnet", async ({ - getNativeBridgeTransactionsCount, - waitForNewTxAdditionToTxList, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeFormSettings, - toggleShowTestNetworksInNativeBridgeForm, - selectTokenAndInputAmount, - doInitiateBridgeTransaction, - openNativeBridgeTransactionHistory, - closeNativeBridgeTransactionHistory, - }) => { - // Code smell that we may need to refactor E2E tests with blockchain tx into another describe block with a separate timeout - test.setTimeout(90_000); + // Load tx history + await openNativeBridgeTransactionHistory(); + await getNativeBridgeTransactionsCount(); - // Setup testnet UI - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeFormSettings(); - await toggleShowTestNetworksInNativeBridgeForm(); + // Find and click READY_TO_CLAIM TX + const readyToClaimTx = page.getByRole("listitem").filter({ hasText: "Ready to claim" }); + const readyToClaimCount = await readyToClaimTx.count(); + if (readyToClaimCount === 0) return; + await readyToClaimTx.first().click(); - // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory. - await openNativeBridgeTransactionHistory(); - const txnsLengthBefore = await getNativeBridgeTransactionsCount(); - await closeNativeBridgeTransactionHistory(); + await doClaimTransaction(); - // // Actual bridging actions - await selectTokenAndInputAmount(ETH_SYMBOL, WEI_AMOUNT); - await doInitiateBridgeTransaction(); - - // Check that our bridge tx shows up in the tx history - await waitForNewTxAdditionToTxList(txnsLengthBefore); - }); - - test("should be able to initiate bridging USDC from L1 to L2 in testnet", async ({ - getNativeBridgeTransactionsCount, - waitForNewTxAdditionToTxList, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeFormSettings, - toggleShowTestNetworksInNativeBridgeForm, - selectTokenAndInputAmount, - doInitiateBridgeTransaction, - openNativeBridgeTransactionHistory, - closeNativeBridgeTransactionHistory, - doTokenApprovalIfNeeded, - }) => { - // At least 2 blockchain tx in this test - test.setTimeout(120_000); - - // Setup testnet UI - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeFormSettings(); - await toggleShowTestNetworksInNativeBridgeForm(); - - // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory. - await openNativeBridgeTransactionHistory(); - const txnsLengthBefore = await getNativeBridgeTransactionsCount(); - await closeNativeBridgeTransactionHistory(); - - // Actual bridging actions - await selectTokenAndInputAmount(USDC_SYMBOL, USDC_AMOUNT); - await doTokenApprovalIfNeeded(); - await doInitiateBridgeTransaction(); - - // Check that our bridge tx shows up in the tx history - await waitForNewTxAdditionToTxList(txnsLengthBefore); - }); - - test("should be able to claim if available READY_TO_CLAIM transactions", async ({ - page, - connectMetamaskToDapp, - clickNativeBridgeButton, - openNativeBridgeFormSettings, - toggleShowTestNetworksInNativeBridgeForm, - openNativeBridgeTransactionHistory, - getNativeBridgeTransactionsCount, - switchToLineaSepolia, - doClaimTransaction, - waitForTxListUpdateForClaimTx, - }) => { - test.setTimeout(90_000); - - await connectMetamaskToDapp(); - await clickNativeBridgeButton(); - await openNativeBridgeFormSettings(); - await toggleShowTestNetworksInNativeBridgeForm(); - - // Switch to L2 network - await switchToLineaSepolia(); - - // Load tx history - await openNativeBridgeTransactionHistory(); - await getNativeBridgeTransactionsCount(); - - // Find and click READY_TO_CLAIM TX - const readyToClaimTx = page.getByRole("listitem").filter({ hasText: "Ready to claim" }); - const readyToClaimCount = await readyToClaimTx.count(); - if (readyToClaimCount === 0) return; - await readyToClaimTx.first().click(); - - await doClaimTransaction(); - - // Check that tx history has updated accordingly - await waitForTxListUpdateForClaimTx(readyToClaimCount); + // Check that tx history has updated accordingly + await waitForTxListUpdateForClaimTx(readyToClaimCount); + }); }); }); diff --git a/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts b/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts index 878c72b8..36b68db6 100644 --- a/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts +++ b/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts @@ -1,5 +1,5 @@ import { Page } from "@playwright/test"; -import { POLLING_INTERVAL } from "../constants"; +import { POLLING_INTERVAL, PAGE_TIMEOUT } from "../constants"; export async function selectTokenAndWaitForBalance(tokenSymbol: string, page: Page) { const openModalBtn = page.getByTestId("native-bridge-open-token-list-modal"); @@ -9,10 +9,9 @@ export async function selectTokenAndWaitForBalance(tokenSymbol: string, page: Pa console.log(`Fetching token balance for ${tokenSymbol}`); // Timeout implementation - const fetchTokenBalanceTimeout = 5000; let fetchTokenTimeUsed = 0; while ((await tokenBalance.textContent()) === `0 ${tokenSymbol}`) { - if (fetchTokenTimeUsed >= fetchTokenBalanceTimeout) + if (fetchTokenTimeUsed >= PAGE_TIMEOUT) throw `Could not find any balance for ${tokenSymbol}, does the testing wallet have funds?`; await page.waitForTimeout(POLLING_INTERVAL); fetchTokenTimeUsed += POLLING_INTERVAL;