From 731b11bf9820f69bfae9687a210daab24f8d7b22 Mon Sep 17 00:00:00 2001 From: Daniel Izdebski Date: Mon, 8 Jan 2024 02:51:30 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20feat:=20Add?= =?UTF-8?q?=20`TypeDoc`=20comments=20(#1060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/defineWalletSetup.ts | 10 + .../fixtures/src/fixtures/testWithSynpress.ts | 12 + packages/fixtures/src/utils/getExtensionId.ts | 12 + .../src/fixture-actions/unlockForFixture.ts | 7 + wallets/metamask/src/metamask.ts | 207 +++++++++++++++++- 5 files changed, 239 insertions(+), 9 deletions(-) diff --git a/packages/core/src/defineWalletSetup.ts b/packages/core/src/defineWalletSetup.ts index bd97a3a5c..e61f717f2 100644 --- a/packages/core/src/defineWalletSetup.ts +++ b/packages/core/src/defineWalletSetup.ts @@ -1,9 +1,19 @@ import type { BrowserContext, Page } from 'playwright-core' import { getWalletSetupFuncHash } from './utils/getWalletSetupFuncHash' +// TODO: Should we export this type in the `release` package? export type WalletSetupFunction = (context: BrowserContext, walletPage: Page) => Promise // TODO: This runs at least twice. Should we cache it somehow? +/** + * This function is used to define how a wallet should be set up. + * Based on the contents of this function, a browser with the wallet extension is set up and cached so that it can be used by the tests later. + * + * @param walletPassword - The password of the wallet. + * @param fn - A function describing the setup of the wallet. + * + * @returns An object containing the hash of the function, the function itself, and the wallet password. The `testWithWalletSetup` function uses this object. + */ export function defineWalletSetup(walletPassword: string, fn: WalletSetupFunction) { const hash = getWalletSetupFuncHash(fn) diff --git a/packages/fixtures/src/fixtures/testWithSynpress.ts b/packages/fixtures/src/fixtures/testWithSynpress.ts index 908d6cade..abb608e19 100644 --- a/packages/fixtures/src/fixtures/testWithSynpress.ts +++ b/packages/fixtures/src/fixtures/testWithSynpress.ts @@ -125,6 +125,18 @@ const synpressFixtures = ( } }) +/** + * The factory function for the `test` fixture from Playwright extended with Synpress fixtures. + * + * @param walletSetup - An object returned from the `defineWalletSetup` function. + * @param walletSetup.hash - Hash of the cached wallet setup function. + * @param walletSetup.fn - The wallet setup function itself. + * @param walletSetup.walletPassword - The password of the wallet. + * @param unlockWallet - A function that unlocks the wallet. + * @param slowMo - Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to `0`. + * + * @returns The `test` fixture from Playwright extended with Synpress fixtures. See: https://playwright.dev/docs/api/class-test#test-call. + */ export const testWithSynpress = ( walletSetup: ReturnType, unlockWallet: UnlockWalletFunction, diff --git a/packages/fixtures/src/utils/getExtensionId.ts b/packages/fixtures/src/utils/getExtensionId.ts index 951dd8686..a5e3674e4 100644 --- a/packages/fixtures/src/utils/getExtensionId.ts +++ b/packages/fixtures/src/utils/getExtensionId.ts @@ -8,6 +8,18 @@ const Extension = z.object({ const Extensions = z.array(Extension) +/** + * Returns the extension ID for the given extension name. The ID is fetched from the `chrome://extensions` page. + * + * ::: tip + * This function soon will be removed to improve the developer experience! 😇 + * ::: + * + * @param context - The browser context. + * @param extensionName - The name of the extension, e.g., `MetaMask`. + * + * @returns The extension ID. + */ export async function getExtensionId(context: BrowserContext, extensionName: string) { const page = await context.newPage() await page.goto('chrome://extensions') diff --git a/wallets/metamask/src/fixture-actions/unlockForFixture.ts b/wallets/metamask/src/fixture-actions/unlockForFixture.ts index 71e44a73b..c509eebac 100644 --- a/wallets/metamask/src/fixture-actions/unlockForFixture.ts +++ b/wallets/metamask/src/fixture-actions/unlockForFixture.ts @@ -5,6 +5,13 @@ import { CrashPage, HomePage } from '../pages' import { closePopover, closeRecoveryPhraseReminder } from '../pages/HomePage/actions' import { waitForSpinnerToVanish } from '../utils/waitForSpinnerToVanish' +/** + * A more advanced version of the `MetaMask.unlock()` function that incorporates various workarounds for MetaMask issues, among other things. + * This function should be used instead of the `MetaMask.unlock()` when passing it to the `testWithSynpress` function. + * + * @param page - The MetaMask tab page. + * @param password - The password of the MetaMask wallet. + */ export async function unlockForFixture(page: Page, password: string) { const metamask = new MetaMask(page.context(), page, password) diff --git a/wallets/metamask/src/metamask.ts b/wallets/metamask/src/metamask.ts index 14f7b1213..67402410c 100644 --- a/wallets/metamask/src/metamask.ts +++ b/wallets/metamask/src/metamask.ts @@ -6,17 +6,67 @@ import type { GasSetting } from './pages/NotificationPage/actions' const NO_EXTENSION_ID_ERROR = new Error('MetaMask extensionId is not set') +/** + * This class is the heart of Synpress's MetaMask API. + */ export class MetaMask { - crashPage: CrashPage - onboardingPage: OnboardingPage - lockPage: LockPage - homePage: HomePage - notificationPage: NotificationPage - + /** + * This property can be used to access selectors for a given page. + * + * @group Selectors + */ + readonly crashPage: CrashPage + /** + * This property can be used to access selectors for a given page. + * + * @group Selectors + */ + readonly onboardingPage: OnboardingPage + /** + * This property can be used to access selectors for a given page. + * + * @group Selectors + */ + readonly lockPage: LockPage + /** + * This property can be used to access selectors for a given page. + * + * @group Selectors + */ + readonly homePage: HomePage + /** + * This property can be used to access selectors for a given page. + * + * @group Selectors + */ + readonly notificationPage: NotificationPage + + /** + * Class constructor. + * + * @param context - The browser context. + * @param page - The MetaMask tab page. + * @param password - The password of the MetaMask wallet. + * @param extensionId - The extension ID of the MetaMask extension. Optional if no interaction with the dapp is required. + * + * @returns A new instance of the MetaMask class. + */ constructor( + /** + * The browser context. + */ readonly context: BrowserContext, + /** + * The MetaMask tab page. + */ readonly page: Page, + /** + * The password of the MetaMask wallet. + */ readonly password: string, + /** + * The extension ID of the MetaMask extension. Optional if no interaction with the dapp is required. + */ readonly extensionId?: string ) { this.crashPage = new CrashPage() @@ -27,30 +77,68 @@ export class MetaMask { this.notificationPage = new NotificationPage(page) } + /** + * Imports a wallet using the given seed phrase. + * + * @param seedPhrase - The seed phrase to import. + */ async importWallet(seedPhrase: string) { await this.onboardingPage.importWallet(seedPhrase, this.password) } + /** + * Adds a new account with the given name. This account is based on the initially imported seed phrase. + * + * @param accountName - The name of the new account. + */ async addNewAccount(accountName: string) { await this.homePage.addNewAccount(accountName) } + /** + * Imports a wallet using the given private key. + * + * @param privateKey - The private key to import. + */ async importWalletFromPrivateKey(privateKey: string) { await this.homePage.importWalletFromPrivateKey(privateKey) } + /** + * Switches to the account with the given name. + * + * @param accountName - The name of the account to switch to. + */ async switchAccount(accountName: string) { await this.homePage.switchAccount(accountName) } + /** + * Adds a new network. + * + * @param network - The network object to use for adding the new network. + * @param network.name - The name of the network. + * @param network.rpcUrl - The RPC URL of the network. + * @param network.chainId - The chain ID of the network. + * @param network.symbol - The currency symbol of the network. + * @param network.blockExplorerUrl - The block explorer URL of the network. + */ async addNetwork(network: Network) { await this.homePage.addNetwork(network) } + /** + * Switches to the network with the given name. + * + * @param networkName - The name of the network to switch to. + */ async switchNetwork(networkName: string) { await this.homePage.switchNetwork(networkName) } + /** + * Connects to the dapp using the currently selected account. + */ async connectToDapp() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -59,14 +147,23 @@ export class MetaMask { await this.notificationPage.connectToDapp(this.extensionId) } + /** + * Locks MetaMask. + */ async lock() { await this.homePage.lock() } + /** + * Unlocks MetaMask. + */ async unlock() { await this.lockPage.unlock(this.password) } + /** + * Confirms a signature request. This function supports all types of commonly used signatures. + */ async confirmSignature() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -75,6 +172,9 @@ export class MetaMask { await this.notificationPage.signMessage(this.extensionId) } + /** + * Rejects a signature request. This function supports all types of commonly used signatures. + */ async rejectSignature() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -83,6 +183,9 @@ export class MetaMask { await this.notificationPage.rejectMessage(this.extensionId) } + /** + * Approves a new network request. + */ async approveNewNetwork() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -91,6 +194,9 @@ export class MetaMask { await this.notificationPage.approveNewNetwork(this.extensionId) } + /** + * Rejects a new network request. + */ async rejectNewNetwork() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -99,6 +205,9 @@ export class MetaMask { await this.notificationPage.rejectNewNetwork(this.extensionId) } + /** + * Approves a switch network request. + */ async approveSwitchNetwork() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -107,6 +216,9 @@ export class MetaMask { await this.notificationPage.approveSwitchNetwork(this.extensionId) } + /** + * Rejects a switch network request. + */ async rejectSwitchNetwork() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -115,6 +227,12 @@ export class MetaMask { await this.notificationPage.rejectSwitchNetwork(this.extensionId) } + /** + * Confirms a transaction request. + * + * @param options - The transaction options. + * @param options.gasSetting - The gas setting to use for the transaction. + */ async confirmTransaction(options?: { gasSetting?: GasSetting }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -123,6 +241,9 @@ export class MetaMask { await this.notificationPage.confirmTransaction(this.extensionId, options) } + /** + * Rejects a transaction request. + */ async rejectTransaction() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -131,6 +252,17 @@ export class MetaMask { await this.notificationPage.rejectTransaction(this.extensionId) } + /** + * Approves a permission request to spend tokens. + * + * ::: warning + * This function does not work with the NFTs approvals. + * ::: + * + * @param options - The permission options. + * @param options.spendLimit - The spend limit to use for the permission. + * @param options.gasSetting - The gas setting to use for the approval transaction. + */ async approvePermission(options?: { spendLimit?: 'max' | number; gasSetting?: GasSetting }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -139,6 +271,13 @@ export class MetaMask { await this.notificationPage.approvePermission(this.extensionId, options) } + /** + * Rejects a permission request to spend tokens. + * + * ::: warning + * This function does not work with the NFTs approvals. + * ::: + */ async rejectPermission() { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -147,28 +286,65 @@ export class MetaMask { await this.notificationPage.rejectPermission(this.extensionId) } + /** + * Goes back to the home page of MetaMask tab. + */ async goBackToHomePage() { await this.homePage.goBackToHomePage() } + /** + * Opens the settings page. + */ async openSettings() { await this.homePage.openSettings() } + /** + * Opens a given menu in the sidebar. + * + * @param menu - The menu to open. + */ async openSidebarMenu(menu: SettingsSidebarMenus) { await this.homePage.openSidebarMenu(menu) } + /** + * Toggles the "Show Test Networks" setting. + * + * ::: warning + * This function requires the correct menu to be already opened. + * ::: + */ async toggleShowTestNetworks() { await this.homePage.toggleShowTestNetworks() } + /** + * Toggles the "Dismiss Secret Recovery Phrase Reminder" setting. + * + * ::: warning + * This function requires the correct menu to be already opened. + * ::: + */ async toggleDismissSecretRecoveryPhraseReminder() { await this.homePage.toggleDismissSecretRecoveryPhraseReminder() } - // ---- EXPERIMENTAL FEATURES ---- - + /// ------------------------------------------- + /// ---------- EXPERIMENTAL FEATURES ---------- + /// ------------------------------------------- + + /** + * Confirms a transaction request and waits for the transaction to be mined. + * This function utilizes the "Activity" tab of the MetaMask tab. + * + * @param options - The transaction options. + * @param options.gasSetting - The gas setting to use for the transaction. + * + * @experimental + * @group Experimental Methods + */ async confirmTransactionAndWaitForMining(options?: { gasSetting?: GasSetting }) { if (!this.extensionId) { throw NO_EXTENSION_ID_ERROR @@ -177,11 +353,24 @@ export class MetaMask { await this.notificationPage.confirmTransactionAndWaitForMining(this.extensionId, options) } - // Note: `txIndex` starts from 0. + /** + * Opens the transaction details. + * + * @param txIndex - The index of the transaction in the "Activity" tab. Starts from `0`. + * + * @experimental + * @group Experimental Methods + */ async openTransactionDetails(txIndex: number) { await this.homePage.openTransactionDetails(txIndex) } + /** + * Closes the currently opened transaction details. + * + * @experimental + * @group Experimental Methods + */ async closeTransactionDetails() { await this.homePage.closeTransactionDetails() }