@coinbase/onchaintestkit
Version:
End-to-end testing toolkit for blockchain applications, powered by Playwright
256 lines (255 loc) • 12.8 kB
JavaScript
;
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);