require ( "chromedriver" ) ; import { resolve } from "path" ; import { Builder , Locator , until , WebDriver , WebElement , WebElementPromise } from "selenium-webdriver" ; import Chrome , { Options , ServiceBuilder } from "selenium-webdriver/chrome" ; import { EXTENSION_LIST_SELECTOR , METAMASK_EXTENSION_URL_SELECTOR } from "./chrome-selectors" ; import { ACCOUNT_DEPOSIT_SELECTORS , ACCOUNT_REGISTRATION_SELECTORS , LAYOUT_HEADER_SELECTORS } from "./counterfactual-wallet-selectors" ; import { DEPOSIT_SELECTORS , findItemByExactTextMatch , findItemByPartialTextMatch , FIRST_TIME_FLOW_SELECTORS , MAIN_SCREEN_SELECTORS , NETWORK_SELECTORS , NOTIFICATION_SELECTORS , REQUEST_SIGNATURE_SELECTORS , UNLOCK_FLOW_SELECTORS , WALLET_AUTHORIZATION_MODAL_SELECTORS } from "./metamask-selectors" ; import StateCollector from "./state-collector" ; import { CounterfactualScreenName , MetamaskFlowType , MetamaskNetwork , MetamaskOptions , MetamaskTransaction , StringHashMap , TestBrowserContext } from "./types" ; export const EXTENSION_INSPECTOR = "chrome://inspect/#extensions" ; export const LOCATOR_TIMEOUT = 10000 ; export const METAMASK_ETH_ADDRESS = "0x212C90fdF90BbD5E9b352b9d2B086f2666CFEED6" ; export const METAMASK_MNEMONIC = "mistake cash photo pond little nerve neutral adapt item kite radar tray" ; export const METAMASK_PASSWORD = "The Cake Is A Lie" ; export const COUNTERFACTUAL_USER_USERNAME = "teapot" ; export const COUNTERFACTUAL_USER_EMAIL_ADDRESS = "i.am.a@tea.pot" ; /**
 * Creates a Chrome browser, with a build of the Metamask + CF extension.
 */ export class TestBrowser { constructor ( private browser = { } as WebDriver , private homeUrl = "" , private popupUrl = "" , private readonly handlesByContext = { [ TestBrowserContext . MetamaskMain ] : "" , [ TestBrowserContext . MetamaskPopup ] : "" } , private currentContext ? , private readonly locatorTimeout = LOCATOR_TIMEOUT , private readonly stateCollector = new StateCollector ( ) ) { } /**
     * Initializes an instance of the TestBrowser with the following flags:
     *
     * --load-extension: Pointed towards packages/greenboard/extension, to an
     *   unpacked build of the Metamask extension for Chrome.
     *
     * --disable-web-security: Removes the execution of preflight checks for CORS.
     *   Necessary for simple, quick communication with the Hub API.
     *
     * --user-data-dir: Pointed towards packages/greenboard/chrome-profile, it is
     *   a temporary profile directory for Chrome.
     *
     * If the CHROME_DRIVER_PATH environment variable is supplied, it'll try to
     * use that as a ChromeDriver builder. Otherwise, it'll fallback to the
     * "chromedriver" package supplied by Greenboard.
     *
     * If the CHROME_BINARY_PATH environment variable is set, the TestBrowser
     * will use that as a Chrome browser.
     *
     * Unless the `--discover-metamask` flag is passed by the `test:e2e` run-script,
     * the TestBrowser will wait up to 5 seconds for Chrome to automatically
     * open the extension's homepage tab.
     */ async start ( ) { const extensionDirectory = resolve ( __dirname , "../extension" ) ; const chromeProfileDirectory = resolve ( __dirname , "../chrome-profile" ) ; const chromeDriver = new ServiceBuilder ( process . env . CHROME_DRIVER_PATH || require ( "chromedriver" ) . path ) . build ( ) ; Chrome . setDefaultService ( chromeDriver ) ; await chromeDriver . start ( ) ; const browserFactory = new Builder ( ) . forBrowser ( "chrome" ) ; const options = new Options ( ) ; options . addArguments ( `--load-extension=${ extensionDirectory }` , `--disable-web-security` , `--user-data-dir=${ chromeProfileDirectory }` ) ; if ( process . env . CHROME_BINARY_PATH ) { options . setChromeBinaryPath ( process . env . CHROME_BINARY_PATH as string ) ; } this . browser = browserFactory . setChromeOptions ( options ) . setAlertBehavior ( "accept" ) . build ( ) ; if ( ! process . argv . includes ( "--discover-metamask" ) ) { await this . browser . sleep ( 5000 ) ; const handles = await this . browser . getAllWindowHandles ( ) ; this . handlesByContext [ TestBrowserContext . MetamaskMain ] = handles . pop ( ) as string ; await this . switchToMetamask ( ) ; this . homeUrl = await this . browser . getCurrentUrl ( ) ; this . popupUrl = this . homeUrl . replace ( /home/gi , "popup" ) . split ( "#" ) [ 0 ] ; } } /****************************************************************
     * Metamask Automations API
     ****************************************************************/ param flowType { MetamaskFlowType } param networkName { MetamaskNetwork } async prepare ( flowType , networkName = "kovan" ) { await this . openMetamask ( ) ; switch ( flowType ) { case MetamaskFlowType . NewUser : await this . setupMetamask ( ) ; break ; case MetamaskFlowType . ReturningUser : await this . loginToMetamask ( ) ; } await this . waitForMetamaskMainScreen ( ) ; await this . setMetamaskNetwork ( networkName ) ; await this . openCounterfactualWallet ( ) ; await this . authorizeWallet ( ) ; } /**
     * Only used if the `--discover-metamask` flag is passed to the test runner.
     *
     * Opens the Metamask extension page in the current window. It'll automatically
     * lookup the extension ID from the Extensions Inspector and save the `home`
     * and `popup` URLs for later usage. If such URLs are already defined,
     * it'll skip the lookup and navigate to `homeUrl`.
     */ async openMetamask ( ) { if ( ! process . argv . includes ( "--discover-metamask" ) ) { await this . switchToMetamask ( ) ; return ; } if ( this . homeUrl && this . popupUrl ) { await this . navigateTo ( this . homeUrl ) ; return ; } await this . waitForExtensionsList ( ) ; const backgroundPageUrl = await this . getTextFromElement ( METAMASK_EXTENSION_URL_SELECTOR ) ; this . homeUrl = backgroundPageUrl . replace ( "_generated_background_page" , "home" ) ; this . popupUrl = backgroundPageUrl . replace ( "_generated_background_page" , "popup" ) ; await this . navigateTo ( this . homeUrl ) ; await this . switchToMetamask ( ) ; } param settings async setupMetamask ( settings = { seedPhraseValue : METAMASK_MNEMONIC , passwordValue : METAMASK_PASSWORD } ) { const { seedPhraseValue , passwordValue } = settings ; const { getStartedButton , importFromSeedPhraseButton , iAgreeButton , seedPhraseInput , passwordInput , confirmPasswordInput , acceptTermsAndConditionsCheckbox , importSubmitButton , endOfFlowScreen , allDoneButton } = FIRST_TIME_FLOW_SELECTORS ; await this . clickOnElement ( getStartedButton ) ; await this . clickOnElement ( importFromSeedPhraseButton ) ; await this . clickOnElement ( iAgreeButton ) ; await this . typeOnInput ( seedPhraseInput , seedPhraseValue ) ; await this . typeOnInput ( passwordInput , passwordValue ) ; await this . typeOnInput ( confirmPasswordInput , passwordValue ) ; await this . clickOnElement ( acceptTermsAndConditionsCheckbox ) ; await this . clickOnElement ( importSubmitButton ) ; await this . waitForElement ( endOfFlowScreen ) ; await this . clickOnElement ( allDoneButton ) ; } /**
     * Unlocks Metamask by logging in with a provided password
     * (defaults to the password used while filling the registration
     * form on the onboarding flow).
     */ async loginToMetamask ( passwordValue = METAMASK_PASSWORD ) { const { unlockScreen , passwordInput , loginSubmitButton } = UNLOCK_FLOW_SELECTORS ; await this . waitForElement ( unlockScreen ) ; await this . typeOnInput ( passwordInput , passwordValue ) ; await this . clickOnElement ( loginSubmitButton ) ; } /**
     * Waits for Metamask to show the main UI.
     */ async waitForMetamaskMainScreen ( ) { const { mainScreenContainer } = MAIN_SCREEN_SELECTORS ; await this . waitForElement ( mainScreenContainer ) ; } param networkName { MetamaskNetwork } async setMetamaskNetwork ( networkName = "kovan" ) { const { networkMenuButton , networkMenuList , networkMenuItem } = MAIN_SCREEN_SELECTORS ; await this . clickOnElement ( networkMenuButton ) ; await this . waitForElement ( networkMenuList ) ; await this . clickOnElement ( networkMenuItem ( networkName ) ) ; await this . waitForElement ( NETWORK_SELECTORS [ networkName ] ) ; } /**
     * Clicks on the "Counterfactual" button on Metamask's sidebar and waits
     * until the Wallet UI's IFRAME is rendered.
     */ async openCounterfactualWallet ( ) { const { counterfactualToken , pluginIframe } = MAIN_SCREEN_SELECTORS ; await this . clickOnElement ( counterfactualToken ) ; await this . waitForElement ( pluginIframe ) ; } /**
     * Waits for Metamask to request permission to connect the Wallet UI
     * with the Ethereum Provider and grants permission by clicking
     * the "Connect" button.
     */ async authorizeWallet ( ) { const { modalContainer , modalPrimaryButton } = WALLET_AUTHORIZATION_MODAL_SELECTORS ; await this . waitForElement ( modalContainer ) ; await this . clickOnElement ( modalPrimaryButton ) ; } /**
     * Changes the WebDriver's context to operate on the Metamask window.
     * Use this before further actions if you've been interacting with
     * other contexts before (Wallet UI, Metamask Popups).
     */ async switchToMetamask ( ) { await this . updateContext ( TestBrowserContext . MetamaskMain ) ; } param transactionType async switchToMetamaskPopup ( transactionType ) { const popupHandle = await this . openNewTab ( ) ; this . handlesByContext [ TestBrowserContext . MetamaskPopup ] = popupHandle ; await this . updateContext ( TestBrowserContext . MetamaskPopup ) ; await this . navigateTo ( this . popupUrl ) ; await this . waitForElement ( NOTIFICATION_SELECTORS [ transactionType ] ) ; } /**
     * Waits for the Signature Request popup to be opened,
     * clicks the "Sign" button and closes the popup.
     */ async signTransaction ( ) { const { signButton } = REQUEST_SIGNATURE_SELECTORS ; await this . switchToMetamaskPopup ( "signatureRequest" ) ; await this . clickOnElement ( signButton ) ; await this . closeTab ( ) ; await this . switchToWallet ( ) ; } /**
     * Waits for the Deposit Confirmation popup to be opened,
     * clicks the "Confirm" button and closes the popup.
     */ async confirmDeposit ( ) { const { confirmButton } = DEPOSIT_SELECTORS ; await this . switchToMetamaskPopup ( "deposit" ) ; await this . clickOnElement ( confirmButton ) ; await this . closeTab ( ) ; await this . switchToWallet ( ) ; } /****************************************************************
     * Counterfactual Wallet UI Automations API
     ****************************************************************/ /**
     * Changes the WebDriver's context to operate on the Wallet UI's IFRAME.
     * Use this before trying to interact with any elements belonging to the
     * Wallet UI.
     */ async switchToWallet ( ) { await this . switchToMetamask ( ) ; await this . updateContext ( TestBrowserContext . CounterfactualWallet ) ; } param username param email async fillAccountRegistrationFormAndSubmit ( username = COUNTERFACTUAL_USER_USERNAME , email = COUNTERFACTUAL_USER_EMAIL_ADDRESS ) { const { usernameInput , emailInput , createAccountButton } = ACCOUNT_REGISTRATION_SELECTORS ; await this . typeOnInput ( usernameInput , username ) ; await this . typeOnInput ( emailInput , email ) ; await this . clickOnElement ( createAccountButton ) ; await this . waitForElementToHaveText ( createAccountButton , "Check your wallet" ) ; await this . signTransaction ( ) ; await this . waitForElementToHaveText ( createAccountButton , "Creating your account" ) ; } todo async fillAccountDepositFormAndSubmit ( ) { const { formTitle , proceedButton } = ACCOUNT_DEPOSIT_SELECTORS ; const { logoContainer } = LAYOUT_HEADER_SELECTORS ; await this . waitForElementToHaveText ( formTitle , "Fund your account" ) ; await this . clickOnElement ( proceedButton ) ; await this . waitForElementToHaveText ( proceedButton , "Check your wallet" ) ; await this . confirmDeposit ( ) ; await this . waitForElement ( logoContainer , 90000 ) ; } /**
     * Returns the current screen's name according to the current URL.
     */ async getCurrentScreenName ( ) { if ( this . currentContext !== TestBrowserContext . CounterfactualWallet ) { return ; } const url = await this . browser . executeScript < string > ( "return document.location.href" ) ; if ( url . includes ( "setup/register" ) ) { return CounterfactualScreenName . OnboardingRegistration ; } if ( url . includes ( "setup/deposit" ) ) { return CounterfactualScreenName . OnboardingDeposit ; } if ( url . includes ( "channels" ) ) { return CounterfactualScreenName . Channels ; } return CounterfactualScreenName . Welcome ; } /**
     * Waits for the Hub API to respond a login request by holding execution
     * until the AccountContainer UI element is rendered in the screen.
     */ async waitForLoginToBeFinished ( ) { const { accountContainer } = LAYOUT_HEADER_SELECTORS ; await this . waitForElement ( accountContainer ) ; } /****************************************************************
     * Browser Generic API
     ****************************************************************/ /**
     * Closes the current window.
     */ async closeTab ( ) { await this . browser . close ( ) ; } /**
     * Closes the entire browser.
     */ async closeBrowser ( ) { await this . browser . quit ( ) ; } param selector getElements ( selector ) { return new Promise ( async ( resolve , reject ) => { setTimeout ( ( ) => reject ( "Timeout" ) , this . locatorTimeout ) ; const result = await this . browser . findElements ( selector ) ; resolve ( result ) ; } ) ; } param selector getElement ( selector ) { let result = { } as WebElementPromise ; const attempts = 0 ; while ( attempts < 5 ) { try { result = this . browser . wait ( until . elementLocated ( selector ) , this . locatorTimeout , selector . toString ( ) ) ; break ; } catch ( e ) { const error = e as Error ; if ( ! error . message . startsWith ( "StaleElementReferenceError" ) ) { throw e ; } } } return result ; } param selector async waitForElement ( selector , timeout = this . locatorTimeout ) { await this . browser . wait ( until . elementLocated ( selector ) , timeout , selector . toString ( ) ) ; } param selector async clickOnElement ( selector ) { await this . getElement ( selector ) . click ( ) ; } param selector async typeOnInput ( selector , value ) { await this . getElement ( selector ) . sendKeys ( value ) ; } param selector async getTextFromElement ( selector ) { return this . getElement ( selector ) . getText ( ) ; } param selector param text async waitForElementToHaveText ( selector , expectedText ) { let attempts = 0 ; while ( attempts < 5 ) { try { const element = await this . getElement ( selector ) ; const currentText = await element . getText ( ) ; return currentText . includes ( expectedText ) ; } catch ( e ) { const { message } = e as Error ; if ( message . startsWith ( "StaleElementReferenceError" ) ) { await this . browser . sleep ( 50 ) ; attempts += 1 ; } } } return false ; } param elementLocator param partialText async findElementByPartialTextMatch ( elementLocator , partialText ) { return findItemByPartialTextMatch ( this . browser , elementLocator , partialText , this . locatorTimeout ) ; } param elementLocator param partialText async findElementByExactTextMatch ( elementLocator , text ) { return findItemByExactTextMatch ( this . browser , elementLocator , text , this . locatorTimeout ) ; } /**
     * Extracts all LocalStorage data from Metamask.
     */ async collectMetamaskLocalStorage ( ) { await this . switchToMetamask ( ) ; const data = ( await this . browser . executeScript ( `
      const collectedData = {};
      const { length } = window.localStorage;
      for (let index = 0; index < length; index += 1) {
        const key = window.localStorage.key(index);
        collectedData[key] = window.localStorage.getItem(key);
      }
      return collectedData;
    ` ) ) as StringHashMap ; expect ( Object . keys ( data ) ) . not . toHaveLength ( 0 ) ; this . stateCollector . write ( data ) ; } param data async injectIntoMetamaskLocalStorage ( ) { const data = this . stateCollector . read ( ) ; await this . switchToMetamask ( ) ; expect ( Object . keys ( data ) . length ) . toBeGreaterThan ( 0 ) ; for ( const [ key , value ] of Object . entries ( data ) ) { await this . browser . executeScript ( `window.localStorage.setItem(arguments[0], arguments[1]);` , key , value ) ; expect ( await this . browser . executeScript ( `return window.localStorage.getItem(arguments[0]);` , key ) ) . toEqual ( value ) ; } } /****************************************************************
     * Private API
     ****************************************************************/ param url private async navigateTo ( url ) { await this . browser . navigate ( ) . to ( url ) ; } /**
     * Redirects the browser to chrome://inspect/#extensions, to wait until
     * the MetaMask extension is listed as available.
     */ private async waitForExtensionsList ( ) { await this . navigateTo ( EXTENSION_INSPECTOR ) ; const extensionsList = await this . getElement ( EXTENSION_LIST_SELECTOR ) ; await this . browser . wait ( until . elementTextContains ( extensionsList , "MetaMask" ) ) ; } param newContext private async updateContext ( newContext ) { this . currentContext = newContext ; if ( newContext !== TestBrowserContext . CounterfactualWallet ) { await this . browser . switchTo ( ) . window ( this . handlesByContext [ newContext ] ) ; } else { const { pluginIframe } = MAIN_SCREEN_SELECTORS ; const iframe = await this . getElement ( pluginIframe ) ; await this . browser . switchTo ( ) . frame ( iframe ) ; } } param millisecondsToWaitBeforeOpening param millisecondsToWaitAfterOpening private async openNewTab ( millisecondsToWaitBeforeOpening = 1000 , millisecondsToWaitAfterOpening = 100 ) { await this . browser . sleep ( millisecondsToWaitBeforeOpening ) ; await this . browser . executeScript ( `window.open('','popup_${ Date . now ( ) }');` ) ; await this . browser . sleep ( millisecondsToWaitAfterOpening ) ; const windowHandles = await this . browser . getAllWindowHandles ( ) ; return windowHandles . pop ( ) as string ; } }