w3wallets
Version:
browser wallets for playwright
414 lines (403 loc) • 16 kB
JavaScript
// src/withWallets.ts
import path from "path";
import fs from "fs";
// tests/utils/sleep.ts
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// src/withWallets.ts
import {
chromium
} from "@playwright/test";
// src/backpack/backpack.ts
import { expect } from "@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 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 expect(this.page.getByText("You're all good!")).toBeVisible();
}
await this.page.goto(`chrome-extension://${this.extensionId}/popup.html`);
await this.page.getByTestId("AccountBalanceRoundedIcon").waitFor({ state: "visible" });
}
};
// src/polkadotJS/polkadotJS.ts
import { expect as expect2 } from "@playwright/test";
var PolkadotJS = class extends Wallet {
defaultPassword = "11111111";
async gotoOnboardPage() {
await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
await expect2(
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
import { expect as expect3 } from "@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-get-started-button").click();
await this.page.getByTestId("terms-of-use-scroll-button").click();
await this.page.getByTestId("terms-of-use-checkbox").click();
await this.page.getByTestId("terms-of-use-agree-button").click();
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();
}
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 expect3(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.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 = path.join(process.cwd(), w3walletsDir, "backpack");
const polkadotJSPath = path.join(process.cwd(), w3walletsDir, "polkadotJS");
const metamaskPath = path.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 = path.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 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 (fs.existsSync(userDataDir)) {
fs.rmSync(userDataDir, { recursive: true });
}
}
function ensureWalletExtensionExists(walletPath, walletName) {
if (!fs.existsSync(path.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);
}
export {
withWallets
};