[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.
This commit is contained in:
kyzooghost
2025-04-10 20:34:38 +10:00
committed by GitHub
parent 31afe41f77
commit 399f2ffbbe
5 changed files with 229 additions and 185 deletions

View File

@@ -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",

View File

@@ -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<void>;
openMetamaskActivityPage: () => Promise<void>;
submitERC20ApprovalTx: () => Promise<void>;
waitForTransactionToConfirm: () => Promise<void>;
confirmTransactionAndWaitForInclusion: () => Promise<void>;
switchToLineaSepolia: () => Promise<void>;
@@ -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

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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;