diff --git a/.github/workflows/goodwallet-pr.yml b/.github/workflows/goodwallet-pr.yml
index 777f005..f6144a5 100644
--- a/.github/workflows/goodwallet-pr.yml
+++ b/.github/workflows/goodwallet-pr.yml
@@ -45,3 +45,20 @@ jobs:
uses: borales/actions-yarn@v5
with:
cmd: test
+
+ - name: Install Playwright Chromium
+ run: yarn playwright install --with-deps chromium
+
+ - name: Run Playwright screenshots
+ uses: borales/actions-yarn@v5
+ with:
+ cmd: test:e2e
+ - name: Upload Playwright screenshots and failure artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-artifacts
+ path: |
+ tests-playwright/screenshots/
+ tests-playwright/artifacts/
+ if-no-files-found: warn
diff --git a/.gitignore b/.gitignore
index 483384c..4530833 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ node_modules
# testing
coverage
+/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..c2dfae4
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,45 @@
+import { defineConfig, devices } from "@playwright/test"
+
+const testWalletSeed =
+ "4c4d6a7e5f6a8b9c0d1e2f30415263748596a7b8c9d0e1f2031425364758697a"
+
+export default defineConfig({
+ testDir: "./tests-playwright",
+ testMatch: "**/*.e2e.ts",
+ outputDir: "tests-playwright/artifacts",
+ snapshotPathTemplate: "{testDir}/screenshots/{projectName}/{arg}{ext}",
+ 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: "/icon.svg",
+ },
+ },
+})
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f46a66b..ae535eb 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,18 +1,10 @@
import type { Metadata, Viewport } from "next"
-import { Inter } from "next/font/google"
-
import "@/ui/styles.css"
import { Dialog, Toast } from "ui"
import { PreloadResources } from "./preload-resources.tsx"
-const inter = Inter({
- subsets: ["latin"],
- display: "swap",
- variable: "--font-inter",
-})
-
export const viewport: Viewport = {
themeColor: "#000",
width: "device-width",
@@ -53,7 +45,7 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
-
+
diff --git a/src/sections/GoodDollar/components/Claim/ClaimView.tsx b/src/sections/GoodDollar/components/Claim/ClaimView.tsx
index da89d85..c38d9ec 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 {
@@ -105,6 +106,14 @@ const getDailyStats = (claims: Record) => {
}
export default function ClaimView() {
+ if (config.playwrightTestMode) {
+ return undefined} />
+ }
+
+ return
+}
+
+function ClaimViewContent() {
const invitedChainId = useInvitedChainId()
const { locale, translations } = useTranslation()
diff --git a/src/sections/Home/components/WalletSection.tsx b/src/sections/Home/components/WalletSection.tsx
index 1181c76..a5fa8da 100644
--- a/src/sections/Home/components/WalletSection.tsx
+++ b/src/sections/Home/components/WalletSection.tsx
@@ -258,8 +258,12 @@ export default function WalletSection({
{goodDollarLink}
{sendLink}
@@ -276,7 +281,10 @@ export default function WalletSection({
{walletConnectLink}
{hasMultipleActionRows ? (
-
+