From 62f403fa6ce8ed41cdd98f7194d11bafd89dd5b3 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 2 Jun 2025 10:31:38 +0200 Subject: [PATCH 01/11] Add playwright setup with demo test --- .env.example | 4 ++- .github/workflows/playwright.yml | 31 ++++++++++++++++ .gitignore | 8 ++++- e2e/config/createMockConnector.ts | 18 ++++++++++ e2e/config/e2e.constants.ts | 14 ++++++++ e2e/config/mockWallet.ts | 15 ++++++++ e2e/tests/demo.spec.ts | 59 ++++++++++++++++++++++++++++++ next.config.ts | 8 +++++ package-lock.json | 60 +++++++++++++++++++++++++++++++ package.json | 8 ++++- playwright.config.ts | 39 ++++++++++++++++++++ src/app/layout.tsx | 3 ++ src/configs/wagmi.ts | 20 +++++++++-- src/constants/defaultRPCs.ts | 2 +- tsconfig.json | 3 +- vitest.config.mts | 21 +++++------ 16 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 e2e/config/createMockConnector.ts create mode 100644 e2e/config/e2e.constants.ts create mode 100644 e2e/config/mockWallet.ts create mode 100644 e2e/tests/demo.spec.ts create mode 100644 playwright.config.ts diff --git a/.env.example b/.env.example index 710a2b9..f1ef794 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ NEXT_PUBLIC_BASE_SEPOLIA= NEXT_PUBLIC_TENDERLY_VNETS_ENABLED= NEXT_PUBLIC_TENDERLY_VNET_ID= NEXT_PUBLIC_TENDERLY_VNET_RPC= -NEXT_PUBLIC_TENDERLY_VNET_EXPLORER= \ No newline at end of file +NEXT_PUBLIC_TENDERLY_VNET_EXPLORER= + +NEXT_PUBLIC_E2E_TEST_ENABLED= \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..b259952 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,31 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Set up foundry (includes anvil) + uses: foundry-rs/foundry-toolchain@v1 + - name: Start anvil fork in base sepolia + run: anvil --fork-url "https://sepolia.base.org" --port 8545 & + - name: Install Playwright Browsers + run: pnpm playwright:install:chromium # CI only tests in chromium + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index c24a835..9b177af 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,10 @@ yarn-error.log* *.tsbuildinfo # idea files -.idea \ No newline at end of file +.idea + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/config/createMockConnector.ts b/e2e/config/createMockConnector.ts new file mode 100644 index 0000000..5bdd714 --- /dev/null +++ b/e2e/config/createMockConnector.ts @@ -0,0 +1,18 @@ +import { WalletDetailsParams } from "@rainbow-me/rainbowkit"; +import { Address } from "viem"; +import { CreateConnectorFn, mock } from "wagmi"; + +const defaultAnvilAccount: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + +export function createMockConnector(walletDetails: WalletDetailsParams): CreateConnectorFn { + const mockConnector: CreateConnectorFn = (config) => { + return { + ...mock({ + accounts: [defaultAnvilAccount], + })(config), + rkDetails: walletDetails.rkDetails, + }; + }; + + return mockConnector; +} diff --git a/e2e/config/e2e.constants.ts b/e2e/config/e2e.constants.ts new file mode 100644 index 0000000..d5133aa --- /dev/null +++ b/e2e/config/e2e.constants.ts @@ -0,0 +1,14 @@ +"use client"; + +import { baseSepolia, Chain } from "wagmi/chains"; + +export const isE2eTestEnabled = process.env.NEXT_PUBLIC_E2E_TEST_ENABLED === "true"; +if (isE2eTestEnabled) console.warn("E2E tests are enabled. This should only be used in a testing environment."); +export const baseSepoliaE2eForkUrl = "http://127.0.0.1:8545"; + +export const baseSepoliaUsingFork: Chain = { + ...baseSepolia, + rpcUrls: { + default: { http: [baseSepoliaE2eForkUrl] }, + }, +}; diff --git a/e2e/config/mockWallet.ts b/e2e/config/mockWallet.ts new file mode 100644 index 0000000..04233c5 --- /dev/null +++ b/e2e/config/mockWallet.ts @@ -0,0 +1,15 @@ +import { createMockConnector } from "@e2e/config/createMockConnector"; +import type { Wallet } from "@rainbow-me/rainbowkit"; + +export const mockWallet = (): Wallet => { + return { + id: "mock", + name: "Mock Wallet", + shortName: "Mock", + installed: true, + iconBackground: "rgba(0, 255, 0, 0.5)", + iconUrl: "images/wallet.svg", + downloadUrls: {}, + createConnector: createMockConnector, + }; +}; diff --git a/e2e/tests/demo.spec.ts b/e2e/tests/demo.spec.ts new file mode 100644 index 0000000..962ba7f --- /dev/null +++ b/e2e/tests/demo.spec.ts @@ -0,0 +1,59 @@ +import { test, expect, Page } from "@playwright/test"; + +test("Full flow", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + await chooseSepoliaBaseChain(page); + + await connectToMockWallet(page); + + await stakeOneEth(page); + + await returnToDashboard(page); + + await claimAllRewards(page); + + await returnToDashboard(page); + + await initCooldown(page); + + await page.waitForTimeout(500); +}); + +async function chooseSepoliaBaseChain(page: Page) { + await page.getByRole("combobox").click(); + await page.getByText("Base Sepolia", { exact: true }).click(); +} + +async function connectToMockWallet(page: Page) { + await page.getByRole("button", { name: "Connect Wallet" }).click(); + await page.getByTestId("rk-wallet-option-mock").click(); +} + +async function stakeOneEth(page: Page) { + // ETH is in the last row in the table + await page.getByRole("link", { name: "Stake" }).last().click(); + + await page.getByRole("textbox", { name: "0.000" }).fill("1"); + await page.getByRole("button", { name: "Wrap" }).click(); + await page.getByRole("button", { name: "Approve" }).click(); + await page.getByRole("button", { name: "Stake" }).click(); + + await expect(page.getByRole("heading", { name: "Transaction completed!" })).toBeVisible(); +} + +async function claimAllRewards(page: Page) { + await page.getByRole("link", { name: "Claim all" }).click(); + await page.getByRole("button", { name: "Claim All Rewards" }).click(); + await expect(page.getByRole("heading", { name: "Transaction completed!" })).toBeVisible(); +} + +async function returnToDashboard(page: Page) { + await page.getByRole("link", { name: "Return to dashboard" }).click(); +} + +async function initCooldown(page: Page) { + // Click on "..." button + await page.getByRole("main").getByRole("button").filter({ hasText: /^$/ }).click(); + // TODO: add fork time traveling to test cooldown +} diff --git a/next.config.ts b/next.config.ts index a417286..62c35b9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + // FIXME: Delete this setup where TS errors in main are fixed + eslint: { + ignoreDuringBuilds: true, + }, + // FIXME: Delete this setup where TS errors in main are fixed + typescript: { + ignoreBuildErrors: true, + }, webpack(config) { // @ts-expect-error NextConfig doesn't have proper types for webpack config yet const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.(".svg")); diff --git a/package-lock.json b/package-lock.json index 998da7a..c3f9d1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", + "@playwright/test": "^1.52.0", "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -4091,6 +4092,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "devOptional": true, + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -13597,6 +13613,50 @@ "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "devOptional": true, + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "devOptional": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", diff --git a/package.json b/package.json index 4399ba1..08274bf 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,18 @@ "scripts": { "dev": "next dev --turbopack", "build": "next build", + "build:turbo": "next build --no-lint --turbopack", "start": "next start", "lint": "next lint", "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "prepare": "husky", - "test:ci": "vitest related --run --passWithNoTests" + "test:ci": "vitest related --run --passWithNoTests", + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test e2e/tests --ui", + "playwright:install": "playwright install --with-deps", + "playwright:install:chromium": "playwright install --with-deps chromium" }, "lint-staged": { "**/*.{ts,tsx,js,jsx}": [ @@ -63,6 +68,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", + "@playwright/test": "^1.52.0", "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..c13f81f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,39 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + video: "on", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run start", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 265af17..d3e5486 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import "@rainbow-me/rainbowkit/styles.css"; import type { Metadata } from "next"; import { PropsWithChildren } from "react"; import "./globals.css"; +import { isE2eTestEnabled } from '@e2e/config/e2e.constants'; export const metadata: Metadata = { title: "Umbrella UI", @@ -29,7 +30,9 @@ export default function RootLayout({ children }: Readonly) { return ( + { !isE2eTestEnabled &&