Skip to content

Commit

Permalink
✨ feat(metamask): Add support for importing seed phrase
Browse files Browse the repository at this point in the history
  • Loading branch information
duckception committed Oct 5, 2023
1 parent e4dc605 commit cf32332
Show file tree
Hide file tree
Showing 17 changed files with 225 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './confirmSecretRecoveryPhrase'
export * from './createPassword'
23 changes: 23 additions & 0 deletions wallets/metamask/src/pages/OnboardingPage/actions/importWallet.ts
Original file line number Diff line number Diff line change
@@ -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()
}
1 change: 1 addition & 0 deletions wallets/metamask/src/pages/OnboardingPage/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './importWallet'
14 changes: 14 additions & 0 deletions wallets/metamask/src/pages/OnboardingPage/page.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions wallets/metamask/src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './OnboardingPage/page'
1 change: 1 addition & 0 deletions wallets/metamask/src/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './onboarding'
6 changes: 6 additions & 0 deletions wallets/metamask/src/selectors/onboarding/analyticsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector'

export default {
optIn: createDataTestSelector('metametrics-i-agree'),
optOut: createDataTestSelector('metametrics-no-thanks')
}
6 changes: 6 additions & 0 deletions wallets/metamask/src/selectors/onboarding/getStartedPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector'

export default {
createNewWallet: createDataTestSelector('onboarding-create-wallet'),
importWallet: createDataTestSelector('onboarding-import-wallet')
}
25 changes: 25 additions & 0 deletions wallets/metamask/src/selectors/onboarding/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions wallets/metamask/src/selectors/onboarding/pinExtensionPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector'

export default {
nextButton: createDataTestSelector('pin-extension-next'),
confirmButton: createDataTestSelector('pin-extension-done')
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createDataTestSelector } from '../../utils/selectors/createDataTestSelector'

export default {
confirmButton: createDataTestSelector('onboarding-complete-done')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const createDataTestSelector = (dataTestId: string) => {
return `[data-testid="${dataTestId}"]`
}
56 changes: 56 additions & 0 deletions wallets/metamask/test/e2e/metamask.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
40 changes: 0 additions & 40 deletions wallets/metamask/test/e2e/prepareExtension.spec.ts

This file was deleted.

0 comments on commit cf32332

Please sign in to comment.