UNPKG

w3wallets

Version:
414 lines (403 loc) 16 kB
// 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 };