UNPKG

@coinbase/onchaintestkit

Version:

End-to-end testing toolkit for blockchain applications, powered by Playwright

256 lines (255 loc) 12.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MetaMask = exports.MetaMaskSpecificActionType = void 0; const node_path_1 = __importDefault(require("node:path")); const test_1 = require("@playwright/test"); const fs_extra_1 = __importDefault(require("fs-extra")); const extensionManager_1 = require("../../utils/extensionManager"); const BaseWallet_1 = require("../BaseWallet"); const pages_1 = require("./pages"); // Extend BaseActionType with MetaMask-specific actions var MetaMaskSpecificActionType; (function (MetaMaskSpecificActionType) { MetaMaskSpecificActionType["LOCK"] = "lock"; MetaMaskSpecificActionType["UNLOCK"] = "unlock"; MetaMaskSpecificActionType["ADD_TOKEN"] = "addToken"; MetaMaskSpecificActionType["ADD_ACCOUNT"] = "addAccount"; MetaMaskSpecificActionType["RENAME_ACCOUNT"] = "renameAccount"; MetaMaskSpecificActionType["REMOVE_ACCOUNT"] = "removeAccount"; MetaMaskSpecificActionType["SWITCH_ACCOUNT"] = "switchAccount"; MetaMaskSpecificActionType["ADD_NETWORK"] = "addNetwork"; MetaMaskSpecificActionType["APPROVE_ADD_NETWORK"] = "approveAddNetwork"; })(MetaMaskSpecificActionType || (exports.MetaMaskSpecificActionType = MetaMaskSpecificActionType = {})); const WALLET_CONNECTION_ERROR = new Error("Wallet extension connection not established"); // MetaMask extension constants const TARGET_EXTENSION_VERSION = "12.8.1"; const SETUP_COMPLETION_MARKER = ".extraction_complete"; class MetaMask extends BaseWallet_1.BaseWallet { constructor(walletConfig, context, page, extensionId) { super(); this.context = context; this.extensionId = extensionId; this.config = walletConfig; this.onboardingPage = new pages_1.OnboardingPage(page); this.homePage = new pages_1.HomePage(page); this.notificationPage = new pages_1.NotificationPage(page); } static async initialize(currentContext, contextPath, walletConfig) { // Create browser context with MetaMask extension const context = await MetaMask.createContext(contextPath); // Handle cookie and storage transfer if currentContext exists if (currentContext) { const { cookies } = await currentContext.storageState(); if (cookies) { await context.addCookies(cookies); } } const metamaskPage = context.pages()[0]; if (!metamaskPage) { throw new Error("MetaMask page not found"); } const extensionId = await (0, extensionManager_1.getExtensionId)(context, "MetaMask"); // create metamask instance without running setup yet const _metamask = new MetaMask(walletConfig, context, metamaskPage, extensionId); return { metamaskPage, metamaskContext: context }; } static async createContext(contextPath, slowMo = 0) { console.log("Starting context creation..."); // Use retry logic to handle potential race conditions when setting up extension const MAX_RETRIES = 3; let lastError = null; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { if (attempt > 0) { console.log(`Retrying MetaMask setup (attempt ${attempt + 1}/${MAX_RETRIES})...`); } // Get MetaMask extension path (assumes prepare-metamask.mjs was run first) const cacheDir = node_path_1.default.join(process.cwd(), "e2e", ".cache", "metamask-extension"); const metamaskPath = node_path_1.default.join(cacheDir, `metamask-${TARGET_EXTENSION_VERSION}`); const flagPath = node_path_1.default.join(metamaskPath, SETUP_COMPLETION_MARKER); // Validate that extension exists and was properly extracted if (!(await fs_extra_1.default.pathExists(flagPath))) { const manifestPath = node_path_1.default.join(metamaskPath, "manifest.json"); if (!(await fs_extra_1.default.pathExists(manifestPath))) { throw new Error(`MetaMask extension not found at ${metamaskPath}. Please run the extraction command before running tests:\nyarn e2e:metamask:prepare`); } } console.log("MetaMask extension found at:", metamaskPath); const browserArgs = [ `--disable-extensions-except=${metamaskPath}`, "--enable-extensions", "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-accelerated-2d-canvas", "--no-first-run", "--no-zygote", "--disable-gpu", "--force-gpu-mem-available-mb=1024", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding", ]; const context = await test_1.chromium.launchPersistentContext(contextPath, { headless: false, args: browserArgs, slowMo, viewport: { width: 1280, height: 800 }, ignoreHTTPSErrors: true, acceptDownloads: true, }); try { // Increase timeout to 20 seconds and add logging console.log("Waiting for MetaMask extension page to load..."); await context.waitForEvent("page", { timeout: 20000 }); console.log("MetaMask extension page loaded successfully"); // Close the blank page const pages = context.pages(); console.log(`Number of pages after extension load: ${pages.length}`); if (pages.length > 0) { const blankPage = pages[0]; await blankPage.close(); console.log("Closed blank page"); } return context; } catch (error) { // Clean up on failure await context.close(); // On the last attempt, throw the error if (attempt === MAX_RETRIES - 1) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to initialize MetaMask extension: ${errorMessage}`); } // Save error for potential retry lastError = error instanceof Error ? error : new Error(String(error)); // Short delay before retry await new Promise(resolve => setTimeout(resolve, 2000)); } } catch (error) { // If we failed at the setup stage if (attempt === MAX_RETRIES - 1) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to setup MetaMask: ${errorMessage}`); } // Save error for potential retry lastError = error instanceof Error ? error : new Error(String(error)); // Short delay before retry await new Promise(resolve => setTimeout(resolve, 2000)); } } // If we reach here, all attempts failed throw (lastError ?? new Error("Failed to create MetaMask context after multiple attempts")); } async handleAction(action, options) { const { approvalType, ...additionalOptions } = options ?? {}; if (!this.extensionId) { throw WALLET_CONNECTION_ERROR; } switch (action) { // Basic setup actions case BaseWallet_1.BaseActionType.IMPORT_WALLET_FROM_SEED: await this.onboardingPage.importWallet(additionalOptions.seedPhrase, additionalOptions.password); break; case BaseWallet_1.BaseActionType.IMPORT_WALLET_FROM_PRIVATE_KEY: await this.homePage.importPrivateKey(additionalOptions.privateKey); break; case MetaMaskSpecificActionType.LOCK: // TODO: Implement break; case MetaMaskSpecificActionType.UNLOCK: // TODO: Implement break; case MetaMaskSpecificActionType.SWITCH_ACCOUNT: await this.homePage.switchAccount(additionalOptions.accountName); break; case MetaMaskSpecificActionType.RENAME_ACCOUNT: // TODO: Implement break; // dapp actions case BaseWallet_1.BaseActionType.CONNECT_TO_DAPP: await this.notificationPage.connectToDapp(this.extensionId); break; // Network actions case MetaMaskSpecificActionType.ADD_NETWORK: if (!this.extensionId) { throw WALLET_CONNECTION_ERROR; } // Add type safety and validation for network if (!additionalOptions.network) { throw new Error("Network options not provided for ADD_NETWORK action"); } await this.homePage.addNetwork(additionalOptions.network); break; case BaseWallet_1.BaseActionType.SWITCH_NETWORK: if (!this.extensionId) { throw WALLET_CONNECTION_ERROR; } await this.homePage.switchNetwork(additionalOptions.networkName, additionalOptions.isTestnet); break; case MetaMaskSpecificActionType.APPROVE_ADD_NETWORK: if (approvalType === BaseWallet_1.ActionApprovalType.APPROVE) { await this.notificationPage.approveAddNetwork(this.extensionId); } else { await this.notificationPage.rejectAddNetwork(this.extensionId); } break; // spending cap actions case BaseWallet_1.BaseActionType.CHANGE_SPENDING_CAP: if (approvalType === BaseWallet_1.ActionApprovalType.APPROVE) { await this.notificationPage.approveTokenPermission(this.extensionId); } else { await this.notificationPage.rejectTokenPermission(this.extensionId); } break; // transaction and spending cap removal actions case BaseWallet_1.BaseActionType.HANDLE_TRANSACTION: if (approvalType === BaseWallet_1.ActionApprovalType.APPROVE) { await this.notificationPage.confirmTransaction(this.extensionId); } else { await this.notificationPage.rejectTransaction(this.extensionId); } break; case BaseWallet_1.BaseActionType.REMOVE_SPENDING_CAP: if (approvalType === BaseWallet_1.ActionApprovalType.APPROVE) { await this.notificationPage.confirmSpendingCapRemoval(this.extensionId); } else { await this.notificationPage.rejectSpendingCapRemoval(this.extensionId); } break; default: throw new Error(`Unsupported action: ${action}`); } } async identifyNotificationType() { if (!this.extensionId) { throw WALLET_CONNECTION_ERROR; } return this.notificationPage.identifyNotificationType(this.extensionId); } } exports.MetaMask = MetaMask; __exportStar(require("./fixtures"), exports);