import { expect, type Locator, type Page } from '@playwright/test'; import Onboarding from './onboarding'; const strongPW = Onboarding.generateSecurePasswordCrypto(); export default class Wallet { readonly balance: Locator; readonly allUpperButtons: Locator; readonly labelAccountName: Locator; readonly buttonGenerateAccount: Locator; readonly buttonConnectHardwareWallet: Locator; readonly inputName: Locator; readonly buttonAccountOptions: Locator; readonly accountBalance: Locator; readonly buttonRenameAccount: Locator; readonly buttonResetAccountName: Locator; readonly labelInfoRenameAccount: Locator; readonly errorMessageRenameAccount: Locator; readonly manageTokenButton: Locator; readonly buttonMenu: Locator; readonly buttonLock: Locator; readonly buttonConfirm: Locator; readonly buttonDenyDataCollection: Locator; readonly buttonNetwork: Locator; readonly buttonSave: Locator; readonly buttonMainnet: Locator; readonly buttonTestnet: Locator; readonly buttonBack: Locator; readonly inputStacksURL: Locator; readonly inputBTCURL: Locator; readonly inputFallbackBTCURL: Locator; readonly labelCoinTitle: Locator; readonly checkboxToken: Locator; readonly checkboxTokenActive: Locator; readonly checkboxTokenInactive: Locator; readonly buttonStacks: Locator; readonly buttonBRC20: Locator; readonly buttonRunes: Locator; readonly headingTokens: Locator; readonly divTokenRow: Locator; readonly labelTokenSubtitle: Locator; readonly labelCoinBalanceCurrency: Locator; readonly navigationDashboard: Locator; readonly navigationNFT: Locator; readonly navigationStacking: Locator; readonly navigationExplore: Locator; readonly navigationSettings: Locator; readonly divAppSlide: Locator; readonly divAppCard: Locator; readonly divAppTitle: Locator; readonly carouselApp: Locator; readonly buttonDownArrow: Locator; readonly inputSwapAmount: Locator; readonly buttonSelectCoin: Locator; readonly buttonContinue: Locator; readonly buttonDetails: Locator; readonly nameToken: Locator; readonly buttonInsufficientBalance: Locator; readonly imageToken: Locator; readonly swapTokenBalance: Locator; readonly textUSD: Locator; readonly buttonStartStacking: Locator; readonly headingStacking: Locator; readonly containerStackingInfo: Locator; readonly infoTextStacking: Locator; readonly buttonUpdatePassword: Locator; readonly errorMessage: Locator; readonly headerNewPassword: Locator; readonly infoUpdatePassword: Locator; readonly buttonCurrency: Locator; readonly buttonShowSeedphrase: Locator; readonly textCurrency: Locator; readonly iconFlag: Locator; readonly selectCurrency: Locator; readonly totalItem: Locator; readonly tabsCollectiblesItems: Locator; readonly containersCollectibleItem: Locator; readonly containerRareSats: Locator; readonly containerInscription: Locator; readonly nameInscription: Locator; readonly amountInscription: Locator; readonly buttonNext: Locator; readonly inputMemo: Locator; readonly inputRecipientAddress: Locator; readonly inputSendAmount: Locator; readonly errorMessageAddressInvalid: Locator; readonly errorInsufficientBalance: Locator; readonly errorMessageAddressRequired: Locator; readonly containerFeeRate: Locator; readonly inputBTCAddress: Locator; readonly coinBalance: Locator; readonly transactionHistoryAmount: Locator; readonly containerTransactionHistory: Locator; readonly errorMessageSendSelf: Locator; readonly infoMessageSendSelf: Locator; readonly inputBTCAmount: Locator; readonly buttonExpand: Locator; readonly confirmCurrencyAmount: Locator; readonly confirmTotalAmount: Locator; readonly confirmAmount: Locator; readonly sendAddress: Locator; readonly receiveAddress: Locator; readonly confirmBalance: Locator; readonly buttonCancel: Locator; readonly labelCoinBalanceCrypto: Locator; readonly labelBalanceAmountSelector: Locator; readonly buttonClose: Locator; readonly sendTransactionID: Locator; readonly errorInsufficientFunds: Locator; readonly noFundsBTCMessage: Locator; readonly coinSecondaryButton: Locator; readonly coinSecondaryContainer: Locator; readonly coinContractAddress: Locator; readonly textCoinTitle: Locator; readonly sendSTXValue: Locator; readonly buttonList: Locator; readonly tabAvailable: Locator; readonly tabListed: Locator; readonly tabNotListed: Locator; readonly buttonSetPrice: Locator; readonly runeSKIBIDI: Locator; readonly runeItem: Locator; readonly runeItemCheckbox: Locator; readonly buttonFloorPrice: Locator; readonly button5Price: Locator; readonly button10Price: Locator; readonly button20Price: Locator; readonly buttonCustomPrice: Locator; readonly buttonApply: Locator; readonly inputListingPrice: Locator; readonly runeContainer: Locator; readonly runeTitle: Locator; readonly runePrice: Locator; readonly buttonEnable: Locator; readonly buttonSend: Locator; readonly labelSatsValue: Locator; readonly labelOwnedBy: Locator; readonly labelBundle: Locator; readonly buttonSupportRarity: Locator; readonly itemCollection: Locator; readonly backToGallery: Locator; readonly buttonShare: Locator; readonly buttonOpenOrdinalViewer: Locator; readonly numberInscription: Locator; readonly numberOrdinal: Locator; readonly containersCollectibleItemCollection: Locator; readonly containersCollectibleItemSingle: Locator; readonly nameInscriptionCollection: Locator; readonly nameInscriptionSingle: Locator; readonly sendAmount: Locator; readonly sendCurrencyAmount: Locator; readonly listedRune: Locator; readonly signingAddress: Locator; readonly buttonSign: Locator; readonly buttonReload: Locator; readonly listedRunePrice: Locator; readonly buttonGetQuotes: Locator; readonly buttonSwap: Locator; readonly inputField: Locator; readonly buttonEditFee: Locator; readonly feeAmount: Locator; readonly transactionHistoryInfo: Locator; readonly buttonReceive: Locator; readonly sendRuneAmount: Locator; readonly buttonInsufficientFunds: Locator; readonly nameSwapPlace: Locator; readonly quoteAmount: Locator; readonly infoMessage: Locator; readonly buttonSwapPlace: Locator; readonly buttonSlippage: Locator; readonly buttonSwapToken: Locator; readonly minReceivedAmount: Locator; readonly nameRune: Locator; readonly buttonSelectFee: Locator; readonly labelTotalFee: Locator; readonly itemUTXO: Locator; readonly titleUTXO: Locator; readonly buttonQRAddress: Locator; readonly labelAddress: Locator; readonly containerQRCode: Locator; readonly labelFeePriority: Locator; readonly divAddress: Locator; readonly buttonPreferences: Locator; readonly buttonSecurity: Locator; readonly buttonAdvanced: Locator; readonly errorInsufficientBRC20Balance: Locator; readonly BRC20FeeAmount: Locator; readonly buttonTransactionSend: Locator; constructor(readonly page: Page) { this.page = page; this.navigationDashboard = page.getByTestId('nav-dashboard'); this.navigationNFT = page.getByTestId('nav-nft'); this.navigationStacking = page.getByTestId('nav-stacking'); this.navigationExplore = page.getByTestId('nav-explore'); this.navigationSettings = page.getByTestId('nav-settings'); // this.balance = page.getByTestId('total-balance-value'); this.balance = page.getByLabel(/^Total balance/); this.textCurrency = page.getByTestId('currency-text'); this.allUpperButtons = page.getByTestId('transaction-buttons-row').getByRole('button'); this.buttonTransactionSend = this.allUpperButtons.nth(0); this.manageTokenButton = page.getByRole('button', { name: 'Manage token list' }); this.buttonMenu = page.getByRole('button', { name: 'Open Header Options' }); this.buttonLock = page.getByRole('button', { name: 'Lock' }); this.buttonConfirm = page.getByRole('button', { name: 'Confirm' }); this.buttonDenyDataCollection = page.getByRole('button', { name: 'Deny' }); this.labelBalanceAmountSelector = page.getByTestId('balance-label'); this.buttonClose = page.getByRole('button', { name: 'Close' }); this.buttonEditFee = page.getByTestId('fee-button'); this.feeAmount = page.getByTestId('fee-amount'); this.BRC20FeeAmount = page.getByTestId('brc20-fee'); this.buttonSelectFee = page.getByTestId('fee-select-button'); this.labelTotalFee = page.getByTestId('total-fee'); this.labelFeePriority = page.getByTestId('fee-priority'); // Account this.labelAccountName = page.getByLabel('Account Name'); this.buttonAccountOptions = page.getByLabel('Open Account Options'); this.accountBalance = page.getByTestId('account-balance'); this.buttonRenameAccount = page.getByRole('button', { name: 'Rename account' }); this.buttonResetAccountName = page.getByRole('button', { name: 'Reset name' }); this.labelInfoRenameAccount = page .locator('form div') .filter({ hasText: 'name can only include alphabetical and numerical' }); this.buttonGenerateAccount = page.getByRole('button', { name: 'Generate account' }); this.buttonConnectHardwareWallet = page.getByRole('button', { name: 'Add hardware wallet account', }); this.inputName = page.locator('input[type="text"]'); this.errorMessageRenameAccount = page .locator('p') .filter({ hasText: 'contain alphabetic and numeric' }); // Settings network this.buttonNetwork = page.getByRole('button', { name: 'Network' }); this.buttonSave = page.getByRole('button', { name: 'Save' }); this.buttonMainnet = page.getByRole('button', { name: 'Mainnet' }); this.buttonTestnet = page.getByRole('button', { name: 'Testnet' }); this.buttonPreferences = page.getByRole('button', { name: 'Preferences' }); this.buttonSecurity = page.getByRole('button', { name: 'Security' }); this.buttonAdvanced = page.getByRole('button', { name: 'Advanced' }); this.buttonBack = page.getByTestId('back-button'); this.buttonNext = page.getByRole('button', { name: 'Next' }); this.inputStacksURL = page.getByTestId('Stacks URL'); this.inputBTCURL = page.getByTestId('BTC URL'); this.inputFallbackBTCURL = page.getByTestId('Fallback BTC URL'); this.buttonUpdatePassword = page.getByRole('button', { name: 'Update Password' }); this.errorMessage = page.getByText(/incorrect password/i); this.headerNewPassword = page.getByRole('heading', { name: 'Enter your new password' }); this.infoUpdatePassword = page.getByText(/password successfully updated/i); this.buttonCurrency = page.getByRole('button', { name: 'Fiat Currency' }); this.buttonShowSeedphrase = page.getByRole('button', { name: 'Show Seedphrase' }); this.selectCurrency = page.getByTestId('currency-button'); this.iconFlag = page.locator('img[alt="flag"]'); // Token this.labelCoinTitle = page.getByLabel('Coin Title'); this.checkboxToken = page.locator('label[role="checkbox"]'); this.checkboxTokenActive = page.locator('label[role="checkbox"][aria-checked="true"]'); this.checkboxTokenInactive = page.locator('label[role="checkbox"][aria-checked="false"]'); this.buttonStacks = page.getByRole('button', { name: 'STACKS' }); this.buttonBRC20 = page.getByRole('button', { name: 'BRC-20' }); this.buttonRunes = page.getByRole('button', { name: 'RUNES' }); this.headingTokens = page.getByRole('heading', { name: 'Manage tokens' }); this.divTokenRow = page.getByLabel('Token Row'); this.labelTokenSubtitle = page.getByLabel('Token SubTitle'); this.labelCoinBalanceCurrency = page.getByLabel('Currency Balance Container').locator('span'); this.labelCoinBalanceCrypto = page.getByLabel('CoinBalance Container').locator('p'); // Coin details this.coinBalance = page.getByTestId('coin-balance'); this.containerTransactionHistory = page.getByTestId('transaction-container'); this.transactionHistoryAmount = page.getByTestId('transaction-amount'); this.transactionHistoryInfo = page.getByTestId('transaction-info'); this.coinSecondaryButton = page.getByTestId('coin-secondary-button'); this.coinSecondaryContainer = page.getByTestId('coin-secondary-container'); this.coinContractAddress = page.getByTestId('coin-contract-address'); this.textCoinTitle = page.getByTestId('coin-title-text'); // Collectibles this.totalItem = page.getByTestId('total-items'); this.tabsCollectiblesItems = page.getByTestId('tab-list').locator('button'); this.containerRareSats = page.getByTestId('rareSats-container'); this.nameInscription = page.getByTestId('inscription-name'); this.containersCollectibleItem = page.getByTestId('collection-container'); this.amountInscription = page.getByTestId('inscription-amount'); this.containersCollectibleItemCollection = this.containersCollectibleItem.filter({ has: this.amountInscription.filter({ hasText: /\d+\s+item(s)?/i, }), }); this.containersCollectibleItemSingle = this.containersCollectibleItem.filter({ has: this.amountInscription.filter({ hasText: /^$/, }), }); this.nameInscriptionCollection = this.containersCollectibleItemCollection.getByTestId('inscription-name'); this.nameInscriptionSingle = this.containersCollectibleItemSingle.getByTestId('inscription-name'); this.buttonEnable = page.getByRole('button', { name: 'Enable' }); this.containerInscription = page.getByTestId('inscription-container'); this.backToGallery = page.getByTestId('back-to-gallery'); this.itemCollection = page.getByTestId('collection-item'); this.buttonSend = page.locator('button').filter({ hasText: 'Send' }); this.buttonShare = page.locator('button').filter({ hasText: 'Share' }); this.buttonReceive = page.getByRole('button', { name: /^Receive/i }); this.buttonOpenOrdinalViewer = page.getByRole('button', { name: 'Open in Ordinal Viewer' }); this.labelBundle = page.locator('h1').filter({ hasText: 'Bundle' }); this.labelSatsValue = page.locator('h1').filter({ hasText: 'Sats value' }); this.labelOwnedBy = page.locator('h1').filter({ hasText: 'Owned by' }); this.buttonSupportRarity = page.getByRole('button', { name: 'See supported rarity scale' }); this.numberInscription = page.getByTestId('inscription-number'); this.numberOrdinal = page.getByTestId('ordinal-number'); // Explore this.carouselApp = page.getByTestId('app-carousel'); this.divAppSlide = page.getByTestId('app-slide'); this.divAppCard = page.getByTestId('app-card'); this.divAppTitle = page.getByTestId('app-title'); // Receive this.buttonQRAddress = page.getByTestId('qr-button'); this.labelAddress = page.getByTestId('address-label'); this.divAddress = page.getByTestId('address-div'); this.containerQRCode = page.getByTestId('qr-container'); // Swap this.imageToken = page.getByTestId('token-image'); this.buttonSelectCoin = page.getByTestId('select-coin-button'); this.inputSwapAmount = page.getByTestId('swap-amount'); this.nameToken = page.getByTestId('token-name'); this.buttonDownArrow = page.getByTestId('down-arrow-button'); this.buttonContinue = page.getByRole('button', { name: 'Continue' }); this.buttonGetQuotes = page.getByRole('button', { name: 'Get quotes' }); this.buttonSwap = page.getByRole('button', { name: 'Swap', exact: true }); this.nameSwapPlace = page.getByTestId('place-name'); this.quoteAmount = page.getByTestId('quote-label'); this.infoMessage = page.getByTestId('info-message'); this.buttonSwapPlace = page.getByTestId('swap-place-button'); this.buttonSwapToken = page.getByTestId('swap-token-button'); this.buttonSlippage = page.getByTestId('slippage-button'); this.minReceivedAmount = page.getByTestId('min-received-amount'); this.nameRune = page.getByTestId('rune-name'); this.itemUTXO = page.getByTestId('utxo-item'); this.titleUTXO = page.getByTestId('utxo-title'); this.buttonDetails = page.getByRole('button', { name: 'Details' }); this.buttonInsufficientBalance = page.getByRole('button', { name: 'Insufficient balance' }); this.buttonInsufficientFunds = page.getByRole('button', { name: 'Insufficient funds' }); this.swapTokenBalance = page.getByTestId('swap-token-balance'); this.textUSD = page.getByTestId('usd-text'); this.noFundsBTCMessage = page.getByTestId('no-funds-message'); // Send this.inputSendAmount = page.getByTestId('send-input'); this.inputRecipientAddress = page.getByTestId('recipient-address'); this.inputMemo = page.getByTestId('memo-input'); this.errorMessageAddressInvalid = page .locator('p') .filter({ hasText: 'Recipient address invalid' }); this.errorMessageAddressRequired = page .locator('p') .filter({ hasText: 'Recipient address is required' }); this.infoMessageSendSelf = page .locator('p') .filter({ hasText: 'You are transferring to yourself' }); this.errorMessageSendSelf = page.locator('p').filter({ hasText: 'Cannot send to self' }); this.errorInsufficientBalance = page.locator('p').filter({ hasText: 'Insufficient balance' }); this.errorInsufficientBRC20Balance = page.getByText('Insufficient BRC-20 balance'); this.errorInsufficientFunds = page.locator('p').filter({ hasText: 'Insufficient funds' }); this.containerFeeRate = page.getByTestId('feerate-container'); this.inputBTCAddress = page.locator('input[type="text"]'); this.inputBTCAmount = page.getByTestId('btc-amount'); this.buttonExpand = page.getByRole('button', { name: 'Inputs & Outputs' }); this.confirmTotalAmount = page.getByTestId('confirm-total-amount'); this.confirmCurrencyAmount = page.getByTestId('confirm-currency-amount'); this.confirmAmount = page.getByTestId('confirm-amount'); this.sendAddress = page.getByTestId('address-send'); this.receiveAddress = page.getByTestId('address-receive'); this.confirmBalance = page.getByTestId('confirm-balance'); this.buttonCancel = page.getByRole('button', { name: 'Cancel' }); this.buttonSign = page.getByRole('button', { name: 'Sign' }); this.sendTransactionID = page.getByTestId('transaction-id'); this.sendSTXValue = page.getByTestId('send-value'); this.inputField = page.locator('input[type="text"]'); this.sendRuneAmount = page.getByTestId('send-rune-amount'); // List this.buttonList = page.getByRole('button', { name: 'List List' }); this.tabAvailable = page.getByTestId('available-tab'); this.tabListed = page.getByRole('button', { name: 'LISTED', exact: true }); this.tabNotListed = page.getByRole('button', { name: 'NOT LISTED' }); this.listedRune = page.getByTestId('listed-rune-container'); this.buttonSetPrice = page.getByRole('button', { name: 'Set price' }); // this is the test rune to be used for listing this.runeSKIBIDI = page.getByTestId('SKIBIDI•OHIO•RIZZ'); this.runeItem = page.getByTestId('rune-item'); this.runeItemCheckbox = page.locator('#list-rune'); this.buttonFloorPrice = page.getByRole('button', { name: 'Floor', exact: true }); this.button5Price = page.getByRole('button', { name: '+5%', exact: true }); this.button10Price = page.getByRole('button', { name: '+10%', exact: true }); this.button20Price = page.getByRole('button', { name: '+20%', exact: true }); this.buttonCustomPrice = page.getByRole('button', { name: 'Custom', exact: true }); this.buttonApply = page.getByRole('button', { name: 'Apply', exact: true }); this.inputListingPrice = page.locator('input[type="number"]'); this.runeContainer = page.getByTestId('rune-container'); this.runeTitle = page.getByTestId('rune-title'); this.runePrice = page.getByTestId('rune-price').locator('p').filter({ hasText: 'sats' }); this.sendAmount = page.getByTestId('send-amount'); this.sendCurrencyAmount = page.getByTestId('send-currency-amount'); this.signingAddress = page.getByTestId('signing-address'); this.buttonReload = page.getByTestId('reload-button'); this.listedRunePrice = page.getByTestId('listed-price'); // Stacking this.buttonStartStacking = page.getByRole('button', { name: 'Start stacking' }); this.headingStacking = page.getByRole('heading', { name: 'Stack STX, earn BTC' }); this.containerStackingInfo = page.getByTestId('stacking-info'); this.infoTextStacking = page.locator('h1').filter({ hasText: 'STX with other stackers' }); } // Helper function to restore the wallet, switch to testnet if parameter is true and navigate to dashboard async setupTest(extensionId, wallet, testnet) { const onboardingPage = new Onboarding(this.page); await onboardingPage.restoreWallet(strongPW, wallet); await this.page.goto(`chrome-extension://${extensionId}/popup.html`); await this.checkVisualsStartpage(); if (testnet) { await this.navigationSettings.click(); await this.switchToTestnetNetwork(); await this.navigationDashboard.click(); await this.checkVisualsStartpage(); } } async checkVisualsStartpage() { // to-do fix the element itself, after the native-segwit update it resolves to 2 elements // data-testid="total-balance-value" await expect(this.balance.first()).toBeVisible(); await expect(this.manageTokenButton).toBeVisible(); // Deny data collection --> modal window is not always appearing so when it does we deny the data collection if (await this.buttonDenyDataCollection.isVisible()) { await this.buttonDenyDataCollection.click(); } await expect(this.labelAccountName).toBeVisible(); await expect(this.buttonMenu).toBeVisible(); // expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); await expect(this.navigationDashboard).toBeVisible(); await expect(this.navigationNFT).toBeVisible(); await expect(this.navigationStacking).toBeVisible(); await expect(this.navigationExplore).toBeVisible(); await expect(this.navigationSettings).toBeVisible(); expect(await this.divTokenRow.count()).toBeGreaterThan(1); } async checkVisualsSendSTXPage3() { expect(this.page.url()).toContain('confirm-stx-tx'); await expect(this.buttonConfirm).toBeVisible(); await expect(this.buttonCancel).toBeVisible(); await expect(this.receiveAddress).toBeVisible(); await expect(this.confirmAmount).toBeVisible(); await expect(this.buttonEditFee).toBeVisible(); await expect(this.feeAmount).toBeVisible(); await expect(this.buttonBack).toBeVisible(); } /** * Checks the visibility and state of UI elements state on first page in Send Flow * * @param {string} url - The expected URL to validate the correct page navigation. * @param {boolean} moreInputFields - (default: false). */ async checkVisualsSendPage1(url: string, moreInputFields: boolean = false) { expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); if (moreInputFields) { // Recipient Address for STX or amount for BRC20 await expect(this.inputField.first()).toBeVisible(); // Memo for STX or recipient for BRC20 await expect(this.inputField.last()).toBeVisible(); } else { await expect(this.receiveAddress).toBeVisible(); } await expect(this.imageToken).toBeVisible(); // await expect(this.buttonBack).toBeVisible(); } /** * Validates UI element visibility and state on second page in Send Flow * * @param {string} url - URL to verify page navigation; ensures the test is on the correct page. * @param {boolean} isSTX - Indicates if the page is STX-specific; adjusts element checks accordingly (default: false). */ async checkVisualsSendPage2(url: string, isSTX: boolean = false) { expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); // Conditional check based on the type of token if (isSTX) { // STX-specific fields await expect(this.inputField.first()).toBeVisible(); } else { // General send amount field await expect(this.inputSendAmount).toBeVisible(); } await expect(this.labelBalanceAmountSelector).toBeVisible(); await expect(this.buttonEditFee).toBeVisible(); await expect(this.feeAmount).toBeVisible(); await expect(this.imageToken).toBeVisible(); // await expect(this.buttonBack).toBeVisible(); } /** * Checks the visuals and elements on the send transaction review page. * * @param {string} url - The expected partial URL of the review page. * @param {boolean} editableFees - Optional. Indicates whether the fees can be edited on the Review page * @param {string} sendAddress - Optional. The expected last 4 characters of the sender's address * @param {string} recipientAddress - Optional. The expected last 4 characters of the receiver's address * @param {boolean} totalAmountShown - Optional. Indicates whether the total amount is shown. Default is true. * @param {boolean} tokenImageShown - Optional. Indicates whether the token image is shown. Default is true. * @param {string} ordinalNumber - Optional. The expected ordinal number to be displayed for single Inscriptions */ async checkVisualsSendTransactionReview( url: string, editableFees?: boolean, sendAddress?: string, recipientAddress?: string, totalAmountShown: boolean = true, tokenImageShown: boolean = true, ordinalNumber?: string, ) { expect(this.page.url()).toContain(url); await expect(this.buttonExpand).toBeVisible(); await expect(this.buttonCancel).toBeEnabled(); await expect(this.buttonConfirm).toBeEnabled(); await expect(this.feeAmount).toBeVisible(); // Not all TX Screens show a total amount if (totalAmountShown) { await expect(this.confirmTotalAmount.first()).toBeVisible(); await expect(this.confirmCurrencyAmount.first()).toBeVisible(); } if (tokenImageShown) { await expect(this.imageToken.first()).toBeVisible(); } if (editableFees) { await expect(this.buttonEditFee).toBeVisible(); } await this.buttonExpand.click(); await expect(this.confirmAmount.first()).toBeVisible(); await expect(this.confirmBalance.first()).toBeVisible(); // Execute these checks only if sendAddress is provided if (sendAddress) { await expect(this.sendAddress.first()).toBeVisible(); expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); } // Execute these checks only if recipientAddress is provided if (recipientAddress) { await expect(this.receiveAddress.first()).toBeVisible(); expect(await this.receiveAddress.first().innerText()).toContain(recipientAddress.slice(-4)); } // Collection Inscriptions don't have the ordinal number displayed in the Review // Check if the right ordinal number is shown if (ordinalNumber) { const reviewNumberOrdinal = await this.numberInscription.first().innerText(); expect(ordinalNumber).toMatch(reviewNumberOrdinal); } } // Check Visuals of Rune Dashboard (without List button), return balance amount async checkVisualsRunesDashboard(runeName: string) { await expect(this.imageToken.first()).toBeVisible(); await expect(this.textCoinTitle).toBeVisible(); await expect(this.textCoinTitle).toContainText(runeName); await expect(this.coinBalance).toBeVisible(); await expect(this.buttonReceive).toBeVisible(); await expect(this.buttonSend).toBeVisible(); const originalBalanceText = await this.coinBalance.innerText(); const numericOriginalValue = parseFloat(originalBalanceText.replace(/[^\d.-]/g, '')); return numericOriginalValue; } async checkVisualsListRunesPage() { await expect(this.tabNotListed).toBeVisible(); await expect(this.tabListed).toBeVisible(); await expect(this.runeItem.first()).toBeVisible(); expect(await this.runeItem.count()).toBeGreaterThanOrEqual(1); } async checkVisualsSwapPage() { expect(this.page.url()).toContain('swap'); await expect(this.buttonDownArrow.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeDisabled(); await expect(this.inputSwapAmount.first()).toBeVisible(); await expect(this.swapTokenBalance).toContainText('--'); await expect(this.buttonBack).toBeVisible(); await expect(this.nameToken.first()).toContainText('Select asset'); await expect(this.nameToken).toHaveCount(2); await expect(this.buttonDownArrow).toHaveCount(2); await expect(this.buttonGetQuotes).toBeVisible(); await expect(this.textUSD).toBeVisible(); await expect(this.buttonSwapToken).toBeVisible(); } // Helper function to fill in swap amount and returns usd value as number async fillSwapAmount(amount) { // .Fill() did not work with the field so we need to use this method await this.inputSwapAmount.pressSequentially(amount.toString()); await expect(this.buttonGetQuotes).toBeEnabled(); const usdAmount = await this.textUSD.innerText(); const numericUSDValue = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); expect(numericUSDValue).toBeGreaterThan(0); return numericUSDValue; } // had to disable this rule as my first assertion was always changed to a wrong assertion /* eslint-disable playwright/prefer-web-first-assertions */ async checkVisualsQuotePage( tokenName: string, slippage: boolean, numericQuoteValue: number, numericUSDValue: number, ) { await expect(this.buttonSwap).toBeVisible(); await expect(this.buttonEditFee).toBeVisible(); if (slippage) { await expect(this.buttonSlippage).toBeVisible(); } // Only 2 token should be visible expect(await this.buttonSwapPlace.count()).toBe(2); // await expect(await this.imageToken.count()).toBe(2); // Check Rune token name await expect(this.infoMessage.last()).toContainText(tokenName); // Check if USD amount from quote page is the same as from th swap start flow page const usdAmountQuote = await this.textUSD.first().innerText(); const numericUSDQuote = parseFloat(usdAmountQuote.replace(/[^0-9.]/g, '')); expect(numericUSDQuote).toEqual(numericUSDValue); } // check visuals of List on ME page async checkVisualsListOnMEPage() { await expect(this.buttonFloorPrice).toBeVisible(); await expect(this.button5Price).toBeVisible(); await expect(this.button10Price).toBeVisible(); await expect(this.button20Price).toBeVisible(); await expect(this.buttonCustomPrice).toBeVisible(); await expect(this.buttonContinue).toBeVisible(); await expect(this.buttonContinue).toBeDisabled(); await expect(this.runeContainer.first()).toBeVisible(); } async invalidAddressCheck(addressField) { await addressField.fill(`Test Address 123`); await this.buttonNext.click(); await expect(this.errorMessageAddressInvalid).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); } // had to disable this rule as my first assertion was always changed to a wrong assertion /* eslint-disable playwright/prefer-web-first-assertions */ async switchToHighFees(feePriorityShown: boolean = true) { // Save the current fee amount for comparison const originalFee = await this.feeAmount.innerText(); const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); expect(numericOriginalFee).toBeGreaterThan(0); let feePriority = 'Medium'; if (feePriorityShown) { feePriority = await this.labelFeePriority.innerText(); } // Click on edit Fee button await this.buttonEditFee.click(); await expect(this.buttonSelectFee.first()).toBeVisible(); await expect(this.labelTotalFee.first()).toBeVisible(); // Compare fee to previous saved fee const fee = await this.buttonSelectFee .filter({ hasText: feePriority }) .locator(this.labelTotalFee) .innerText(); const numericFee = parseFloat(fee.replace(/[^0-9.]/g, '')); expect(numericFee).toBe(numericOriginalFee); // Save high fee rate for comparison const highFee = await this.labelTotalFee.first().innerText(); const numericHighFee = parseFloat(highFee.replace(/[^0-9.]/g, '')); // Switch to high fee await this.buttonSelectFee.first().click(); const newFee = await this.feeAmount.innerText(); const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); expect(numericNewFee).toBe(numericHighFee); } async navigateToCollectibles() { await this.navigationNFT.click(); expect(this.page.url()).toContain('nft-dashboard'); // If 'enable' rare sats pop up is appearing if (await this.buttonEnable.isVisible()) { await this.buttonEnable.click(); } // Check visuals on opening Collectibles page await expect(this.tabsCollectiblesItems.first()).toBeVisible(); await expect(this.totalItem).toBeVisible(); } // had to disable this rule as my first assertion was always changed to a wrong assertion async checkAmountsSendingSTX(amountSTXSend, STXTest, sendFee) { expect(await this.receiveAddress.first().innerText()).toContain(STXTest.slice(-4)); // Sending amount without Fee const sendAmount = await this.confirmAmount.first().innerText(); const numericValueSendAmount = parseFloat(sendAmount.replace(/[^0-9.]/g, '')); expect(numericValueSendAmount).toEqual(amountSTXSend); // Fees const fee = await this.feeAmount.innerText(); const numericValueFee = parseFloat(fee.replace(/[^0-9.]/g, '')); expect(numericValueFee).toEqual(sendFee); } async checkAmountsSendingBTC(selfBTCTest, BTCTest, amountBTCSend) { // Sending amount without Fee const amountText = await this.confirmAmount.first().innerText(); const numericValueAmountText = parseFloat(amountText.replace(/[^0-9.]/g, '')); expect(numericValueAmountText).toEqual(amountBTCSend); // Address check sending and receiving expect(await this.sendAddress.innerText()).toContain(selfBTCTest.slice(-4)); expect(await this.receiveAddress.first().innerText()).toContain(BTCTest.slice(-4)); const confirmAmountAfter = await this.confirmAmount.last().innerText(); const originalFee = await this.feeAmount.innerText(); const confirmBalance = await this.confirmBalance.innerText(); // Extract amounts for balance, sending amount and amount afterwards const num1 = parseFloat(confirmAmountAfter.replace(/[^0-9.]/g, '')); // We need to convert the sats value to BTC for this calculation const feeSatsAmount = parseFloat(originalFee.replace(/[^0-9.]/g, '')); const num2 = feeSatsAmount / 100000000; const num3 = parseFloat(confirmBalance.replace(/[^0-9.]/g, '')); // Balance - fees - sending amount const roundedResult = Number((num3 - num2 - amountBTCSend).toFixed(9)); // Check if Balance value after the transaction is the same as the calculated value expect(num1).toEqual(roundedResult); } async confirmSendTransaction(transactionIDShown: boolean = true) { await expect(this.buttonConfirm).toBeEnabled(); await this.buttonConfirm.click(); await expect(this.buttonClose).toBeVisible({ timeout: 30000 }); if (transactionIDShown) { await expect(this.sendTransactionID).toBeVisible(); } await this.buttonClose.click(); } async getAddress(whichAddress: string): Promise { // click on 'Receive' button await this.allUpperButtons.nth(1).click(); // Locate the QR button to the address const button = this.divAddress.filter({ hasText: whichAddress }).locator(this.buttonQRAddress); // Need to click on the QR Code button to get the full Address await button.click(); await expect(this.containerQRCode).toBeVisible(); const address = await this.labelAddress.innerText(); await this.buttonBack.click(); return address; } async getTokenBalance(tokenname: string) { try { const locator = this.page .locator('button') .filter({ hasText: new RegExp(`${tokenname}.*\\d`) }); console.log(`Looking for balance for ${tokenname}`); const balanceText = await locator.innerText(); console.log('Found balance text:', balanceText); // Extract just the numeric portion (matches any number with decimal points) const matches = balanceText.match(/\d+\.?\d*/); if (!matches) { throw new Error(`Could not extract balance from text: ${balanceText}`); } const numericValue = parseFloat(matches[0]); console.log('Parsed numeric value:', numericValue); return numericValue; } catch (error) { console.error(`Error getting balance for ${tokenname}:`, error); throw error; } } async clickOnSpecificToken(tokenname: string) { const specificToken = this.page.getByRole('button').getByLabel(`Token Title: ${tokenname}`); // filter({ // has: this.labelTokenSubtitle.getByText(tokenname, { exact: true }), await specificToken.last().click(); } async clickOnSpecificInscription(inscriptionName: string) { const specificToken = this.containersCollectibleItem .filter({ has: this.nameInscription.getByText(inscriptionName, { exact: true }), }) .getByTestId('inscription-container'); await specificToken.last().click(); } // This function tries to click on a specific rune, if the rune is not enabled it will enable the test rune and then click on it async checkAndClickOnSpecificRune(tokenname: string) { // Check if test rune is enabled and if not enabled the test rune try { // click on the test rune await this.clickOnSpecificToken(tokenname); } catch (error) { // if the rune was not clickable we need to enable the test rune // Execute alternative commands if an error occurs console.log('Test Rune is not active and need to be enabled'); // Insert your fallback logic here // check if the test rune is enabled await this.manageTokenButton.click(); await this.buttonRunes.click(); await expect(this.divTokenRow.first()).toBeVisible(); await expect(this.runeSKIBIDI).toBeVisible({ timeout: 30000 }); // if clause for enabled check const count = await this.checkboxTokenActive.count(); // If no runes are enabled enable the Test rune if (count !== 1) { console.log( 'No active token checkbox found or there are multiple, taking alternative action.', ); // Activate Test rune await this.runeSKIBIDI.locator('label[role="checkbox"]').click(); } else { console.log('One active token checkbox is present.'); } // Switch back to dashboard and click on the rune await this.buttonBack.click(); await this.clickOnSpecificToken(tokenname); } } async checkNetworkSettingVisuals() { await expect(this.buttonSave).toBeVisible(); await expect(this.buttonBack).toBeVisible(); await expect(this.buttonMainnet).toBeVisible(); await expect(this.buttonTestnet).toBeVisible(); await expect(this.inputStacksURL).toBeVisible(); await expect(this.inputBTCURL).toBeVisible(); await expect(this.inputFallbackBTCURL).toBeVisible(); } async checkTestnetUrls(shouldContainTestnet: boolean) { const inputsURL = [this.inputStacksURL, this.inputBTCURL, this.inputFallbackBTCURL]; const checks = inputsURL.map(async (input) => { const inputValue = await input.inputValue(); const message = `URL does not contain 'testnet' in ${input}`; if (shouldContainTestnet) { return expect(inputValue, message).toContain('testnet'); } return expect(inputValue, message).not.toContain('testnet'); }); await Promise.all(checks); } async switchToTestnetNetwork() { await expect(this.buttonNetwork).toBeVisible(); await expect(this.buttonNetwork).toHaveText('NetworkMainnet'); await this.buttonNetwork.click(); await this.checkNetworkSettingVisuals(); await expect(this.buttonMainnet.locator('img')).toHaveAttribute('alt', 'tick'); await expect(this.buttonTestnet.locator('img[alt="tick"]')).toHaveCount(0); await this.buttonTestnet.click(); await expect(this.buttonTestnet.locator('img')).toHaveAttribute('alt', 'tick'); await expect(this.buttonMainnet.locator('img[alt="tick"]')).toHaveCount(0); await this.inputStacksURL.fill('https://api.nakamoto.testnet.hiro.so'); // To speed up some checks we use our own servers // await this.inputBTCURL.fill('https://btc-testnet.xverse.app'); await this.checkTestnetUrls(true); // TODO think of a better way to do this // Wait for the network to be switched so that API doesn't fail because of the rate limiting await this.page.waitForTimeout(15000); await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); } async switchToMainnetNetwork() { await expect(this.buttonNetwork).toBeVisible(); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); await this.buttonNetwork.click(); await this.checkNetworkSettingVisuals(); await expect(this.buttonTestnet.locator('img')).toHaveAttribute('alt', 'tick'); await expect(this.buttonMainnet.locator('img[alt="tick"]')).toHaveCount(0); await this.buttonMainnet.click(); await expect(this.buttonMainnet.locator('img')).toHaveAttribute('alt', 'tick'); await expect(this.buttonTestnet.locator('img[alt="tick"]')).toHaveCount(0); await this.checkTestnetUrls(false); // TODO think of a better way to do this // Wait for the network to be switched so that API doesn't fail because of the rate limiting await this.page.waitForTimeout(15000); await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkMainnet'); } async getBalanceOfAllAccounts() { const count = await this.accountBalance.count(); let totalBalance = 0; if (count > 1) { for (let i = 0; i < count; i++) { const balanceText = await this.accountBalance.nth(i).innerText(); const numericValue = parseFloat(balanceText.replace(/[^\d.-]/g, '')); totalBalance += numericValue; } } else { const balanceText = await this.accountBalance.innerText(); totalBalance = parseFloat(balanceText.replace(/[^\d.-]/g, '')); } return totalBalance; } async getBalanceOfAllTokens() { const count = await this.labelCoinBalanceCurrency.count(); let totalBalance = 0; if (count > 1) { for (let i = 0; i < count; i++) { const balanceText = await this.labelCoinBalanceCurrency.nth(i).innerText(); const numericValue = parseFloat(balanceText.replace(/[^\d.-]/g, '')); totalBalance += numericValue; } } else { const balanceText = await this.labelCoinBalanceCurrency.innerText(); totalBalance = parseFloat(balanceText.replace(/[^\d.-]/g, '')); } return totalBalance; } async selectLastToken(tokenType: 'BRC20' | 'STACKS'): Promise { await this.manageTokenButton.click(); expect(this.page.url()).toContain('manage-tokens'); // Click on the specific token type button if BRC20 is selected if (tokenType === 'BRC20') { await this.buttonBRC20.click(); } else if (tokenType === 'STACKS') { await this.buttonStacks.click(); } const chosenToken = this.divTokenRow.last(); const tokenName = (await chosenToken.getAttribute('data-testid')) || 'default-value'; await this.buttonBack.click(); return tokenName; } // The function toggleRandomToken takes a boolean parameter enable to determine the action: // true indicates enabling a token (using inactive tokens). // false indicates disabling a token (using active tokens). async toggleRandomToken(enable: boolean): Promise { const tokenStateLocator = enable ? this.checkboxTokenInactive : this.checkboxTokenActive; await expect(tokenStateLocator.first()).toBeVisible(); const numberOfTokens = await tokenStateLocator.count(); // Generate a random index within the range of available tokens const chosenIndex = Math.floor(Math.random() * numberOfTokens); // Access the nth token (adjusting for zero-based indexing) const chosenToken = this.divTokenRow.filter({ has: tokenStateLocator }).nth(chosenIndex); const tokenName = (await chosenToken.getAttribute('data-testid')) || 'default-value'; // Click the switch handle to toggle the token's state await chosenToken.locator('label[role="checkbox"]').click(); return tokenName; } // The function toggleAllTokens takes a boolean parameter enable. // true indicates enabling token (using inactive tokens). // false indicates disabling token (using active tokens). async toggleAllTokens(enable: boolean) { // Determine which tokens to interact with based on the 'enable' parameter const tokenSelector = enable ? this.checkboxTokenInactive : this.checkboxTokenActive; const actionTokens = this.divTokenRow.filter({ has: tokenSelector }); const count = await actionTokens.count(); for (let i = 0; i < count; i++) { // Since clicking the switch will change its state, always interact with the first one await actionTokens.first().locator('label[role="checkbox"]').click(); } } // Helper function to wait for a field to get greater than 0. Some fields are slowly to load for the E2E so we need to ensure that their is a value loaded before continue async waitForTextAboveZero(selector, timeout = 30000) { const startTime = Date.now(); while (true) { if (Date.now() - startTime > timeout) { throw new Error('Timeout waiting for text to be above 0'); } const text = await selector.innerText(); const numericValue = parseFloat(text.replace(/[^0-9.]/g, '')); if (numericValue > 0) { return; // Exit the function when the condition is met } await this.page.waitForTimeout(1000); // Check every second } } }