From cf323321f98694b34381c3efc85cbee19e955f19 Mon Sep 17 00:00:00 2001 From: Daniel Izdebski Date: Thu, 5 Oct 2023 21:14:52 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(metamask):=20Add=20support=20f?= =?UTF-8?q?or=20importing=20seed=20phrase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helpers/confirmSecretRecoveryPhrase.ts | 30 ++++++++++ .../actions/helpers/createPassword.ts | 24 ++++++++ .../OnboardingPage/actions/helpers/index.ts | 2 + .../OnboardingPage/actions/importWallet.ts | 23 ++++++++ .../src/pages/OnboardingPage/actions/index.ts | 1 + .../metamask/src/pages/OnboardingPage/page.ts | 14 +++++ wallets/metamask/src/pages/index.ts | 1 + wallets/metamask/src/selectors/index.ts | 1 + .../src/selectors/onboarding/analyticsPage.ts | 6 ++ .../selectors/onboarding/getStartedPage.ts | 6 ++ .../src/selectors/onboarding/index.ts | 25 +++++++++ .../selectors/onboarding/pinExtensionPage.ts | 6 ++ .../onboarding/secretRecoveryPhrasePage.ts | 22 ++++++++ .../onboarding/walletCreationSuccessPage.ts | 5 ++ .../utils/selectors/createDataTestSelector.ts | 3 + wallets/metamask/test/e2e/metamask.spec.ts | 56 +++++++++++++++++++ .../test/e2e/prepareExtension.spec.ts | 40 ------------- 17 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 wallets/metamask/src/pages/OnboardingPage/actions/helpers/confirmSecretRecoveryPhrase.ts create mode 100644 wallets/metamask/src/pages/OnboardingPage/actions/helpers/createPassword.ts create mode 100644 wallets/metamask/src/pages/OnboardingPage/actions/helpers/index.ts create mode 100644 wallets/metamask/src/pages/OnboardingPage/actions/importWallet.ts create mode 100644 wallets/metamask/src/pages/OnboardingPage/actions/index.ts create mode 100644 wallets/metamask/src/pages/OnboardingPage/page.ts create mode 100644 wallets/metamask/src/pages/index.ts create mode 100644 wallets/metamask/src/selectors/index.ts create mode 100644 wallets/metamask/src/selectors/onboarding/analyticsPage.ts create mode 100644 wallets/metamask/src/selectors/onboarding/getStartedPage.ts create mode 100644 wallets/metamask/src/selectors/onboarding/index.ts create mode 100644 wallets/metamask/src/selectors/onboarding/pinExtensionPage.ts create mode 100644 wallets/metamask/src/selectors/onboarding/secretRecoveryPhrasePage.ts create mode 100644 wallets/metamask/src/selectors/onboarding/walletCreationSuccessPage.ts create mode 100644 wallets/metamask/src/utils/selectors/createDataTestSelector.ts create mode 100644 wallets/metamask/test/e2e/metamask.spec.ts delete mode 100644 wallets/metamask/test/e2e/prepareExtension.spec.ts diff --git a/wallets/metamask/src/pages/OnboardingPage/actions/helpers/confirmSecretRecoveryPhrase.ts b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/confirmSecretRecoveryPhrase.ts new file mode 100644 index 000000000..cf3162b5d --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/confirmSecretRecoveryPhrase.ts @@ -0,0 +1,30 @@ +import type { Page } from '@playwright/test' +import { SecretRecoveryPhrasePageSelectors } from '../../../../selectors' + +const StepSelectors = SecretRecoveryPhrasePageSelectors.recoveryStep + +export async function confirmSecretRecoveryPhrase(page: Page, seedPhrase: string) { + const seedPhraseWords = seedPhrase.split(' ') + const seedPhraseLength = seedPhraseWords.length + + // TODO: This should be validated! + await page + .locator(StepSelectors.selectNumberOfWordsDropdown) + .selectOption(StepSelectors.selectNumberOfWordsOption(seedPhraseLength)) + + for (const [index, word] of seedPhraseWords.entries()) { + await page.locator(StepSelectors.secretRecoveryPhraseWord(index)).fill(word) + } + + const confirmSRPButton = page.locator(StepSelectors.confirmSecretRecoveryPhraseButton) + + if (await confirmSRPButton.isDisabled()) { + const errorText = await page.locator(StepSelectors.error).textContent({ + timeout: 1000 + }) + + throw new Error(`[ConfirmSecretRecoveryPhrase] Invalid seed phrase. Error from MetaMask: ${errorText}`) + } + + await confirmSRPButton.click() +} diff --git a/wallets/metamask/src/pages/OnboardingPage/actions/helpers/createPassword.ts b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/createPassword.ts new file mode 100644 index 000000000..43c27f23a --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/createPassword.ts @@ -0,0 +1,24 @@ +import type { Page } from '@playwright/test' +import { SecretRecoveryPhrasePageSelectors } from '../../../../selectors' + +const StepSelectors = SecretRecoveryPhrasePageSelectors.passwordStep + +export async function createPassword(page: Page, password: string) { + await page.locator(StepSelectors.passwordInput).fill(password) + await page.locator(StepSelectors.confirmPasswordInput).fill(password) + + // Using `locator.click()` instead of `locator.check()` as a workaround due to dynamically appearing elements. + await page.locator(StepSelectors.acceptTermsCheckbox).click() + + const importWalletButton = page.locator(StepSelectors.importWalletButton) + + if (await importWalletButton.isDisabled()) { + const errorText = await page.locator(StepSelectors.error).textContent({ + timeout: 1000 + }) + + throw new Error(`[CreatePassword] Invalid password. Error from MetaMask: ${errorText}`) + } + + await importWalletButton.click() +} diff --git a/wallets/metamask/src/pages/OnboardingPage/actions/helpers/index.ts b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/index.ts new file mode 100644 index 000000000..79b348f20 --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/actions/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './confirmSecretRecoveryPhrase' +export * from './createPassword' diff --git a/wallets/metamask/src/pages/OnboardingPage/actions/importWallet.ts b/wallets/metamask/src/pages/OnboardingPage/actions/importWallet.ts new file mode 100644 index 000000000..82dab6afe --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/actions/importWallet.ts @@ -0,0 +1,23 @@ +import type { Page } from '@playwright/test' +import { + AnalyticsPageSelectors, + GetStartedPageSelectors, + PinExtensionPageSelectors, + WalletCreationSuccessPageSelectors +} from '../../../selectors' +import { confirmSecretRecoveryPhrase, createPassword } from './helpers' + +export async function importWallet(page: Page, seedPhrase: string, password: string) { + await page.locator(GetStartedPageSelectors.importWallet).click() + + await page.locator(AnalyticsPageSelectors.optOut).click() + + // Secret Recovery Phrase Page + await confirmSecretRecoveryPhrase(page, seedPhrase) + await createPassword(page, password) + + await page.locator(WalletCreationSuccessPageSelectors.confirmButton).click() + + await page.locator(PinExtensionPageSelectors.nextButton).click() + await page.locator(PinExtensionPageSelectors.confirmButton).click() +} diff --git a/wallets/metamask/src/pages/OnboardingPage/actions/index.ts b/wallets/metamask/src/pages/OnboardingPage/actions/index.ts new file mode 100644 index 000000000..2b7f14e22 --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/actions/index.ts @@ -0,0 +1 @@ +export * from './importWallet' diff --git a/wallets/metamask/src/pages/OnboardingPage/page.ts b/wallets/metamask/src/pages/OnboardingPage/page.ts new file mode 100644 index 000000000..3bd0f827d --- /dev/null +++ b/wallets/metamask/src/pages/OnboardingPage/page.ts @@ -0,0 +1,14 @@ +import type { Page } from '@playwright/test' +import { importWallet } from './actions' + +export class OnboardingPage { + readonly page: Page + + constructor(page: Page) { + this.page = page + } + + async importWallet(seedPhrase: string, password: string) { + return importWallet(this.page, seedPhrase, password) + } +} diff --git a/wallets/metamask/src/pages/index.ts b/wallets/metamask/src/pages/index.ts new file mode 100644 index 000000000..2db1a41d6 --- /dev/null +++ b/wallets/metamask/src/pages/index.ts @@ -0,0 +1 @@ +export * from './OnboardingPage/page' diff --git a/wallets/metamask/src/selectors/index.ts b/wallets/metamask/src/selectors/index.ts new file mode 100644 index 000000000..cb00e1b6d --- /dev/null +++ b/wallets/metamask/src/selectors/index.ts @@ -0,0 +1 @@ +export * from './onboarding' diff --git a/wallets/metamask/src/selectors/onboarding/analyticsPage.ts b/wallets/metamask/src/selectors/onboarding/analyticsPage.ts new file mode 100644 index 000000000..6f807d41e --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/analyticsPage.ts @@ -0,0 +1,6 @@ +import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector' + +export default { + optIn: createDataTestSelector('metametrics-i-agree'), + optOut: createDataTestSelector('metametrics-no-thanks') +} diff --git a/wallets/metamask/src/selectors/onboarding/getStartedPage.ts b/wallets/metamask/src/selectors/onboarding/getStartedPage.ts new file mode 100644 index 000000000..c7aa4d24c --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/getStartedPage.ts @@ -0,0 +1,6 @@ +import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector' + +export default { + createNewWallet: createDataTestSelector('onboarding-create-wallet'), + importWallet: createDataTestSelector('onboarding-import-wallet') +} diff --git a/wallets/metamask/src/selectors/onboarding/index.ts b/wallets/metamask/src/selectors/onboarding/index.ts new file mode 100644 index 000000000..85e9fd7a3 --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/index.ts @@ -0,0 +1,25 @@ +import AnalyticsPageSelectors from './analyticsPage' +import GetStartedPageSelectors from './getStartedPage' +import PinExtensionPageSelectors from './pinExtensionPage' +import SecretRecoveryPhrasePageSelectors from './secretRecoveryPhrasePage' +import WalletCreationSuccessPageSelectors from './walletCreationSuccessPage' + +// biome-ignore format: empty lines should be preserved +export { + // Initial Welcome Page + GetStartedPageSelectors, + + // 2nd Page + AnalyticsPageSelectors, + + // 3rd Page with two steps: + // - Input Secret Recovery Phrase + // - Create Password + SecretRecoveryPhrasePageSelectors, + + // 4th Page + WalletCreationSuccessPageSelectors, + + // 5th Page + PinExtensionPageSelectors +} diff --git a/wallets/metamask/src/selectors/onboarding/pinExtensionPage.ts b/wallets/metamask/src/selectors/onboarding/pinExtensionPage.ts new file mode 100644 index 000000000..9a737d94a --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/pinExtensionPage.ts @@ -0,0 +1,6 @@ +import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector' + +export default { + nextButton: createDataTestSelector('pin-extension-next'), + confirmButton: createDataTestSelector('pin-extension-done') +} diff --git a/wallets/metamask/src/selectors/onboarding/secretRecoveryPhrasePage.ts b/wallets/metamask/src/selectors/onboarding/secretRecoveryPhrasePage.ts new file mode 100644 index 000000000..4d0a92339 --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/secretRecoveryPhrasePage.ts @@ -0,0 +1,22 @@ +import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector' + +const recoveryStep = { + selectNumberOfWordsDropdown: '.import-srp__number-of-words-dropdown > .dropdown__select', + selectNumberOfWordsOption: (option: number | string) => `${option}`, + secretRecoveryPhraseWord: (index: number) => createDataTestSelector(`import-srp__srp-word-${index}`), + confirmSecretRecoveryPhraseButton: createDataTestSelector('import-srp-confirm'), + error: '.actionable-message.actionable-message--danger.import-srp__srp-error > .actionable-message__message' +} + +const passwordStep = { + passwordInput: createDataTestSelector('create-password-new'), + confirmPasswordInput: createDataTestSelector('create-password-confirm'), + acceptTermsCheckbox: createDataTestSelector('create-password-terms'), + importWalletButton: createDataTestSelector('create-password-import'), + error: `${createDataTestSelector('create-password-confirm')} + h6` +} + +export default { + recoveryStep, + passwordStep +} diff --git a/wallets/metamask/src/selectors/onboarding/walletCreationSuccessPage.ts b/wallets/metamask/src/selectors/onboarding/walletCreationSuccessPage.ts new file mode 100644 index 000000000..0b6315f1c --- /dev/null +++ b/wallets/metamask/src/selectors/onboarding/walletCreationSuccessPage.ts @@ -0,0 +1,5 @@ +import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector' + +export default { + confirmButton: createDataTestSelector('onboarding-complete-done') +} diff --git a/wallets/metamask/src/utils/selectors/createDataTestSelector.ts b/wallets/metamask/src/utils/selectors/createDataTestSelector.ts new file mode 100644 index 000000000..a57d1a482 --- /dev/null +++ b/wallets/metamask/src/utils/selectors/createDataTestSelector.ts @@ -0,0 +1,3 @@ +export const createDataTestSelector = (dataTestId: string) => { + return `[data-testid="${dataTestId}"]` +} diff --git a/wallets/metamask/test/e2e/metamask.spec.ts b/wallets/metamask/test/e2e/metamask.spec.ts new file mode 100644 index 000000000..31abe01f3 --- /dev/null +++ b/wallets/metamask/test/e2e/metamask.spec.ts @@ -0,0 +1,56 @@ +import { type Page, chromium, test as base } from '@playwright/test' +import { OnboardingPage } from '../../src/pages' +import { prepareExtension } from '../../src/prepareExtension' + +const DEFAULT_SEED_PHRASE = 'test test test test test test test test test test test junk' +const DEFAULT_PASSWORD = 'Tester@1234' + +// Fixture for the test. +const test = base.extend({ + context: async ({ context: _ }, use) => { + const metamaskPath = await prepareExtension() + + // biome-ignore format: the array should not be formatted + const browserArgs = [ + `--disable-extensions-except=${metamaskPath}`, + `--load-extension=${metamaskPath}` + ] + + if (process.env.HEADLESS) { + browserArgs.push('--headless=new') + } + + const context = await chromium.launchPersistentContext('', { + headless: false, + args: browserArgs + }) + + try { + await context.waitForEvent('page', { timeout: 5000 }) + } catch { + throw new Error('[FIXTURE] MetaMask extension did not load in time') + } + + await use(context) + }, + page: async ({ context }, use) => { + const metamaskOnboardingPage = context.pages()[1] as Page + await use(metamaskOnboardingPage) + } +}) + +const { describe, expect } = test + +// Currently testing only happy paths until we have proper setup for parallel tests. +describe('MetaMask', () => { + describe('importWallet', () => { + test('should go through the onboarding flow and import wallet from seed phrase', async ({ page }) => { + const onboardingPage = new OnboardingPage(page) + + await onboardingPage.importWallet(DEFAULT_SEED_PHRASE, DEFAULT_PASSWORD) + + await expect(page.getByText('Account 1')).toBeVisible() + await expect(page.getByText('0xf39...2266')).toBeVisible() + }) + }) +}) diff --git a/wallets/metamask/test/e2e/prepareExtension.spec.ts b/wallets/metamask/test/e2e/prepareExtension.spec.ts deleted file mode 100644 index b034c1304..000000000 --- a/wallets/metamask/test/e2e/prepareExtension.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { type Page, chromium, test as base } from '@playwright/test' -import { prepareExtension } from '../../src/prepareExtension' - -const test = base.extend({ - context: async ({ context: _ }, use) => { - const metamaskPath = await prepareExtension() - - const browserArgs = [`--disable-extensions-except=${metamaskPath}`, `--load-extension=${metamaskPath}`] - - if (process.env.HEADLESS) { - browserArgs.push('--headless=new') - } - - const context = await chromium.launchPersistentContext('', { - headless: false, - args: browserArgs - }) - - try { - await context.waitForEvent('page', { timeout: 5000 }) - } catch { - throw new Error('[FIXTURE] MetaMask extension did not load in time') - } - - await use(context) - }, - page: async ({ context }, use) => { - const metamaskOnboardingPage = context.pages()[1] as Page - await use(metamaskOnboardingPage) - } -}) - -const { describe, expect } = test - -describe('prepareExtension', () => { - test('onboarding page opens up', async ({ page }) => { - await expect(page).toHaveTitle('MetaMask') - await expect(page.getByRole('heading', { name: "Let's get started" })).toBeVisible() - }) -})