diff --git a/.gitignore b/.gitignore index 483384c..1299db8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ node_modules # testing coverage +/tests-playwright/*.png +/tests-playwright/artifacts/ # next.js .next/ diff --git a/config.ts b/config.ts index a98b082..07f586c 100644 --- a/config.ts +++ b/config.ts @@ -77,6 +77,10 @@ const enabledLoginOptions: EnabledLoginOptions = { const includeTestnetTokens = process.env.NEXT_PUBLIC_INCLUDE_TESTNET_TOKENS === "true" +// Enables deterministic UI states exclusively for the Playwright test server. +const playwrightTestMode = + process.env.NEXT_PUBLIC_PLAYWRIGHT_TEST_MODE === "true" + const lifiConfig = { apiUrl: "https://li.quest/v1", chainTypes: ["EVM", "UTXO", "SVM"], @@ -215,6 +219,7 @@ export const config = Object.freeze({ vercelConfig, env, includeTestnetTokens, + playwrightTestMode, enabledLoginOptions, lifiConfig, alchemyConfig, diff --git a/package.json b/package.json index 072598e..42829a4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "start": "next start -p 80", "lint": "next lint", "test": "vitest run", + "test:e2e": "playwright test", + "test:e2e:screenshots": "playwright test --update-snapshots", "biome:verify:all": "biome check", "biome:fix:staged": "biome check --write --no-errors-on-unmatched --staged", "biome:fix:all": "biome check --write --no-errors-on-unmatched", @@ -69,6 +71,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.3", + "@playwright/test": "1.61.1", "@types/async-retry": "^1.4.9", "@types/hdkey": "^2.1.0", "@types/node": "26.1.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..04a7623 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,44 @@ +import { randomBytes } from "node:crypto" +import { defineConfig, devices } from "@playwright/test" + +const testWalletSeed = randomBytes(32).toString("hex") + +export default defineConfig({ + testDir: "./tests-playwright", + testMatch: "**/*.e2e.ts", + outputDir: "tests-playwright/artifacts", + fullyParallel: false, + workers: 1, + reporter: "list", + use: { + baseURL: "http://localhost:3000", + trace: "retain-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "desktop", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "mobile", + use: { ...devices["iPhone 13"], browserName: "chromium" }, + }, + ], + webServer: { + command: "yarn next dev --turbo -p 3000", + url: "http://localhost:3000", + reuseExistingServer: false, + env: { + ...process.env, + NEXT_PUBLIC_PLAYWRIGHT_TEST_MODE: "true", + NEXT_PUBLIC_LOGIN_GOOGLE_ENABLED: "false", + NEXT_PUBLIC_LOGIN_FACEBOOK_ENABLED: "false", + NEXT_PUBLIC_LOGIN_PWLESS_ENABLED: "false", + NEXT_PUBLIC_TEST_LOGIN_ENABLED: "true", + NEXT_PUBLIC_TEST_LOGIN_MASTER_SEED: testWalletSeed, + NEXT_PUBLIC_TEST_LOGIN_USER_NAME: "Playwright test wallet", + NEXT_PUBLIC_TEST_LOGIN_PROFILE_IMAGE: "", + }, + }, +}) diff --git a/src/sections/GoodDollar/components/Claim/ClaimView.tsx b/src/sections/GoodDollar/components/Claim/ClaimView.tsx index da89d85..7ff31ab 100644 --- a/src/sections/GoodDollar/components/Claim/ClaimView.tsx +++ b/src/sections/GoodDollar/components/Claim/ClaimView.tsx @@ -23,6 +23,7 @@ import { getChainName } from "@/chain/chains" import { setBottomSheetProps } from "@/components/Snippet/BottomSheet/bottomSheetStore" import { LoadingSpinner } from "@/components/Snippet/LoadingSpinner" import { formatTokenAmount } from "@/components/Utils/tokenFormat" +import { config } from "@/config" import { topWallet } from "@/gooddollar/methods/faucet" import { collectBountyStore } from "@/gooddollar/stores/collectBountyStore" import { @@ -396,6 +397,10 @@ export default function ClaimView() { const goHome = useRouteTransition(`/${locale}`) const addPropagatingTx = useSnapshot(activityHistoryStore).addPropagatingTx + if (config.playwrightTestMode) { + return undefined} /> + } + const onClaim = () => { const ongoingClaims = new Map>() diff --git a/src/sections/Home/components/WalletSection.tsx b/src/sections/Home/components/WalletSection.tsx index b923ad7..f7123de 100644 --- a/src/sections/Home/components/WalletSection.tsx +++ b/src/sections/Home/components/WalletSection.tsx @@ -225,10 +225,14 @@ export default function WalletSection({ -
+
{goodDollarLink} {sendLink} {receiveLink} diff --git a/tests-playwright/wallet-flows.e2e.ts b/tests-playwright/wallet-flows.e2e.ts new file mode 100644 index 0000000..2389b4e --- /dev/null +++ b/tests-playwright/wallet-flows.e2e.ts @@ -0,0 +1,102 @@ +import { expect, type Page, test } from "@playwright/test" + +const screenshotPath = (name: string, project: string) => + `tests-playwright/${name}-${project}.png` + +const preparePage = async (page: Page, showOnboarding: boolean) => { + await page.context().route("**/*", (route) => { + const { hostname, pathname } = new URL(route.request().url()) + if (pathname === "/api/tokens") { + return route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ tokens: {} }), + }) + } + return hostname === "localhost" ? route.continue() : route.abort() + }) + await page.addInitScript( + ({ showOnboarding }) => { + localStorage.setItem("ShowWelcomeDialog", String(showOnboarding)) + localStorage.setItem("defaultLoginMethod", "testlogin") + localStorage.setItem("Tracking_Sentry", "denied") + localStorage.setItem("Tracking_Amplitude", "denied") + sessionStorage.setItem("gd-claim-view-seen", "true") + sessionStorage.removeItem("deep_link_url") + }, + { showOnboarding }, + ) +} + +const login = async (page: Page) => { + await page.goto("/en?login=master_seed") + await page.getByRole("button", { name: "Playwright test wallet" }).click() + await expect(page.getByText("Playwright test wallet")).toBeVisible() +} + +test("captures the locale-aware onboarding and login entry", async ({ + page, +}, testInfo) => { + await preparePage(page, true) + await page.goto("/da?login=master_seed") + + await expect( + page.getByText("Velkommen til din nye og forbedrede GoodWallet!"), + ).toBeVisible() + await page.screenshot({ + path: screenshotPath("login-onboarding-da", testInfo.project.name), + fullPage: true, + }) + + await page.getByRole("button", { name: "Sign In" }).click() + await expect( + page.getByRole("button", { name: "Playwright test wallet" }), + ).toBeVisible() +}) + +test("captures the authenticated home balance and mobile action overflow", async ({ + page, +}, testInfo) => { + await preparePage(page, false) + await login(page) + + await expect(page.getByText("$124.68")).toBeVisible() + const walletActions = page.getByTestId("wallet-actions") + await expect(walletActions).toBeVisible() + + if (testInfo.project.name === "mobile") { + const hasOverflow = await walletActions.evaluate( + (element) => element.scrollWidth > element.clientWidth, + ) + expect(hasOverflow).toBe(true) + await walletActions.evaluate((element) => { + element.scrollLeft = element.scrollWidth + }) + } + + await page.screenshot({ + path: screenshotPath("home-balances-overflow-en", testInfo.project.name), + fullPage: true, + }) +}) + +test("captures the claim verification requirement", async ({ + page, +}, testInfo) => { + await preparePage(page, false) + await login(page) + await page.getByRole("link", { name: "GoodDollar" }).click() + + await expect( + page.getByText( + "Before you can start to claim your GoodDollars you first need to pass face verification to whitelist your account.", + ), + ).toBeVisible() + await expect(page.getByRole("button", { name: "Verify" })).toBeVisible() + await page.screenshot({ + path: screenshotPath( + "claim-requires-verification-en", + testInfo.project.name, + ), + fullPage: true, + }) +}) diff --git a/yarn.lock b/yarn.lock index 5bb088b..992ff8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1799,6 +1799,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:1.61.1": + version: 1.61.1 + resolution: "@playwright/test@npm:1.61.1" + dependencies: + playwright: "npm:1.61.1" + bin: + playwright: cli.js + checksum: 10c0/45004139eaa7c7ec8129e4ed77a9d734aa09ed40b69162b871e4123f1d70217b7b73651431a9343ed5090d5fed11c6df1bff2a56ff4b87dc7b0371b5da87cf9c + languageName: node + linkType: hard + "@polymarket/builder-abstract-signer@npm:0.0.1": version: 0.0.1 resolution: "@polymarket/builder-abstract-signer@npm:0.0.1" @@ -7043,6 +7054,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -7053,6 +7074,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -7189,6 +7219,7 @@ __metadata: "@lifi/sdk-provider-solana": "npm:^4.0.2" "@lottiefiles/react-lottie-player": "npm:^3.6.0" "@neondatabase/serverless": "npm:^1.1.0" + "@playwright/test": "npm:1.61.1" "@polymarket/builder-relayer-client": "npm:^0.0.10" "@polymarket/clob-client": "npm:^5.8.1" "@react-hookz/web": "npm:^25.2.0" @@ -8542,6 +8573,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.61.1": + version: 1.61.1 + resolution: "playwright-core@npm:1.61.1" + bin: + playwright-core: cli.js + checksum: 10c0/c28896ba82a602182e240ed4f9c467fb0dd9cb57d510f8aba9f25183fe1cde3a0105725a29baffa4157fe885bbb11e228032a2aad0424111584fde45303f07bd + languageName: node + linkType: hard + +"playwright@npm:1.61.1": + version: 1.61.1 + resolution: "playwright@npm:1.61.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.61.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/cea1bc4d2a64ec3ef683891606774029da7b8a8036ed98332565cec2f8e89549ca1fab9a89fbfba4e08e27e0d7e7d148031e965af66d5ff97bb3c34058cfcad0 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0"