w3wallets
Version:
browser wallets for playwright
448 lines (435 loc) • 17.7 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, {
withWallets: () => withWallets
});
module.exports = __toCommonJS(index_exports);
// src/withWallets.ts
var import_path = __toESM(require("path"));
var import_fs = __toESM(require("fs"));
// tests/utils/sleep.ts
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// src/withWallets.ts
var import_test4 = require("@playwright/test");
// src/backpack/backpack.ts
var import_test = require("@playwright/test");
// src/wallet.ts
var Wallet = class {
constructor(page, extensionId) {
this.page = page;
this.extensionId = extensionId;
}
};
// src/backpack/backpack.ts
var Backpack = class extends Wallet {
defaultPassword = "11111111";
currentAccountId = 0;
maxAccountId = 0;
async gotoOnboardPage() {
await this.page.goto(
`chrome-extension://${this.extensionId}/onboarding.html`
);
await (0, import_test.expect)(this.page.getByText("Welcome to Backpack")).toBeVisible();
}
async onboard(network, privateKey) {
this.currentAccountId++;
this.maxAccountId++;
return this._importAccount(network, privateKey, true);
}
async addAccount(network, privateKey) {
this.maxAccountId++;
this.currentAccountId = this.maxAccountId;
await this.page.goto(
`chrome-extension://${this.extensionId}/onboarding.html?add-user-account=true`
);
await this._importAccount(network, privateKey, false);
}
/**
* Switch account
* @param id The first added account has id 1, the second – 2, and so on
*/
async switchAccount(id) {
await this.page.getByRole("button", { name: `A${this.currentAccountId}` }).click();
await this.page.getByRole("button", { name: `Account ${id}` }).click();
this.currentAccountId = id;
}
async unlock() {
await this.page.getByPlaceholder("Password").fill(this.defaultPassword);
await this.page.getByRole("button", { name: "Unlock" }).click();
}
async setRPC(network, rpc) {
await this._clickOnAccount();
await this.page.getByRole("button", { name: "Settings" }).click();
await this.page.getByRole("button", { name: network }).click();
await this.page.getByRole("button", { name: "RPC Connection" }).click();
await this.page.getByRole("button", { name: "Custom" }).click();
await this.page.getByPlaceholder("RPC URL").fill(rpc);
await this.page.keyboard.press("Enter");
}
async ignoreAndProceed() {
const ignoreButton = this.page.getByText("Ignore and proceed anyway.");
await ignoreButton.click();
}
async approve() {
await this.page.getByText("Approve", { exact: true }).click();
}
async deny() {
await this.page.getByText("Deny", { exact: true }).click();
}
async _clickOnAccount() {
return this.page.getByRole("button", { name: `A${this.currentAccountId}`, exact: true }).click();
}
async _importAccount(network, privateKey, isOnboard) {
{
await this.page.waitForTimeout(2e3);
if (await this.page.getByText("You're all good!").isVisible()) {
await this.page.getByLabel("Go back").click();
}
}
await this.page.getByRole("button", { name: "I agree to the terms" }).click();
await this.page.getByText("I already have a wallet").click();
await this.page.getByText("View all").click();
await this.page.getByText(network).click();
await this.page.getByText("Private key").click();
await this.page.getByPlaceholder("Private key").fill(privateKey);
await this.page.waitForTimeout(1e3);
await this.page.getByText("Import", { exact: true }).click();
if (isOnboard) {
await this.page.getByRole("textbox").nth(1).fill(this.defaultPassword);
await this.page.getByRole("textbox").nth(2).fill(this.defaultPassword);
await this.page.getByText("Next", { exact: true }).click();
await (0, import_test.expect)(this.page.getByText("You're all good!")).toBeVisible();
}
await this.page.goto(`chrome-extension://${this.extensionId}/popup.html`);
await this.page.getByTestId("__CAROUSEL_ITEM_0__").waitFor({ state: "visible" });
}
};
// src/polkadotJS/polkadotJS.ts
var import_test2 = 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_test2.expect)(
this.page.getByText("Before we start, just a couple of notes")
).toBeVisible();
}
async onboard(seed, password, name) {
await this.page.getByRole("button", { name: "Understood, let me continue" }).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) {
await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span").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/metamask/metamask.ts
var import_test3 = require("@playwright/test");
// tests/utils/address.ts
var shortenAddress = (address) => {
return `${address.slice(0, 7)}...${address.slice(-5)}`;
};
// src/metamask/metamask.ts
var Metamask = class extends Wallet {
defaultPassword = "11111111";
async gotoOnboardPage() {
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
}
/**
*
* @param mnemonic 12-word mnemonic seed phrase
*/
async onboard(mnemonic, password = this.defaultPassword) {
await this.page.getByTestId("onboarding-import-wallet").click();
await this.page.getByTestId("onboarding-import-with-srp-button").click();
await this.page.getByTestId("srp-input-import__srp-note").pressSequentially(mnemonic);
await this.page.getByTestId("import-srp-confirm").click();
await this.page.getByTestId("create-password-new-input").fill(password);
await this.page.getByTestId("create-password-confirm-input").fill(password);
await this.page.getByTestId("create-password-terms").click();
await this.page.getByTestId("create-password-submit").click();
await this.page.getByTestId("metametrics-no-thanks").click();
await this.page.getByTestId("onboarding-complete-done").click();
await this.page.getByTestId("download-app-continue").click();
await this.page.getByTestId("pin-extension-done").click();
await this.clickTopRightCornerToCloseAllTheMarketingBullshit();
}
async switchAccount(accountNameOrAddress) {
await this.page.getByTestId("account-menu-icon").click();
if ("name" in accountNameOrAddress) {
await this.page.locator(".multichain-account-list-item__account-name").getByRole("button", { name: accountNameOrAddress.name, exact: true }).click();
} else {
await this.page.getByTestId("account-list-address").filter({ hasText: shortenAddress(accountNameOrAddress.address) }).click();
}
}
async importAccount(privateKey) {
await this.page.getByTestId("account-menu-icon").click();
await this.page.getByTestId("multichain-account-menu-popover-action-button").click();
await this.page.getByTestId("multichain-account-menu-popover-add-imported-account").click();
await this.page.locator("#private-key-box").fill(privateKey);
await this.page.getByTestId("import-account-confirm-button").click();
}
async addAccount(accountName) {
await this.page.getByTestId("account-menu-icon").click();
await this.page.getByTestId("multichain-account-menu-popover-action-button").click();
await this.page.getByTestId("multichain-account-menu-popover-add-account").click();
if (accountName) {
await this.page.locator("#account-name").fill(accountName);
}
await this.page.getByTestId("submit-add-account-with-name").click();
}
async getAccountName() {
const accountSelect = this.page.getByTestId("account-menu-icon");
await (0, import_test3.expect)(accountSelect).toBeVisible();
const text = await accountSelect.textContent();
if (!text) throw Error("Cannot get account name");
return text;
}
async connectToNetwork(networkName, networkType = "Popular") {
await this.page.getByTestId("sort-by-networks").click();
await this.page.getByRole("button", { name: networkType, exact: true }).click();
const additionalNetwork = this.page.getByTestId("additional-network-item").getByText(networkName);
try {
await additionalNetwork.isEnabled({ timeout: 1e3 });
await additionalNetwork.click();
await this.page.getByTestId("confirmation-submit-button").click();
} catch (error) {
await this.page.getByText(networkName).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 approve() {
const p = await this.page.context().newPage();
await p.goto(`chrome-extension://${this.extensionId}/notification.html`);
await p.locator(
'[data-testid="confirm-footer-button"], [data-testid="confirm-btn"], [data-testid="page-container-footer-next"], [data-testid="confirmation-submit-button"]'
).click();
await p.waitForSelector(".main-container-wrapper:empty", {
timeout: 1e4
});
await p.close();
}
async deny() {
return this.usingNotificationPage(
(p) => p.getByTestId("cancel-btn").click()
);
}
async usingNotificationPage(action) {
const p = await this.page.context().newPage();
await p.goto(`chrome-extension://${this.extensionId}/notification.html`);
await action(p);
await p.close();
}
async clickTopRightCornerToCloseAllTheMarketingBullshit() {
await this.page.waitForTimeout(500);
await this.page.keyboard.press("Escape");
await this.page.mouse.click(1e3, 10);
}
};
// src/withWallets.ts
var w3walletsDir = ".w3wallets";
function withWallets(test, ...config) {
const withBackpack = config.includes("backpack");
const withPolkadotJS = config.includes("polkadotJS");
const withMetamask = config.includes("metamask");
const backpackPath = import_path.default.join(process.cwd(), w3walletsDir, "backpack");
const polkadotJSPath = import_path.default.join(process.cwd(), w3walletsDir, "polkadotJS");
const metamaskPath = import_path.default.join(process.cwd(), w3walletsDir, "metamask");
return test.extend({
/**
* Sets up a persistent browser context with the requested extensions loaded.
*/
context: async ({}, use, testInfo) => {
const userDataDir = import_path.default.join(
process.cwd(),
".w3wallets",
".context",
testInfo.testId
);
cleanUserDataDir(userDataDir);
const extensionPaths = [];
if (withBackpack) {
ensureWalletExtensionExists(backpackPath, "backpack");
extensionPaths.push(backpackPath);
}
if (withPolkadotJS) {
ensureWalletExtensionExists(polkadotJSPath, "polkadotJS");
extensionPaths.push(polkadotJSPath);
}
if (withMetamask) {
ensureWalletExtensionExists(metamaskPath, "metamask");
extensionPaths.push(metamaskPath);
}
const context = await import_test4.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();
},
backpack: async ({ context }, use) => {
if (!withBackpack) {
throw Error(
"The Backpack wallet hasn't been loaded. Add it to the withWallets function."
);
}
const backpack = await initializeExtension(
context,
Backpack,
"Backpack is not initialized"
);
await use(backpack);
},
polkadotJS: async ({ context }, use) => {
if (!withPolkadotJS) {
throw Error(
"The Polkadot{.js} wallet hasn't been loaded. Add it to the withWallets function."
);
}
const polkadotJS = await initializeExtension(
context,
PolkadotJS,
"Polkadot{.js} is not initialized"
);
await use(polkadotJS);
},
metamask: async ({ context }, use) => {
if (!withMetamask) {
throw Error(
"The Metamask wallet hasn't been loaded. Add it to the withWallets function."
);
}
const metamask = await initializeExtension(
context,
Metamask,
"Metamask is not initialized"
);
await use(metamask);
}
});
}
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"))) {
throw new Error(
`Cannot find ${walletName}. Please download it via 'npx w3wallets ${walletName}'.`
);
}
}
async function initializeExtension(context, ExtensionClass, notInitializedErrorMessage) {
const serviceWorkers = context.serviceWorkers();
let page = await context.newPage();
for (const worker of serviceWorkers) {
const extensionId = worker.url().split("/")[2];
if (!extensionId) {
continue;
}
const extension = new ExtensionClass(page, extensionId);
try {
await extension.gotoOnboardPage();
return extension;
} catch {
await page.close();
page = await context.newPage();
}
}
throw new Error(notInitializedErrorMessage);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
withWallets
});