From 28f4677ce075b33643d135e445e5fa656293b5ef Mon Sep 17 00:00:00 2001 From: emiride Date: Tue, 22 Oct 2024 14:58:07 +0200 Subject: [PATCH] test(cat-voices): refactoring playwright --- .gitignore | 3 +- .../catalyst_cardano/wallet-automation/.env | 4 - .../wallet-automation/Earthfile | 28 +- .../wallet-automation/README.md | 6 +- .../wallet-automation/compose.yml | 27 +- .../wallet-automation/package-lock.json | 12 + .../wallet-automation/package.json | 5 +- .../wallet-automation/pages/homePage.ts | 288 ++++++++++++++++ .../wallet-automation/pages/modal.ts | 46 +++ .../wallet-automation/playwright.config.ts | 9 +- .../wallet-automation/test-fixtures.ts | 49 +++ .../tests/wallet-tests.spec.ts | 313 +++++++++--------- .../wallet-automation/tests/wallets.spec.ts | 95 ++++++ .../wallet-automation/tsconfig.json | 2 +- .../wallet-automation/utils.ts | 4 +- .../utils/extensionDownloader.ts | 135 ++++++++ .../utils/extension_downloader.ts | 75 ----- .../wallet-automation/utils/extensions.ts | 30 +- .../wallet-automation/utils/walletConfigs.ts | 62 ++++ .../utils/wallets/laceUtils.ts | 89 +++++ .../utils/wallets/typhonUtils.ts | 34 ++ .../utils/wallets/walletUtils.ts | 54 +++ 22 files changed, 1102 insertions(+), 268 deletions(-) delete mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/.env create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts delete mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extension_downloader.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/laceUtils.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/typhonUtils.ts create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts diff --git a/.gitignore b/.gitignore index 39169dbc566..1ccc58b6dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,5 @@ test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ -extensions/ \ No newline at end of file +extensions/ +catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/.env diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/.env b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/.env deleted file mode 100644 index 48116cbfdfd..00000000000 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/.env +++ /dev/null @@ -1,4 +0,0 @@ -APP_URL=http://localhost:8000 -WALLET1_SEED_WORD=stomach,horn,rail,afraid,flip,also,abandon,speed,chaos,daring,soon,soft,okay,online,benefit -WALLET1_USERNAME=test123 -WALLET1_PASSWORD=test12345678@ \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile index cfd8644552b..9bcc4698b15 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.18 AS flutter-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/playwright:v3.2.18 AS playwright-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.20 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/playwright:v3.2.20 AS playwright-ci deps: DO playwright-ci+SETUP --workdir=/wallet-automation @@ -13,11 +13,11 @@ deps: src: FROM +deps - COPY keys.txt . + COPY --dir pages ./pages + COPY --dir tests ./tests + COPY --dir utils ./utils COPY playwright.config.ts . - COPY global-setup.ts . - COPY wallet-tests.spec.ts . - COPY utils.ts . + COPY test-fixtures.ts . package-test: FROM +src @@ -48,16 +48,20 @@ package-app: nightly-test: FROM earthly/dind:alpine-3.20-docker-26.1.5-r0 COPY compose.yml . + RUN mkdir -p results WITH DOCKER \ --compose compose.yml \ --load test-app:latest=(+package-app) \ --load test:latest=(+package-test) \ - --service app \ --allow-privileged - RUN docker run --network=default_default --name=test test:latest && \ - docker cp test:/results/cardano-wallet.junit-report.xml cardano-wallet.junit-report.xml + # Start the services + RUN docker-compose up --abort-on-container-exit test + END - WAIT - SAVE ARTIFACT cardano-wallet.junit-report.xml AS LOCAL cardano-wallet.junit-report.xml - END \ No newline at end of file + + # Copy the result file from /output to the current directory + COPY /output/results/cardano-wallet.junit-report.xml . + + # Save the result file as an artifact + SAVE ARTIFACT cardano-wallet.junit-report.xml AS LOCAL cardano-wallet.junit-report.xml diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/README.md b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/README.md index ea9dcb9323d..ce131cc9f7b 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/README.md +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/README.md @@ -36,4 +36,8 @@ docker-compose up ``` The app should be running on `localhost:8000`. -6. You can now \ No newline at end of file +6. You can now run tests with the following command: + +```sh +npx playwright test +``` \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml index 3a79143e11a..a0a689768b6 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml @@ -1,6 +1,29 @@ -version: "3" services: app: image: test-app:latest ports: - - 8000:80 \ No newline at end of file + - "8000:80" + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:80"] + interval: 5s + timeout: 2s + retries: 12 + networks: + - test_network + + test: + image: test:latest + depends_on: + app: + condition: service_healthy + # environment: + # - APP_URL=http://app:80 + volumes: + - /output/results:/results + networks: + - test_network + +networks: + test_network: + name: test_network + driver: bridge \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json index d8d876b4189..0a1991b8a3d 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", @@ -37,6 +38,17 @@ "node": ">=18" } }, + "node_modules/@tomjs/unzip-crx": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@tomjs/unzip-crx/-/unzip-crx-1.1.3.tgz", + "integrity": "sha512-uqolp78TcG5q2ZBOZ57Nf7m7o3kaKAz1E9uFf4FCSO/nCI11HaDWpw7PaGUk1MImeIjNradiLpT2b9kTKSs4uw==", + "dependencies": { + "jszip": "^3.10.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@types/node": { "version": "20.16.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json index 64d4f463604..af26c6d9bef 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json @@ -21,16 +21,17 @@ }, "homepage": "https://github.com/input-output-hk/catalyst-voices#readme", "devDependencies": { - "@playwright/test": "^1.45.3", + "@playwright/test": "^1.48.0", "@types/node": "^20.14.12" }, "dependencies": { + "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", "install": "^0.13.0", "node-fetch": "^2.6.7", - "playwright": "^1.45.3", + "playwright": "^1.48.0", "unzip-crx-3": "^0.2.0" } } diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts new file mode 100644 index 00000000000..f1afa2208c9 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts @@ -0,0 +1,288 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { BrowserExtensionName } from '../utils/extensions'; +import { Modal, ModalName } from './modal'; + +export interface UTxO { + tx: string; + index: number; + amount: number; +} + +export interface WalletCipData { + balance: number; + extensions: string; + networkId: string; + changeAddress: string; + rewardAddresses: string[]; + unusedAddresses: string[]; + usedAddresses: string[]; + utxos: UTxO[]; + publicDRepKey: string; + registeredPublicStakeKeys: string; + unregisteredPublicStakeKeys: string[]; +} + +export class HomePage { + + readonly page: Page; + readonly balanceLabel: Locator; + readonly extensionsLabel: Locator; + readonly networkIdLabel: Locator; + readonly changeAddressLabel: Locator; + readonly rewardAddressesLabel: Locator; + readonly unusedAddressesLabel: Locator; + readonly usedAddressesLabel: Locator; + readonly utxosLabel: Locator; + readonly publicDRepKeyLabel: Locator; + readonly registeredPublicStakeKeysLabel: Locator; + readonly unregisteredPublicStakeKeysLabel: Locator; + readonly signDataButton: Locator; + readonly signAndSubmitTxButton: Locator; + readonly signAndSubmitRBACTxButton: Locator; + + constructor(page: Page) { + this.page = page; + this.balanceLabel = page.getByText(/^Balance: Ada \(lovelaces\):/); + this.extensionsLabel = page.getByText(/^Extensions:/); + this.networkIdLabel = page.getByText(/^Network ID:/); + this.changeAddressLabel = page.getByText(/^Change address:/); + this.rewardAddressesLabel = page.getByText(/^Reward addresses:/); + this.unusedAddressesLabel = page.getByText(/^Unused addresses:/); + this.usedAddressesLabel = page.getByText(/^Used addresses:/); + this.utxosLabel = page.getByText(/^UTXOs:/); + this.publicDRepKeyLabel = page.getByText(/^Public DRep Key:/); + this.registeredPublicStakeKeysLabel = page.getByText(/^Registered Public Stake Keys:/); + this.unregisteredPublicStakeKeysLabel = page.getByText(/^Unregistered Public Stake Keys:/); + this.signDataButton = page.getByRole('button', { name: 'Sign data' }); + this.signAndSubmitTxButton = page.getByRole('button', { name: 'Sign & submit tx' }); + this.signAndSubmitRBACTxButton = page.getByRole('button', { name: 'Sign & submit RBAC tx' }); + } + + async getWalletCipData() { + const walletCipData: WalletCipData = { + balance: 0, + extensions: '', + networkId: '', + changeAddress: '', + rewardAddresses: [], + unusedAddresses: [], + usedAddresses: [], + utxos: [], + publicDRepKey: '', + registeredPublicStakeKeys: '', + unregisteredPublicStakeKeys: [], + }; + await this.balanceLabel.waitFor({state: 'visible', timeout: 10000}); + walletCipData.balance = await this.getBalance(); + walletCipData.extensions = await this.getExtensions(); + walletCipData.networkId = await this.getNetworkId(); + walletCipData.changeAddress = await this.getChangeAddress(); + walletCipData.rewardAddresses = await this.getRewardAddresses(); + walletCipData.unusedAddresses = await this.getUnusedAddresses(); + walletCipData.usedAddresses = await this.getUsedAddresses(); + walletCipData.utxos = await this.getUTXOs(); + walletCipData.publicDRepKey = await this.getPublicDRepKey(); + walletCipData.registeredPublicStakeKeys = await this.getRegisteredPublicStakeKeys(); + walletCipData.unregisteredPublicStakeKeys = await this.getUnregisteredPublicStakeKeys(); + return walletCipData; + } + + async assertModal(modalName: ModalName) { + const modal = new Modal(this.page, modalName); + await modal.assertModalIsVisible(); + } + + async getBalance(): Promise { + const isVisible = await this.balanceLabel.isVisible(); + if (!isVisible) { + throw new Error('Balance label is not visible'); + } + const balanceText = await this.balanceLabel.textContent(); + const match = balanceText?.match(/^Balance: Ada \(lovelaces\): (\d+)/); + if (match && match[1]) { + return Number(match[1]); + } else { + throw new Error(`Unable to extract balance from text: ${balanceText}`); + } + } + + async getExtensions(): Promise { + const isVisible = await this.extensionsLabel.isVisible(); + if (!isVisible) { + throw new Error('Extensions label is not visible'); + } + const extensionsText = await this.extensionsLabel.textContent(); + const match = extensionsText?.trim().match(/^Extensions:\s*(.+)$/); + if (match && match[1]) { + return match[1].trim(); + } else { + throw new Error(`Unable to extract extensions from text: ${extensionsText}`); + } + } + + async getNetworkId(): Promise { + const isVisible = await this.networkIdLabel.isVisible(); + if (!isVisible) { + throw new Error('Network ID label is not visible'); + } + const networkIdText = await this.networkIdLabel.textContent(); + const match = networkIdText?.trim().match(/^Network ID:\s*(.+)$/); + if (match && match[1]) { + return match[1].trim(); + } else { + throw new Error(`Unable to extract network ID from text: ${networkIdText}`); + } + } + + async getChangeAddress(): Promise { + const isVisible = await this.changeAddressLabel.isVisible(); + if (!isVisible) { + throw new Error('Change address label is not visible'); + } + const changeAddressText = await this.changeAddressLabel.textContent(); + const match = changeAddressText?.trim().match(/^Change address:\s*(.+)$/s); + if (match && match[1]) { + return match[1].trim(); + } else { + throw new Error(`Unable to extract change address from text: ${changeAddressText}`); + } + } + + async getRewardAddresses(): Promise { + const isVisible = await this.rewardAddressesLabel.isVisible(); + if (!isVisible) { + throw new Error('Reward addresses label is not visible'); + } + const rewardAddressesText = await this.rewardAddressesLabel.textContent(); + const match = rewardAddressesText?.match(/^Reward addresses:\s*(.+)$/s); + if (match && match[1]) { + const addresses = match[1].trim().split('\n').map(addr => addr.trim()).filter(addr => addr.length > 0); + return addresses; + } else { + throw new Error(`Unable to extract reward addresses from text: ${rewardAddressesText}`); + } + } + + async getUnusedAddresses(): Promise { + const isVisible = await this.unusedAddressesLabel.isVisible(); + if (!isVisible) { + throw new Error('Unused addresses label is not visible'); + } + const unusedAddressesText = await this.unusedAddressesLabel.textContent(); + const match = unusedAddressesText?.match(/^Unused addresses:\s*(.+)$/s); + if (match && match[1]) { + const addresses = match[1].trim().split('\n').map(addr => addr.trim()).filter(addr => addr.length > 0); + return addresses; + } else { + throw new Error(`Unable to extract unused addresses from text: ${unusedAddressesText}`); + } + } + + async getUsedAddresses(): Promise { + const isVisible = await this.usedAddressesLabel.isVisible(); + if (!isVisible) { + throw new Error('Used addresses label is not visible'); + } + const usedAddressesText = await this.usedAddressesLabel.textContent(); + const match = usedAddressesText?.match(/^Used addresses:\s*(.+)$/s); + if (match && match[1]) { + const addresses = match[1].trim().split('\n').map(addr => addr.trim()).filter(addr => addr.length > 0); + return addresses; + } else { + throw new Error(`Unable to extract used addresses from text: ${usedAddressesText}`); + } + } + + async getUTXOs(): Promise { + const isVisible = await this.utxosLabel.isVisible(); + if (!isVisible) { + throw new Error('UTXOs label is not visible'); + } + const utxosText = await this.utxosLabel.textContent(); + const match = utxosText?.match(/^UTXOs:\s*(.+)$/s); + if (match && match[1]) { + const utxosData = match[1].trim(); + const utxoEntries = utxosData.split(/\n\n+/).map(entry => entry.trim()).filter(entry => entry.length > 0); + const utxos: UTxO[] = []; + for (const entry of utxoEntries) { + const txMatch = entry.match(/Tx:\s*([a-fA-F0-9]+)/); + const indexMatch = entry.match(/Index:\s*(\d+)/); + const amountMatch = entry.match(/Amount:\s*Ada \(lovelaces\):\s*(\d+)/); + if (txMatch && indexMatch && amountMatch) { + utxos.push({ + tx: txMatch[1], + index: Number(indexMatch[1]), + amount: Number(amountMatch[1]), + }); + } else { + throw new Error(`Unable to parse UTXO entry: ${entry}`); + } + } + return utxos; + } else { + throw new Error(`Unable to extract UTXOs from text: ${utxosText}`); + } + } + + async getPublicDRepKey(): Promise { + const isVisible = await this.publicDRepKeyLabel.isVisible(); + if (!isVisible) { + throw new Error('Public DRep Key label is not visible'); + } + const publicDRepKeyText = await this.publicDRepKeyLabel.textContent(); + const match = publicDRepKeyText?.trim().match(/^Public DRep Key:\s*([a-fA-F0-9]+)$/); + if (match && match[1]) { + return match[1]; + } else { + throw new Error(`Unable to extract public DRep key from text: ${publicDRepKeyText}`); + } + } + + async getRegisteredPublicStakeKeys(): Promise { + const isVisible = await this.registeredPublicStakeKeysLabel.isVisible(); + if (!isVisible) { + throw new Error('Registered Public Stake Keys label is not visible'); + } + const stakeKeysText = await this.registeredPublicStakeKeysLabel.textContent(); + const match = stakeKeysText?.trim().match(/^Registered Public Stake Keys:\s*([a-fA-F0-9]+)$/); + if (match && match[1]) { + return match[1]; + } else { + throw new Error(`Unable to extract registered public stake keys from text: ${stakeKeysText}`); + } + } + + async getUnregisteredPublicStakeKeys(): Promise { + const isVisible = await this.unregisteredPublicStakeKeysLabel.isVisible(); + if (!isVisible) { + throw new Error('Unregistered Public Stake Keys label is not visible'); + } + const keysText = await this.unregisteredPublicStakeKeysLabel.textContent(); + const match = keysText?.trim().match(/^Unregistered Public Stake Keys:\s*(.*)$/s); + if (match) { + const keysData = match[1].trim(); + if (keysData) { + const keys = keysData.split('\n').map(key => key.trim()).filter(key => key.length > 0); + return keys; + } else { + return []; + } + } else { + throw new Error(`Unable to extract unregistered public stake keys from text: ${keysText}`); + } + } + + async assertBasicWalletCipData(actualWalletCipData: WalletCipData, extensionName: BrowserExtensionName) { + expect(actualWalletCipData.balance).toBeGreaterThan(500000000); + expect(actualWalletCipData.extensions).toBe(extensionName === BrowserExtensionName.Typhon ? 'cip-30' : 'cip-95'); + expect(actualWalletCipData.networkId).not.toBeNaN(); + expect(actualWalletCipData.changeAddress).not.toBeNaN(); + expect(actualWalletCipData.rewardAddresses.length).toBeGreaterThan(0); + expect(actualWalletCipData.unusedAddresses.length).toBeGreaterThan(0); + expect(actualWalletCipData.usedAddresses.length).toBeGreaterThan(0); + expect(actualWalletCipData.utxos.length).toBeGreaterThan(0); + expect(actualWalletCipData.publicDRepKey).not.toBeNaN(); + expect(actualWalletCipData.registeredPublicStakeKeys).not.toBeNaN(); + + } +} diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts new file mode 100644 index 00000000000..05b2c4220e8 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts @@ -0,0 +1,46 @@ +import { expect, Locator, Page } from "@playwright/test"; + +export enum ModalName { + SignData = 'SignData', + SignAndSubmitTx = 'SignAndSubmitTx', + SignAndSubmitRBACTx = 'SignAndSubmitRBACTx', +} + +export interface ModalContent { + header: string; + unchangingText: string; +} + +export const modalContents: { [key in ModalName]: ModalContent } = { + [ModalName.SignData]: { + header: 'Sign data', + unchangingText: 'Signature:', + }, + [ModalName.SignAndSubmitTx]: { + header: 'Sign & submit tx', + unchangingText: 'Tx hash:', + }, + [ModalName.SignAndSubmitRBACTx]: { + header: 'Sign & submit RBAC tx', + unchangingText: 'Tx hash:', + }, +}; + +export class Modal { + readonly page: Page; + readonly content: ModalContent; + readonly modalHeader: Locator; + readonly modalBody: Locator; + + constructor(page: Page, modalName: ModalName) { + this.page = page; + this.content = modalContents[modalName]; + this.modalHeader = this.page.getByText(this.content.header) + this.modalBody = this.page.getByText(this.content.unchangingText) + } + + async assertModalIsVisible() { + await expect(this.modalHeader).toBeVisible(); + await expect(this.modalBody).toBeVisible(); + } +} \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts index 83d2843f868..1af789bcad0 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts @@ -11,16 +11,17 @@ export default defineConfig({ testDir: './tests', fullyParallel: false, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 1, + //retries: process.env.CI ? 2 : 1, workers: process.env.CI ? 1 : 1, use: { - baseURL: process.env.APP_URL, + baseURL: process.env.APP_URL || 'http://app:80', screenshot: 'only-on-failure', trace: 'on-first-retry', }, - reporter: [['junit', { outputFile: '/results/cardano-wallet.junit-report.xml' }]], - timeout: 60 * 1000, + reporter: [ + ['junit', { outputFile: '/results/cardano-wallet.junit-report.xml' }]], + timeout: 120 * 1000, projects: [ // { // name: 'setup', diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/test-fixtures.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/test-fixtures.ts index e69de29bb2d..d57a21cd71f 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/test-fixtures.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/test-fixtures.ts @@ -0,0 +1,49 @@ +import { test as base, BrowserContext, chromium, Page } from '@playwright/test'; +import { allowExtension, onboardWallet, WalletConfig } from './utils/wallets/walletUtils'; +import { BrowserExtensionName } from './utils/extensions'; +import { ExtensionDownloader } from './utils/extensionDownloader'; +import { HomePage } from './pages/homePage'; + +type MyFixtures = { + enableWallet: (walletConfig: WalletConfig) => Promise; + installExtension: (extensionName: BrowserExtensionName) => Promise; +}; + +export const test = base.extend({ + enableWallet: async ({ installExtension }, use) => { + await use(async (walletConfig: WalletConfig) => { + const browser = await installExtension(walletConfig.extension.Name); + const extensionTab = browser.pages()[0]; + await extensionTab.goto(walletConfig.extension.HomeUrl); + await onboardWallet(extensionTab, walletConfig); + await extensionTab.goto('/') + const [walletPopup] = await Promise.all([ + browser.waitForEvent('page'), + // Trigger the action that opens the wallet window + extensionTab.locator('//*[text()="Enable wallet"]').click() + ]); + // const popupPromise = page.waitForEvent('popup'); + // await extensionTab.locator('//*[text()="Enable wallet"]').click(); + await allowExtension(walletPopup, walletConfig.extension.Name); + await new HomePage(extensionTab).balanceLabel.waitFor({ state: 'visible' }); + return browser; + }); + }, + + installExtension: async ({ }, use) => { + await use(async (extensionName: BrowserExtensionName) => { + const extensionPath = await new ExtensionDownloader().getExtension(extensionName); + const browser = await chromium.launchPersistentContext('', { + headless: false, // extensions only work in headfull mode + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + let [background] = browser.serviceWorkers(); + if (!background) + background = await browser.waitForEvent('serviceworker'); + return browser; + }); + } +}); \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallet-tests.spec.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallet-tests.spec.ts index 269a760ed3c..bca70a47dd5 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallet-tests.spec.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallet-tests.spec.ts @@ -1,156 +1,157 @@ -import { test, chromium, expect, BrowserContext, Page } from '@playwright/test'; -import { allowExtension, downloadExtension, getWalletCredentials, importWallet, signData, signDataBadPwd} from '../utils'; - -let browser: BrowserContext; -let extensionPath: string; -let extTab: Page; -/* cSpell:disable */ -let wallets = - [{ name: 'Typhon', id: 'kfdniefadaanbjodldohaedphafoffoh', url: 'chrome-extension://changeme/tab.html#/wallet/access/' }, - { name: 'Lace', id: 'gafhhkghbfjjkeiendhlofajokpaflmk', url: 'chrome-extension://changeme/app.html#/setup' },] -/* cSpell:enable */ - -wallets.forEach(({ name, id, url }) => { - test.describe(`Testing with ${name}`,() => { - test.skip(name === 'Typhon', 'https://github.com/input-output-hk/catalyst-voices/issues/753'); - // test.afterAll(async () => { - // browser.close() - // }); - - test.beforeAll(async () => { - // Download extension and import wallet into wallet extension - test.setTimeout(300000); - extensionPath = await downloadExtension(id); - browser = await chromium.launchPersistentContext('', { - headless: false, // extensions only work in headful mode - args: [ - `--disable-extensions-except=${extensionPath}`, - `--load-extension=${extensionPath}`, - ], - }); - let [background] = browser.serviceWorkers(); - if (!background) - background = await browser.waitForEvent('serviceworker'); - const extensionId = background.url().split('/')[2]; - extTab = await browser.newPage(); - const extUrl = url.replace('changeme', extensionId ); - await extTab.goto(extUrl); - await extTab.waitForTimeout(5000); - await importWallet(extTab,name); - await extTab.waitForTimeout(5000); - await extTab.goto('/') - await extTab.locator('//*[text()="Enable wallet"]').click(); - - await expect.poll(async () => { - return browser.pages().length; - }, { timeout: 15000 }).toBe(3); - - const updatedPages = browser.pages(); - const allowTab = updatedPages[updatedPages.length - 1]; - await allowTab.bringToFront(); - await allowExtension(allowTab,name); - await extTab.bringToFront(); - //wait for data to load - const textContent = await extTab.locator('#flt-semantic-node-13').textContent({ timeout: 5000 }); - expect(textContent).not.toBeNull(); - }); - - // Get and match text content - const matchTextContent = async (selector: string, regex: RegExp) => { - const textContent = await extTab.locator(selector).textContent({ timeout: 5000 }); - expect(textContent).not.toBeNull(); - const match = textContent!.match(regex); - expect(match).not.toBeNull(); - return match![1].trim(); - }; - - test('Get wallet details for ' + name , async () => { - await extTab.waitForTimeout(5000); - const balanceTextContent = await matchTextContent('#flt-semantic-node-13', /Balance: Ada \(lovelaces\): (\d+)/); - const balanceAda = (parseInt(balanceTextContent, 10) / 1_000_000).toFixed(2); - expect(parseInt(balanceAda)).toBeGreaterThan(500); - - const cleanedExtensionInfo = await matchTextContent('#flt-semantic-node-14', /Extensions:\s*(.+)/); - switch (name) { - case 'Typhon': - expect(cleanedExtensionInfo).toMatch('cip-30'); - break; - case 'Lace': - expect(cleanedExtensionInfo).toMatch('cip-95'); - break; - default: - throw new Error('Wallet not in use') - } - - expect(matchTextContent('#flt-semantic-node-15', /Network ID: (.+)/)).not.toBeNaN(); - - expect(await matchTextContent('#flt-semantic-node-17', /Reward addresses:\s*(\S+)/)).not.toBeNaN(); - - expect(matchTextContent('#flt-semantic-node-19', /Used addresses:\s*(\S+)/)).not.toBeNaN(); - - const utxoTextContent = await extTab.locator('#flt-semantic-node-20').textContent({ timeout: 5000 }); - expect(utxoTextContent).not.toBeNull(); - const utxoLines = utxoTextContent!.split('\n').map(line => line.trim()); - expect(utxoLines.length).toBeGreaterThanOrEqual(4); - const tx = utxoLines[1].split(':')[1].trim(); - const index = utxoLines[2].split(':')[1].trim(); - const amount = utxoLines[3].split(':')[2].trim(); - const amountAda = (parseInt(amount, 10) / 1_000_000).toFixed(2); - expect(tx).not.toBeUndefined(); - expect(index).not.toBeUndefined(); - expect(amount).not.toBeNaN(); - expect(parseInt(amountAda)).toBeGreaterThan(500); - }); - - async function openSignTab(buttonName: string) { - await extTab.getByRole('button', { name: buttonName }).click(); - await expect.poll(async () => browser.pages().length, { timeout: 15000 }).toBe(3); - const signPage = browser.pages(); - const signTab = signPage[signPage.length - 1]; - await signTab.bringToFront(); - return signTab; - } - - test('Sign data ' + name, async () => { - const signTab = await openSignTab('Sign data'); - const WalletCredentials = await getWalletCredentials('WALLET1'); - await signData(name, signTab, WalletCredentials.password); - await expect(extTab.getByText('Sign Data')).toBeVisible(); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - - test('Sign and submit tx ' + name, async () => { - const signTab = await openSignTab('Sign & submit tx') - const WalletCredentials = await getWalletCredentials('WALLET1'); - await signData(name, signTab, WalletCredentials.password); - await expect(extTab.getByText('Tx hash')).toBeVisible(); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - - test('Sign and submit RBAC tx ' + name, async () => { - const signTab = await openSignTab('Sign & submit RBAC tx'); - const WalletCredentials = await getWalletCredentials('WALLET1'); - await signData(name, signTab, WalletCredentials.password); - await expect(extTab.getByText('Tx hash')).toBeVisible(); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - - test('Fail to Sign data with incorrect password ' + name, async () => { - const signTab = await openSignTab('Sign data'); - await signDataBadPwd(name, signTab); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - - test('Fail to Sign & submit tx with incorrect password ' + name, async () => { - const signTab = await openSignTab('Sign & submit tx'); - await signDataBadPwd(name, signTab); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - - test('Fail to Sign & submit RBAC tx with incorrect password ' + name, async () => { - const signTab = await openSignTab('Sign & submit RBAC tx'); - await signDataBadPwd(name, signTab); - await extTab.getByRole('button', { name: 'Close' }).click(); - }); - }); - }); +// import { test, chromium, expect, BrowserContext, Page } from '@playwright/test'; +// import { allowExtension, downloadExtension, getWalletCredentials, importWallet, signData, signDataBadPwd} from '../utils'; + +// let browser: BrowserContext; +// let extensionPath: string; +// let extTab: Page; +// /* cSpell:disable */ +// let wallets = +// [{ name: 'Typhon', id: 'kfdniefadaanbjodldohaedphafoffoh', url: 'chrome-extension://changeme/tab.html#/wallet/access/' }, +// { name: 'Lace', id: 'gafhhkghbfjjkeiendhlofajokpaflmk', url: 'chrome-extension://changeme/app.html#/setup' },] +// /* cSpell:enable */ + +// wallets.forEach(({ name, id, url }) => { +// test.describe(`Testing with ${name}`,() => { +// test.skip(name === 'Typhon', 'https://github.com/input-output-hk/catalyst-voices/issues/753'); +// // test.afterAll(async () => { +// // browser.close() +// // }); + +// test.beforeAll(async () => { +// // Download extension and import wallet into wallet extension +// test.setTimeout(300000); + +// extensionPath = await downloadExtension(id); +// browser = await chromium.launchPersistentContext('', { +// headless: false, // extensions only work in headful mode +// args: [ +// `--disable-extensions-except=${extensionPath}`, +// `--load-extension=${extensionPath}`, +// ], +// }); +// let [background] = browser.serviceWorkers(); +// if (!background) +// background = await browser.waitForEvent('serviceworker'); +// const extensionId = background.url().split('/')[2]; +// extTab = await browser.newPage(); +// const extUrl = url.replace('changeme', extensionId ); +// await extTab.goto(extUrl); +// await extTab.waitForTimeout(5000); +// await importWallet(extTab,name); +// await extTab.waitForTimeout(5000); +// await extTab.goto('/') +// await extTab.locator('//*[text()="Enable wallet"]').click(); + +// await expect.poll(async () => { +// return browser.pages().length; +// }, { timeout: 15000 }).toBe(3); + +// const updatedPages = browser.pages(); +// const allowTab = updatedPages[updatedPages.length - 1]; +// await allowTab.bringToFront(); +// await allowExtension(allowTab,name); +// await extTab.bringToFront(); +// //wait for data to load +// const textContent = await extTab.locator('#flt-semantic-node-13').textContent({ timeout: 5000 }); +// expect(textContent).not.toBeNull(); +// }); + +// // Get and match text content +// const matchTextContent = async (selector: string, regex: RegExp) => { +// const textContent = await extTab.locator(selector).textContent({ timeout: 5000 }); +// expect(textContent).not.toBeNull(); +// const match = textContent!.match(regex); +// expect(match).not.toBeNull(); +// return match![1].trim(); +// }; + +// test('Get wallet details for ' + name , async () => { +// await extTab.waitForTimeout(5000); +// const balanceTextContent = await matchTextContent('#flt-semantic-node-13', /Balance: Ada \(lovelaces\): (\d+)/); +// const balanceAda = (parseInt(balanceTextContent, 10) / 1_000_000).toFixed(2); +// expect(parseInt(balanceAda)).toBeGreaterThan(500); + +// const cleanedExtensionInfo = await matchTextContent('#flt-semantic-node-14', /Extensions:\s*(.+)/); +// switch (name) { +// case 'Typhon': +// expect(cleanedExtensionInfo).toMatch('cip-30'); +// break; +// case 'Lace': +// expect(cleanedExtensionInfo).toMatch('cip-95'); +// break; +// default: +// throw new Error('Wallet not in use') +// } + +// expect(matchTextContent('#flt-semantic-node-15', /Network ID: (.+)/)).not.toBeNaN(); + +// expect(await matchTextContent('#flt-semantic-node-17', /Reward addresses:\s*(\S+)/)).not.toBeNaN(); + +// expect(matchTextContent('#flt-semantic-node-19', /Used addresses:\s*(\S+)/)).not.toBeNaN(); + +// const utxoTextContent = await extTab.locator('#flt-semantic-node-20').textContent({ timeout: 5000 }); +// expect(utxoTextContent).not.toBeNull(); +// const utxoLines = utxoTextContent!.split('\n').map(line => line.trim()); +// expect(utxoLines.length).toBeGreaterThanOrEqual(4); +// const tx = utxoLines[1].split(':')[1].trim(); +// const index = utxoLines[2].split(':')[1].trim(); +// const amount = utxoLines[3].split(':')[2].trim(); +// const amountAda = (parseInt(amount, 10) / 1_000_000).toFixed(2); +// expect(tx).not.toBeUndefined(); +// expect(index).not.toBeUndefined(); +// expect(amount).not.toBeNaN(); +// expect(parseInt(amountAda)).toBeGreaterThan(500); +// }); + +// async function openSignTab(buttonName: string) { +// await extTab.getByRole('button', { name: buttonName }).click(); +// await expect.poll(async () => browser.pages().length, { timeout: 15000 }).toBe(3); +// const signPage = browser.pages(); +// const signTab = signPage[signPage.length - 1]; +// await signTab.bringToFront(); +// return signTab; +// } + +// test('Sign data ' + name, async () => { +// const signTab = await openSignTab('Sign data'); +// const WalletCredentials = await getWalletCredentials('WALLET1'); +// await signData(name, signTab, WalletCredentials.password); +// await expect(extTab.getByText('Sign Data')).toBeVisible(); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); + +// test('Sign and submit tx ' + name, async () => { +// const signTab = await openSignTab('Sign & submit tx') +// const WalletCredentials = await getWalletCredentials('WALLET1'); +// await signData(name, signTab, WalletCredentials.password); +// await expect(extTab.getByText('Tx hash')).toBeVisible(); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); + +// test('Sign and submit RBAC tx ' + name, async () => { +// const signTab = await openSignTab('Sign & submit RBAC tx'); +// const WalletCredentials = await getWalletCredentials('WALLET1'); +// await signData(name, signTab, WalletCredentials.password); +// await expect(extTab.getByText('Tx hash')).toBeVisible(); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); + +// test('Fail to Sign data with incorrect password ' + name, async () => { +// const signTab = await openSignTab('Sign data'); +// await signDataBadPwd(name, signTab); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); + +// test('Fail to Sign & submit tx with incorrect password ' + name, async () => { +// const signTab = await openSignTab('Sign & submit tx'); +// await signDataBadPwd(name, signTab); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); + +// test('Fail to Sign & submit RBAC tx with incorrect password ' + name, async () => { +// const signTab = await openSignTab('Sign & submit RBAC tx'); +// await signDataBadPwd(name, signTab); +// await extTab.getByRole('button', { name: 'Close' }).click(); +// }); +// }); +// }); diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts index e69de29bb2d..37f67953723 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts @@ -0,0 +1,95 @@ +import { HomePage } from '../pages/homePage'; +import { test } from '../test-fixtures'; +import { walletConfigs } from '../utils/walletConfigs'; +import { signWalletPopup } from '../utils/wallets/walletUtils'; +import { ModalName } from '../pages/modal'; +import { expect } from '@playwright/test'; + +walletConfigs.forEach(( walletConfig ) => { + test.describe(`Testing with ${walletConfig.extension.Name}`, () => { + test.skip(walletConfig.extension.Name === 'Typhon', 'https://github.com/input-output-hk/catalyst-voices/issues/753'); + test('Get wallet details for ' + walletConfig.extension.Name, async ({ enableWallet }) => { + const page = (await enableWallet(walletConfig)).pages()[0]; + const homePage = new HomePage(page); + const walletCipData = await homePage.getWalletCipData(); + await homePage.assertBasicWalletCipData(walletCipData, walletConfig.extension.Name); + }); + // test('Sign data with ' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signDataButton.click() + // ]); + // await signWalletPopup(walletPopup, walletConfig); + // await homePage.assertModal(ModalName.SignData); + // }); + + // test('Sign and submit tx with ' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signAndSubmitTxButton.click() + // ]); + // await signWalletPopup(walletPopup, walletConfig); + // await homePage.assertModal(ModalName.SignAndSubmitTx); + // }); + + // test('Sign and submit RBAC tx with ' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signAndSubmitRBACTxButton.click() + // ]); + // await signWalletPopup(walletPopup, walletConfig); + // await homePage.assertModal(ModalName.SignAndSubmitRBACTx); + // }); + + // test('Fail to Sign data with incorrect password ' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signDataButton.click() + // ]); + // const walletConfigClone = structuredClone(walletConfig); + // walletConfigClone.password = 'BadPassword'; + // await signWalletPopup(walletPopup, walletConfigClone, false); + // await expect(walletPopup.getByTestId('password-input-error')).toBeVisible(); + // }); + + // test('Fail to Sign & submit tx with incorrect password' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signAndSubmitTxButton.click() + // ]); + // const walletConfigClone = structuredClone(walletConfig); + // walletConfigClone.password = 'BadPassword'; + // await signWalletPopup(walletPopup, walletConfigClone, false); + // await expect(walletPopup.getByTestId('password-input-error')).toBeVisible(); + // }); + + // test('Fail to Sign & submit RBAC tx with incorrect password' + walletConfig.extension.Name, async ({ enableWallet }) => { + // const browser = await enableWallet(walletConfig); + // const page = browser.pages()[0]; + // const homePage = new HomePage(page); + // const [walletPopup] = await Promise.all([ + // browser.waitForEvent('page'), + // homePage.signAndSubmitRBACTxButton.click() + // ]); + // const walletConfigClone = structuredClone(walletConfig); + // walletConfigClone.password = 'BadPassword'; + // await signWalletPopup(walletPopup, walletConfigClone, false); + // await expect(walletPopup.getByTestId('password-input-error')).toBeVisible(); + // }); + }); +}) diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tsconfig.json b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tsconfig.json index afe105476cc..a200eb0ca46 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tsconfig.json +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2018", "module": "commonjs", "strict": true, "esModuleInterop": true, diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts index 5d69b521feb..048dbb1f47b 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts @@ -3,6 +3,8 @@ import * as fsi from 'fs'; import path from 'path'; import nodeFetch from "node-fetch"; import { expect, Page } from '@playwright/test'; +import { ExtensionDownloader } from './utils/extensionDownloader'; +import { BrowserExtensionName } from './utils/extensions'; interface WalletCredentials { username: string; @@ -30,7 +32,7 @@ export { getSeedPhrase }; const downloadExtension = async (extID: string): Promise => { const unzip = require("unzip-crx-3"); - const url = `https://clients2.google.com/service/update2/crx?response=redirect&os=win&arch=x64&os_arch=x86_64&nacl_arch=x86-64&prod=chromiumcrx&prodchannel=stable&prodversion=latest&lang=en&acceptformat=crx3&x=id%3D${extID}%26installsource%3Dondemand%26uc`; + const url = new ExtensionDownloader().getCrxUrl(BrowserExtensionName.Lace); const downloadPath = path.resolve(__dirname, 'extensions'); await fs.mkdir(downloadPath, { recursive: true }); const filePath = path.join(downloadPath, extID + '.crx'); diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts new file mode 100644 index 00000000000..22e3605a7f2 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts @@ -0,0 +1,135 @@ +import * as os from 'os'; +import { promises as fsPromises } from 'fs'; +import { pipeline } from 'stream/promises'; +import unzip from '@tomjs/unzip-crx'; +import nodeFetch from "node-fetch"; +import fs from 'fs'; +import { BrowserExtensionName, getBrowserExtension } from "./extensions"; +import path from 'path'; + +interface PlatformInfo { + os: string; + arch: string; + nacl_arch: string; +} + +export class ExtensionDownloader { + private extensionsDir: string; + + constructor() { + this.extensionsDir = path.resolve(__dirname, '..', 'extensions'); + } + + /** + * Downloads and extracts the specified browser extension. + * @param extensionName The name of the extension to download. + * @returns The path to the extracted extension. + * + * @example + * const extensionPath = await new ExtensionDownloader().getExtension(BrowserExtensionName.Lace); + * console.log(extensionPath); + * Output: /path/to/extension + * + */ + public async getExtension(extensionName: BrowserExtensionName): Promise { + const extensionId = getBrowserExtension(extensionName).Id; + const extensionPath = path.join(this.extensionsDir, extensionId); + + // Check if the extension has already been downloaded + if (fs.existsSync(extensionPath)) { + console.log(`Extension already exists at: ${extensionPath}`); + return extensionPath; + } + + // Download the extension + const crxPath = await this.downloadExtension(extensionName); + + // Extract the extension + await this.extractExtension(crxPath, extensionPath); + + return extensionPath; + } + + private async extractExtension(extensionPath: string, extractPath: string): Promise { + // Ensure the extraction directory exists + await fsPromises.mkdir(extractPath, { recursive: true }); + + // Use unzip-crx to extract the CRX file + try { + await unzip(extensionPath, extractPath); + console.log(`Extension has been extracted to: ${extractPath}`); + } catch (error) { + console.error(`Failed to extract extension: ${(error as Error).message}`); + throw error; + } + } + + private async downloadExtension(extensionName: BrowserExtensionName): Promise { + const extensionId = getBrowserExtension(extensionName).Id; + const url = this.getCrxUrl(extensionName); + + // Ensure the download directory exists + await fsPromises.mkdir(this.extensionsDir, { recursive: true }); + + const filePath = path.join(this.extensionsDir, `${extensionId}.crx`); + + // Fetch the extension + const res = await nodeFetch(url); + if (!res.ok) { + throw new Error(`Failed to download extension: ${res.statusText}`); + } + + // Stream the response directly to a file + const fileStream = fs.createWriteStream(filePath); + await pipeline(res.body, fileStream); + + console.log(`Extension has been downloaded to: ${filePath}`); + return filePath; + } + + private getCrxUrl(extensionName: BrowserExtensionName): string { + const extensionId = getBrowserExtension(extensionName).Id; + + const platformInfo = this.getPlatformInfo(); + const productId = 'chromecrx'; + const productChannel = 'unknown'; + let productVersion = '9999.0.9999.0'; + + let url = 'https://clients2.google.com/service/update2/crx?response=redirect'; + url += '&os=' + platformInfo.os; + url += '&arch=' + platformInfo.arch; + url += '&os_arch=' + platformInfo.os_arch; + url += '&nacl_arch=' + platformInfo.nacl_arch; + url += '&prod=' + productId; + url += '&prodchannel=' + productChannel; + url += '&prodversion=' + productVersion; + url += '&lang=en' + url += '&acceptformat=crx3'; + url += '&x=id%3D' + extensionId + '%26installsource%3Dondemand%26uc'; + return url; + } + + private getPlatformInfo(): PlatformInfo & { os_arch: string } { + // Determine OS + let osType = os.type().toLowerCase(); + let osName = 'win'; + if (osType.includes('darwin')) { + osName = 'mac'; + } else if (osType.includes('linux')) { + osName = 'linux'; + } else if (osType.includes('win')) { + osName = 'win'; + } else if (osType.includes('cros')) { + osName = 'cros'; + } + + // Determine architecture + const arch = os.arch(); // Returns 'x64', 'arm', 'ia32', etc. + const is64Bit = arch === 'x64' || arch === 'arm64'; + const archName = is64Bit ? 'x64' : 'x86'; + const os_arch = is64Bit ? 'x86_64' : 'x86'; + const naclArch = is64Bit ? 'x86-64' : 'x86-32'; + + return { os: osName, arch: archName, os_arch, nacl_arch: naclArch }; +} +} diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extension_downloader.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extension_downloader.ts deleted file mode 100644 index 3a9953a5bdc..00000000000 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extension_downloader.ts +++ /dev/null @@ -1,75 +0,0 @@ -interface PlatformInfo { - os: string; - arch: string; - nacl_arch: string; -} - -class ExtensionDownloader { - public getCrxUrl(extensionIDOrUrl: string | ExtensionID): string { - let extensionID: string; - - if (typeof extensionIDOrUrl === 'string') { - const idMatch = this.getExtensionID(extensionIDOrUrl); - extensionID = idMatch ? idMatch : extensionIDOrUrl; - } else { - extensionID = extensionIDOrUrl; - } - - if (!/^[a-z]{32}$/.test(extensionID)) { - return extensionIDOrUrl; - } - - const platformInfo = this.getPlatformInfo(); - const productId = this.isChromeNotChromium() ? 'chromecrx' : 'chromiumcrx'; - const productChannel = 'unknown'; - let productVersion = '9999.0.9999.0'; - - const crVersion = /Chrome\/((\d+)\.0\.(\d+)\.\d+)/.exec(navigator.userAgent); - if (crVersion && +crVersion[2] >= 31 && +crVersion[3] >= 1609) { - productVersion = crVersion[1]; - } - - let url = 'https://clients2.google.com/service/update2/crx?response=redirect'; - url += '&os=' + platformInfo.os; - url += '&arch=' + platformInfo.arch; - url += '&os_arch=' + platformInfo.os_arch; - url += '&nacl_arch=' + platformInfo.nacl_arch; - url += '&prod=' + productId; - url += '&prodchannel=' + productChannel; - url += '&prodversion=' + productVersion; - url += '&x=id%3D' + extensionID + '%26installsource%3Dondemand%26uc'; - return url; - } - - private getExtensionID(url: string): string | null { - const pattern = /chrome\.google\.com\/webstore\/detail\/[^\/]+\/([a-z]{32})/i; - const match = pattern.exec(url); - return match ? match[1] : null; - } - - private isChromeNotChromium(): boolean { - return ( - navigator.userAgent.includes('Chrome') && - !navigator.userAgent.includes('Chromium') - ); - } - - private getPlatformInfo(): PlatformInfo & { os_arch: string } { - const platform = navigator.platform.toLowerCase(); - let os = 'win'; - if (platform.startsWith('mac')) { - os = 'mac'; - } else if (platform.startsWith('linux')) { - os = 'linux'; - } else if (platform.startsWith('cros')) { - os = 'cros'; - } - - const is64Bit = /x86_64|Win64|WOW64|AMD64/.test(navigator.userAgent); - const arch = is64Bit ? 'x64' : 'x86'; - const os_arch = is64Bit ? 'x86_64' : 'x86'; - const naclArch = is64Bit ? 'x86-64' : 'x86-32'; - - return { os, arch, os_arch, nacl_arch: naclArch }; - } -} diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts index d3a9c17fc6e..9b7584f5fb8 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts @@ -1,20 +1,32 @@ -export interface Extension { - Name: string, +export interface BrowserExtension { + Name: BrowserExtensionName, Id: string + HomeUrl: string } -export enum ExtensionName { +export enum BrowserExtensionName { Lace = 'Lace', Typhon = 'Typhon' } -const extensions: Extension[] = [ +export const browserExtensions: BrowserExtension[] = [ { - Name: ExtensionName.Lace, - Id: 'lace-id' + Name: BrowserExtensionName.Lace, + Id: 'gafhhkghbfjjkeiendhlofajokpaflmk', + HomeUrl: 'chrome-extension://gafhhkghbfjjkeiendhlofajokpaflmk/app.html#/setup' + }, { - Name: ExtensionName.Typhon, - Id: 'typhon-id' + Name: BrowserExtensionName.Typhon, + Id: 'kfdniefadaanbjodldohaedphafoffoh', + HomeUrl: 'chrome-extension://kfdniefadaanbjodldohaedphafoffoh/tab.html#/wallet/access/' + } +]; + +export const getBrowserExtension = (name: BrowserExtensionName): BrowserExtension => { + const extension = browserExtensions.find(extension => extension.Name === name); + if (!extension) { + throw new Error(`Browser extension with name ${name} not found`); } -]; \ No newline at end of file + return extension; +} \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts new file mode 100644 index 00000000000..6fc27b33e36 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts @@ -0,0 +1,62 @@ +import { BrowserExtensionName, getBrowserExtension } from "./extensions"; +import { WalletConfig } from "./wallets/walletUtils"; + +export const walletConfigs: WalletConfig[] = [ + { + id: '1', + extension: getBrowserExtension(BrowserExtensionName.Lace), + seed: [ + 'stomach', + 'horn', + 'rail', + 'afraid', + 'flip', + 'also', + 'abandon', + 'speed', + 'chaos', + 'daring', + 'soon', + 'soft', + 'okay', + 'online', + 'benefit', + ], + username: 'test123', + password: 'test12345678@', + }, + { + id: '2', + extension: getBrowserExtension(BrowserExtensionName.Typhon), + seed: [ + 'stomach', + 'horn', + 'rail', + 'afraid', + 'flip', + 'also', + 'abandon', + 'speed', + 'chaos', + 'daring', + 'soon', + 'soft', + 'okay', + 'online', + 'benefit', + ], + username: 'test123', + password: 'test12345678@', + }, +]; + +export const getWalletConfig = (id: string): WalletConfig => { + const walletConfig = walletConfigs.find(walletConfig => walletConfig.id === id); + if (!walletConfig) { + throw new Error(`Wallet config with id ${id} not found`); + } + return walletConfig; +} + +export const getWalletConfigs = (): WalletConfig[] => walletConfigs; + \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/laceUtils.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/laceUtils.ts new file mode 100644 index 00000000000..699d90e0a83 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/laceUtils.ts @@ -0,0 +1,89 @@ +import { expect, Page } from "@playwright/test"; +import { WalletConfig } from "./walletUtils"; + +const clickRestoreWalletButton = async (page: Page): Promise => { + const maxAttempts = 3; + + // Selector for the restore wallet button + const restoreWalletButtonSelector = '[data-testid="restore-wallet-button"]'; + + // Selector for an element that exists only on the next page + const nextPageSelector = '[data-testid="wallet-setup-step-btn-next"]'; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + // Wait for the restore wallet button to be visible and enabled + const restoreWalletButton = page.locator(restoreWalletButtonSelector); + await restoreWalletButton.waitFor({ state: 'visible', timeout: 5000 }); + await expect(restoreWalletButton).toBeEnabled(); + + // Click the restore wallet button and wait for the next page to load + await Promise.all([ + page.waitForSelector(nextPageSelector, { timeout: 10000 }), + restoreWalletButton.click(), + ]); + + // Verify that the next page has loaded by checking for a unique element + const nextPageElement = page.locator(nextPageSelector); + await nextPageElement.waitFor({ state: 'visible', timeout: 5000 }); + + // If the next page is detected, exit the function + console.log('Successfully navigated to the next page.'); + return; + } catch (error) { + if (attempt === maxAttempts) { + // If it's the last attempt, rethrow the error + throw new Error(`Failed to click 'restore-wallet-button' after ${maxAttempts} attempts: ${error}`); + } else { + // Log the attempt and retry + console.warn(`Attempt ${attempt} to click 'restore-wallet-button' failed. Retrying...`); + // Optionally, you can add a short delay before retrying + await page.waitForTimeout(1000); + } + } + } +}; + +/* + * This handles the situation where after clicking restore Lace sometimes leads directly to recovery phrase page + * and sometimes leads to a page where the user has to click on the recovery phrase button to get to the recovery phrase page + */ +const handleNextPage = async (page: Page): Promise => { + const title = await page.getByTestId('wallet-setup-step-title').textContent(); + if(title === 'Choose recovery method') { + await page.locator('[data-testid="wallet-setup-step-btn-next"]').click(); + } else { + return; + } +} + +export const onboardLaceWallet = async (page: Page, walletConfig: WalletConfig): Promise => { + await page.locator('[data-testid="analytics-accept-button"]').click(); + await clickRestoreWalletButton(page); + await handleNextPage(page); + await page.getByTestId('recovery-phrase-15').click(); + const seedPhrase = walletConfig.seed; + for (let i = 0; i < seedPhrase.length; i++) { + const ftSeedPhraseSelector = `//*[@id="mnemonic-word-${i + 1}"]`; + await page.locator(ftSeedPhraseSelector).fill(seedPhrase[i]); + } + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByTestId('wallet-name-input').fill(walletConfig.username); + await page.getByTestId('wallet-password-verification-input').fill(walletConfig.password); + await page.getByTestId('wallet-password-confirmation-input').fill(walletConfig.password); + await page.getByRole('button', { name: 'Open wallet' }).click(); + //Lace is very slow at loading + await page.getByTestId('profile-dropdown-trigger-menu').click({timeout: 300000}); + await page.getByTestId('header-menu').getByTestId('header-menu-network-choice-container').click(); + await page.getByTestId('header-menu').getByTestId('network-preprod-radio-button').click(); +}; + +export const signLaceData = async (page: Page, password: string, isCorrectPassword: boolean): Promise => { + await page.locator('[data-testid="dapp-transaction-confirm"]').click(); + await page.getByTestId('password-input').fill(password); + await page.locator('[data-testid="sign-transaction-confirm"]').click(); + if (!isCorrectPassword) { + return; + } + await page.getByRole('button', { name: 'Close' }).click(); + } \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/typhonUtils.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/typhonUtils.ts new file mode 100644 index 00000000000..416ad09ff3c --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/typhonUtils.ts @@ -0,0 +1,34 @@ +import { Page } from "playwright"; +import { WalletConfig } from "./walletUtils"; + +export const onboardTyphonWallet = async (page: Page, walletConfig: WalletConfig): Promise => { + //switch to preprod network + await page.locator('button#headlessui-menu-button-1').click(); + await page.locator('button#headlessui-menu-item-6').click(); + //import wallet + await page.getByRole('button', { name: 'Import' }).click(); + await page.getByPlaceholder('Wallet Name').fill(walletConfig.username); + await page.getByPlaceholder('Password', { exact: true }).fill(walletConfig.password); + await page.getByPlaceholder('Confirm Password', { exact: true }).fill(walletConfig.password); + await page.locator('input#termsAndConditions').click(); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Input seed phrase + const seedPhrase = walletConfig.seed; + for (let i = 0; i < seedPhrase.length; i++) { + const ftSeedPhraseSelector = `(//input[@type='text'])[${i + 1}]`; + await page.locator(ftSeedPhraseSelector).fill(seedPhrase[i]); + } + + await page.locator('//*[@id="app"]/div/div/div[3]/div/div[2]/div/div/div/div[1]/div[1]/div[1]/span[1]').click(); + await page.getByRole('button', { name: 'Unlock Wallet' }).click(); +}; + +export const signTyphonData = async (signTab: Page, password: string, isCorrectPassword: boolean): Promise => { + await signTab.getByRole('button', { name: 'Sign' }).click(); + await signTab.getByPlaceholder('Password', { exact: true }).fill(password); + if (!isCorrectPassword) { + return + } + await signTab.getByRole('button', { name: 'confirm' }).click(); +} \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts new file mode 100644 index 00000000000..2ace59f2ef7 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts @@ -0,0 +1,54 @@ +import { Page } from "@playwright/test"; +import { BrowserExtension, BrowserExtensionName } from "../extensions"; +import { onboardTyphonWallet, signTyphonData } from "./typhonUtils"; +import { onboardLaceWallet, signLaceData } from "./laceUtils"; +import { HomePage } from "../../pages/homePage"; + +export interface WalletConfig { + id: string; + extension: BrowserExtension; + seed: string[]; + username: string; + password: string; +} + +export const onboardWallet = async (page: Page, walletConfig: WalletConfig): Promise => { + switch (walletConfig.extension.Name) { + case BrowserExtensionName.Typhon: + await onboardTyphonWallet(page, walletConfig); + break; + case BrowserExtensionName.Lace: + await onboardLaceWallet(page, walletConfig); + break; + default: + throw new Error('Wallet not in use') + } + await page.waitForTimeout(2000); +} + +export const allowExtension = async (tab: Page, wallet: string): Promise => { + switch (wallet) { + case 'Typhon': + await tab.getByRole('button', { name: 'Allow' }).click(); + break; + case 'Lace': + await tab.getByTestId('connect-authorize-button').click(); + await tab.getByRole('button', { name: 'Always' }).click(); + break; + default: + throw new Error('Wallet not in use') + } +} + +export const signWalletPopup = async (page: Page, walletConfig: WalletConfig, isCorrectPassword = true): Promise => { + switch (walletConfig.extension.Name) { + case BrowserExtensionName.Typhon: + await signTyphonData(page, walletConfig.password, isCorrectPassword); + break; + case BrowserExtensionName.Lace: + await signLaceData(page, walletConfig.password, isCorrectPassword); + break; + default: + throw new Error('Wallet not in use') + } +} \ No newline at end of file