From d2425efbf571003e102745fdf9d79b5d68273d60 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Mon, 15 Apr 2024 14:34:01 +0200 Subject: [PATCH] [ENG-3999] Onboarding part 2 (#184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add storage seedphrase * add 12 word seedphrase error message check * add error message check for 24 word seedphrase * optimize code, add switch 12 to 24 seedphrase test case * add confirm address copy and save address in file * [ENG-4031] Implement Sharding for test execution * Add Lock and login flow * Added attributes to elements in the homedashboard for better locators * fix: prefer data-testid over id, and aria-label for options button * Use static method (#187) * [ENG-3979] Implement Smoketest execution for PR build --------- Co-authored-by: DuskaT021 Co-authored-by: Tim Man Co-authored-by: Eduard Bardají Puig --- .eslintrc.json | 22 ++- .github/workflows/build.yml | 2 +- .github/workflows/playwright.yml | 95 ++++++++-- .gitignore | 2 + playwright.config.ts | 4 +- src/app/components/accountHeader/index.tsx | 2 +- src/app/components/accountRow/index.tsx | 8 +- src/app/screens/home/balanceCard/index.tsx | 2 +- src/app/screens/home/index.tsx | 2 +- tests/fixtures/base.ts | 3 +- tests/pages/onboarding.ts | 90 ++++++++- tests/pages/startPage.ts | 68 +++++++ tests/specs/createWallet.spec.ts | 195 +++++++++++++++++++ tests/specs/healthcheck.spec.ts | 2 +- tests/specs/onboarding.spec.ts | 206 ++++++++++++--------- 15 files changed, 583 insertions(+), 120 deletions(-) create mode 100644 tests/pages/startPage.ts create mode 100644 tests/specs/createWallet.spec.ts diff --git a/.eslintrc.json b/.eslintrc.json index 95a8db8a..4af13d6e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,8 +9,7 @@ "airbnb-typescript", "airbnb/hooks", "prettier", - "plugin:@tanstack/eslint-plugin-query/recommended", - "plugin:playwright/recommended" + "plugin:@tanstack/eslint-plugin-query/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -21,17 +20,10 @@ "sourceType": "module", "project": "./tsconfig.json" }, - "plugins": [ - "react", - "prettier", - "eslint-plugin-no-inline-styles", - "@tanstack/query", - "eslint-plugin-playwright" - ], + "plugins": ["react", "prettier", "eslint-plugin-no-inline-styles", "@tanstack/query"], "rules": { "consistent-return": "off", "no-await-in-loop": "off", - "playwright/valid-title": "off", "import/prefer-default-export": 1, "no-restricted-imports": [ "warn", @@ -62,6 +54,16 @@ "@tanstack/query/exhaustive-deps": 1, "import/order": 0 }, + "overrides": [ + { + "files": ["tests/**/*.{js,jsx,ts,tsx}"], + "plugins": ["playwright"], + "extends": ["plugin:playwright/playwright-test"], + "rules": { + "playwright/expect-expect": "off" + } + } + ], "settings": { "import/resolver": { "node": { diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 752e6f23..147dc247 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: - name: Install Playwright Browsers run: npx playwright install chromium --with-deps - name: Run UI test suite - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --reporter=html + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep "#smoketest" --reporter=html - name: Upload Playwright report if: always() uses: actions/upload-artifact@v3 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 062c5e1b..95eb2e2b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,14 +1,9 @@ name: Playwright Tests on: workflow_dispatch: - inputs: - branch: - description: Branch name - required: true - default: develop jobs: - test: - timeout-minutes: 10 + build: + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,6 +19,11 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} run: npm ci + - name: Test + run: | + npx eslint . + npx tsc --noEmit + npm test - name: Build env: TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} @@ -31,14 +31,85 @@ jobs: MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} MIX_PANEL_EXPLORE_APP_TOKEN: ${{ secrets.MIX_PANEL_EXPLORE_APP_TOKEN }} run: npm run build --if-present + - name: Upload Archive + uses: actions/upload-artifact@v3 + with: + name: web-extension1 + path: ./build + retention-days: 5 + if-no-files-found: error + UItest: + needs: [build] + name: UI Test ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} + timeout-minutes: 10 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4, 5] + shardTotal: [5] + steps: + - uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v3 + with: + name: web-extension1 + path: ./build + - name: Use Node.js + uses: actions/setup-node@v4 + with: + always-auth: true + node-version: 18 + registry-url: https://npm.pkg.github.com + scope: '@secretkeylabs' + cache: npm + - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} + run: npm install playwright - name: Install Playwright Browsers run: npx playwright install chromium --with-deps - name: Run UI test suite - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --reporter=html + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload Playwright report - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v3 with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + name: all-blob-reports + path: blob-report + retention-days: 1 + + merge-reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [UItest] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + always-auth: true + node-version: 18 + registry-url: https://npm.pkg.github.com + scope: '@secretkeylabs' + cache: npm + - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} + run: npm install playwright + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v3 + with: + name: all-blob-reports + path: all-blob-reports + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v3 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 5 diff --git a/.gitignore b/.gitignore index de051323..b47996ee 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,5 @@ yarn-error.log* /playwright-report/ /blob-report/ /playwright/.cache/ +tests/specs/seedWords.json +tests/specs/addresses.json diff --git a/playwright.config.ts b/playwright.config.ts index 6af157aa..36533c55 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,10 +18,10 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 1 : 0, - /* Opt out of parallel tests on CI. */ + /* Opt out of 2 tests parallel on CI. */ workers: process.env.CI ? 2 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['list'], ['html']], + reporter: process.env.CI ? 'blob' : 'html', snapshotDir: './playwright-snapshots', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index 1a95d033..5143b8a3 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -176,7 +176,7 @@ function AccountHeaderComponent({ disabledAccountSelect={disableAccountSwitch} /> {!disableMenuOption && ( - + )} diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index e493646b..fa054d56 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -45,7 +45,7 @@ const AccountInfoContainer = styled.div({ alignItems: 'center', }); -const CurrentAcountContainer = styled.div((props) => ({ +const CurrentAccountContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', paddingLeft: props.theme.space.s, @@ -331,7 +331,7 @@ function AccountRow({ thirdGradient={gradient[2]} isBig={isAccountListView} /> - + {account && ( @@ -365,11 +365,11 @@ function AccountRow({ )} - + {isAccountListView && ( - + )} diff --git a/src/app/screens/home/balanceCard/index.tsx b/src/app/screens/home/balanceCard/index.tsx index efd7b677..65c728c6 100644 --- a/src/app/screens/home/balanceCard/index.tsx +++ b/src/app/screens/home/balanceCard/index.tsx @@ -144,7 +144,7 @@ function BalanceCard(props: BalanceCardProps) { prefix={`${currencySymbolMap[fiatCurrency]}`} thousandSeparator renderText={(value: string) => ( - {value} + {value} )} /> {isRefetching && ( diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index ae9ff1fc..ae3a006e 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -513,7 +513,7 @@ function Home() { refetchingRunesData } /> - + } text={t('SEND')} diff --git a/tests/fixtures/base.ts b/tests/fixtures/base.ts index cbbb64c5..0e5834a4 100644 --- a/tests/fixtures/base.ts +++ b/tests/fixtures/base.ts @@ -9,9 +9,10 @@ export const test = baseTest.extend<{ }>({ // parts of the setup for the persistent context from https://playwright.dev/docs/chrome-extensions#testing context: async ({}, use) => { - const extPath = path.join(__dirname, '../../build'); + const extPath = process.env.BUILD_EXTENSION_PATH || path.join(__dirname, '../../build'); const context = await chromium.launchPersistentContext('', { args: [`--disable-extensions-except=${extPath}`, `--load-extension=${extPath}`], + // slowMo: 400, // Slows down Playwright operations by 400 milliseconds for showcasing or testing reasons }); await context.grantPermissions(['clipboard-read', 'clipboard-write']); await use(context); diff --git a/tests/pages/onboarding.ts b/tests/pages/onboarding.ts index f84d42de..b0e18dcb 100644 --- a/tests/pages/onboarding.ts +++ b/tests/pages/onboarding.ts @@ -1,4 +1,6 @@ import { expect, type Locator, type Page } from '@playwright/test'; +import crypto from 'crypto'; +import Landing from './landing'; export default class Onboarding { readonly linkTOS: Locator; @@ -25,6 +27,8 @@ export default class Onboarding { readonly errorMessage2: Locator; + readonly errorMessageSeedPhrase: Locator; + readonly buttonContinue: Locator; readonly labelSecurityLevelWeak: Locator; @@ -47,12 +51,27 @@ export default class Onboarding { readonly instruction: Locator; + readonly headingWalletRestored: Locator; + readonly buttonCloseTab: Locator; readonly imageSuccess: Locator; + readonly headingRestoreWallet: Locator; + + readonly button24SeedPhrase: Locator; + + readonly button12SeedPhrase: Locator; + + readonly inputSeedPhraseWord: Locator; + + readonly inputSeedPhraseWordDisabled: Locator; + + readonly buttonUnlock: Locator; + constructor(readonly page: Page) { this.page = page; + this.buttonContinue = page.getByRole('button', { name: 'Continue' }); this.buttonBack = page.getByRole('button', { name: 'Back' }); this.buttonBackupNow = page.getByRole('button', { name: 'Backup now' }); @@ -70,6 +89,7 @@ export default class Onboarding { this.inputPassword = page.locator('input[type="password"]'); this.errorMessage = page.getByRole('heading', { name: 'Your password should be at' }); this.errorMessage2 = page.getByRole('heading', { name: 'Please make sure your' }); + this.errorMessageSeedPhrase = page.locator('p').filter({ hasText: 'Invalid seed phrase' }); this.labelSecurityLevelWeak = page.locator('p').filter({ hasText: 'Weak' }); this.labelSecurityLevelMedium = page.locator('p').filter({ hasText: 'Medium' }); this.labelSecurityLevelStrong = page.locator('p').filter({ hasText: 'Strong' }); @@ -78,11 +98,16 @@ export default class Onboarding { this.buttonAccept = page.getByRole('button', { name: 'Accept' }); this.imageSuccess = page.locator('img[alt="success"]'); this.instruction = page.getByRole('heading', { name: 'Locate Xverse' }); + this.headingWalletRestored = page.getByRole('heading', { name: 'Wallet restored' }); this.buttonCloseTab = page.getByRole('button', { name: 'Close this tab' }); + this.headingRestoreWallet = page.getByRole('heading', { name: 'restore your wallet' }); + this.button24SeedPhrase = page.getByRole('button', { name: '24 words' }); + this.button12SeedPhrase = page.getByRole('button', { name: '12 words' }); + this.inputSeedPhraseWord = page.locator('input'); + this.inputSeedPhraseWordDisabled = page.locator('input[type="password"][disabled]'); + this.buttonUnlock = page.getByRole('button', { name: 'Unlock' }); } - // TODO add function here for the steps of the onboarding for the case a created wallet should always be created via the UI and is then needed for all following test suits - // id starts from 0 inputWord = (id: number) => this.page.locator(`#input${id}`); @@ -93,6 +118,7 @@ export default class Onboarding { } async checkLegalPage(context) { + await expect(this.page.url()).toContain('legal'); // TODO: Selector outsource await expect(this.page.locator('div > h1:first-child')).toHaveText(/Legal/); // check that the links contain href values @@ -118,6 +144,14 @@ export default class Onboarding { await newPage.close(); } + async navigateToBackupPage() { + const landingpage = new Landing(this.page); + await landingpage.buttonCreateWallet.click(); + await expect(this.page.url()).toContain('legal'); + await this.buttonAccept.click(); + await expect(this.page.url()).toContain('backup'); + } + async checkBackupPage() { await expect(this.buttonBackupNow).toBeVisible(); await expect(this.buttonBackupLater).toBeVisible(); @@ -126,7 +160,25 @@ export default class Onboarding { await expect(this.subTitleBackupOnboarding).toBeVisible(); } - // Check the viuals on the first password page before inputting any values in the input field + async navigateToRestorePage() { + const landingpage = new Landing(this.page); + await expect(landingpage.buttonRestoreWallet).toBeVisible(); + await landingpage.buttonRestoreWallet.click(); + await expect(this.page.url()).toContain('legal'); + await this.buttonAccept.click(); + await expect(this.page.url()).toContain('restore'); + await this.checkRestoreWalletSeedPhrasePage(); + } + + async checkRestoreWalletSeedPhrasePage() { + await expect(this.buttonContinue).toBeDisabled(); + await expect(this.headingRestoreWallet).toBeVisible(); + await expect(this.button24SeedPhrase).toBeVisible(); + await expect(this.inputSeedPhraseWordDisabled).toHaveCount(12); + await expect(this.inputSeedPhraseWord).toHaveCount(24); + } + + // Check the viuals on the first password page before inputing any values in the input field async checkPasswordPage() { await expect(this.buttonBack).toBeVisible(); await expect(this.inputPassword).toBeVisible(); @@ -138,6 +190,23 @@ export default class Onboarding { await expect(this.labelSecurityLevelStrong).toBeHidden(); } + static async multipleClickCheck(button: Locator) { + await button.click(); + await button.click(); + await button.click(); + } + + async createWalletSkipBackup(password) { + await this.navigateToBackupPage(); + await this.buttonBackupLater.click(); + await expect(this.page.url()).toContain('create-password'); + await this.inputPassword.fill(password); + await this.buttonContinue.click(); + await this.inputPassword.fill(password); + await this.buttonContinue.click(); + await expect(this.imageSuccess).toBeVisible(); + } + async testPasswordInput({ password, expectations }) { // Fill in the password input field with the specified password. await this.inputPassword.fill(password); @@ -183,4 +252,19 @@ export default class Onboarding { // Clear the password input field after all checks are done. await this.inputPassword.clear(); } + + static generateSecurePasswordCrypto() { + const length = 9; + const charset = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':,.<>/?`~"; + + let password = ''; + while (password.length < length) { + // Generate a random byte + const randomValue = crypto.randomInt(charset.length); + password += charset[randomValue]; + } + + return password; + } } diff --git a/tests/pages/startPage.ts b/tests/pages/startPage.ts new file mode 100644 index 00000000..363f37a8 --- /dev/null +++ b/tests/pages/startPage.ts @@ -0,0 +1,68 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export default class StartPage { + readonly balance: Locator; + + readonly allupperButtons: Locator; + + readonly manageTokenButton: Locator; + + readonly buttonMenu: Locator; + + readonly buttonLock: Locator; + + readonly buttonResetWallet: Locator; + + readonly buttonDenyDataCollection: Locator; + + readonly buttonCopyBitcoinAddress: Locator; + + readonly buttonCopyOrdinalsAddress: Locator; + + readonly buttonCopyStacksAddress: Locator; + + readonly buttonConfirmCopyAddress: Locator; + + constructor(readonly page: Page) { + this.page = page; + this.balance = page.getByTestId('total-balance-value'); + this.allupperButtons = page.getByTestId('transaction-buttons-row').getByRole('button'); + 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.buttonResetWallet = page.getByRole('button', { name: 'Reset Wallet' }); + this.buttonDenyDataCollection = page.getByRole('button', { name: 'Deny' }); + this.buttonCopyBitcoinAddress = page.locator('#copy-address-Bitcoin'); + this.buttonCopyOrdinalsAddress = page.locator( + '#copy-address-Ordinals\\,\\ BRC-20\\ \\&\\ Runes', + ); + this.buttonCopyStacksAddress = page.locator( + '#copy-address-Stacks\\ NFTs\\ \\&\\ SIP-10\\ tokens', + ); + + this.buttonConfirmCopyAddress = page.getByRole('button', { name: 'I understand' }); + } + + async checkVisuals() { + // 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(); + } + + // Check if specific visual elements are loaded + await expect(this.balance).toBeVisible(); + await expect(this.manageTokenButton).toBeVisible(); + await expect(this.buttonMenu).toBeVisible(); + // Check if all 4 buttons (send, receive, swap, buy) are visible + await expect(this.allupperButtons).toHaveCount(4); + } + + async getAddress(button) { + await expect(button).toBeVisible(); + await button.click(); + await expect(this.buttonConfirmCopyAddress).toBeVisible(); + await this.buttonConfirmCopyAddress.click(); + const address = await this.page.evaluate('navigator.clipboard.readText()'); + return address; + } +} diff --git a/tests/specs/createWallet.spec.ts b/tests/specs/createWallet.spec.ts new file mode 100644 index 00000000..7b439f64 --- /dev/null +++ b/tests/specs/createWallet.spec.ts @@ -0,0 +1,195 @@ +import { expect, test } from '../fixtures/base'; +import Landing from '../pages/landing'; +import Onboarding from '../pages/onboarding'; +import StartPage from '../pages/startPage'; + +test.beforeEach(async ({ page, extensionId, context }) => { + await page.goto(`chrome-extension://${extensionId}/options.html#/landing`); + // TODO: this fixes a temporary issue with two tabs at the start see technical debt https://linear.app/xverseapp/issue/ENG-3992/two-tabs-open-instead-of-one-since-version-0323-for-start-extension + const pages = await context.pages(); + if (pages.length === 2) { + await pages[1].close(); // pages[1] is the second (newest) page + } +}); +test.afterEach(async ({ context }) => { + if (context.pages().length > 0) { + // Close the context only if there are open pages + await context.close(); + } +}); + +const strongPW = Onboarding.generateSecurePasswordCrypto(); +const fs = require('fs'); +const path = require('path'); + +// Specify the file path for Addresses and Seedphrase +const filePathSeedWords = path.join(__dirname, 'seedWords.json'); +const filePathAddresses = path.join(__dirname, 'addresses.json'); + +test.describe('Create and Restore Wallet Flow', () => { + test('create and restore a wallet via Menu', async ({ page, extensionId, context }) => { + const onboardingpage = new Onboarding(page); + const startpage = new StartPage(page); + await test.step('backup seedphrase and successfully create a wallet', async () => { + await onboardingpage.navigateToBackupPage(); + await onboardingpage.buttonBackupNow.click(); + await expect(page.url()).toContain('backupWalletSteps'); + await expect(onboardingpage.buttonContinue).toBeDisabled(); + await expect(onboardingpage.buttonShowSeed).toBeVisible(); + await expect(onboardingpage.firstParagraphBackupStep).toBeVisible(); + await onboardingpage.buttonShowSeed.click(); + await expect(onboardingpage.buttonContinue).toBeEnabled(); + const seedWords = await onboardingpage.textSeedWords.allTextContents(); + await onboardingpage.buttonContinue.click(); + + // check if 12 words are displayed + await expect(onboardingpage.buttonSeedWords).toHaveCount(12); + await expect(onboardingpage.secondParagraphBackupStep).toBeVisible(); + let seedword = await onboardingpage.selectSeedWord(seedWords); + + // Save the seedwords into a file to read it out later to restore + fs.writeFileSync(filePathSeedWords, JSON.stringify(seedWords), 'utf8'); + + // get all displayed values and filter the value from the actual seedphrase out to do an error message check + const buttonValues = await onboardingpage.buttonSeedWords.evaluateAll((buttons) => + buttons.map((button) => { + // Assert that the button is an HTMLButtonElement to access the `value` property + if (button instanceof HTMLButtonElement) { + return button.value; + } + return 'testvalue'; + }), + ); + + const filteredValues = buttonValues.filter((value) => value !== seedword); + const randomValue = filteredValues[Math.floor(Math.random() * filteredValues.length)]; + await page.locator(`button[value="${randomValue}"]`).click(); + + // Check if error message is displayed when clicking the wrong seedword + await expect(page.locator('p:has-text("This word is not")')).toBeVisible(); + + await page.locator(`button[value="${seedword}"]`).click(); + seedword = await onboardingpage.selectSeedWord(seedWords); + await page.locator(`button[value="${seedword}"]`).click(); + seedword = await onboardingpage.selectSeedWord(seedWords); + await page.locator(`button[value="${seedword}"]`).click(); + + await onboardingpage.inputPassword.fill(strongPW); + await onboardingpage.buttonContinue.click(); + await onboardingpage.inputPassword.fill(strongPW); + await onboardingpage.buttonContinue.click(); + + await expect(onboardingpage.imageSuccess).toBeVisible(); + await expect(onboardingpage.instruction).toBeVisible(); + await expect(onboardingpage.buttonCloseTab).toBeVisible(); + + // Open the startpage directly via URL + await page.goto(`chrome-extension://${extensionId}/popup.html`); + const startPage = new StartPage(page); + await startPage.checkVisuals(); + + const balanceText = await startPage.balance.innerText(); + await expect(balanceText).toBe('$0.00'); + + // TODO: find better selector for the receive button + await startPage.allupperButtons.nth(1).click(); + + // Get the addresses and save it in variables + const addressBitcoin = await startPage.getAddress(startPage.buttonCopyBitcoinAddress); + const addressOrdinals = await startPage.getAddress(startPage.buttonCopyOrdinalsAddress); + // Stack Address doesn't have the confirm message + await expect(startPage.buttonCopyStacksAddress).toBeVisible(); + await startPage.buttonCopyStacksAddress.click(); + const addressStack = await page.evaluate('navigator.clipboard.readText()'); + + // Reload the page to close the modal window for the addresses as the X button needs to have a better locator + await page.reload(); + // click close for the modal window + // TODO find better locator for close button --> issue https://linear.app/xverseapp/issue/ENG-4039/adjust-id-or-add-titles-for-copy-address-button-for-receive-menu + // await expect(page.locator('button.sc-hceviv > svg')).toBeVisible(); + // await page.locator('button.sc-hceviv > svg').click(); + + // Save the Address in a file so that other tests can access them + const dataAddress = JSON.stringify({ + addressBitcoin, + addressOrdinals, + addressStack, + }); + + // Write the file + fs.writeFileSync(filePathAddresses, dataAddress, 'utf8'); + }); + await test.step('reset Wallet via Menu', async () => { + await expect(startpage.buttonMenu).toBeVisible(); + await startpage.buttonMenu.click(); + await expect(startpage.buttonResetWallet).toBeVisible(); + await startpage.buttonResetWallet.click(); + await startpage.buttonResetWallet.click(); + await expect(onboardingpage.inputPassword).toBeVisible(); + await onboardingpage.inputPassword.fill(strongPW); + await onboardingpage.buttonContinue.click(); + }); + await test.step('Restore wallet with 12 word seed phrase', async () => { + const landingpage = new Landing(page); + await expect(landingpage.buttonRestoreWallet).toBeVisible(); + await landingpage.buttonRestoreWallet.click(); + + // Clicking on restore opens in this setup a new page for legal + const newPage = await context.pages()[context.pages().length - 1]; + await expect(newPage.url()).toContain('legal'); + // old page needs to be closed as the test is continuing in the new tab + const pages = await context.pages(); + await pages[0].close(); // pages[0] is the first (oldest) page + const onboardingpage2 = new Onboarding(newPage); + + await onboardingpage2.buttonAccept.click(); + await expect(newPage.url()).toContain('restore'); + await onboardingpage2.checkRestoreWalletSeedPhrasePage(); + + // TODO: There is an bug that the page is refreshed after clicking on any button: https://linear.app/xverseapp/issue/ENG-4028/restore-wallet-reload-page-instead-of-showing-error-message + await onboardingpage2.button24SeedPhrase.click(); + await onboardingpage2.checkRestoreWalletSeedPhrasePage(); + + const seedWords = JSON.parse(fs.readFileSync(filePathSeedWords, 'utf8')); + + for (let i = 0; i < seedWords.length; i++) { + await onboardingpage2.inputWord(i).fill(seedWords[i]); + } + await expect(onboardingpage2.buttonContinue).toBeEnabled(); + await onboardingpage2.buttonContinue.click(); + await onboardingpage2.inputPassword.fill(strongPW); + await onboardingpage2.buttonContinue.click(); + await onboardingpage2.inputPassword.fill(strongPW); + await onboardingpage2.buttonContinue.click(); + await expect(onboardingpage2.imageSuccess).toBeVisible(); + await expect(onboardingpage2.headingWalletRestored).toBeVisible(); + await expect(onboardingpage2.buttonCloseTab).toBeVisible(); + // Open the startpage directly via URL + await newPage.goto(`chrome-extension://${extensionId}/popup.html`); + const startPage = new StartPage(newPage); + await startPage.checkVisuals(); + + const balanceText = await startPage.balance.innerText(); + await expect(balanceText).toBe('$0.00'); + + await startPage.allupperButtons.nth(1).click(); + + // Get the Addresses + const addressBitcoinCheck = await startPage.getAddress(startPage.buttonCopyBitcoinAddress); + const addressOrdinalsCheck = await startPage.getAddress(startPage.buttonCopyOrdinalsAddress); + // Stack Address doesn't have the confirm message + await expect(startPage.buttonCopyStacksAddress).toBeVisible(); + await startPage.buttonCopyStacksAddress.click(); + const addressStackCheck = await newPage.evaluate('navigator.clipboard.readText()'); + + // Read and parse the file + const rawData = fs.readFileSync(filePathAddresses, 'utf8'); + const { addressBitcoin, addressOrdinals, addressStack } = JSON.parse(rawData); + + // Check if the Addresses are the same as from the file + await expect(addressBitcoin).toBe(addressBitcoinCheck); + await expect(addressOrdinals).toBe(addressOrdinalsCheck); + await expect(addressStack).toBe(addressStackCheck); + }); + }); +}); diff --git a/tests/specs/healthcheck.spec.ts b/tests/specs/healthcheck.spec.ts index ea9275fc..e7ac4f77 100644 --- a/tests/specs/healthcheck.spec.ts +++ b/tests/specs/healthcheck.spec.ts @@ -6,7 +6,7 @@ test.describe('healthcheck', () => { await context.close(); }); - test.skip('healthcheck', async ({ page, extensionId }) => { + test('healthcheck #smoketest', async ({ page, extensionId }) => { await page.goto(`chrome-extension://${extensionId}/options.html#/landing`); const landingpage = new Landing(page); await landingpage.initialize(); diff --git a/tests/specs/onboarding.spec.ts b/tests/specs/onboarding.spec.ts index 486767db..dbe00edf 100644 --- a/tests/specs/onboarding.spec.ts +++ b/tests/specs/onboarding.spec.ts @@ -1,10 +1,10 @@ +import * as bip39 from 'bip39'; import { expect, test } from '../fixtures/base'; import { passwordTestCases } from '../fixtures/passwordTestData'; -import Landing from '../pages/landing'; import Onboarding from '../pages/onboarding'; +import StartPage from '../pages/startPage'; -// TODO outsoure Password value -const strongPW = 'Admin12345567!!!!'; +const strongPW = Onboarding.generateSecurePasswordCrypto(); test.describe('onboarding flow', () => { test.beforeEach(async ({ page, extensionId, context }) => { @@ -22,38 +22,25 @@ test.describe('onboarding flow', () => { } }); - test('visual check legal page', async ({ page, context }) => { - const landingpage = new Landing(page); - - // Click on create Wallet and check legal page elements - await landingpage.buttonCreateWallet.click(); + test('visual check legal page', async ({ page, context, extensionId }) => { const onboardingpage = new Onboarding(page); - await expect(page.url()).toContain('legal'); + // Skip Landing and go directly to legal via URL + await page.goto(`chrome-extension://${extensionId}/options.html#/legal`); await onboardingpage.checkLegalPage(context); }); // Visual check of the first page for backup - test('visual check backup page main', async ({ page }) => { - const landingpage = new Landing(page); - - await landingpage.buttonCreateWallet.click(); + test('visual check backup page main #smoketest', async ({ page }) => { const onboardingpage = new Onboarding(page); - await expect(page.url()).toContain('legal'); - await onboardingpage.buttonAccept.click(); - await expect(page.url()).toContain('backup'); + await onboardingpage.navigateToBackupPage(); await onboardingpage.checkBackupPage(); }); // Visual check of the first page for password creation - test('skip backup and visual check password page', async ({ page }) => { - const landingpage = new Landing(page); - - await landingpage.buttonCreateWallet.click(); + test('skip backup and visual check password page', async ({ page, extensionId }) => { const onboardingpage = new Onboarding(page); - await expect(page.url()).toContain('legal'); - await onboardingpage.buttonAccept.click(); - await expect(page.url()).toContain('backup'); - await onboardingpage.buttonBackupLater.click(); + // Skip landing and go directly to create password via URL + await page.goto(`chrome-extension://${extensionId}/options.html#/create-password`); await expect(page.url()).toContain('create-password'); await onboardingpage.checkPasswordPage(); await onboardingpage.buttonBack.click(); @@ -61,14 +48,9 @@ test.describe('onboarding flow', () => { }); // No Wallet is created in this step as we only check the display of the error messages and that you can't create a wallet if passwords don't align - test('Skip backup and check password error messages', async ({ page }) => { - const landingpage = new Landing(page); - - await landingpage.buttonCreateWallet.click(); + test('Skip backup and check password error messages #smoketest', async ({ page }) => { const onboardingpage = new Onboarding(page); - await expect(page.url()).toContain('legal'); - await onboardingpage.buttonAccept.click(); - await expect(page.url()).toContain('backup'); + await onboardingpage.navigateToBackupPage(); await onboardingpage.buttonBackupLater.click(); await expect(page.url()).toContain('create-password'); @@ -89,9 +71,7 @@ test.describe('onboarding flow', () => { await onboardingpage.buttonContinue.click(); await expect(onboardingpage.errorMessage2).toBeVisible(); // multiple times clicking on continue to check that the user stays on the page and can't continue even of clicked multiple times - await onboardingpage.buttonContinue.click(); - await onboardingpage.buttonContinue.click(); - await onboardingpage.buttonContinue.click(); + await Onboarding.multipleClickCheck(onboardingpage.buttonContinue); await expect(onboardingpage.errorMessage2).toBeVisible(); await onboardingpage.buttonBack.click(); await expect(onboardingpage.inputPassword).toHaveValue(/.+/); @@ -100,65 +80,125 @@ test.describe('onboarding flow', () => { await expect(onboardingpage.errorMessage2).toBeVisible(); }); - test('backup seedphrase and successfully create a wallet', async ({ page, context }) => { - const landingpage = new Landing(page); - - await landingpage.buttonCreateWallet.click(); + test('Restore wallet error message check for false 12 word seed phrase #smoketest', async ({ + page, + }) => { const onboardingpage = new Onboarding(page); - await expect(page.url()).toContain('legal'); - await onboardingpage.buttonAccept.click(); - await expect(page.url()).toContain('backup'); - await onboardingpage.buttonBackupNow.click(); - await expect(page.url()).toContain('backupWalletSteps'); - await expect(onboardingpage.buttonContinue).toBeDisabled(); - await expect(onboardingpage.buttonShowSeed).toBeVisible(); - await expect(onboardingpage.firstParagraphBackupStep).toBeVisible(); - await onboardingpage.buttonShowSeed.click(); + await onboardingpage.navigateToRestorePage(); + + // TODO: There is an bug that the page is refreshed after clicking on any button: https://linear.app/xverseapp/issue/ENG-4028/restore-wallet-reload-page-instead-of-showing-error-message + await onboardingpage.button24SeedPhrase.click(); + await onboardingpage.checkRestoreWalletSeedPhrasePage(); + + // get 12 words from bip39 + const mnemonic = bip39.generateMnemonic(); + const wordsArray = mnemonic.split(' '); // Split the mnemonic by spaces + + // We only input 11 word to cause the error message + for (let i = 0; i < wordsArray.length - 1; i++) { + await onboardingpage.inputWord(i).fill(wordsArray[i]); + } + + await expect(onboardingpage.buttonContinue).toBeEnabled(); + await onboardingpage.buttonContinue.click(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(12); + await expect(onboardingpage.buttonContinue).toBeEnabled(); + await expect(onboardingpage.errorMessageSeedPhrase).toBeVisible(); + + // multiple times clicking on continue to check that the user stays on the page and can't continue even of clicked multiple times + await Onboarding.multipleClickCheck(onboardingpage.buttonContinue); + await expect(page.url()).toContain('restoreWallet'); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(12); + await expect(onboardingpage.errorMessageSeedPhrase).toBeVisible(); + + // empty all fields and check if continue button is disabled + for (let i = 0; i < 12; i++) { + await onboardingpage.inputWord(i).clear(); + } + + await expect(onboardingpage.buttonContinue).toBeDisabled(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(12); + await expect(onboardingpage.errorMessageSeedPhrase).toBeHidden(); + }); + + test('Restore wallet Error Message check for false 24 word seed phrase', async ({ page }) => { + const onboardingpage = new Onboarding(page); + await onboardingpage.navigateToRestorePage(); + + await onboardingpage.button24SeedPhrase.click(); + // TODO: There is an bug that the page is refreshed after clicking on any button https://linear.app/xverseapp/issue/ENG-4028/restore-wallet-reload-page-instead-of-showing-error-message + await onboardingpage.checkRestoreWalletSeedPhrasePage(); + await onboardingpage.button24SeedPhrase.click(); + + // All input fields should now be visible and enabled + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(0); + await expect(onboardingpage.inputSeedPhraseWord).toHaveCount(24); + + // get 24 words from bip39 + const mnemonic = bip39.generateMnemonic(256); + const wordsArray = mnemonic.split(' '); // Split the mnemonic by spaces + + for (let i = 0; i < wordsArray.length - 1; i++) { + await onboardingpage.inputWord(i).fill(wordsArray[i]); + } await expect(onboardingpage.buttonContinue).toBeEnabled(); - const seedWords = await onboardingpage.textSeedWords.allTextContents(); await onboardingpage.buttonContinue.click(); - // check if 12 words are displayed - await expect(onboardingpage.buttonSeedWords).toHaveCount(12); - await expect(onboardingpage.secondParagraphBackupStep).toBeVisible(); - let seedword = await onboardingpage.selectSeedWord(seedWords); + // As the seed phrase is not complete an error should be shown and the continue button is still enabled + await expect(onboardingpage.buttonContinue).toBeEnabled(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(0); + await expect(onboardingpage.errorMessageSeedPhrase).toBeVisible(); - // get all displayed values and filter the value from the actual seedphrase out to do an error message check - const buttonValues = await onboardingpage.buttonSeedWords.evaluateAll((buttons) => - buttons.map((button) => { - // Assert that the button is an HTMLButtonElement to access the `value` property - if (button instanceof HTMLButtonElement) { - return button.value; - } - return 'testvalue'; - }), - ); + // multiple times clicking on continue to check that the user stays on the page and can't continue even of clicked multiple times + await Onboarding.multipleClickCheck(onboardingpage.buttonContinue); + await expect(page.url()).toContain('restoreWallet'); - const filteredValues = buttonValues.filter((value) => value !== seedword); - const randomValue = filteredValues[Math.floor(Math.random() * filteredValues.length)]; - await page.locator(`button[value="${randomValue}"]`).click(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(0); + await expect(onboardingpage.errorMessageSeedPhrase).toBeVisible(); - // Check if error message is displayed when clicking the wrong seedword - await expect(page.locator('p:has-text("This word is not")')).toBeVisible(); + // empty all fields and check if continue button is disabled + for (let i = 0; i < 24; i++) { + await onboardingpage.inputWord(i).clear(); + } + await expect(onboardingpage.buttonContinue).toBeDisabled(); + await expect(onboardingpage.errorMessageSeedPhrase).toBeHidden(); + }); - await page.locator(`button[value="${seedword}"]`).click(); - seedword = await onboardingpage.selectSeedWord(seedWords); - await page.locator(`button[value="${seedword}"]`).click(); - seedword = await onboardingpage.selectSeedWord(seedWords); - await page.locator(`button[value="${seedword}"]`).click(); + test('Restore wallet check switch 12 to 24 seed phrase', async ({ page, extensionId }) => { + const onboardingpage = new Onboarding(page); - // TODO: currently the error messages as not shown for the passwords in this step so this check needs to be commment out until it is fixed - /* for (const testCase of passwordTestCases) { - await onboardingpage.testPasswordInput(testCase); - } */ + // Skip Landing and go directly to restore wallet via URL + await page.goto(`chrome-extension://${extensionId}/options.html#/restoreWallet`); + await onboardingpage.button24SeedPhrase.click(); + // TODO: There is an bug that the page is refreshed after clicking on any button https://linear.app/xverseapp/issue/ENG-4028/restore-wallet-reload-page-instead-of-showing-error-message + await onboardingpage.checkRestoreWalletSeedPhrasePage(); + + await onboardingpage.button24SeedPhrase.click(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(0); + await expect(onboardingpage.inputSeedPhraseWord).toHaveCount(24); + await expect(onboardingpage.buttonContinue).toBeDisabled(); + await expect(onboardingpage.button12SeedPhrase).toBeVisible(); + + await onboardingpage.button12SeedPhrase.click(); + await expect(onboardingpage.inputSeedPhraseWordDisabled).toHaveCount(12); + await expect(onboardingpage.inputSeedPhraseWord).toHaveCount(24); + await expect(onboardingpage.buttonContinue).toBeDisabled(); + await expect(onboardingpage.button24SeedPhrase).toBeVisible(); + }); + + test('Lock and login #smoketest', async ({ page, extensionId }) => { + const onboardingpage = new Onboarding(page); + const startpage = new StartPage(page); + await onboardingpage.createWalletSkipBackup(strongPW); + await page.goto(`chrome-extension://${extensionId}/popup.html`); + await expect(startpage.buttonMenu).toBeVisible(); + await startpage.buttonMenu.click(); + await expect(startpage.buttonLock).toBeVisible(); + await startpage.buttonLock.click(); + await expect(onboardingpage.inputPassword).toBeVisible(); await onboardingpage.inputPassword.fill(strongPW); - await onboardingpage.buttonContinue.click(); - await onboardingpage.inputPassword.fill(strongPW); - await onboardingpage.buttonContinue.click(); - - await expect(onboardingpage.imageSuccess).toBeVisible(); - await expect(onboardingpage.instruction).toBeVisible(); - await expect(onboardingpage.buttonCloseTab).toBeVisible(); + await onboardingpage.buttonUnlock.click(); + await startpage.checkVisuals(); }); });