diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000000..9c551bce918 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,46 @@ +name: E2E Tests with Core Wallet + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Install Playwright Browsers + run: yarn playwright install --with-deps chromium + + - name: Run Playwright tests + run: yarn test + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Upload test screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-screenshots + path: tests/screenshots/ + retention-days: 30 + diff --git a/.gitignore b/.gitignore index 2b54d7125e1..686296662d8 100644 --- a/.gitignore +++ b/.gitignore @@ -298,4 +298,10 @@ content/docs/cross-chain/teleporter/deep-dive.mdx content/docs/cross-chain/teleporter/overview.mdx content/docs/cross-chain/teleporter/upgradeability.mdx +# Playwright test artifacts +/tests/extensions/ +/tests/screenshots/ +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/package.json b/package.json index 2971a58bc50..7854682100f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "start": "pnpm build:remote && next build && next start", "check-links": "tsx .github/linkChecker.ts", "postinstall": "prisma generate && fumadocs-mdx && node ./scripts/update_docker_tags.mjs", - "postbuild": "tsx ./utils/update-index.ts" + "postbuild": "tsx ./utils/update-index.ts", + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:ui": "playwright test --ui", + "test:debug": "playwright test --debug" }, "dependencies": { "@ai-sdk/openai": "^1.3.24", @@ -129,14 +133,19 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@playwright/test": "^1.48.2", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/typography": "^0.5.16", + "@types/adm-zip": "^0.5.5", "@types/canvas-confetti": "^1.9.0", "@types/mdx": "^2.0.13", "@types/node": "^24.5.0", "@types/prismjs": "^1.26.5", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", + "adm-zip": "^0.5.16", + "dotenv": "^17.2.3", + "playwright-core": "^1.48.2", "postcss": "^8.5.6", "prisma": "^6.16.1", "tailwindcss": "^4.1.13", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000000..7c6638f542e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,61 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + + /* Run tests in files in parallel */ + fullyParallel: false, // Set to false when using extensions + + /* 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 : 1, // Extensions work best with single worker + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html'], + ['list'], + ], + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.BASE_URL || 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Screenshot on failure */ + screenshot: 'only-on-failure', + + /* Video on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium-with-core-wallet', + use: { + ...devices['Desktop Chrome'], + // Extension-specific settings will be handled in test setup + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'yarn dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); + diff --git a/scripts/versions.json b/scripts/versions.json index 59a52ad6d9a..b6783e25182 100644 --- a/scripts/versions.json +++ b/scripts/versions.json @@ -5,9 +5,9 @@ "avaplatform/icm-relayer": "v1.7.3" }, "testnet": { - "avaplatform/avalanchego": "v1.14.0", - "avaplatform/subnet-evm_avalanchego": "v0.8.0_v1.14.0", - "avaplatform/icm-relayer": "v1.7.3" + "avaplatform/avalanchego": "v1.14.0-fuji", + "avaplatform/subnet-evm_avalanchego": "v0.8.0-fuji_v1.14.0-fuji", + "avaplatform/icm-relayer": "v1.7.2-fuji" }, "ava-labs/icm-contracts": "4d5ab0b6dbc653770cfe9709878c9406eb28b71c" -} +} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000000..69a9dda65f1 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,137 @@ +# Automated QA Testing with Core Wallet + +This directory contains automated end-to-end tests using Playwright with the Core Wallet extension preinstalled. + +## Setup + +1. Install dependencies: +```bash +yarn install +``` + +2. Install Playwright browsers (first time only): +```bash +yarn playwright install chromium +``` + +3. Configure your wallet (in project root `.env`): +```bash +# NEVER use a wallet with real funds for testing! +CORE_WALLET_MNEMONIC="your twelve or twenty four word recovery phrase goes here" +CORE_WALLET_PASSWORD="TestPassword123!" +``` + +See `tests/.env.example` for the template. + +## Running Tests + +### Run all tests (headless): +```bash +yarn test +``` + +### Run tests with visible browser: +```bash +yarn test:headed +``` + +### Run tests with Playwright UI (interactive): +```bash +yarn test:ui +``` + +### Debug tests step-by-step: +```bash +yarn test:debug +``` + +## Core Wallet Setup + +The Core Wallet extension is automatically downloaded and configured the first time you run the tests. The extension files are cached in `tests/extensions/core-wallet/` and will be reused for subsequent test runs. + +### How it works: + +1. **Automatic Download**: The setup script downloads Core Wallet from the Chrome Web Store +2. **Extension Loading**: Playwright launches Chrome with the extension pre-installed +3. **Persistent Context**: Tests run with the extension active + +## Test Structure + +``` +tests/ +├── setup/ +│ └── core-wallet.setup.ts # Extension download and setup utilities +├── core-wallet.spec.ts # Example test file +└── README.md # This file +``` + +## Writing Tests + +Example test with Core Wallet: + +```typescript +import { test, expect } from '@playwright/test'; +import { createBrowserWithCoreWallet } from './setup/core-wallet.setup'; + +test('my test', async () => { + const context = await createBrowserWithCoreWallet(); + const page = await context.newPage(); + + // Your test code here + await page.goto('https://your-app.com'); + + // Core Wallet is now available in the browser + + await context.close(); +}); +``` + +## GitHub Actions + +To run tests in CI/CD, add this workflow configuration (the extension will be automatically downloaded in CI): + +```yaml +name: E2E Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: yarn install + - run: yarn playwright install --with-deps chromium + - run: yarn test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ +``` + +## Troubleshooting + +### Extension not loading +- Make sure you're running in headed mode for debugging: `yarn test:headed` +- Check that the extension directory exists: `tests/extensions/core-wallet/` +- Delete the extensions folder and re-run to force a fresh download + +### Tests timing out +- Increase timeout in `playwright.config.ts` +- Extensions may take longer to initialize, add `await page.waitForTimeout(5000)` after opening pages + +### Can't find Core Wallet in window +- The extension may inject different global variables depending on the page +- Check browser console for what's actually injected +- Some extensions only inject on specific domains + +## Notes + +- Tests run with `headless: false` by default because Chrome extensions require a visible browser window +- The Core Wallet extension files are gitignored and will be downloaded automatically +- Each test gets a fresh browser context with the extension pre-loaded + diff --git a/tests/env-template.txt b/tests/env-template.txt new file mode 100644 index 00000000000..c5bec82bd62 --- /dev/null +++ b/tests/env-template.txt @@ -0,0 +1,10 @@ +# Core Wallet Testing Configuration +# Copy this to your project root .env file + +# Your 12 or 24 word recovery phrase for testing +# NEVER use a wallet with real funds for testing! +CORE_WALLET_MNEMONIC="your twelve or twenty four word recovery phrase goes here" + +# Password to set for the wallet in tests (optional, defaults to TestPassword123!) +CORE_WALLET_PASSWORD="TestPassword123!" + diff --git a/tests/setup/core-wallet.setup.ts b/tests/setup/core-wallet.setup.ts new file mode 100644 index 00000000000..437f80b018a --- /dev/null +++ b/tests/setup/core-wallet.setup.ts @@ -0,0 +1,200 @@ +import { chromium, type BrowserContext } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as https from 'https'; +import AdmZip from 'adm-zip'; + +const CORE_WALLET_EXTENSION_ID = 'agoakfejjabomempkjlepdflaleeobhb'; +const CORE_WALLET_CRX_URL = `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=120.0.0.0&acceptformat=crx2,crx3&x=id%3D${CORE_WALLET_EXTENSION_ID}%26uc`; + +const EXTENSION_DIR = path.join(process.cwd(), 'tests', 'extensions'); +const CORE_WALLET_DIR = path.join(EXTENSION_DIR, 'core-wallet'); + +/** + * Extracts ZIP data from a CRX file by removing the CRX header + * CRX format: [CRX header][ZIP data] + */ +function extractZipFromCrx(crxPath: string): Buffer { + const crxBuffer = fs.readFileSync(crxPath); + + // Check for CRX3 format (starts with "Cr24") + const magicNumber = crxBuffer.toString('utf8', 0, 4); + + if (magicNumber !== 'Cr24') { + throw new Error('Invalid CRX file format'); + } + + // CRX3 format: + // [4 bytes] Magic number ("Cr24") + // [4 bytes] CRX format version (3) + // [4 bytes] Header size + // [header size bytes] Header data + // [remaining bytes] ZIP data + + const version = crxBuffer.readUInt32LE(4); + + if (version === 3) { + const headerSize = crxBuffer.readUInt32LE(8); + const zipStartOffset = 12 + headerSize; + return crxBuffer.slice(zipStartOffset); + } else if (version === 2) { + // CRX2 format (older) + // [4 bytes] Magic number + // [4 bytes] Version (2) + // [4 bytes] Public key length + // [4 bytes] Signature length + // [public key length bytes] Public key + // [signature length bytes] Signature + // [remaining] ZIP data + const publicKeyLength = crxBuffer.readUInt32LE(8); + const signatureLength = crxBuffer.readUInt32LE(12); + const zipStartOffset = 16 + publicKeyLength + signatureLength; + return crxBuffer.slice(zipStartOffset); + } else { + throw new Error(`Unsupported CRX version: ${version}`); + } +} + +/** + * Downloads the Core Wallet extension from Chrome Web Store + */ +async function downloadCoreWalletExtension(): Promise { + // Create extensions directory if it doesn't exist + if (!fs.existsSync(EXTENSION_DIR)) { + fs.mkdirSync(EXTENSION_DIR, { recursive: true }); + } + + const crxPath = path.join(EXTENSION_DIR, 'core-wallet.crx'); + const zipPath = path.join(EXTENSION_DIR, 'core-wallet.zip'); + + // Skip download if already exists + if (fs.existsSync(CORE_WALLET_DIR) && fs.readdirSync(CORE_WALLET_DIR).length > 0) { + console.log('✓ Core Wallet extension already downloaded'); + return CORE_WALLET_DIR; + } + + console.log('Downloading Core Wallet extension...'); + + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(crxPath); + + https.get(CORE_WALLET_CRX_URL, (response) => { + // Follow redirects + if (response.statusCode === 301 || response.statusCode === 302) { + const redirectUrl = response.headers.location; + if (!redirectUrl) { + reject(new Error('Redirect location not found')); + return; + } + + https.get(redirectUrl, (redirectResponse) => { + redirectResponse.pipe(file); + + file.on('finish', () => { + file.close(); + console.log('✓ Downloaded Core Wallet extension'); + + try { + // Extract ZIP data from CRX + const zipData = extractZipFromCrx(crxPath); + fs.writeFileSync(zipPath, zipData); + console.log('✓ Extracted ZIP from CRX'); + + // Extract the ZIP file + const zip = new AdmZip(zipPath); + zip.extractAllTo(CORE_WALLET_DIR, true); + console.log('✓ Extracted Core Wallet extension'); + + // Clean up temporary files + fs.unlinkSync(crxPath); + fs.unlinkSync(zipPath); + + resolve(CORE_WALLET_DIR); + } catch (error) { + // Clean up on error + if (fs.existsSync(crxPath)) fs.unlinkSync(crxPath); + if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); + reject(error); + } + }); + }).on('error', (err) => { + if (fs.existsSync(crxPath)) fs.unlinkSync(crxPath); + reject(err); + }); + } else { + response.pipe(file); + + file.on('finish', () => { + file.close(); + console.log('✓ Downloaded Core Wallet extension'); + + try { + // Extract ZIP data from CRX + const zipData = extractZipFromCrx(crxPath); + fs.writeFileSync(zipPath, zipData); + console.log('✓ Extracted ZIP from CRX'); + + // Extract the ZIP file + const zip = new AdmZip(zipPath); + zip.extractAllTo(CORE_WALLET_DIR, true); + console.log('✓ Extracted Core Wallet extension'); + + // Clean up temporary files + fs.unlinkSync(crxPath); + fs.unlinkSync(zipPath); + + resolve(CORE_WALLET_DIR); + } catch (error) { + // Clean up on error + if (fs.existsSync(crxPath)) fs.unlinkSync(crxPath); + if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); + reject(error); + } + }); + } + }).on('error', (err) => { + if (fs.existsSync(crxPath)) fs.unlinkSync(crxPath); + reject(err); + }); + }); +} + +/** + * Creates a browser context with Core Wallet extension loaded + */ +export async function createBrowserWithCoreWallet(): Promise { + // Download extension if needed + const extensionPath = await downloadCoreWalletExtension(); + + // Launch browser with extension + const browser = await chromium.launchPersistentContext('', { + headless: false, // Extensions require headed mode + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + '--disable-blink-features=AutomationControlled', + ], + }); + + console.log('✓ Browser launched with Core Wallet extension'); + + return browser; +} + +/** + * Gets the extension URL for Core Wallet + * This is useful for navigating to the extension's popup or options page + */ +export function getCoreWalletExtensionUrl(page: string = 'index.html'): string { + return `chrome-extension://${CORE_WALLET_EXTENSION_ID}/${page}`; +} + +/** + * Setup function to be called before tests + */ +export async function setupCoreWallet() { + console.log('Setting up Core Wallet extension for tests...'); + await downloadCoreWalletExtension(); + console.log('✓ Core Wallet setup complete'); +} + diff --git a/tests/setup/wallet-import.setup.ts b/tests/setup/wallet-import.setup.ts new file mode 100644 index 00000000000..7a96e762d01 --- /dev/null +++ b/tests/setup/wallet-import.setup.ts @@ -0,0 +1,386 @@ +import { type Page, type BrowserContext } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +const WALLET_PASSWORD = process.env.CORE_WALLET_PASSWORD || 'TestPassword123!'; +const WALLET_MNEMONIC = process.env.CORE_WALLET_MNEMONIC; + +if (!WALLET_MNEMONIC) { + throw new Error('CORE_WALLET_MNEMONIC environment variable is required'); +} + +/** + * Gets the actual extension ID from the loaded extension + */ +async function getExtensionId(context: BrowserContext): Promise { + // Navigate to chrome://extensions to find the actual extension ID + const page = await context.newPage(); + + try { + // We can't directly access chrome:// pages, so we'll check service workers + // or look for extension pages that might have auto-opened + const pages = context.pages(); + for (const p of pages) { + const url = p.url(); + if (url.startsWith('chrome-extension://')) { + // Extract the extension ID from the URL + const match = url.match(/chrome-extension:\/\/([a-z]+)\//); + if (match) { + await page.close(); + return match[1]; + } + } + } + + // If no pages found, try opening a common extension path + // and see if the extension redirects or opens + await page.goto('chrome://extensions/', { waitUntil: 'domcontentloaded', timeout: 3000 }).catch(() => {}); + + await page.close(); + return null; + } catch (error) { + await page.close(); + return null; + } +} + +/** + * Imports a wallet into Core Wallet using a mnemonic phrase + */ +export async function importWalletWithMnemonic( + context: BrowserContext, + mnemonic: string = WALLET_MNEMONIC!, + password: string = WALLET_PASSWORD +): Promise { + console.log('Starting wallet import process...'); + + // Extension ID (detected from service worker) + const extensionId = 'agoakfejjabomempkjlepdflaleeobhb'; + + // Wait for Core Wallet to auto-open its page + console.log('Waiting for Core Wallet to open...'); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Find the Core Wallet page that was auto-opened + let pages = context.pages(); + let walletPage = pages.find(p => p.url().includes(extensionId)); + + if (!walletPage) { + console.log('Core Wallet did not auto-open, opening manually...'); + walletPage = await context.newPage(); + await walletPage.goto(`chrome-extension://${extensionId}/home.html`, { + waitUntil: 'load', + timeout: 10000 + }); + await walletPage.waitForTimeout(2000); + } else { + console.log(`✓ Found auto-opened Core Wallet page: ${walletPage.url()}`); + await walletPage.bringToFront(); + } + + // Close ALL other tabs except the wallet page + pages = context.pages(); + for (const page of pages) { + if (page !== walletPage) { + await page.close(); + console.log(`✓ Closed extra tab: ${page.url()}`); + } + } + + console.log(`Active tabs: ${context.pages().length}`); + + // Take initial screenshot + await walletPage.screenshot({ path: 'tests/screenshots/01-onboarding.png', fullPage: true }); + console.log('✓ Screenshot: 01-onboarding.png'); + + // Override window.open to prevent new tabs throughout the process + await walletPage.evaluate(() => { + // Override window.open to navigate in same tab instead + window.open = (url?: any) => { + if (typeof url === 'string') { + window.location.href = url; + } + return null; + }; + + // Also override window.focus to prevent focusing other tabs + window.focus = () => {}; + }); + console.log('✓ Intercepted window.open to prevent new tabs'); + + // Listen for new pages and close them immediately + context.on('page', async (page) => { + if (page !== walletPage) { + console.log(`✗ New tab detected: ${page.url()} - closing it`); + await page.close(); + } + }); + + // Click "Access Existing Wallet" button + console.log('Looking for "Access Existing Wallet" button...'); + const accessExistingButton = walletPage.getByRole('button', { name: 'Access Existing Wallet' }); + + if (await accessExistingButton.isVisible({ timeout: 5000 })) { + console.log('✓ Found "Access Existing Wallet" button'); + await accessExistingButton.click(); + console.log('✓ Clicked "Access Existing Wallet"'); + + // Wait for the options to appear + await walletPage.waitForTimeout(2000); + await walletPage.screenshot({ path: 'tests/screenshots/02-access-options.png', fullPage: true }); + console.log('✓ Screenshot: 02-access-options.png'); + } else { + throw new Error('Could not find "Access Existing Wallet" button'); + } + + // Click "Enter recovery phrase" option (it's a heading, not a button) + console.log('Looking for "Enter recovery phrase" option...'); + const enterRecoveryPhrase = walletPage.getByText('Enter recovery phrase'); + + if (await enterRecoveryPhrase.isVisible({ timeout: 5000 })) { + console.log('✓ Found "Enter recovery phrase" option'); + await enterRecoveryPhrase.click(); + console.log('✓ Clicked "Enter recovery phrase"'); + + // Wait for recovery phrase input to appear + await walletPage.waitForTimeout(2000); + await walletPage.screenshot({ path: 'tests/screenshots/03-recovery-phrase-input.png', fullPage: true }); + console.log('✓ Screenshot: 03-recovery-phrase-input.png'); + } else { + throw new Error('Could not find "Enter recovery phrase" option'); + } + + // Now we should see recovery phrase import options + console.log('Looking for recovery phrase import option...'); + + // Look for "Recovery Phrase" or similar button/tab + const recoveryPhrasePatterns = [ + 'Recovery Phrase', + 'Use Recovery Phrase', + 'Import with Recovery Phrase', + 'Mnemonic', + 'Seed Phrase' + ]; + + let foundRecoveryOption = false; + for (const pattern of recoveryPhrasePatterns) { + try { + const element = walletPage.getByRole('button', { name: new RegExp(pattern, 'i') }).first(); + if (await element.isVisible({ timeout: 2000 })) { + console.log(`✓ Found recovery phrase option: "${pattern}"`); + await element.click(); + await walletPage.waitForTimeout(2000); + await walletPage.screenshot({ path: 'tests/screenshots/03-recovery-phrase-selected.png', fullPage: true }); + foundRecoveryOption = true; + break; + } + } catch (error) { + // Try next pattern + } + } + + if (!foundRecoveryOption) { + console.log('Recovery phrase option may already be selected or has different text'); + } + + // Fill in the recovery phrase + console.log('Looking for mnemonic input fields...'); + const words = mnemonic.trim().split(/\s+/); + console.log(`Mnemonic has ${words.length} words`); + + // Wait for input fields to appear + await walletPage.waitForTimeout(1000); + + // Fill each word into its corresponding field + console.log('Filling recovery phrase word by word...'); + for (let i = 0; i < words.length; i++) { + const wordNumber = (i + 1).toString() + '.'; + const input = walletPage.getByRole('textbox', { name: wordNumber }).first(); + + if (await input.isVisible({ timeout: 2000 })) { + await input.fill(words[i]); + if (i % 6 === 0) { + console.log(` Filled words 1-${i + 1}...`); + } + } else { + throw new Error(`Could not find input field for word ${i + 1}`); + } + } + + console.log('✓ Filled all recovery phrase words'); + + // Blur the last input to trigger validation + const lastInput = walletPage.getByRole('textbox', { name: `${words.length}.` }).first(); + await lastInput.blur(); + await walletPage.waitForTimeout(500); + + await walletPage.screenshot({ path: 'tests/screenshots/04-mnemonic-filled.png', fullPage: true }); + + // Click continue/import button (wait for it to be enabled) + console.log('Looking for Next button...'); + const nextButton = walletPage.getByRole('button', { name: 'Next' }); + + // Wait for button to become enabled + await walletPage.waitForTimeout(1000); + + if (await nextButton.isVisible({ timeout: 5000 })) { + // Check if enabled, if not try clicking elsewhere first + const isEnabled = await nextButton.isEnabled(); + + if (!isEnabled) { + console.log('Next button is disabled, clicking on page to trigger validation...'); + // Click on the heading to trigger focus change + await walletPage.getByRole('heading', { name: 'Enter Recovery Phrase' }).click(); + await walletPage.waitForTimeout(1000); + } + + // Try to click Next button + await nextButton.click({ force: true }); + console.log('✓ Clicked Next button'); + await walletPage.waitForTimeout(3000); + await walletPage.screenshot({ path: 'tests/screenshots/05-after-next.png', fullPage: true }); + } else { + throw new Error('Could not find Next button'); + } + + // Remove the old continue button search patterns since we already handled it + const continuePatterns = ['Continue', 'Import', 'Import Wallet', 'Confirm']; + + for (const pattern of continuePatterns) { + try { + const button = walletPage.getByRole('button', { name: new RegExp(pattern, 'i') }).first(); + if (await button.isVisible({ timeout: 2000 })) { + console.log(`✓ Found continue button: "${pattern}"`); + await button.click(); + await walletPage.waitForTimeout(3000); + await walletPage.screenshot({ path: 'tests/screenshots/05-after-continue.png', fullPage: true }); + break; + } + } catch (error) { + // Try next pattern + } + } + + // Fill wallet setup page (name, password, terms) + console.log('Setting up wallet details...'); + + // Fill wallet name + const walletNameInput = walletPage.getByPlaceholder('Enter a Name'); + if (await walletNameInput.isVisible({ timeout: 5000 })) { + console.log('✓ Found wallet name field'); + await walletNameInput.fill('Test Wallet'); + console.log('✓ Filled wallet name'); + await walletPage.waitForTimeout(500); + } + + // Fill password (use placeholder to be more specific) + const passwordInput = walletPage.getByPlaceholder('Enter a Password'); + if (await passwordInput.isVisible({ timeout: 3000 })) { + console.log('✓ Found password field'); + await passwordInput.fill(password); + console.log('✓ Filled password'); + await walletPage.waitForTimeout(500); + } + + // Fill confirm password + const confirmPasswordInput = walletPage.getByPlaceholder('Confirm Password'); + if (await confirmPasswordInput.isVisible({ timeout: 3000 })) { + console.log('✓ Found confirm password field'); + await confirmPasswordInput.fill(password); + console.log('✓ Filled confirm password'); + await walletPage.waitForTimeout(500); + } + + await walletPage.waitForTimeout(1000); + + // Check Terms of Use checkbox + const termsCheckbox = walletPage.getByRole('checkbox', { name: /Terms of Use/i }); + if (await termsCheckbox.isVisible({ timeout: 3000 })) { + console.log('✓ Found Terms of Use checkbox'); + await termsCheckbox.check(); + console.log('✓ Checked Terms of Use'); + } + + await walletPage.screenshot({ path: 'tests/screenshots/06-wallet-setup-filled.png', fullPage: true }); + + // Click Save button + const saveButton = walletPage.getByRole('button', { name: 'Save' }); + await walletPage.waitForTimeout(1000); + + if (await saveButton.isVisible({ timeout: 5000 })) { + console.log('✓ Found Save button'); + await saveButton.click(); + console.log('✓ Clicked Save'); + + await walletPage.waitForTimeout(5000); + await walletPage.screenshot({ path: 'tests/screenshots/07-after-save.png', fullPage: true }); + } else { + throw new Error('Could not find Save button'); + } + + // Handle "Unlock Airdrops" screen - click "No Thanks" + console.log('Looking for airdrop opt-in screen...'); + const noThanksButton = walletPage.getByRole('button', { name: 'No Thanks' }); + + if (await noThanksButton.isVisible({ timeout: 5000 })) { + console.log('✓ Found "No Thanks" button on airdrop screen'); + await noThanksButton.click(); + console.log('✓ Clicked "No Thanks"'); + await walletPage.waitForTimeout(3000); + await walletPage.screenshot({ path: 'tests/screenshots/08-wallet-ready.png', fullPage: true }); + } else { + console.log('No airdrop screen appeared (might have skipped)'); + } + + console.log('✓ Wallet import process completed (check screenshots for details)'); + console.log(`Final tab count: ${context.pages().length}`); + + // Remove the page listener so tests can create new pages + context.removeAllListeners('page'); + console.log('✓ Removed page listeners - tests can now create new tabs'); +} + +/** + * Helper to get the Core Wallet extension page from context + */ +export async function getWalletPage(context: BrowserContext): Promise { + const pages = context.pages(); + + for (const page of pages) { + if (page.url().includes('chrome-extension://')) { + return page; + } + } + + return undefined; +} + +/** + * Gets the extension ID dynamically from the context + */ +export async function getLoadedExtensionId(context: BrowserContext): Promise { + // Check pages for extension URLs + const pages = context.pages(); + for (const page of pages) { + const url = page.url(); + if (url.startsWith('chrome-extension://')) { + const match = url.match(/chrome-extension:\/\/([a-z]+)\//); + if (match) return match[1]; + } + } + + // Check service workers + const serviceWorkers = context.serviceWorkers(); + for (const worker of serviceWorkers) { + const url = worker.url(); + if (url.includes('chrome-extension://')) { + const match = url.match(/chrome-extension:\/\/([a-z]+)\//); + if (match) return match[1]; + } + } + + return null; +} + diff --git a/tests/wallet-setup.spec.ts b/tests/wallet-setup.spec.ts new file mode 100644 index 00000000000..0215e375ba0 --- /dev/null +++ b/tests/wallet-setup.spec.ts @@ -0,0 +1,73 @@ +import { test, expect, type BrowserContext } from '@playwright/test'; +import { createBrowserWithCoreWallet } from './setup/core-wallet.setup'; +import { importWalletWithMnemonic } from './setup/wallet-import.setup'; + +test.describe('Core Wallet Setup', () => { + let context: BrowserContext; + + // Setup wallet ONCE before all tests + test.beforeAll(async () => { + console.log('\n🔧 Setting up Core Wallet for all tests...\n'); + context = await createBrowserWithCoreWallet(); + await importWalletWithMnemonic(context); + console.log('\n✅ Core Wallet setup complete!\n'); + }); + + // Close browser after all tests + test.afterAll(async () => { + if (context) { + await context.close(); + } + }); + + test('should have wallet imported successfully', async () => { + // Get the wallet page + const pages = context.pages(); + console.log(`Open pages: ${pages.length}`); + + const walletPage = pages[pages.length - 1]; // Get the last page (wallet page) + + // Take a screenshot to verify wallet is imported + await walletPage.screenshot({ path: 'tests/screenshots/wallet-imported.png', fullPage: true }); + console.log('✓ Wallet screenshot saved'); + + // Keep browser open for inspection + await walletPage.waitForTimeout(5000); + }); + + test('should connect wallet on localhost console', async () => { + // Create a new page and go to console + const page = await context.newPage(); + console.log('Opening localhost:3000/console...'); + await page.goto('http://localhost:3000/console'); + + // Wait for page to load + await page.waitForTimeout(2000); + await page.screenshot({ path: 'tests/screenshots/console-loaded.png', fullPage: true }); + + // Click "Connect Wallet" button + console.log('Looking for Connect Wallet button...'); + const connectButton = page.getByRole('button', { name: /Connect Wallet/i }); + + if (await connectButton.isVisible({ timeout: 5000 })) { + console.log('✓ Found Connect Wallet button'); + await connectButton.click(); + console.log('✓ Clicked Connect Wallet'); + + await page.waitForTimeout(3000); + await page.screenshot({ path: 'tests/screenshots/wallet-connect-clicked.png', fullPage: true }); + } else { + console.log('Connect Wallet button not found'); + } + + // Check if window.avalanche is available + const hasAvalanche = await page.evaluate(() => { + return typeof (window as any).avalanche !== 'undefined'; + }); + + console.log(`Avalanche provider available: ${hasAvalanche}`); + + expect(hasAvalanche).toBe(true); + }); +}); + diff --git a/yarn.lock b/yarn.lock index e5317dd2f4c..9b16ed2adf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1133,6 +1133,13 @@ pvtsutils "^1.3.6" tslib "^2.8.1" +"@playwright/test@^1.48.2": + version "1.56.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" + integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== + dependencies: + playwright "1.56.1" + "@posthog/core@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.5.0.tgz#56900cf9fbb37e9a5687020ce3864d05e67dabe1" @@ -2292,6 +2299,13 @@ dependencies: tslib "^2.4.0" +"@types/adm-zip@^0.5.5": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.7.tgz#eec10b6f717d3948beb64aca0abebc4b344ac7e9" + integrity sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw== + dependencies: + "@types/node" "*" + "@types/canvas-confetti@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz#d1077752e046413c9881fbb2ba34a70ebe3c1773" @@ -2582,7 +2596,7 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@^24.5.0": +"@types/node@*", "@types/node@^24.5.0": version "24.10.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f" integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A== @@ -2723,6 +2737,11 @@ acorn@^8.0.0, acorn@^8.15.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + ai@^4.3.19: version "4.3.19" resolved "https://registry.yarnpkg.com/ai/-/ai-4.3.19.tgz#e94f5b37f3885bc9c9637f892e13bddd0a1857e5" @@ -3654,6 +3673,11 @@ dotenv@^16.6.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== +dotenv@^17.2.3: + version "17.2.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" + integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== + dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -4039,6 +4063,11 @@ framer-motion@^12.23.12: motion-utils "^12.23.6" tslib "^2.4.0" +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -5936,6 +5965,20 @@ pkg-types@^2.2.0, pkg-types@^2.3.0: exsolve "^1.0.7" pathe "^2.0.3" +playwright-core@1.56.1, playwright-core@^1.48.2: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== + +playwright@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== + dependencies: + playwright-core "1.56.1" + optionalDependencies: + fsevents "2.3.2" + points-on-curve@0.2.0, points-on-curve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1"