diff --git a/.storybook/main.js b/.storybook/main.js index d63d924aa2e2..a9d6070a5917 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -52,6 +52,9 @@ module.exports = { config.resolve.alias['@ethereumjs/util'] = require.resolve( '../ui/__mocks__/ethereumjs-util.js', ); + config.resolve.alias['./useNftCollectionsMetadata'] = require.resolve( + '../ui/__mocks__/useNftCollectionsMetadata.js', + ); config.resolve.fallback = { child_process: false, constants: false, diff --git a/development/README.md b/development/README.md index 33ab036975e4..86733ef172e6 100644 --- a/development/README.md +++ b/development/README.md @@ -65,7 +65,7 @@ or `https://api.segment.io/v1/batch` respectively. 2. To display Sentry logs, include `DEBUG=metamask:sentry:*` in `.metamaskrc`. -3. To display more verbose logs if not in a developer build, include `METAMASK_DEBUG=true` in `.metamaskrc`. +3. To display more verbose logs if not in a developer build, include `METAMASK_DEBUG=1` in `.metamaskrc`. 4. Ensure metrics are enabled during onboarding or via `Settings > Security & privacy > Participate in MetaMetrics`. diff --git a/test/e2e/page-objects/flows/login.flow.ts b/test/e2e/page-objects/flows/login.flow.ts index 87239e3f19f1..fcd0bcb22d8a 100644 --- a/test/e2e/page-objects/flows/login.flow.ts +++ b/test/e2e/page-objects/flows/login.flow.ts @@ -18,10 +18,6 @@ export const loginWithoutBalanceValidation = async ( const loginPage = new LoginPage(driver); await loginPage.check_pageIsLoaded(); await loginPage.loginToHomepage(password); - - // user should land on homepage after successfully logging in with password - const homePage = new HomePage(driver); - await homePage.check_pageIsLoaded(); }; /** @@ -37,10 +33,14 @@ export const loginWithBalanceValidation = async ( password?: string, ) => { await loginWithoutBalanceValidation(driver, password); + // user should land on homepage after successfully logging in with password + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + // Verify the expected balance on the homepage if (ganacheServer) { - await new HomePage(driver).check_ganacheBalanceIsDisplayed(ganacheServer); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); } else { - await new HomePage(driver).check_expectedBalanceIsDisplayed(); + await homePage.check_expectedBalanceIsDisplayed(); } }; diff --git a/test/e2e/page-objects/flows/onboarding.flow.ts b/test/e2e/page-objects/flows/onboarding.flow.ts new file mode 100644 index 000000000000..70a4f7df82f2 --- /dev/null +++ b/test/e2e/page-objects/flows/onboarding.flow.ts @@ -0,0 +1,64 @@ +import { Driver } from '../../webdriver/driver'; +import OnboardingMetricsPage from '../pages/onboarding/onboarding-metrics-page'; +import OnboardingPasswordPage from '../pages/onboarding/onboarding-password-page'; +import OnboardingSrpPage from '../pages/onboarding/onboarding-srp-page'; +import StartOnboardingPage from '../pages/onboarding/start-onboarding-page'; +import SecureWalletPage from '../pages/onboarding/secure-wallet-page'; +import OnboardingCompletePage from '../pages/onboarding/onboarding-complete-page'; + +export const importSRPOnboardingFlow = async (driver: Driver) => { + console.log('start import srp onboarding flow '); + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + await onboardingSrpPage.fillSrp(); + await onboardingSrpPage.clickConfirmButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.createImportedWalletPassword(); +}; + +export const completeCreateNewWalletOnboardingFlow = async (driver: Driver) => { + console.log('start to complete create new wallet onboarding flow '); + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickCreateWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.createWalletPassword(); + + const secureWalletPage = new SecureWalletPage(driver); + await secureWalletPage.check_pageIsLoaded(); + await secureWalletPage.revealAndConfirmSRP(); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_congratulationsMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; + +export const completeImportSRPOnboardingFlow = async (driver: Driver) => { + console.log('start to complete import srp onboarding flow '); + await importSRPOnboardingFlow(driver); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 8bd29ea8c602..360b7a2a5d72 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -15,8 +15,16 @@ class HeaderNavbar { private readonly mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; + private readonly selectNetworkMessage = { + text: 'Select a network', + tag: 'h4', + }; + private readonly settingsButton = '[data-testid="global-menu-settings"]'; + private readonly switchNetworkDropDownButton = + '[data-testid="network-display"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -63,6 +71,28 @@ class HeaderNavbar { await this.driver.clickElement(this.settingsButton); } + /** + * Switches to the specified network. + * + * @param networkName - The name of the network to switch to. + */ + async switchToNetwork(networkName: string): Promise { + console.log(`Switch to network ${networkName} in header bar`); + await this.driver.clickElement(this.switchNetworkDropDownButton); + await this.driver.waitForSelector(this.selectNetworkMessage); + await this.driver.clickElementAndWaitToDisappear( + `[data-testid="${networkName}"]`, + ); + // check the toaster message is displayed and the network is correctly selected + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully added!`, + }); + await this.driver.waitForSelector( + `${this.switchNetworkDropDownButton}[aria-label="Network Menu ${networkName}"]`, + ); + } + /** * Verifies that the displayed account label in header matches the expected label. * diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 326ecc3188b7..432106c5c520 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -13,6 +13,11 @@ class HomePage { private readonly balance = '[data-testid="eth-overview__primary-currency"]'; + private readonly basicFunctionalityOffWarningMessage = { + text: 'Basic functionality is off', + css: '.mm-banner-alert', + }; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; private readonly confirmedTransactions = { @@ -60,6 +65,13 @@ class HomePage { await this.driver.clickElement(this.activityTab); } + async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { + console.log( + 'Check if basic functionality off warning message is displayed on homepage', + ); + await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); + } + /** * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts new file mode 100644 index 000000000000..827f89899bad --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts @@ -0,0 +1,95 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingCompletePage { + private driver: Driver; + + private readonly congratulationsMessage = { + text: 'Congratulations!', + tag: 'h2', + }; + + private readonly defaultPrivacySettingsButton = { + text: 'Manage default privacy settings', + tag: 'button', + }; + + private readonly installCompleteMessage = { + text: 'Your MetaMask install is complete!', + tag: 'h2', + }; + + private readonly onboardingCompleteDoneButton = + '[data-testid="onboarding-complete-done"]'; + + private readonly pinExtensionDoneButton = + '[data-testid="pin-extension-done"]'; + + private readonly pinExtensionMessage = { + text: 'Click browser extension icon to access it instantly', + tag: 'p', + }; + + private readonly pinExtensionNextButton = + '[data-testid="pin-extension-next"]'; + + private readonly walletReadyMessage = { + text: 'Your wallet is ready', + tag: 'h2', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.defaultPrivacySettingsButton, + this.onboardingCompleteDoneButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding wallet creation complete page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding wallet creation complete page is loaded'); + } + + async clickCreateWalletDoneButton(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.onboardingCompleteDoneButton, + ); + } + + async completeOnboarding(): Promise { + console.log('Complete onboarding'); + await this.clickCreateWalletDoneButton(); + await this.driver.waitForSelector(this.installCompleteMessage); + await this.driver.clickElement(this.pinExtensionNextButton); + + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.waitForSelector(this.pinExtensionMessage); + await this.driver.waitForElementToStopMoving(this.pinExtensionDoneButton); + await this.driver.clickElementAndWaitToDisappear( + this.pinExtensionDoneButton, + ); + } + + async navigateToDefaultPrivacySettings(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.defaultPrivacySettingsButton, + ); + } + + async check_congratulationsMessageIsDisplayed(): Promise { + await this.driver.waitForSelector(this.congratulationsMessage); + } + + async check_walletReadyMessageIsDisplayed(): Promise { + await this.driver.waitForSelector(this.walletReadyMessage); + } +} + +export default OnboardingCompletePage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts new file mode 100644 index 000000000000..2982acaa40c0 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingMetricsPage { + private driver: Driver; + + private readonly metametricsMessage = { + text: 'Help us improve MetaMask', + tag: 'h2', + }; + + private readonly noThanksButton = '[data-testid="metametrics-no-thanks"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.metametricsMessage, + this.noThanksButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding metametrics page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding metametrics page is loaded'); + } + + async clickNoThanksButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.noThanksButton); + } +} + +export default OnboardingMetricsPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts new file mode 100644 index 000000000000..81c2d21aceb6 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts @@ -0,0 +1,112 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; +import { WALLET_PASSWORD } from '../../../helpers'; + +class OnboardingPasswordPage { + private driver: Driver; + + private readonly confirmPasswordInput = + '[data-testid="create-password-confirm"]'; + + private readonly createPasswordMessage = { + text: 'Create password', + tag: 'h2', + }; + + private readonly createWalletButton = + '[data-testid="create-password-wallet"]'; + + private readonly importWalletButton = + '[data-testid="create-password-import"]'; + + private readonly incorrectPasswordWarningMessage = { + text: "Passwords don't match", + tag: 'h6', + }; + + private readonly newPasswordInput = '[data-testid="create-password-new"]'; + + private readonly passwordTerms = '[data-testid="create-password-terms"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.createPasswordMessage, + this.newPasswordInput, + this.confirmPasswordInput, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for create password page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding password page is loaded'); + } + + /** + * Create a password for new imported wallet + * + * @param newPassword - The new password to create. Defaults to WALLET_PASSWORD. + * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + */ + async createImportedWalletPassword( + newPassword: string = WALLET_PASSWORD, + confirmPassword: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new imported wallet'); + await this.fillWalletPassword(newPassword, confirmPassword); + await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); + } + + /** + * Create a password for new created wallet + * + * @param newPassword - The new password to create. Defaults to WALLET_PASSWORD. + * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + */ + async createWalletPassword( + newPassword: string = WALLET_PASSWORD, + confirmPassword: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new created wallet'); + await this.fillWalletPassword(newPassword, confirmPassword); + await this.driver.clickElementAndWaitToDisappear(this.createWalletButton); + } + + /** + * Fill the wallet password fields + * + * @param newPassword - The new password to fill. + * @param confirmPassword - The confirm password to fill. + */ + async fillWalletPassword( + newPassword: string, + confirmPassword: string, + ): Promise { + console.log('Fill the wallet password fields'); + await this.driver.fill(this.newPasswordInput, newPassword); + await this.driver.fill(this.confirmPasswordInput, confirmPassword); + await this.driver.clickElement(this.passwordTerms); + } + + async check_confirmPasswordButtonIsDisabled(): Promise { + console.log('Check the confirm password button is disabled'); + const confirmPasswordButton = await this.driver.findElement( + this.createWalletButton, + ); + assert.equal(await confirmPasswordButton.isEnabled(), false); + } + + async check_incorrectPasswordWarningMessageIsDisplayed(): Promise { + console.log('Check the incorrect password warning message is displayed'); + await this.driver.waitForSelector(this.incorrectPasswordWarningMessage); + } +} + +export default OnboardingPasswordPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts new file mode 100644 index 000000000000..dac2ab447710 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts @@ -0,0 +1,194 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingPrivacySettingsPage { + private driver: Driver; + + private readonly assetsSettings = '[data-testid="category-item-Assets"]'; + + private readonly categoryBackButton = '[data-testid="category-back-button"]'; + + private readonly generalSettings = '[data-testid="category-item-General"]'; + + private readonly privacySettingsBackButton = + '[data-testid="privacy-settings-back-button"]'; + + private readonly securitySettings = '[data-testid="category-item-Security"]'; + + // General settings + private readonly basicFunctionalityCheckbox = + '[id="basic-configuration-checkbox"]'; + + private readonly basicFunctionalityToggle = + '[data-testid="basic-functionality-toggle"] .toggle-button'; + + private readonly basicFunctionalityTurnOffButton = { + text: 'Turn off', + tag: 'button', + }; + + private readonly basicFunctionalityTurnOffMessage = { + text: 'Turn off basic functionality', + tag: 'h4', + }; + + private readonly generalSettingsMessage = { text: 'General', tag: 'h2' }; + + // General settings - add custom network section + private readonly addCustomNetworkButton = { + text: 'Add a network', + tag: 'p', + }; + + private readonly addCustomNetworkFormMessage = { + text: 'Add a custom network', + tag: 'h4', + }; + + private readonly addRpcUrlButton = { + text: 'Add RPC URL', + tag: 'button', + }; + + private readonly addRpcUrlDialogMessage = { + text: 'Add RPC URL', + tag: 'h4', + }; + + private readonly addRpcUrlDropDown = '[data-testid="test-add-rpc-drop-down"]'; + + private readonly chainIdInput = '[data-testid="network-form-chain-id"]'; + + private readonly confirmAddCustomNetworkButton = { + text: 'Save', + tag: 'button', + }; + + private readonly confirmAddRpcUrlButton = { + text: 'Add URL', + tag: 'button', + }; + + private readonly currencySymbolInput = + '[data-testid="network-form-ticker-input"]'; + + private readonly networkNameInput = + '[data-testid="network-form-network-name"]'; + + private readonly rpcUrlInput = '[data-testid="rpc-url-input-test"]'; + + // Assets settings + private readonly assetsPrivacyToggle = '.toggle-button.toggle-button--on'; + + private readonly assetsSettingsMessage = { text: 'Assets', tag: 'h2' }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.generalSettings, + this.assetsSettings, + this.securitySettings, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding privacy settings page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding privacy settings page is loaded'); + } + + /** + * Adds a custom network to MetaMask during the onboarding process. + * + * @param networkName - The name of the custom network. + * @param chainId - The chain ID of the custom network. + * @param currencySymbol - The currency symbol for the custom network. + * @param networkUrl - The RPC URL for the custom network. + * @returns A promise that resolves when the custom network has been added. + */ + async addCustomNetwork( + networkName: string, + chainId: number, + currencySymbol: string, + networkUrl: string, + ): Promise { + await this.navigateToGeneralSettings(); + console.log('Adding custom network'); + await this.driver.clickElement(this.addCustomNetworkButton); + await this.driver.waitForSelector(this.addCustomNetworkFormMessage); + await this.driver.fill(this.networkNameInput, networkName); + await this.driver.fill(this.chainIdInput, chainId.toString()); + await this.driver.fill(this.currencySymbolInput, currencySymbol); + // Add rpc url + await this.driver.clickElement(this.addRpcUrlDropDown); + await this.driver.clickElement(this.addRpcUrlButton); + await this.driver.waitForSelector(this.addRpcUrlDialogMessage); + await this.driver.fill(this.rpcUrlInput, networkUrl); + await this.driver.clickElement(this.confirmAddRpcUrlButton); + await this.driver.clickElementAndWaitToDisappear( + this.confirmAddCustomNetworkButton, + ); + // Navigate back to default privacy settings + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + + /** + * Navigate back to the onboarding complete page. + */ + async navigateBackToOnboardingCompletePage(): Promise { + console.log('Navigate back to onboarding complete page'); + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.waitForElementToStopMoving( + this.privacySettingsBackButton, + ); + await this.driver.clickElementAndWaitToDisappear( + this.privacySettingsBackButton, + ); + } + + async navigateToGeneralSettings(): Promise { + console.log('Navigate to general settings'); + await this.check_pageIsLoaded(); + await this.driver.clickElement(this.generalSettings); + await this.driver.waitForSelector(this.generalSettingsMessage); + } + + /** + * Go to assets settings and toggle options, then navigate back. + */ + async toggleAssetsSettings(): Promise { + console.log('Toggle advanced assets settings in privacy settings'); + await this.check_pageIsLoaded(); + await this.driver.clickElement(this.assetsSettings); + await this.driver.waitForSelector(this.assetsSettingsMessage); + await Promise.all( + ( + await this.driver.findClickableElements(this.assetsPrivacyToggle) + ).map((toggle) => toggle.click()), + ); + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + + /** + * Go to general settings and toggle options, then navigate back. + */ + async toggleBasicFunctionalitySettings(): Promise { + console.log('Toggle basic functionality settings in privacy settings'); + await this.navigateToGeneralSettings(); + await this.driver.clickElement(this.basicFunctionalityToggle); + await this.driver.waitForSelector(this.basicFunctionalityTurnOffMessage); + await this.driver.clickElement(this.basicFunctionalityCheckbox); + await this.driver.clickElement(this.basicFunctionalityTurnOffButton); + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } +} + +export default OnboardingPrivacySettingsPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts new file mode 100644 index 000000000000..da3e74153c67 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts @@ -0,0 +1,105 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; +import { TEST_SEED_PHRASE } from '../../../helpers'; + +class OnboardingSrpPage { + private driver: Driver; + + private readonly srpConfirmButton = '[data-testid="import-srp-confirm"]'; + + private readonly srpDropdown = '.import-srp__number-of-words-dropdown'; + + private readonly srpDropdownOptions = + '.import-srp__number-of-words-dropdown option'; + + private readonly srpMessage = { + text: 'Access your wallet with your Secret Recovery Phrase', + tag: 'h2', + }; + + private readonly srpWord0 = '[data-testid="import-srp__srp-word-0"]'; + + private readonly srpWords = '.import-srp__srp-word'; + + private readonly wrongSrpWarningMessage = { + text: 'Invalid Secret Recovery Phrase', + css: '.import-srp__banner-alert-text', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.srpMessage, + this.srpWord0, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding srp page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding srp page is loaded'); + } + + async clickConfirmButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.srpConfirmButton); + } + + /** + * Fill the SRP words with the provided seed phrase + * + * @param seedPhrase - The seed phrase to fill. Defaults to TEST_SEED_PHRASE. + */ + async fillSrp(seedPhrase: string = TEST_SEED_PHRASE): Promise { + await this.driver.pasteIntoField(this.srpWord0, seedPhrase); + } + + async check_confirmSrpButtonIsDisabled(): Promise { + console.log('Check that confirm SRP button is disabled'); + const confirmSeedPhrase = await this.driver.findElement( + this.srpConfirmButton, + ); + assert.equal(await confirmSeedPhrase.isEnabled(), false); + } + + /** + * Check the SRP dropdown iterates through each option + * + * @param numOptions - The number of options to check. Defaults to 5. + */ + async check_srpDropdownIterations(numOptions: number = 5) { + console.log( + `Check the SRP dropdown iterates through ${numOptions} options`, + ); + await this.driver.clickElement(this.srpDropdown); + await this.driver.wait(async () => { + const options = await this.driver.findElements(this.srpDropdownOptions); + return options.length === numOptions; + }, this.driver.timeout); + + const options = await this.driver.findElements(this.srpDropdownOptions); + for (let i = 0; i < options.length; i++) { + if (i !== 0) { + await this.driver.clickElement(this.srpDropdown); + } + await options[i].click(); + const expectedNumFields = 12 + i * 3; + await this.driver.wait(async () => { + const srpWordsFields = await this.driver.findElements(this.srpWords); + return expectedNumFields === srpWordsFields.length; + }, this.driver.timeout); + } + } + + async check_wrongSrpWarningMessage(): Promise { + console.log('Check that wrong SRP warning message is displayed'); + await this.driver.waitForSelector(this.wrongSrpWarningMessage); + } +} + +export default OnboardingSrpPage; diff --git a/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts new file mode 100644 index 000000000000..cff7549a0f75 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts @@ -0,0 +1,107 @@ +import { Driver } from '../../../webdriver/driver'; + +class SecureWalletPage { + private driver: Driver; + + private readonly confirmRecoveryPhraseButton = + '[data-testid="recovery-phrase-confirm"]'; + + private readonly confirmSecretRecoveryPhraseMessage = { + text: 'Confirm Secret Recovery Phrase', + tag: 'h2', + }; + + private readonly recoveryPhraseChips = + '[data-testid="recovery-phrase-chips"]'; + + private readonly recoveryPhraseInputIndex2 = + '[data-testid="recovery-phrase-input-2"]'; + + private readonly recoveryPhraseInputIndex3 = + '[data-testid="recovery-phrase-input-3"]'; + + private readonly recoveryPhraseInputIndex7 = + '[data-testid="recovery-phrase-input-7"]'; + + private readonly recoveryPhraseNextButton = + '[data-testid="recovery-phrase-next"]'; + + private readonly revealSecretRecoveryPhraseButton = + '[data-testid="recovery-phrase-reveal"]'; + + private readonly secureWalletButton = + '[data-testid="secure-wallet-recommended"]'; + + private readonly secureWalletLaterButton = + '[data-testid="secure-wallet-later"]'; + + private readonly secureWalletMessage = { + text: 'Secure your wallet', + tag: 'h2', + }; + + private readonly writeDownSecretRecoveryPhraseMessage = { + text: 'Write down your Secret Recovery Phrase', + tag: 'h2', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.secureWalletMessage, + this.secureWalletButton, + this.secureWalletLaterButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for secure wallet page to be loaded', + e, + ); + throw e; + } + console.log('Secure wallet page is loaded'); + } + + async revealAndConfirmSRP(): Promise { + console.log( + 'Reveal and confirm SRP on secure wallet page during onboarding', + ); + // click secure my wallet button to reveal SRP + await this.driver.clickElement(this.secureWalletButton); + await this.driver.waitForMultipleSelectors([ + this.writeDownSecretRecoveryPhraseMessage, + this.revealSecretRecoveryPhraseButton, + ]); + + // click reveal button to reveal SRP + await this.driver.clickElement(this.revealSecretRecoveryPhraseButton); + await this.driver.waitForSelector(this.recoveryPhraseChips); + + let finalWords: string[] = []; + await this.driver.wait(async () => { + const recoveryPhraseChips = await this.driver.findElement( + this.recoveryPhraseChips, + ); + const recoveryPhrase = await recoveryPhraseChips.getText(); + const words = recoveryPhrase.split(/\s*(?:[0-9)]+|\n|\.|^$|$)\s*/u); + finalWords = words.filter((str) => str !== ''); + return finalWords.length === 12; + }, this.driver.timeout); + await this.driver.clickElement(this.recoveryPhraseNextButton); + + // confirm SRP + await this.driver.waitForSelector(this.confirmSecretRecoveryPhraseMessage); + await this.driver.fill(this.recoveryPhraseInputIndex2, finalWords[2]); + await this.driver.fill(this.recoveryPhraseInputIndex3, finalWords[3]); + await this.driver.fill(this.recoveryPhraseInputIndex7, finalWords[7]); + await this.driver.clickElementAndWaitToDisappear( + this.confirmRecoveryPhraseButton, + ); + } +} + +export default SecureWalletPage; diff --git a/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts b/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts new file mode 100644 index 000000000000..47c0b53a2b2f --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts @@ -0,0 +1,52 @@ +import { Driver } from '../../../webdriver/driver'; + +class StartOnboardingPage { + private driver: Driver; + + private readonly createWalletButton = + '[data-testid="onboarding-create-wallet"]'; + + private readonly importWalletButton = + '[data-testid="onboarding-import-wallet"]'; + + private readonly startMessage = { + text: "Let's get started", + tag: 'h2', + }; + + private readonly termsCheckbox = '[data-testid="onboarding-terms-checkbox"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.startMessage, + this.termsCheckbox, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for start onboarding page to be loaded', + e, + ); + throw e; + } + console.log('Start onboarding page is loaded'); + } + + async checkTermsCheckbox(): Promise { + await this.driver.clickElement(this.termsCheckbox); + } + + async clickCreateWalletButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.createWalletButton); + } + + async clickImportWalletButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); + } +} + +export default StartOnboardingPage; diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts index fd2fe013c3c1..98cfe9b18337 100644 --- a/test/e2e/tests/account/snap-account-signatures.spec.ts +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -18,7 +18,7 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring- import TestDapp from '../../page-objects/pages/test-dapp'; describe('Snap Account Signatures @no-mmi', function (this: Suite) { - this.timeout(120000); // This test is very long, so we need an unusually high timeout + this.timeout(150000); // This test is very long, so we need an unusually high timeout // Run sync, async approve, and async reject flows // (in Jest we could do this with test.each, but that does not exist here) diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js deleted file mode 100644 index de040f825ee6..000000000000 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ /dev/null @@ -1,748 +0,0 @@ -const { strict: assert } = require('assert'); -const { toHex } = require('@metamask/controller-utils'); -const { By } = require('selenium-webdriver'); -const { - TEST_SEED_PHRASE, - convertToHexValue, - withFixtures, - completeCreateNewWalletOnboardingFlow, - completeImportSRPOnboardingFlow, - importSRPOnboardingFlow, - importWrongSRPOnboardingFlow, - testSRPDropdownIterations, - locateAccountBalanceDOM, - defaultGanacheOptions, - WALLET_PASSWORD, - onboardingBeginCreateNewWallet, - onboardingChooseMetametricsOption, - onboardingCreatePassword, - onboardingRevealAndConfirmSRP, - onboardingCompleteWalletCreation, - regularDelayMs, - unlockWallet, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); -const { - FirstTimeFlowType, -} = require('../../../../shared/constants/onboarding'); - -describe('MetaMask onboarding @no-mmi', function () { - const wrongSeedPhrase = - 'test test test test test test test test test test test test'; - const wrongTestPassword = 'test test test test'; - - const ganacheOptions2 = { - accounts: [ - { - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - balance: convertToHexValue(10000000000000000000), - }, - ], - }; - - it('Clicks create a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await completeCreateNewWalletOnboardingFlow(driver, WALLET_PASSWORD); - - const homePage = await driver.findElement('.home__main-view'); - const homePageDisplayed = await homePage.isDisplayed(); - - assert.equal(homePageDisplayed, true); - }, - ); - }); - - it('Clicks import a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await completeImportSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - const homePage = await driver.findElement('.home__main-view'); - const homePageDisplayed = await homePage.isDisplayed(); - - assert.equal(homePageDisplayed, true); - }, - ); - }); - - it('User import wrong Secret Recovery Phrase', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await importWrongSRPOnboardingFlow(driver, wrongSeedPhrase); - - const confirmSeedPhrase = await driver.findElement( - '[data-testid="import-srp-confirm"]', - ); - - assert.equal(await confirmSeedPhrase.isEnabled(), false); - }, - ); - }); - - it('Check if user select different type of secret recovery phrase', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - // accept terms of use - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - - // welcome - await driver.clickElement('[data-testid="onboarding-import-wallet"]'); - - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - const dropdownElement = await driver.findElement( - '.import-srp__number-of-words-dropdown', - ); - await dropdownElement.click(); - const options = await dropdownElement.findElements(By.css('option')); - - const iterations = options.length; - - await testSRPDropdownIterations(options, driver, iterations); - - const finalFormFields = await driver.findElements( - '.import-srp__srp-word-label', - ); - const expectedFinalNumFields = 24; // The last iteration will have 24 fields - const actualFinalNumFields = finalFormFields.length; - assert.equal(actualFinalNumFields, expectedFinalNumFields); - }, - ); - }); - - it('User enters the wrong password during password creation', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - await driver.clickElement('[data-testid="onboarding-create-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Fill in confirm password field with incorrect password - await driver.fill( - '[data-testid="create-password-new"]', - WALLET_PASSWORD, - ); - await driver.fill( - '[data-testid="create-password-confirm"]', - wrongTestPassword, - ); - - // Check that the error message is displayed for the password fields - await driver.isElementPresent( - { text: "Passwords don't match", tag: 'h6' }, - true, - ); - - // Check that the "Confirm Password" button is disabled - const confirmPasswordButton = await driver.findElement( - '[data-testid="create-password-wallet"]', - ); - assert.equal(await confirmPasswordButton.isEnabled(), false); - }, - ); - }); - - it('Verify that the user has been redirected to the correct page after importing their wallet', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - // Verify site - assert.equal( - await driver.isElementPresent({ - text: 'Your wallet is ready', - tag: 'h2', - }), - true, - ); - }, - ); - }); - - it('Verify that the user has been redirected to the correct page after creating a password for their new wallet', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - await driver.clickElement('[data-testid="onboarding-create-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Fill in confirm password field with correct password - await driver.fill( - '[data-testid="create-password-new"]', - WALLET_PASSWORD, - ); - await driver.fill( - '[data-testid="create-password-confirm"]', - WALLET_PASSWORD, - ); - await driver.clickElement('[data-testid="create-password-terms"]'); - await driver.clickElement('[data-testid="create-password-wallet"]'); - - // Verify site - assert.equal( - await driver.isElementPresent({ - text: 'Secure your wallet', - tag: 'h2', - }), - true, - ); - }, - ); - }); - - it('User can add custom network during onboarding', async function () { - const networkName = 'Localhost 8546'; - const networkUrl = 'http://127.0.0.1:8546'; - const currencySymbol = 'ETH'; - const port = 8546; - const chainId = 1338; - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [{ port, chainId, ganacheOptions2 }], - }, - title: this.test.fullTitle(), - }, - - async ({ driver, secondaryGanacheServer }) => { - try { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - await driver.clickElement({ - text: 'Manage default privacy settings', - tag: 'button', - }); - - await driver.clickElement({ - text: 'General', - }); - await driver.clickElement({ text: 'Add a network' }); - - await driver.waitForSelector( - '.multichain-network-list-menu-content-wrapper__dialog', - ); - - await driver.fill( - '[data-testid="network-form-network-name"]', - networkName, - ); - await driver.fill( - '[data-testid="network-form-chain-id"]', - chainId.toString(), - ); - await driver.fill( - '[data-testid="network-form-ticker-input"]', - currencySymbol, - ); - - // Add rpc url - const rpcUrlInputDropDown = await driver.waitForSelector( - '[data-testid="test-add-rpc-drop-down"]', - ); - await rpcUrlInputDropDown.click(); - await driver.clickElement({ - text: 'Add RPC URL', - tag: 'button', - }); - const rpcUrlInput = await driver.waitForSelector( - '[data-testid="rpc-url-input-test"]', - ); - await rpcUrlInput.clear(); - await rpcUrlInput.sendKeys(networkUrl); - await driver.clickElement({ - text: 'Add URL', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - tag: 'button', - text: 'Save', - }); - - await driver.clickElement('[data-testid="category-back-button"]'); - - // Wait until the onboarding carousel has stopped moving - // otherwise the click has no effect. - await driver.waitForElementToStopMoving( - '[data-testid="privacy-settings-back-button"]', - ); - - await driver.clickElement( - '[data-testid="privacy-settings-back-button"]', - ); - - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - // Wait until the onboarding carousel has stopped moving - // otherwise the click has no effect. - await driver.waitForElementToStopMoving({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElement('.mm-picker-network'); - await driver.clickElement( - `[data-rbd-draggable-id="${toHex(chainId)}"]`, - ); - // Check localhost 8546 is selected and its balance value is correct - await driver.findElement({ - css: '[data-testid="network-display"]', - text: networkName, - }); - - await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); - } catch (error) { - console.error('Error in test:', error); - throw error; - } - }, - ); - }); - - it('User can turn off basic functionality in default settings', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - await driver.clickElement({ - text: 'Manage default privacy settings', - tag: 'button', - }); - await driver.clickElement('[data-testid="category-item-General"]'); - await driver.clickElement( - '[data-testid="basic-functionality-toggle"] .toggle-button', - ); - await driver.clickElement('[id="basic-configuration-checkbox"]'); - await driver.clickElement({ text: 'Turn off', tag: 'button' }); - await driver.clickElement('[data-testid="category-back-button"]'); - - // Wait until the onboarding carousel has stopped moving - // otherwise the click has no effect. - await driver.waitForElementToStopMoving( - '[data-testid="privacy-settings-back-button"]', - ); - await driver.clickElement( - '[data-testid="privacy-settings-back-button"]', - ); - - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // Check that the 'basic functionality is off' banner is displayed on the home screen after onboarding completion - await driver.waitForSelector({ - text: 'Basic functionality is off', - css: '.mm-banner-alert', - }); - }, - ); - }); - - it("doesn't make any network requests to infura before onboarding is completed", async function () { - async function mockInfura(mockServer) { - const infuraUrl = - 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; - const sampleAddress = '1111111111111111111111111111111111111111'; - - return [ - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_blockNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x1', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBalance' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x0', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: {}, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_call' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: `0x000000000000000000000000${sampleAddress}`, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'net_version' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, - }; - }), - ]; - } - - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockInfura, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - const password = 'password'; - - await driver.navigate(); - - await onboardingBeginCreateNewWallet(driver); - await onboardingChooseMetametricsOption(driver, false); - await onboardingCreatePassword(driver, password); - await onboardingRevealAndConfirmSRP(driver); - await onboardingCompleteWalletCreation(driver); - - // pin extension walkthrough screen - await driver.clickElement('[data-testid="pin-extension-next"]'); - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - const isPending = await mockedEndpoint.isPending(); - assert.equal( - isPending, - true, - `${mockedEndpoints[i]} mock should still be pending before onboarding`, - ); - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length, - 0, - `${mockedEndpoints[i]} should make no requests before onboarding`, - ); - } - - await driver.clickElement('[data-testid="pin-extension-done"]'); - // requests happen here! - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - - await driver.wait(async () => { - const isPending = await mockedEndpoint.isPending(); - return isPending === false; - }, driver.timeout); - - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length > 0, - true, - `${mockedEndpoints[i]} should make requests after onboarding`, - ); - } - }, - ); - }); - - it("doesn't make any network requests to infura before onboarding by import is completed", async function () { - async function mockInfura(mockServer) { - const infuraUrl = - 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; - const sampleAddress = '1111111111111111111111111111111111111111'; - - return [ - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_blockNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x1', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBalance' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x0', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: {}, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_call' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: `0x000000000000000000000000${sampleAddress}`, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'net_version' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, - }; - }), - ]; - } - - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockInfura, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length, - 0, - `${mockedEndpoints[i]} should make no requests before onboarding`, - ); - } - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // pin extension walkthrough screen - await driver.findElement('[data-testid="account-menu-icon"]'); - // requests happen here! - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - - await driver.wait(async () => { - const isPending = await mockedEndpoint.isPending(); - return isPending === false; - }, driver.timeout); - - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length > 0, - true, - `${mockedEndpoints[i]} should make requests after onboarding`, - ); - } - }, - ); - }); - - it('Provides an onboarding path for a user who has restored their account from state persistence failure', async function () { - // We don't use onboarding:true here because we want there to be a vault, - // simulating what will happen when a user eventually restores their vault - // during a state persistence failure. Instead, we set the - // firstTimeFlowType to 'restore' and completedOnboarding to false. as well - // as some other first time state options to get us into an onboarding - // state similar to a new state tree. - await withFixtures( - { - fixtures: new FixtureBuilder() - .withOnboardingController({ - completedOnboarding: false, - firstTimeFlowType: FirstTimeFlowType.restore, - seedPhraseBackedUp: null, - }) - .withMetaMetricsController({ - participateInMetaMetrics: null, - metaMetricsId: null, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // First screen we should be on is MetaMetrics - assert.equal( - await driver.isElementPresent({ - text: 'Help us improve MetaMask', - tag: 'h2', - }), - true, - 'First screen should be MetaMetrics', - ); - - // select no thanks - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Next should be Secure your wallet screen - assert.equal( - await driver.isElementPresent({ - text: 'Secure your wallet', - tag: 'h2', - }), - true, - ); - }, - ); - }); -}); diff --git a/test/e2e/tests/onboarding/onboarding.spec.ts b/test/e2e/tests/onboarding/onboarding.spec.ts new file mode 100644 index 000000000000..1af2d5170841 --- /dev/null +++ b/test/e2e/tests/onboarding/onboarding.spec.ts @@ -0,0 +1,269 @@ +import { + convertToHexValue, + WALLET_PASSWORD, + withFixtures, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingMetricsPage from '../../page-objects/pages/onboarding/onboarding-metrics-page'; +import OnboardingPasswordPage from '../../page-objects/pages/onboarding/onboarding-password-page'; +import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import OnboardingSrpPage from '../../page-objects/pages/onboarding/onboarding-srp-page'; +import SecureWalletPage from '../../page-objects/pages/onboarding/secure-wallet-page'; +import StartOnboardingPage from '../../page-objects/pages/onboarding/start-onboarding-page'; +import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + completeCreateNewWalletOnboardingFlow, + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; + +describe('MetaMask onboarding @no-mmi', function () { + const ganacheOptions2 = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: convertToHexValue(10000000000000000000), + }, + ], + }; + + it('Creates a new wallet, sets up a secure password, and completes the onboarding process', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await completeCreateNewWalletOnboardingFlow(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + }, + ); + }); + + it('Imports an existing wallet, sets up a secure password, and completes the onboarding process', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await completeImportSRPOnboardingFlow(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + }, + ); + }); + + it('Attempts to import a wallet with an incorrect Secret Recovery Phrase and verifies the error message', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + const wrongSeedPhrase = + 'test test test test test test test test test test test test'; + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + await onboardingSrpPage.fillSrp(wrongSeedPhrase); + + // check the wrong SRP warning message is displayed + await onboardingSrpPage.check_wrongSrpWarningMessage(); + await onboardingSrpPage.check_confirmSrpButtonIsDisabled(); + }, + ); + }); + + it('Verifies the functionality of selecting different Secret Recovery Phrase word counts', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + await onboardingSrpPage.check_srpDropdownIterations(); + }, + ); + }); + + it('Verifies error handling when entering an incorrect password during wallet creation', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + const wrongTestPassword = 'test test test test'; + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickCreateWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.fillWalletPassword( + WALLET_PASSWORD, + wrongTestPassword, + ); + + // check the incorrect password warning message is displayed + await onboardingPasswordPage.check_incorrectPasswordWarningMessageIsDisplayed(); + await onboardingPasswordPage.check_confirmPasswordButtonIsDisabled(); + }, + ); + }); + + it('User can add custom network during onboarding', async function () { + const networkName = 'Localhost 8546'; + const networkUrl = 'http://127.0.0.1:8546'; + const currencySymbol = 'ETH'; + const port = 8546; + const chainId = 1338; + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions: { + concurrent: [{ port, chainId, ganacheOptions2 }], + }, + title: this.test?.fullTitle(), + }, + async ({ driver, secondaryGanacheServer }) => { + await importSRPOnboardingFlow(driver); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, + ); + await onboardingPrivacySettingsPage.addCustomNetwork( + networkName, + chainId, + currencySymbol, + networkUrl, + ); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.headerNavbar.switchToNetwork(networkName); + + // Check the correct balance for the custom network is displayed + if (secondaryGanacheServer && Array.isArray(secondaryGanacheServer)) { + await homePage.check_ganacheBalanceIsDisplayed( + secondaryGanacheServer[0], + ); + } else { + throw new Error('Custom network Ganache server not available'); + } + }, + ); + }); + + it('User can turn off basic functionality in default settings', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await importSRPOnboardingFlow(driver); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, + ); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + // check the basic functionality is off warning message is displayed + await homePage.check_basicFunctionalityOffWarnigMessageIsDisplayed(); + }, + ); + }); + + it('Provides an onboarding path for a user who has restored their account from state persistence failure', async function () { + // We don't use onboarding: true here because we want there to be a vault, + // simulating what will happen when a user eventually restores their vault + // during a state persistence failure. Instead, we set the + // firstTimeFlowType to 'restore' and completedOnboarding to false. as well + // as some other first time state options to get us into an onboarding + // state similar to a new state tree. + await withFixtures( + { + fixtures: new FixtureBuilder() + .withOnboardingController({ + completedOnboarding: false, + firstTimeFlowType: FirstTimeFlowType.restore, + seedPhraseBackedUp: null, + }) + .withMetaMetricsController({ + participateInMetaMetrics: null, + metaMetricsId: null, + }) + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithoutBalanceValidation(driver); + // First screen we should be on is MetaMetrics + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + // Next screen should be Secure your wallet screen + const secureWalletPage = new SecureWalletPage(driver); + await secureWalletPage.check_pageIsLoaded(); + }, + ); + }); +}); diff --git a/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts b/test/e2e/tests/privacy/account-tracker-api-usage.spec.ts similarity index 100% rename from test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts rename to test/e2e/tests/privacy/account-tracker-api-usage.spec.ts diff --git a/test/e2e/tests/privacy/onboarding-privacy.spec.js b/test/e2e/tests/privacy/onboarding-privacy.spec.js new file mode 100644 index 000000000000..7febe8eb1a9f --- /dev/null +++ b/test/e2e/tests/privacy/onboarding-privacy.spec.js @@ -0,0 +1,283 @@ +const { strict: assert } = require('assert'); +const { + TEST_SEED_PHRASE, + withFixtures, + importSRPOnboardingFlow, + defaultGanacheOptions, + onboardingBeginCreateNewWallet, + onboardingChooseMetametricsOption, + onboardingCreatePassword, + onboardingRevealAndConfirmSRP, + onboardingCompleteWalletCreation, + regularDelayMs, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('MetaMask onboarding @no-mmi', function () { + it("doesn't make any network requests to infura before onboarding is completed", async function () { + async function mockInfura(mockServer) { + const infuraUrl = + 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; + const sampleAddress = '1111111111111111111111111111111111111111'; + + return [ + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_blockNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x1', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x0', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: {}, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_call' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: `0x000000000000000000000000${sampleAddress}`, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'net_version' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, + }; + }), + ]; + } + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockInfura, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + const password = 'password'; + + await driver.navigate(); + + await onboardingBeginCreateNewWallet(driver); + await onboardingChooseMetametricsOption(driver, false); + await onboardingCreatePassword(driver, password); + await onboardingRevealAndConfirmSRP(driver); + await onboardingCompleteWalletCreation(driver); + + // pin extension walkthrough screen + await driver.clickElement('[data-testid="pin-extension-next"]'); + + await driver.delay(regularDelayMs); + + for (let i = 0; i < mockedEndpoints.length; i += 1) { + const mockedEndpoint = await mockedEndpoints[i]; + const isPending = await mockedEndpoint.isPending(); + assert.equal( + isPending, + true, + `${mockedEndpoints[i]} mock should still be pending before onboarding`, + ); + const requests = await mockedEndpoint.getSeenRequests(); + + assert.equal( + requests.length, + 0, + `${mockedEndpoints[i]} should make no requests before onboarding`, + ); + } + + await driver.clickElement('[data-testid="pin-extension-done"]'); + // requests happen here! + + for (let i = 0; i < mockedEndpoints.length; i += 1) { + const mockedEndpoint = await mockedEndpoints[i]; + + await driver.wait(async () => { + const isPending = await mockedEndpoint.isPending(); + return isPending === false; + }, driver.timeout); + + const requests = await mockedEndpoint.getSeenRequests(); + + assert.equal( + requests.length > 0, + true, + `${mockedEndpoints[i]} should make requests after onboarding`, + ); + } + }, + ); + }); + + it("doesn't make any network requests to infura before onboarding by import is completed", async function () { + async function mockInfura(mockServer) { + const infuraUrl = + 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; + const sampleAddress = '1111111111111111111111111111111111111111'; + + return [ + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_blockNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x1', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x0', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: {}, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_call' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: `0x000000000000000000000000${sampleAddress}`, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'net_version' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, + }; + }), + ]; + } + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockInfura, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + const password = 'password'; + + await driver.navigate(); + + await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); + + await driver.delay(regularDelayMs); + + for (let i = 0; i < mockedEndpoints.length; i += 1) { + const mockedEndpoint = await mockedEndpoints[i]; + const requests = await mockedEndpoint.getSeenRequests(); + + assert.equal( + requests.length, + 0, + `${mockedEndpoints[i]} should make no requests before onboarding`, + ); + } + + // complete + await driver.clickElement('[data-testid="onboarding-complete-done"]'); + + // pin extension + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); + + // pin extension walkthrough screen + await driver.findElement('[data-testid="account-menu-icon"]'); + // requests happen here! + + for (let i = 0; i < mockedEndpoints.length; i += 1) { + const mockedEndpoint = await mockedEndpoints[i]; + + await driver.wait(async () => { + const isPending = await mockedEndpoint.isPending(); + return isPending === false; + }, driver.timeout); + + const requests = await mockedEndpoint.getSeenRequests(); + + assert.equal( + requests.length > 0, + true, + `${mockedEndpoints[i]} should make requests after onboarding`, + ); + } + }, + ); + }); +}); diff --git a/test/e2e/tests/settings/auto-lock.spec.js b/test/e2e/tests/settings/auto-lock.spec.js index 7d7a159d4a1b..46021ed0bb46 100644 --- a/test/e2e/tests/settings/auto-lock.spec.js +++ b/test/e2e/tests/settings/auto-lock.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, openMenuSafe, @@ -41,12 +40,11 @@ describe('Auto-Lock Timer', function () { '[data-testid="advanced-setting-auto-lock"] button', ); // Verify the wallet is locked - const pageTitle = await driver.findElement( - '[data-testid="unlock-page-title"]', - ); - const unlockButton = await driver.findElement('.unlock-page button'); - assert.equal(await pageTitle.getText(), 'Welcome back!'); - assert.equal(await unlockButton.isDisplayed(), true); + await driver.waitForSelector({ + css: '[data-testid="unlock-page-title"]', + text: 'Welcome back!', + }); + await driver.waitForSelector('.unlock-page button'); }, ); }); diff --git a/test/e2e/tests/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 57dbfd5f68cf..1fb1e8d1e8a6 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -6,6 +5,21 @@ const { } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); +async function mockPhpConversion(mockServer) { + return await mockServer + .forGet('https://min-api.cryptocompare.com/data/price') + .withQuery({ fsym: 'ETH', tsyms: 'PHP,USD' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + PHP: '100000', + USD: '2500', + }, + }; + }); +} + describe('Localization', function () { it('can correctly display Philippine peso symbol and code', async function () { await withFixtures( @@ -22,18 +36,22 @@ describe('Localization', function () { }) .build(), ganacheOptions: defaultGanacheOptions, + testSpecificMock: mockPhpConversion, title: this.test.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); // After the removal of displaying secondary currency in coin-overview.tsx, we will test localization on main balance with showNativeTokenAsMainBalance = false - const primaryBalance = await driver.findElement( - '[data-testid="eth-overview__primary-currency"]', - ); - const balanceText = await primaryBalance.getText(); - assert.ok(balanceText.startsWith('₱')); - assert.ok(balanceText.endsWith('PHP')); + await driver.waitForSelector({ + tag: 'span', + text: 'PHP', + }); + + await driver.waitForSelector({ + tag: 'span', + text: '₱2,500,000.00', + }); }, ); }); diff --git a/test/e2e/tests/settings/settings-general.spec.js b/test/e2e/tests/settings/settings-general.spec.js index 5e75c857a7f8..ef07a53d01f2 100644 --- a/test/e2e/tests/settings/settings-general.spec.js +++ b/test/e2e/tests/settings/settings-general.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, openMenuSafe, @@ -28,25 +27,15 @@ describe('Settings', function () { '[data-testid="jazz_icon"] .settings-page__content-item__identicon__item__icon--active', ); - const jazziconText = await driver.findElement({ + await driver.waitForSelector({ tag: 'h6', text: 'Jazzicons', }); - assert.equal( - await jazziconText.getText(), - 'Jazzicons', - 'Text for icon should be Jazzicons', - ); - const blockiesText = await driver.findElement({ + await driver.waitForSelector({ tag: 'h6', text: 'Blockies', }); - assert.equal( - await blockiesText.getText(), - 'Blockies', - 'Text for icon should be Blockies', - ); }, ); }); diff --git a/test/e2e/tests/settings/settings-security-reveal-srp.spec.js b/test/e2e/tests/settings/settings-security-reveal-srp.spec.js index 7cbdaefe73ed..7f68f53f8dc7 100644 --- a/test/e2e/tests/settings/settings-security-reveal-srp.spec.js +++ b/test/e2e/tests/settings/settings-security-reveal-srp.spec.js @@ -52,10 +52,10 @@ describe('Reveal SRP through settings', function () { await tapAndHoldToRevealSRP(driver); // confirm SRP text matches expected - const displayedSRP = await driver.findVisibleElement( - '[data-testid="srp_text"]', - ); - assert.equal(await displayedSRP.getText(), E2E_SRP); + await driver.waitForSelector({ + css: '[data-testid="srp_text"]', + text: E2E_SRP, + }); // copy SRP text to clipboard await driver.clickElement({ diff --git a/test/e2e/tests/settings/show-hex-data.spec.js b/test/e2e/tests/settings/show-hex-data.spec.js index 5e0ae9a133a0..4bef79ca0a3b 100644 --- a/test/e2e/tests/settings/show-hex-data.spec.js +++ b/test/e2e/tests/settings/show-hex-data.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -84,15 +83,10 @@ describe('Check the toggle for hex data', function () { await sendTransactionAndVerifyHexData(driver); // Verify hex data in the container content - const pageContentContainer = await driver.findElement( - selectors.containerContent, - ); - const pageContentContainerText = await pageContentContainer.getText(); - assert.equal( - pageContentContainerText.includes(inputData.hexDataText), - true, - 'Hex data is incorrect', - ); + await driver.waitForSelector({ + tag: 'p', + text: '0x0abc', + }); }, ); }); diff --git a/test/e2e/tests/transaction/ens.spec.ts b/test/e2e/tests/transaction/ens.spec.ts index 4700ab8fac3f..47bae3e5e4cc 100644 --- a/test/e2e/tests/transaction/ens.spec.ts +++ b/test/e2e/tests/transaction/ens.spec.ts @@ -112,6 +112,7 @@ describe('ENS', function (this: Suite) { // click send button on homepage to start send flow const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); await homepage.check_expectedBalanceIsDisplayed('<0.000001'); await homepage.startSendFlow(); diff --git a/ui/__mocks__/useNftCollectionsMetadata.js b/ui/__mocks__/useNftCollectionsMetadata.js new file mode 100644 index 000000000000..fd99ff219364 --- /dev/null +++ b/ui/__mocks__/useNftCollectionsMetadata.js @@ -0,0 +1,14 @@ +module.exports = { + useNftCollectionsMetadata: () => { + return { + '0x1': { + '0xc0ffee254729296a45a3885639ac7e10f9d54979': { + name: 'Everything I Own', + image: + 'https://img.reservoir.tools/images/v2/mainnet/z9JRSpLYGu7%2BCZoKWtAuAN%2F%2FMfWcOGcwki5%2FxXYtCb4OfGsOPvxN1LZHZ5%2BcuQGwJciTvgr58ThRjooWLMWehc1nSTXtbfFJ1TNtL%2FeIjglkPKsEG%2Fbem0E%2B3yo7tAUqlZ1ou0SMzGOfq%2FG1BHwIpgHQ524PRAlaynVkDcp8y58kALOPTQSDN1tgaqkZD%2FZiNBEaYq6Bp9XH8Vm8tMXsaQ%3D%3D?width=250', + isSpam: false, + }, + }, + }; + }, +}; diff --git a/ui/components/app/name/name.stories.tsx b/ui/components/app/name/name.stories.tsx index fb23334a8776..732c9059b530 100644 --- a/ui/components/app/name/name.stories.tsx +++ b/ui/components/app/name/name.stories.tsx @@ -3,108 +3,75 @@ import React from 'react'; import { NameType } from '@metamask/name-controller'; import { Provider } from 'react-redux'; import configureStore from '../../../store/store'; -import Name from './name'; -import { mockNetworkState } from '../../../../test/stub/networks'; +import Name, { NameProps } from './name'; +import mockState from '../../../../test/data/mock-state.json'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../../../shared/constants/first-party-contracts'; +import { cloneDeep } from 'lodash'; -const addressNoSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; -const addressSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54977'; -const addressSavedTokenMock = '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d'; -const addressUnsavedTokenMock = '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8'; -const chainIdMock = '0x1'; +const ADDRESS_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; +const ADDRESS_NFT_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979'; +const VARIATION_MOCK = '0x1'; +const NAME_MOCK = 'Saved Name'; -const storeMock = configureStore({ +const ADDRESS_FIRST_PARTY_MOCK = + FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][ + VARIATION_MOCK + ].toLowerCase(); + +const PROPOSED_NAMES_MOCK = { + ens: { + proposedNames: ['test.eth'], + lastRequestTime: 123, + retryDelay: null, + }, + etherscan: { + proposedNames: ['TestContract'], + lastRequestTime: 123, + retryDelay: null, + }, + token: { + proposedNames: ['Test Token'], + lastRequestTime: 123, + retryDelay: null, + }, + lens: { + proposedNames: ['test.lens'], + lastRequestTime: 123, + retryDelay: null, + }, +}; + +const STATE_MOCK = { + ...mockState, metamask: { - ...mockNetworkState({chainId: chainIdMock}), + ...mockState.metamask, useTokenDetection: true, - tokenList: { - '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': { - address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d', - symbol: 'IUSD', - name: 'iZUMi Bond USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', - }, - '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': { - address: '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8', - symbol: 'USX', - name: 'dForce USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8.png', - }, - }, + tokensChainsCache: {}, names: { [NameType.ETHEREUM_ADDRESS]: { - [addressNoSavedNameMock]: { - [chainIdMock]: { - proposedNames: { - ens: { - proposedNames: ['test.eth'], - lastRequestTime: 123, - retryDelay: null, - }, - etherscan: { - proposedNames: ['TestContract'], - lastRequestTime: 123, - retryDelay: null, - }, - token: { - proposedNames: ['Test Token'], - lastRequestTime: 123, - retryDelay: null, - }, - lens: { - proposedNames: ['test.lens'], - lastRequestTime: 123, - retryDelay: null, - }, - }, + [ADDRESS_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, - [addressSavedNameMock]: { - [chainIdMock]: { - proposedNames: { - ens: { - proposedNames: ['test.eth'], - lastRequestTime: 123, - retryDelay: null, - }, - etherscan: { - proposedNames: ['TestContract'], - lastRequestTime: 123, - retryDelay: null, - }, - token: { - proposedNames: ['Test Token'], - lastRequestTime: 123, - retryDelay: null, - }, - lens: { - proposedNames: ['test.lens'], - lastRequestTime: 123, - retryDelay: null, - }, - }, - name: 'Test Token', - sourceId: 'token', + [ADDRESS_NFT_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, - [addressSavedTokenMock]: { - [chainIdMock]: { - proposedNames: {}, - name: 'Saved Token Name', - sourceId: 'token', + [ADDRESS_FIRST_PARTY_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, }, }, - nameSources: { - ens: { label: 'Ethereum Name Service (ENS)' }, - etherscan: { label: 'Etherscan (Verified Contract Name)' }, - token: { label: 'Blockchain (Token Name)' }, - lens: { label: 'Lens Protocol' }, - }, + nameSources: {}, }, -}); +}; /** * Displays the saved name for a raw value such as an Ethereum address.

@@ -125,6 +92,10 @@ export default { description: `The type of value.

Limited to the values in the \`NameType\` enum.`, }, + variation: { + control: 'text', + description: `The variation of the value.

For example, the chain ID if the type is Ethereum address.`, + }, disableEdit: { control: 'boolean', description: `Whether to prevent the modal from opening when the component is clicked.`, @@ -134,68 +105,141 @@ export default { }, }, args: { - value: addressNoSavedNameMock, + value: ADDRESS_MOCK, type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, disableEdit: false, }, - decorators: [(story) => {story()}], + render: ({ state, ...args }) => { + const finalState = cloneDeep(STATE_MOCK); + state?.(finalState); + + return ( + + + + ); + }, }; -// eslint-disable-next-line jsdoc/require-param /** * No name has been saved for the value and type. */ -export const DefaultStory = (args) => { - return ; +export const NoSavedName = { + name: 'No Saved Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, }; -DefaultStory.storyName = 'No Saved Name'; - /** * A name was previously saved for this value and type.

* The component will still display a modal when clicked to edit the name. */ -export const SavedNameStory = () => { - return ; +export const SavedNameStory = { + name: 'Saved Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.names[NameType.ETHEREUM_ADDRESS][ADDRESS_MOCK][ + VARIATION_MOCK + ].name = NAME_MOCK; + }, + }, }; -SavedNameStory.storyName = 'Saved Name'; - /** * No name was previously saved for this recognized token.

* The component will still display a modal when clicked to edit the name. */ -export const UnsavedTokenNameStory = () => { - return ( - - ); +export const DefaultTokenNameStory = { + name: 'Default ERC-20 Token Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.tokensChainsCache = { + [VARIATION_MOCK]: { + data: { + [ADDRESS_MOCK]: { + address: ADDRESS_MOCK, + symbol: 'IUSD', + name: 'iZUMi Bond USD', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', + }, + }, + }, + }; + }, + }, }; -UnsavedTokenNameStory.storyName = 'Unsaved Token Name'; +/** + * No name was previously saved for this watched NFT.

+ * The component will still display a modal when clicked to edit the name. + */ +export const DefaultWatchedNFTNameStory = { + name: 'Default Watched NFT Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.allNftContracts = { + '0x123': { + [VARIATION_MOCK]: [ + { + address: ADDRESS_MOCK, + name: 'Everything I Own', + }, + ], + }, + }; + }, + }, +}; /** - * A name was previously saved for this recognized token.

+ * No name was previously saved for this recognized NFT.

* The component will still display a modal when clicked to edit the name. */ -export const SavedTokenNameStory = () => { - return ( - - ); +export const DefaultNFTNameStory = { + name: 'Default NFT Name', + args: { + value: ADDRESS_NFT_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, }; -SavedTokenNameStory.storyName = 'Saved Token Name'; +/** + * No name was previously saved for this first-party contract.

+ * The component will still display a modal when clicked to edit the name. + */ +export const DefaultFirstPartyNameStory = { + name: 'Default First-Party Name', + args: { + value: ADDRESS_FIRST_PARTY_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, +}; /** * Clicking the component will not display a modal to edit the name. */ -export const EditDisabledStory = () => { - return ( - - ); +export const EditDisabledStory = { + name: 'Edit Disabled', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + disableEdit: true, + }, }; - -EditDisabledStory.storyName = 'Edit Disabled'; diff --git a/ui/pages/notification-details/notification-details-footer/notification-details-footer.tsx b/ui/pages/notification-details/notification-details-footer/notification-details-footer.tsx index 2af87b2d6730..7a1f9a1c8890 100644 --- a/ui/pages/notification-details/notification-details-footer/notification-details-footer.tsx +++ b/ui/pages/notification-details/notification-details-footer/notification-details-footer.tsx @@ -35,9 +35,15 @@ export const NotificationDetailsFooter = ({ )} {footer.type === 'footer_feature_announcement' && ( - <> + + - + )} ); diff --git a/ui/pages/notifications/notification-components/feature-announcement/feature-announcement.tsx b/ui/pages/notifications/notification-components/feature-announcement/feature-announcement.tsx index 281112b6172d..d64015d590a3 100644 --- a/ui/pages/notifications/notification-components/feature-announcement/feature-announcement.tsx +++ b/ui/pages/notifications/notification-components/feature-announcement/feature-announcement.tsx @@ -114,6 +114,20 @@ export const components: NotificationComponent href={`/${notification.data.extensionLink.extensionLinkRoute}`} id={notification.id} endIconName={false} + // Even if the link is not external, it will open in a new tab + // to avoid breaking the popup + isExternal={true} + /> + ) : null, + ExternalLink: ({ notification }) => + notification.data.externalLink ? ( + ) : null, diff --git a/ui/pages/notifications/notification-components/types/notifications/notifications.ts b/ui/pages/notifications/notification-components/types/notifications/notifications.ts index 445646904ecb..15831627bf9c 100644 --- a/ui/pages/notifications/notification-components/types/notifications/notifications.ts +++ b/ui/pages/notifications/notification-components/types/notifications/notifications.ts @@ -40,6 +40,7 @@ type FooterOnChainNotification = { type FooterFeatureAnnouncement = { type: 'footer_feature_announcement'; ExtensionLink: NotificationFC; + ExternalLink: NotificationFC; }; /**