From b702143084c8422ebfb78e747c7a15f934601bc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:07:50 +0000 Subject: [PATCH 1/2] Initial plan From f88f7fa746b91663b33db46678ffbbfe06837dce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:17:01 +0000 Subject: [PATCH 2/2] Update README with hipster/GitHub-trending style and Shadowgraph Labs branding Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --- .svelte-kit/generated/server/internal.js | 2 +- README.md | 329 +++++++++++++++++------ tests/e2e/navigation.spec.ts | 2 +- tests/e2e/theme-integration.spec.ts | 10 +- tests/e2e/wallet-connection.spec.ts | 63 +++-- 5 files changed, 300 insertions(+), 106 deletions(-) diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index 66c0778..e1522cb 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -23,7 +23,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n \n \n \n \n \n " + head + "\n \n \n
" + body + "
\n \n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "1xkp038" + version_hash: "qbzkuy" }; export async function get_hooks() { diff --git a/README.md b/README.md index 85508a9..e8eb738 100755 --- a/README.md +++ b/README.md @@ -1,118 +1,291 @@ -# Shadowgraph Reputation-Scaled Airdrop Client +
-This is a SvelteKit dApp client for participating in a Shadowgraph reputation-scaled airdrop campaign. It supports both ECDSA-based claims and ZK-proof-based claims. +# ๐ŸŒŸ Shadowgraph Reputation-Gated Airdrop -## Features +### _The Future of Merit-Based Token Distribution_ ๐Ÿš€ -- **Wallet Connection**: Connect via MetaMask, WalletConnect, or Coinbase Wallet. -- **Score Checking**: Fetches your reputation score from the Shadowgraph backend. -- **Payout Preview**: See your potential airdrop amount based on your score and the configured curve. -- **Claim Flow**: A guided process to claim your tokens via an on-chain transaction. -- **Chain Awareness**: Automatically detects and prompts for switching to the correct network. -- **Debug Mode**: A special view for developers to inspect configuration and state. +

+ Built by Shadowgraph Labs + Web3 Ready + ZK Enabled +

-## Tech Stack - -- **Framework**: SvelteKit -- **Styling**: TailwindCSS -- **Blockchain**: `viem` for EVM interactions, `@web3-onboard` for wallet connections. -- **Validation**: `zod` for environment and API response validation. -- **Icons**: `lucide-svelte` -- **Testing**: Vitest (unit), Playwright (e2e) +

+ GitHub Stars + GitHub Forks + GitHub Watchers +

--- -## Getting Started +_Revolutionizing airdrops through reputation-based distribution with cutting-edge zero-knowledge proofs._ + +**Powered by [Shadowgraph Labs](https://shadowgraph.io) ๐Ÿงช** + +
+ +## ๐ŸŽฏ Why This Project Rocks + +> **Traditional airdrops are broken.** They reward bots, incentivize Sybil attacks, and dilute value for genuine contributors. + +**Our solution?** A sophisticated reputation-gated system that: + +- ๐Ÿ›ก๏ธ **Prevents Sybil attacks** with cryptographic reputation scoring +- ๐Ÿ”ฎ **Rewards genuine contributors** based on provable on-chain activity +- ๐Ÿš€ **Scales infinitely** with zero-knowledge proof technology +- ๐ŸŒŠ **Flows seamlessly** with multi-wallet support and intuitive UX + +## โœจ Features That Matter + +### ๐Ÿ”— **Universal Wallet Support** + +Connect with MetaMask, WalletConnect, Coinbase Wallet, and more. We've got you covered. + +### ๐Ÿ“Š **Real-Time Reputation Scoring** + +Your contributions are continuously evaluated and reflected in your reputation score. + +### ๐ŸŽข **Dynamic Payout Curves** + +Choose from linear, square root, or quadratic distribution curves to optimize fairness. + +### ๐Ÿง™โ€โ™‚๏ธ **Zero-Knowledge Privacy** + +Prove your reputation without revealing sensitive data using cutting-edge ZK-SNARK technology. + +### ๐ŸŒ **Multi-Chain Ready** + +Built for Ethereum and EVM-compatible networks with seamless chain switching. -### 1. Installation +### ๐Ÿ”ง **Developer Experience** -Clone the repository and install the dependencies: +Comprehensive debug mode and developer tools for seamless integration. + +## ๐Ÿ› ๏ธ Tech Stack That Slaps + +
+ +| Frontend | Blockchain | Security | Testing | +| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| ![SvelteKit](https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=svelte&logoColor=white) | ![Viem](https://img.shields.io/badge/Viem-1B1B1D?style=for-the-badge) | ![Zod](https://img.shields.io/badge/Zod-3E67B1?style=for-the-badge) | ![Vitest](https://img.shields.io/badge/Vitest-6E9F18?style=for-the-badge) | +| ![TailwindCSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) | ![Web3 Onboard](https://img.shields.io/badge/Web3%20Onboard-627EEA?style=for-the-badge) | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) | ![Playwright](https://img.shields.io/badge/Playwright-45BA4B?style=for-the-badge) | + +
+ +### ๐Ÿงฌ **Architecture Highlights** + +- **๐Ÿ”ฅ Svelte 4** - Blazing fast reactive framework +- **โšก Vite** - Lightning-fast development experience +- **๐ŸŽจ TailwindCSS** - Utility-first styling that scales +- **๐Ÿ”— Viem** - Type-safe Ethereum interactions +- **๐Ÿงช Zod** - Runtime type validation for bulletproof APIs +- **๐ŸŽญ Lucide Icons** - Beautiful, consistent iconography + +## ๐Ÿš€ Quick Start Guide + +Ready to dive in? Let's get you up and running in under 5 minutes! + +### ๐Ÿ“ฆ **1. Installation** + +Clone this masterpiece and install dependencies: ```bash -git clone -cd shadowgraph-airdrop-client +# Clone the repository +git clone https://github.com/Steake/Reputation-Gated-Airdrop.git +cd Reputation-Gated-Airdrop + +# Install dependencies (Node.js 18+ required) npm install + +# You're ready to rock! ๐ŸŽธ ``` -### 2. Environment Variables +### โš™๏ธ **2. Environment Configuration** + +Create your `.env` file - this is where the magic happens! โœจ -Create a `.env` file in the root of the project. This file is critical for configuring the dApp to point to the correct contracts, chain, and backend services. +```bash +# ๐Ÿ”ฎ Create your environment configuration +cp .env.example .env +``` + +
+๐Ÿ“‹ Complete Environment Variables Guide ```env -# REQUIRED: Web3 & Chain Configuration -VITE_CHAIN_ID="11155111" # e.g., Sepolia -VITE_RPC_URL="https://rpc.sepolia.org" -VITE_TOKEN_ADDR="0x..." # The ERC20 token being airdropped - -# REQUIRED: Airdrop Campaign Configuration -VITE_CAMPAIGN="0x..." # 32-byte campaign identifier -VITE_FLOOR_SCORE="600000" # Min score to claim (1e6 scale) -VITE_CAP_SCORE="1000000" # Score for max payout (1e6 scale) -VITE_MIN_PAYOUT="100" # Min token payout (in token units, e.g., "100" for 100 tokens) -VITE_MAX_PAYOUT="1000" # Max token payout -VITE_CURVE="SQRT" # Payout curve: "LIN", "SQRT", or "QUAD" - -# REQUIRED: Backend API -VITE_API_BASE="https://api.shadowgraph.io/v1" # Base URL for score/artifact endpoints - -# REQUIRED: Web3-Onboard Project ID -# Get one from https://cloud.walletconnect.com/ -VITE_WALLETCONNECT_PROJECT_ID="YOUR_PROJECT_ID" - -# OPTIONAL: Contract Addresses (at least one path must be enabled) -# To enable the ECDSA claim path: -VITE_AIRDROP_ECDSA_ADDR="0x..." # ReputationAirdropScaled contract -# To enable the ZK claim path: -VITE_AIRDROP_ZK_ADDR="0x..." # ReputationAirdropZKScaled contract -VITE_VERIFIER_ADDR="0x..." # EZKL Verifier contract - -# OPTIONAL: Debug Mode -# Set to 'true' to enable the /debug route -VITE_DEBUG="true" +# ๐ŸŒ BLOCKCHAIN CONFIGURATION +VITE_CHAIN_ID="11155111" # Sepolia testnet (or your preferred network) +VITE_RPC_URL="https://rpc.sepolia.org" # RPC endpoint +VITE_TOKEN_ADDR="0x..." # ERC20 token being airdropped + +# ๐ŸŽฏ AIRDROP CAMPAIGN SETTINGS +VITE_CAMPAIGN="0x..." # 32-byte campaign identifier +VITE_FLOOR_SCORE="600000" # Minimum score to claim (1e6 scale) +VITE_CAP_SCORE="1000000" # Score for maximum payout (1e6 scale) +VITE_MIN_PAYOUT="100" # Minimum token payout +VITE_MAX_PAYOUT="1000" # Maximum token payout +VITE_CURVE="SQRT" # Payout curve: "LIN", "SQRT", or "QUAD" + +# ๐Ÿ”— API & SERVICES +VITE_API_BASE="https://api.shadowgraph.io/v1" # Shadowgraph backend API +VITE_WALLETCONNECT_PROJECT_ID="YOUR_PROJECT_ID" # Get from https://cloud.walletconnect.com/ + +# ๐Ÿ“œ SMART CONTRACTS (Choose your path!) +# ๐Ÿ–‹๏ธ Traditional ECDSA path: +VITE_AIRDROP_ECDSA_ADDR="0x..." # ReputationAirdropScaled contract + +# ๐Ÿง™โ€โ™‚๏ธ Zero-Knowledge path: +VITE_AIRDROP_ZK_ADDR="0x..." # ReputationAirdropZKScaled contract +VITE_VERIFIER_ADDR="0x..." # EZKL Verifier contract + +# ๐Ÿ”ง DEVELOPER TOOLS +VITE_DEBUG="true" # Enable debug mode (/debug route) ``` -### 3. Running the Development Server +
+ +### ๐Ÿš€ **3. Launch Your Dev Server** + +Time to see your work come to life! ```bash +# ๐Ÿ”ฅ Fire up the development server npm run dev + +# ๐ŸŒ Open your browser to http://localhost:5173 +# Watch the magic happen! โœจ ``` -Open [http://localhost:5173](http://localhost:5173) to view the application. +> **Pro Tip:** The app will automatically reload as you make changes. Happy coding! ๐Ÿ‘จโ€๐Ÿ’ป ---- +## ๐ŸŽญ Development Modes -## Development Modes +### ๐ŸŽช **Mock Mode** (Perfect for Development) -### Mock Mode +No backend? No problem! When `VITE_API_BASE` is **not set**, we've got you covered: -If the `VITE_API_BASE` environment variable is **not set**, the application will run in **Mock Mode**. In this mode, it does not make real network requests to a backend. Instead, it uses mock data generators to simulate API responses. This is useful for UI development and testing without needing a live backend. +- ๐ŸŽฒ **Deterministic scores** based on wallet addresses +- ๐ŸŽญ **Realistic mock data** for `/claim-artifact` and `/proof-meta` endpoints +- ๐Ÿš€ **Zero setup** - just start coding! -- `/scores/:addr` returns a deterministic score based on the address. -- `/claim-artifact` and `/proof-meta` return fake but correctly-shaped data. +Perfect for UI development and testing without infrastructure dependencies. -### Production Mode +### ๐ŸŒ **Production Mode** (The Real Deal) -Set `VITE_API_BASE` to your backend's URL to connect to the live services. +Set `VITE_API_BASE` to connect to live Shadowgraph services. Experience the full power of reputation-gated airdrops! -## Claim Paths (ECDSA vs. ZK) +## ๐Ÿ›ค๏ธ Claim Paths: Choose Your Adventure -The application can be configured for one or both claim paths: +### ๐Ÿ–‹๏ธ **ECDSA Path** (Traditional & Reliable) -- **ECDSA Path**: Requires `VITE_AIRDROP_ECDSA_ADDR` to be set. The client fetches a signed EIP-712 artifact from the backend and submits it to the `ReputationAirdropScaled` contract. -- **ZK Path**: Requires `VITE_AIRDROP_ZK_ADDR` and `VITE_VERIFIER_ADDR` to be set. The client fetches ZK proof calldata from the backend and submits it to the `ReputationAirdropZKScaled` contract. +- **Setup**: Configure `VITE_AIRDROP_ECDSA_ADDR` +- **How it works**: Fetches signed EIP-712 artifacts from Shadowgraph backend +- **Submits to**: `ReputationAirdropScaled` smart contract +- **Best for**: Standard deployments and maximum compatibility -If both are configured, the UI will prioritize the ZK path by default. +### ๐Ÿง™โ€โ™‚๏ธ **Zero-Knowledge Path** (Cutting-Edge Privacy) -## Available Scripts +- **Setup**: Configure `VITE_AIRDROP_ZK_ADDR` and `VITE_VERIFIER_ADDR` +- **How it works**: Generates zero-knowledge proofs for reputation verification +- **Submits to**: `ReputationAirdropZKScaled` smart contract +- **Best for**: Privacy-focused applications and advanced cryptographic setups -- `npm run dev`: Start the dev server. -- `npm run build`: Build the application for production. -- `npm run preview`: Preview the production build locally. -- `npm run test:unit`: Run unit tests with Vitest. -- `npm run test:e2e`: Run end-to-end tests with Playwright. -- `npm run lint`: Check for linting and formatting issues. -- `npm run format`: Automatically format the code. +> **๐Ÿ’ก Pro Tip:** Configure both paths for maximum flexibility! The UI will intelligently prioritize ZK when available. -``` +## ๐Ÿ“œ Available Scripts + +| Command | Description | Duration | +| ------------------- | ----------------------------- | -------- | +| `npm run dev` | ๐Ÿš€ Start development server | ~2s | +| `npm run build` | ๐Ÿ—๏ธ Build for production | ~30s | +| `npm run preview` | ๐Ÿ‘๏ธ Preview production build | ~2s | +| `npm run test:unit` | ๐Ÿงช Run unit tests (Vitest) | ~2s | +| `npm run test:e2e` | ๐ŸŽญ Run E2E tests (Playwright) | Variable | +| `npm run lint` | ๐Ÿ” Check code quality | ~8s | +| `npm run format` | โœจ Auto-format code | ~9s | + +> **๐Ÿ’ก Quick Commands:** Run `npm run format && npm run lint` before committing to ensure pristine code quality! + +## ๐Ÿ—๏ธ Project Structure ``` +src/ +โ”œโ”€โ”€ lib/ +โ”‚ โ”œโ”€โ”€ components/ # ๐Ÿงฉ Reusable Svelte components +โ”‚ โ”œโ”€โ”€ stores/ # ๐Ÿ“ฆ Svelte stores (state management) +โ”‚ โ”œโ”€โ”€ web3/ # ๐Ÿ”— Blockchain interaction logic +โ”‚ โ”œโ”€โ”€ abi/ # ๐Ÿ“œ Smart contract ABIs +โ”‚ โ””โ”€โ”€ utils/ # ๐Ÿ› ๏ธ Utility functions +โ”œโ”€โ”€ routes/ # ๐Ÿ›ค๏ธ SvelteKit routes and pages +โ””โ”€โ”€ app.html # ๐ŸŒ HTML template + +tests/ +โ”œโ”€โ”€ unit/ # ๐Ÿงช Vitest unit tests +โ””โ”€โ”€ e2e/ # ๐ŸŽญ Playwright E2E tests +``` + +## ๐Ÿค Contributing + +We love contributions! Here's how to get involved: + +1. ๐Ÿด **Fork** the repository +2. ๐ŸŒฟ **Create** a feature branch (`git checkout -b feature/amazing-feature`) +3. โœจ **Make** your changes (don't forget to run `npm run format && npm run lint`) +4. ๐Ÿ“ **Commit** your changes (`git commit -m 'Add amazing feature'`) +5. ๐Ÿš€ **Push** to the branch (`git push origin feature/amazing-feature`) +6. ๐ŸŽ‰ **Open** a Pull Request + +### ๐ŸŒŸ Contributing Guidelines + +- Follow existing code style and conventions +- Add tests for new features +- Update documentation as needed +- Be respectful and collaborative + +## ๐Ÿšจ Issues & Support + +Encountered a bug? Have a feature request? We'd love to hear from you! + +- ๐Ÿ› **Bug Reports**: [Open an issue](https://github.com/Steake/Reputation-Gated-Airdrop/issues/new?template=bug_report.md) +- ๐Ÿ’ก **Feature Requests**: [Request a feature](https://github.com/Steake/Reputation-Gated-Airdrop/issues/new?template=feature_request.md) +- ๐Ÿ’ฌ **Discussions**: [Join the conversation](https://github.com/Steake/Reputation-Gated-Airdrop/discussions) + +## ๐Ÿ“Š Stats & Recognition + +
+ +![GitHub repo size](https://img.shields.io/github/repo-size/Steake/Reputation-Gated-Airdrop?style=for-the-badge) +![GitHub code size](https://img.shields.io/github/languages/code-size/Steake/Reputation-Gated-Airdrop?style=for-the-badge) +![GitHub top language](https://img.shields.io/github/languages/top/Steake/Reputation-Gated-Airdrop?style=for-the-badge) + +
+ +## ๐Ÿ™ Acknowledgments + +This project builds upon the incredible work of: + +- **SvelteKit Team** - For the amazing framework +- **Viem Contributors** - For type-safe Ethereum interactions +- **Web3-Onboard Team** - For seamless wallet connections +- **EZKL Community** - For zero-knowledge proof infrastructure +- **The entire Web3 community** - For pushing the boundaries of decentralized technology + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +
+ +### ๐ŸŒŸ **Built with โค๏ธ by [Shadowgraph Labs](https://shadowgraph.io)** ๐ŸŒŸ + +_Revolutionizing decentralized reputation systems, one commit at a time._ + +**[๐ŸŒ Website](https://shadowgraph.io) โ€ข [๐Ÿ“ง Contact](mailto:team@shadowgraph.io) โ€ข [๐Ÿฆ Twitter](https://twitter.com/shadowgraphlabs) โ€ข [๐Ÿ’ผ LinkedIn](https://linkedin.com/company/shadowgraph-labs)** + +--- + +_"In cryptography we trust, in reputation we thrive." - Shadowgraph Labs_ + +
diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts index 0e305a3..c15c661 100644 --- a/tests/e2e/navigation.spec.ts +++ b/tests/e2e/navigation.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from "@playwright/test"; test.describe("Navigation and Routing", () => { test("should navigate to all main pages", async ({ page }) => { await page.goto("/"); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); // Test navigation to Earn Reputation page with more flexible link matching const earnLink = page.getByRole("link", { name: /earn/i }); diff --git a/tests/e2e/theme-integration.spec.ts b/tests/e2e/theme-integration.spec.ts index c840969..40bedd4 100644 --- a/tests/e2e/theme-integration.spec.ts +++ b/tests/e2e/theme-integration.spec.ts @@ -107,13 +107,15 @@ test.describe("Theme Integration", () => { // Test in light theme - look for Connect Wallet button const connectButton = page.locator("button").filter({ hasText: "Connect Wallet" }).first(); await connectButton.click(); - + // Wait for modal to appear and check if it has wallet options - const modal = page.locator('[data-testid="onboard-modal"], [role="dialog"], .onboard-modal').first(); + const modal = page + .locator('[data-testid="onboard-modal"], [role="dialog"], .onboard-modal') + .first(); await expect(modal).toBeVisible({ timeout: 5000 }); - + // Close modal by clicking outside or finding close button - await page.keyboard.press('Escape'); + await page.keyboard.press("Escape"); await page.waitForTimeout(500); // Switch to dark theme diff --git a/tests/e2e/wallet-connection.spec.ts b/tests/e2e/wallet-connection.spec.ts index a4632b1..65b7295 100644 --- a/tests/e2e/wallet-connection.spec.ts +++ b/tests/e2e/wallet-connection.spec.ts @@ -3,18 +3,21 @@ import { test, expect } from "@playwright/test"; test.describe("Wallet Connection", () => { test("should display wallet connection modal on desktop", async ({ page }) => { await page.goto("/"); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); // Click connect wallet button with more resilient selector - const connectButton = page.locator('button').filter({ hasText: /connect/i }).first(); - + const connectButton = page + .locator("button") + .filter({ hasText: /connect/i }) + .first(); + // Check if button exists before trying to click const buttonExists = await connectButton.isVisible().catch(() => false); if (!buttonExists) { - console.log('Connect wallet button not found'); + console.log("Connect wallet button not found"); return; } - + await connectButton.click(); // Give modal time to appear and be more flexible about what we expect @@ -23,32 +26,39 @@ test.describe("Wallet Connection", () => { // Check if any modal-like content appears (more flexible) const modalSelectors = [ '[role="dialog"]', - '.modal', + ".modal", '[data-testid="wallet-modal"]', - 'text=metamask', - 'text=coinbase', - 'text=trust' + "text=metamask", + "text=coinbase", + "text=trust", ]; - + let modalFound = false; for (const selector of modalSelectors) { - const exists = await page.locator(selector).first().isVisible().catch(() => false); + const exists = await page + .locator(selector) + .first() + .isVisible() + .catch(() => false); if (exists) { modalFound = true; break; } } - - console.log('Modal or wallet-related content found:', modalFound); + + console.log("Modal or wallet-related content found:", modalFound); }); test("should display wallet connection modal on mobile", async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto("/"); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); // Find and click connect wallet button with more flexible selectors - const connectButton = page.locator('button').filter({ hasText: /connect wallet/i }).first(); + const connectButton = page + .locator("button") + .filter({ hasText: /connect wallet/i }) + .first(); await expect(connectButton).toBeVisible({ timeout: 10000 }); await connectButton.click(); @@ -57,29 +67,38 @@ test.describe("Wallet Connection", () => { // Check if any wallet-related content appears (flexible) await page.waitForTimeout(1000); - const hasWalletContent = await page.locator('text=wallet, text=connect, text=MetaMask').first().isVisible().catch(() => false); - console.log('Mobile wallet modal content found:', hasWalletContent); + const hasWalletContent = await page + .locator("text=wallet, text=connect, text=MetaMask") + .first() + .isVisible() + .catch(() => false); + console.log("Mobile wallet modal content found:", hasWalletContent); }); test("should close modal when clicking close button", async ({ page }) => { await page.goto("/"); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); // Open modal with more flexible selectors - const connectButton = page.locator('button').filter({ hasText: /connect wallet/i }).first(); + const connectButton = page + .locator("button") + .filter({ hasText: /connect wallet/i }) + .first(); await expect(connectButton).toBeVisible({ timeout: 10000 }); await connectButton.click(); // Wait for modal and look for close functionality await page.waitForTimeout(1000); - + // Try to find close button or click outside modal - const closeButton = page.locator('button[aria-label*="close"], button[title*="close"], button:has-text("ร—")').first(); + const closeButton = page + .locator('button[aria-label*="close"], button[title*="close"], button:has-text("ร—")') + .first(); if (await closeButton.isVisible()) { await closeButton.click(); } else { // Alternative: press Escape key - await page.keyboard.press('Escape'); + await page.keyboard.press("Escape"); } });