w3wallets
Version:
browser wallets for playwright
390 lines (380 loc) • 15.1 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Metamask: () => Metamask,
PolkadotJS: () => PolkadotJS,
config: () => config,
createWallet: () => createWallet,
metamask: () => metamask,
polkadotJS: () => polkadotJS,
withWallets: () => withWallets
});
module.exports = __toCommonJS(index_exports);
// src/withWallets.ts
var import_path = __toESM(require("path"));
var import_fs = __toESM(require("fs"));
var import_crypto = __toESM(require("crypto"));
var import_test = require("@playwright/test");
var W3WALLETS_DIR = ".w3wallets";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function withWallets(test, ...wallets) {
const extensionInfo = wallets.map((w) => {
const extPath = import_path.default.join(process.cwd(), W3WALLETS_DIR, w.extensionDir);
ensureWalletExtensionExists(extPath, w.name);
const extensionId = w.extensionId ?? getExtensionId(extPath);
return { path: extPath, id: extensionId, name: w.name };
});
const extensionPaths = extensionInfo.map((e) => e.path);
const fixtures = {
context: async ({}, use, testInfo) => {
const userDataDir = import_path.default.join(
process.cwd(),
W3WALLETS_DIR,
".context",
testInfo.testId
);
cleanUserDataDir(userDataDir);
const context = await import_test.chromium.launchPersistentContext(userDataDir, {
headless: testInfo.project.use.headless ?? true,
channel: "chromium",
args: [
`--disable-extensions-except=${extensionPaths.join(",")}`,
`--load-extension=${extensionPaths.join(",")}`
]
});
while (context.serviceWorkers().length < extensionPaths.length) {
await sleep(1e3);
}
await use(context);
await context.close();
}
};
for (let i = 0; i < wallets.length; i++) {
const wallet = wallets[i];
const info = extensionInfo[i];
fixtures[wallet.name] = async ({ context }, use) => {
const instance = await initializeExtension(
context,
wallet.WalletClass,
info.id,
wallet.name
);
await use(instance);
};
}
return test.extend(fixtures);
}
function cleanUserDataDir(userDataDir) {
if (import_fs.default.existsSync(userDataDir)) {
import_fs.default.rmSync(userDataDir, { recursive: true });
}
}
function ensureWalletExtensionExists(walletPath, walletName) {
if (!import_fs.default.existsSync(import_path.default.join(walletPath, "manifest.json"))) {
const cliAlias = walletName.toLowerCase();
throw new Error(
`Cannot find ${walletName}. Please download it via 'npx w3wallets ${cliAlias}'.`
);
}
}
function getExtensionId(extensionPath) {
const absolutePath = import_path.default.resolve(extensionPath);
const manifestPath = import_path.default.join(absolutePath, "manifest.json");
const manifest = JSON.parse(import_fs.default.readFileSync(manifestPath, "utf-8"));
let dataToHash;
if (manifest.key) {
dataToHash = Buffer.from(manifest.key, "base64");
} else {
dataToHash = Buffer.from(absolutePath);
}
const hash = import_crypto.default.createHash("sha256").update(dataToHash).digest();
const ALPHABET = "abcdefghijklmnop";
let extensionId = "";
for (let i = 0; i < 16; i++) {
const byte = hash[i];
extensionId += ALPHABET[byte >> 4 & 15];
extensionId += ALPHABET[byte & 15];
}
return extensionId;
}
async function initializeExtension(context, ExtensionClass, expectedExtensionId, walletName) {
const expectedUrl = `chrome-extension://${expectedExtensionId}/`;
const worker = context.serviceWorkers().find((w) => w.url().startsWith(expectedUrl));
if (!worker) {
const availableIds = context.serviceWorkers().map((w) => w.url().split("/")[2]).filter(Boolean);
throw new Error(
`Service worker for ${walletName} (ID: ${expectedExtensionId}) not found. Available extension IDs: [${availableIds.join(", ")}]`
);
}
const page = await context.newPage();
const extension = new ExtensionClass(page, expectedExtensionId);
return extension;
}
// src/core/types.ts
function createWallet(config2) {
return config2;
}
// src/wallets/metamask/metamask.ts
var import_test2 = require("@playwright/test");
// src/config.ts
var config = {
/**
* Timeout for actions like click, fill, waitFor, goto.
* Set via W3WALLETS_ACTION_TIMEOUT env variable.
* @default 30000 (30 seconds)
*/
get actionTimeout() {
const value = process.env.W3WALLETS_ACTION_TIMEOUT;
return value ? parseInt(value, 10) : void 0;
}
};
// src/core/wallet.ts
var Wallet = class {
constructor(page, extensionId) {
this.page = page;
this.extensionId = extensionId;
if (config.actionTimeout) {
page.setDefaultTimeout(config.actionTimeout);
}
}
};
// src/wallets/metamask/metamask.ts
var Metamask = class extends Wallet {
defaultPassword = "TestPassword123!";
async gotoOnboardPage() {
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
await (0, import_test2.expect)(
this.page.getByRole("button", { name: "I have an existing wallet" })
).toBeVisible();
}
/**
* Onboard MetaMask with a mnemonic phrase
* @param mnemonic - 12 or 24 word recovery phrase
* @param password - Optional password (defaults to TestPassword123!)
*/
async onboard(mnemonic, password) {
const pwd = password ?? this.defaultPassword;
await this.gotoOnboardPage();
await this.page.getByRole("button", { name: "I have an existing wallet" }).click();
await this.page.getByRole("button", { name: "Import using Secret Recovery Phrase" }).click();
const textbox = this.page.getByRole("textbox");
await textbox.click();
for (const word of mnemonic.split(" ")) {
await this.page.keyboard.type(word);
await this.page.keyboard.type(" ");
await this.page.waitForTimeout(30);
}
const continueBtn = this.page.getByTestId("import-srp-confirm");
await continueBtn.click();
const passwordInputs = this.page.locator('input[type="password"]');
await passwordInputs.nth(0).fill(pwd);
await passwordInputs.nth(1).fill(pwd);
await this.page.getByRole("checkbox").click();
await this.page.getByRole("button", { name: "Create password" }).click();
const metametricsBtn = this.page.getByTestId("metametrics-i-agree");
await metametricsBtn.click();
const openWalletBtn = this.page.getByRole("button", {
name: /open wallet/i
});
await openWalletBtn.click();
await this.page.goto(
`chrome-extension://${this.extensionId}/sidepanel.html`
);
}
async approve() {
await this.page.getByTestId("confirm-btn").or(this.page.getByTestId("confirm-footer-button")).or(this.page.getByTestId("page-container-footer-next")).or(this.page.getByRole("button", { name: /confirm/i })).click();
}
async deny() {
const cancelBtn = this.page.getByTestId("cancel-btn").or(this.page.getByTestId("confirm-footer-cancel-button")).or(this.page.getByTestId("page-container-footer-cancel")).or(this.page.getByRole("button", { name: /cancel|reject/i }));
await cancelBtn.first().click();
}
/**
* Lock the MetaMask wallet
*/
async lock() {
await this.page.getByTestId("account-options-menu-button").click();
await this.page.locator("text=Lock MetaMask").click();
}
/**
* Unlock MetaMask with password
*/
async unlock(password) {
const pwd = password ?? this.defaultPassword;
const passwordInput = this.page.getByTestId("unlock-password");
await passwordInput.fill(pwd);
await this.page.getByTestId("unlock-submit").click();
}
/**
* Switch to an existing network in MetaMask
* @param networkName - Name of the network to switch to (e.g., "Ethereum Mainnet", "Sepolia")
*/
async switchNetwork(networkName, networkType = "Popular") {
await this.page.getByTestId("sort-by-networks").click();
if (networkType === "Custom") {
await this.page.getByRole("tab", { name: "Custom" }).click();
}
await this.page.getByText(networkName).click();
await (0, import_test2.expect)(this.page.getByTestId("sort-by-networks")).toHaveText(
networkName
);
}
async switchAccount(accountName) {
await this.page.getByTestId("account-menu-icon").click();
await this.page.getByText(accountName, { exact: true }).click();
}
/**
* Add a custom network to MetaMask
*/
async addNetwork(network) {
await this.page.goto(
`chrome-extension://${this.extensionId}/home.html#settings/networks/add-network`
);
await this.page.getByTestId("network-form-network-name").fill(network.name);
await this.page.getByTestId("network-form-rpc-url").fill(network.rpc);
await this.page.getByTestId("network-form-chain-id").fill(network.chainId.toString());
await this.page.getByTestId("network-form-ticker-input").fill(network.currencySymbol);
await this.page.getByRole("button", { name: /save/i }).click();
}
async addCustomNetwork(settings) {
await this.page.getByTestId("account-options-menu-button").click();
await this.page.getByTestId("global-menu-networks").click();
await this.page.getByRole("button", { name: "Add a custom network" }).click();
await this.page.getByTestId("network-form-network-name").fill(settings.name);
await this.page.getByTestId("network-form-chain-id").fill(settings.chainId.toString());
await this.page.getByTestId("network-form-ticker-input").fill(settings.currencySymbol);
await this.page.getByTestId("test-add-rpc-drop-down").click();
await this.page.getByRole("button", { name: "Add RPC URL" }).click();
await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
await this.page.getByRole("button", { name: "Add URL" }).click();
await this.page.getByRole("button", { name: "Save" }).click();
}
async enableTestNetworks() {
await this.page.getByTestId("account-options-menu-button").click();
await this.page.getByTestId("global-menu-networks").click();
await this.page.locator("text=Show test networks >> xpath=following-sibling::label").click();
await this.page.keyboard.press("Escape");
}
async importAccount(privateKey) {
await this.page.getByTestId("account-menu-icon").click();
await this.page.getByTestId("account-list-add-wallet-button").click();
await this.page.getByTestId("add-wallet-modal-import-account").click();
await this.page.locator("#private-key-box").fill(privateKey);
await this.page.getByTestId("import-account-confirm-button").click();
await this.page.getByRole("button", { name: "Back" }).click();
}
async accountNameIs(accountName) {
await (0, import_test2.expect)(this.page.getByTestId("account-menu-icon")).toContainText(
accountName
);
}
};
// src/wallets/polkadot-js/polkadot-js.ts
var import_test3 = require("@playwright/test");
var PolkadotJS = class extends Wallet {
defaultPassword = "11111111";
async gotoOnboardPage() {
await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
await (0, import_test3.expect)(
this.page.getByText("Before we start, just a couple of notes")
).toBeVisible();
}
async onboard(seed, password, name) {
await this.gotoOnboardPage();
await this.page.getByRole("button", { name: "Understood, let me continue" }).click();
await this.page.getByRole("button", { name: "I Understand" }).click();
await this.page.locator(".popupToggle").first().click();
await this.page.getByText("Import account from pre-existing seed").click();
await this.page.locator(".seedInput").getByRole("textbox").fill(seed);
await this.page.getByRole("button", { name: "Next" }).click();
await this._getLabeledInput("A descriptive name for your account").fill(
name ?? "Test"
);
await this._getLabeledInput("A new password for this account").fill(
password ?? this.defaultPassword
);
await this._getLabeledInput("Repeat password for verification").fill(
password ?? this.defaultPassword
);
await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click();
}
async selectAllAccounts() {
await this.page.getByText("Select all").click();
}
async selectAccount(accountId) {
const cb = this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span");
await cb.check().catch(() => cb.check());
}
async enterPassword(password) {
await this._getLabeledInput("Password for this account").fill(
password ?? this.defaultPassword
);
}
async approve() {
const connect = this.page.getByRole("button", { name: "Connect" });
const signTransaction = this.page.getByRole("button", {
name: "Sign the transaction"
});
await connect.or(signTransaction).click();
}
async deny() {
const reject = this.page.getByRole("button", { name: "Reject" });
const cancel = this.page.getByRole("link", { name: "Cancel" });
await reject.or(cancel).click();
}
_getLabeledInput(label) {
return this.page.locator(
`//label[text()="${label}"]/following-sibling::input`
);
}
};
// src/wallets/index.ts
var metamask = createWallet({
name: "metamask",
extensionDir: "metamask",
WalletClass: Metamask
});
var polkadotJS = createWallet({
name: "polkadotJS",
extensionDir: "polkadotjs",
WalletClass: PolkadotJS
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Metamask,
PolkadotJS,
config,
createWallet,
metamask,
polkadotJS,
withWallets
});