text: // Licensed to the Software Freedom Conservancy (SFC) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The SFC licenses this file
  // to you under the Apache License, Version 2.0 (the
  // "License"); you may not use this file except in compliance
  // with the License.  You may obtain a copy of the License at
  //
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing,
  // software distributed under the License is distributed on an
  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  // KIND, either express or implied.  See the License for the
  // specific language governing permissions and limitations
  // under the License.
  
  /**
   * @fileoverview Defines an abstract {@linkplain Driver WebDriver} client for
   * Chromium-based web browsers. These classes should not be instantiated
   * directly.
   *
   * There are three primary classes exported by this module:
   *
   * 1. {@linkplain ServiceBuilder}: configures the
   *     {@link selenium-webdriver/remote.DriverService remote.DriverService}
   *     that manages a WebDriver server child process.
   *
   * 2. {@linkplain Options}: defines configuration options for each new Chromium
   *     session, such as which {@linkplain Options#setProxy proxy} to use,
   *     what {@linkplain Options#addExtensions extensions} to install, or
   *     what {@linkplain Options#addArguments command-line switches} to use when
   *     starting the browser.
   *
   * 3. {@linkplain Driver}: the WebDriver client; each new instance will control
   *     a unique browser session with a clean user profile (unless otherwise
   *     configured through the {@link Options} class).
   *
   * __Headless Chromium__ <a id="headless"></a>
   *
   * To start the browser in headless mode, simply call
   * {@linkplain Options#headless Options.headless()}.
   *
   *     let chrome = require('selenium-webdriver/chrome');
   *     let {Builder} = require('selenium-webdriver');
   *
   *     let driver = new Builder()
   *         .forBrowser('chrome')
   *         .setChromeOptions(new chrome.Options().headless())
   *         .build();
   *
   * __Customizing the Chromium WebDriver Server__ <a id="custom-server"></a>
   *
   * Subclasses of {@link Driver} are expected to provide a static
   * getDefaultService method. By default, this method will be called every time
   * a {@link Driver} instance is created to obtain the default driver service
   * for that specific browser (e.g. Chrome or Chromium Edge). Subclasses are
   * responsible for managing the lifetime of the default service.
   *
   * You may also create a {@link Driver} with its own driver service. This is
   * useful if you need to capture the server's log output for a specific session:
   *
   *     let chrome = require('selenium-webdriver/chrome');
   *
   *     let service = new chrome.ServiceBuilder()
   *         .loggingTo('/my/log/file.txt')
   *         .enableVerboseLogging()
   *         .build();
   *
   *     let options = new chrome.Options();
   *     // configure browser options ...
   *
   *     let driver = chrome.Driver.createSession(options, service);
   */
  
  'use strict'
  
  const http = require('./http')
  const io = require('./io')
  const { Capabilities, Capability } = require('./lib/capabilities')
  const command = require('./lib/command')
  const error = require('./lib/error')
  const Symbols = require('./lib/symbols')
  const webdriver = require('./lib/webdriver')
  const WebSocket = require('ws')
  const cdp = require('./devtools/CDPConnection')
  const remote = require('./remote')
  const cdpTargets = ['page', 'browser']
  const fs = require('fs')
  
  /**
   * Custom command names supported by Chromium WebDriver.
   * @enum {string}
   */
  const Command = {
    LAUNCH_APP: 'launchApp',
    GET_NETWORK_CONDITIONS: 'getNetworkConditions',
    SET_NETWORK_CONDITIONS: 'setNetworkConditions',
    SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand',
    SEND_AND_GET_DEVTOOLS_COMMAND: 'sendAndGetDevToolsCommand',
    SET_PERMISSION: 'setPermission',
    GET_CAST_SINKS: 'getCastSinks',
    SET_CAST_SINK_TO_USE: 'setCastSinkToUse',
    START_CAST_TAB_MIRRORING: 'setCastTabMirroring',
    GET_CAST_ISSUE_MESSAGE: 'getCastIssueMessage',
    STOP_CASTING: 'stopCasting',
  }
  
  /**
   * Creates a command executor with support for Chromium's custom commands.
   * @param {!Promise<string>} url The server's URL.
   * @return {!command.Executor} The new command executor.
   */
  function createExecutor(url, vendorPrefix) {
    let agent = new http.Agent({ keepAlive: true })
    let client = url.then((url) => new http.HttpClient(url, agent))
    let executor = new http.Executor(client)
    configureExecutor(executor, vendorPrefix)
    return executor
  }
  
  /**
   * Configures the given executor with Chromium-specific commands.
   * @param {!http.Executor} executor the executor to configure.
   */
  function configureExecutor(executor, vendorPrefix) {
    executor.defineCommand(
      Command.LAUNCH_APP,
      'POST',
      '/session/:sessionId/chromium/launch_app'
    )
    executor.defineCommand(
      Command.GET_NETWORK_CONDITIONS,
      'GET',
      '/session/:sessionId/chromium/network_conditions'
    )
    executor.defineCommand(
      Command.SET_NETWORK_CONDITIONS,
      'POST',
      '/session/:sessionId/chromium/network_conditions'
    )
    executor.defineCommand(
      Command.SEND_DEVTOOLS_COMMAND,
      'POST',
      '/session/:sessionId/chromium/send_command'
    )
    executor.defineCommand(
      Command.SEND_AND_GET_DEVTOOLS_COMMAND,
      'POST',
      '/session/:sessionId/chromium/send_command_and_get_result'
    )
    executor.defineCommand(
      Command.SET_PERMISSION,
      'POST',
      '/session/:sessionId/permissions'
    )
    executor.defineCommand(
      Command.GET_CAST_SINKS,
      'GET',
      `/session/:sessionId/${vendorPrefix}/cast/get_sinks`
    )
    executor.defineCommand(
      Command.SET_CAST_SINK_TO_USE,
      'POST',
      `/session/:sessionId/${vendorPrefix}/cast/set_sink_to_use`
    )
    executor.defineCommand(
      Command.START_CAST_TAB_MIRRORING,
      'POST',
      `/session/:sessionId/${vendorPrefix}/cast/start_tab_mirroring`
    )
    executor.defineCommand(
      Command.GET_CAST_ISSUE_MESSAGE,
      'GET',
      `/session/:sessionId/${vendorPrefix}/cast/get_issue_message`
    )
    executor.defineCommand(
      Command.STOP_CASTING,
      'POST',
      `/session/:sessionId/${vendorPrefix}/cast/stop_casting`
    )
  }
  
  /**
   * Creates {@link selenium-webdriver/remote.DriverService} instances that manage
   * a WebDriver server in a child process.
   */
  class ServiceBuilder extends remote.DriverService.Builder {
    /**
     * @param {string=} exe Path to the server executable to use. Subclasses
     * should ensure a valid path to the appropriate exe is provided.
     */
    constructor(exe) {
      super(exe)
      this.setLoopback(true) // Required
    }
  
    /**
     * Sets which port adb is listening to. _The driver will connect to adb
     * if an {@linkplain Options#androidPackage Android session} is requested, but
     * adb **must** be started beforehand._
     *
     * @param {number} port Which port adb is running on.
     * @return {!ServiceBuilder} A self reference.
     */
    setAdbPort(port) {
      return this.addArguments('--adb-port=' + port)
    }
  
    /**
     * Sets the path of the log file the driver should log to. If a log file is
     * not specified, the driver will log to stderr.
     * @param {string} path Path of the log file to use.
     * @return {!ServiceBuilder} A self reference.
     */
    loggingTo(path) {
      return this.addArguments('--log-path=' + path)
    }
  
    /**
     * Enables verbose logging.
     * @return {!ServiceBuilder} A self reference.
     */
    enableVerboseLogging() {
      return this.addArguments('--verbose')
    }
  
    /**
     * Sets the number of threads the driver should use to manage HTTP requests.
     * By default, the driver will use 4 threads.
     * @param {number} n The number of threads to use.
     * @return {!ServiceBuilder} A self reference.
     */
    setNumHttpThreads(n) {
      return this.addArguments('--http-threads=' + n)
    }
  
    /**
     * @override
     */
    setPath(path) {
      super.setPath(path)
      return this.addArguments('--url-base=' + path)
    }
  }
  
  /**
   * Class for managing WebDriver options specific to a Chromium-based browser.
   */
  class Options extends Capabilities {
    /**
     * @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
     *     capabilities to initialize this instance from.
     */
    constructor(other = undefined) {
      super(other)
  
      /** @private {!Object} */
      this.options_ = this.get(this.CAPABILITY_KEY) || {}
  
      this.setBrowserName(this.BROWSER_NAME_VALUE)
      this.set(this.CAPABILITY_KEY, this.options_)
    }
  
    /**
     * Add additional command line arguments to use when launching the browser.
     * Each argument may be specified with or without the "--" prefix
     * (e.g. "--foo" and "foo"). Arguments with an associated value should be
     * delimited by an "=": "foo=bar".
     *
     * @param {...(string|!Array<string>)} args The arguments to add.
     * @return {!Options} A self reference.
     */
    addArguments(...args) {
      let newArgs = (this.options_.args || []).concat(...args)
      if (newArgs.length) {
        this.options_.args = newArgs
      }
      return this
    }
  
    /**
     * Configures the driver to start the browser in headless mode.
     *
     * > __NOTE:__ Resizing the browser window in headless mode is only supported
     * > in Chromium 60+. Users are encouraged to set an initial window size with
     * > the {@link #windowSize windowSize({width, height})} option.
     *
     * > __NOTE__: For security, Chromium disables downloads by default when
     * > in headless mode (to prevent sites from silently downloading files to
     * > your machine). After creating a session, you may call
     * > {@link ./chrome.Driver#setDownloadPath setDownloadPath} to re-enable
     * > downloads, saving files in the specified directory.
     *
     * @return {!Options} A self reference.
     */
    headless() {
      return this.addArguments('headless')
    }
  
    /**
     * Sets the initial window size.
     *
     * @param {{width: number, height: number}} size The desired window size.
     * @return {!Options} A self reference.
     * @throws {TypeError} if width or height is unspecified, not a number, or
     *     less than or equal to 0.
     */
    windowSize({ width, height }) {
      function checkArg(arg) {
        if (typeof arg !== 'number' || arg <= 0) {
          throw TypeError('Arguments must be {width, height} with numbers > 0')
        }
      }
      checkArg(width)
      checkArg(height)
      return this.addArguments(`window-size=${width},${height}`)
    }
  
    /**
     * List of Chrome command line switches to exclude that ChromeDriver by default
     * passes when starting Chrome.  Do not prefix switches with "--".
     *
     * @param {...(string|!Array<string>)} args The switches to exclude.
     * @return {!Options} A self reference.
     */
    excludeSwitches(...args) {
      let switches = (this.options_.excludeSwitches || []).concat(...args)
      if (switches.length) {
        this.options_.excludeSwitches = switches
      }
      return this
    }
  
    /**
     * Add additional extensions to install when launching the browser. Each extension
     * should be specified as the path to the packed CRX file, or a Buffer for an
     * extension.
     * @param {...(string|!Buffer|!Array<(string|!Buffer)>)} args The
     *     extensions to add.
     * @return {!Options} A self reference.
     */
    addExtensions(...args) {
      let current = this.options_.extensions || []
      this.options_.extensions = current.concat(...args)
      return this
    }
  
    /**
     * Sets the path to the browser binary to use. On Mac OS X, this path should
     * reference the actual Chromium executable, not just the application binary
     * (e.g. "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").
     *
     * The binary path can be absolute or relative to the WebDriver server
     * executable, but it must exist on the machine that will launch the browser.
     *
     * @param {string} path The path to the browser binary to use.
     * @return {!Options} A self reference.
     */
    setBinaryPath(path) {
      this.options_.binary = path
      return this
    }
  
    /**
     * Sets whether to leave the started browser process running if the controlling
     * driver service is killed before {@link webdriver.WebDriver#quit()} is
     * called.
     * @param {boolean} detach Whether to leave the browser running if the
     *     driver service is killed before the session.
     * @return {!Options} A self reference.
     */
    detachDriver(detach) {
      this.options_.detach = detach
      return this
    }
  
    /**
     * Sets the user preferences for Chrome's user profile. See the "Preferences"
     * file in Chrome's user data directory for examples.
     * @param {!Object} prefs Dictionary of user preferences to use.
     * @return {!Options} A self reference.
     */
    setUserPreferences(prefs) {
      this.options_.prefs = prefs
      return this
    }
  
    /**
     * Sets the performance logging preferences. Options include:
     *
     * - `enableNetwork`: Whether or not to collect events from Network domain.
     * - `enablePage`: Whether or not to collect events from Page domain.
     * - `enableTimeline`: Whether or not to collect events from Timeline domain.
     *     Note: when tracing is enabled, Timeline domain is implicitly disabled,
     *     unless `enableTimeline` is explicitly set to true.
     * - `traceCategories`: A comma-separated string of Chromium tracing
     *     categories for which trace events should be collected. An unspecified
     *     or empty string disables tracing.
     * - `bufferUsageReportingInterval`: The requested number of milliseconds
     *     between DevTools trace buffer usage events. For example, if 1000, then
     *     once per second, DevTools will report how full the trace buffer is. If
     *     a report indicates the buffer usage is 100%, a warning will be issued.
     *
     * @param {{enableNetwork: boolean,
     *          enablePage: boolean,
     *          enableTimeline: boolean,
     *          traceCategories: string,
     *          bufferUsageReportingInterval: number}} prefs The performance
     *     logging preferences.
     * @return {!Options} A self reference.
     */
    setPerfLoggingPrefs(prefs) {
      this.options_.perfLoggingPrefs = prefs
      return this
    }
  
    /**
     * Sets preferences for the "Local State" file in Chrome's user data
     * directory.
     * @param {!Object} state Dictionary of local state preferences.
     * @return {!Options} A self reference.
     */
    setLocalState(state) {
      this.options_.localState = state
      return this
    }
  
    /**
     * Sets the name of the activity hosting a Chrome-based Android WebView. This
     * option must be set to connect to an [Android WebView](
     * https://chromedriver.chromium.org/getting-started/getting-started---android)
     *
     * @param {string} name The activity name.
     * @return {!Options} A self reference.
     */
    androidActivity(name) {
      this.options_.androidActivity = name
      return this
    }
  
    /**
     * Sets the device serial number to connect to via ADB. If not specified, the
     * WebDriver server will select an unused device at random. An error will be
     * returned if all devices already have active sessions.
     *
     * @param {string} serial The device serial number to connect to.
     * @return {!Options} A self reference.
     */
    androidDeviceSerial(serial) {
      this.options_.androidDeviceSerial = serial
      return this
    }
  
    /**
     * Sets the package name of the Chrome or WebView app.
     *
     * @param {?string} pkg The package to connect to, or `null` to disable Android
     *     and switch back to using desktop browser.
     * @return {!Options} A self reference.
     */
    androidPackage(pkg) {
      this.options_.androidPackage = pkg
      return this
    }
  
    /**
     * Sets the process name of the Activity hosting the WebView (as given by
     * `ps`). If not specified, the process name is assumed to be the same as
     * {@link #androidPackage}.
     *
     * @param {string} processName The main activity name.
     * @return {!Options} A self reference.
     */
    androidProcess(processName) {
      this.options_.androidProcess = processName
      return this
    }
  
    /**
     * Sets whether to connect to an already-running instead of the specified
     * {@linkplain #androidProcess app} instead of launching the app with a clean
     * data directory.
     *
     * @param {boolean} useRunning Whether to connect to a running instance.
     * @return {!Options} A self reference.
     */
    androidUseRunningApp(useRunning) {
      this.options_.androidUseRunningApp = useRunning
      return this
    }
  
    /**
     * Sets the path to the browser's log file. This path should exist on the machine
     * that will launch the browser.
     * @param {string} path Path to the log file to use.
     * @return {!Options} A self reference.
     */
    setBrowserLogFile(path) {
      this.options_.logPath = path
      return this
    }
  
    /**
     * Sets the directory to store browser minidumps in. This option is only
     * supported when the driver is running on Linux.
     * @param {string} path The directory path.
     * @return {!Options} A self reference.
     */
    setBrowserMinidumpPath(path) {
      this.options_.minidumpPath = path
      return this
    }
  
    /**
     * Configures the browser to emulate a mobile device. For more information, refer
     * to the ChromeDriver project page on [mobile emulation][em]. Configuration
     * options include:
     *
     * - `deviceName`: The name of a pre-configured [emulated device][devem]
     * - `width`: screen width, in pixels
     * - `height`: screen height, in pixels
     * - `pixelRatio`: screen pixel ratio
     *
     * __Example 1: Using a Pre-configured Device__
     *
     *     let options = new chrome.Options().setMobileEmulation(
     *         {deviceName: 'Google Nexus 5'});
     *
     *     let driver = chrome.Driver.createSession(options);
     *
     * __Example 2: Using Custom Screen Configuration__
     *
     *     let options = new chrome.Options().setMobileEmulation({deviceMetrics: {
     *         width: 360,
     *         height: 640,
     *         pixelRatio: 3.0
     *     }});
     *
     *     let driver = chrome.Driver.createSession(options);
     *
     *
     * [em]: https://chromedriver.chromium.org/mobile-emulation
     * [devem]: https://developer.chrome.com/devtools/docs/device-mode
     *
     * @param {?({deviceName: string}|
     *           {width: number, height: number, pixelRatio: number})} config The
     *     mobile emulation configuration, or `null` to disable emulation.
     * @return {!Options} A self reference.
     */
    setMobileEmulation(config) {
      this.options_.mobileEmulation = config
      return this
    }
  
    /**
     * Converts this instance to its JSON wire protocol representation. Note this
     * function is an implementation not intended for general use.
     *
     * @return {!Object} The JSON wire protocol representation of this instance.
     * @suppress {checkTypes} Suppress [] access on a struct.
     */
    [Symbols.serialize]() {
      if (this.options_.extensions && this.options_.extensions.length) {
        this.options_.extensions = this.options_.extensions.map(function (
          extension
        ) {
          if (Buffer.isBuffer(extension)) {
            return extension.toString('base64')
          }
          return io
            .read(/** @type {string} */ (extension))
            .then((buffer) => buffer.toString('base64'))
        })
      }
      return super[Symbols.serialize]()
    }
  }
  
  /**
   * Creates a new WebDriver client for Chromium-based browsers.
   */
  class Driver extends webdriver.WebDriver {
    /**
     * Creates a new session with the WebDriver server.
     *
     * @param {(Capabilities|Options)=} opt_config The configuration options.
     * @param {(remote.DriverService|http.Executor)=} opt_serviceExecutor Either
     *     a  DriverService to use for the remote end, or a preconfigured executor
     *     for an externally managed endpoint. If neither is provided, the
     *     {@linkplain ##getDefaultService default service} will be used by
     *     default.
     * @return {!Driver} A new driver instance.
     */
    static createSession(caps, opt_serviceExecutor) {
      let executor
      let onQuit
      if (opt_serviceExecutor instanceof http.Executor) {
        executor = opt_serviceExecutor
        configureExecutor(executor, this.VENDOR_COMMAND_PREFIX)
      } else {
        let service = opt_serviceExecutor || this.getDefaultService()
        executor = createExecutor(service.start(), this.VENDOR_COMMAND_PREFIX)
        onQuit = () => service.kill()
      }
  
      // W3C spec requires noProxy value to be an array of strings, but Chromium
      // expects a single host as a string.
      let proxy = caps.get(Capability.PROXY)
      if (proxy && Array.isArray(proxy.noProxy)) {
        proxy.noProxy = proxy.noProxy[0]
        if (!proxy.noProxy) {
          proxy.noProxy = undefined
        }
      }
  
      return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit))
    }
  
    /**
     * This function is a no-op as file detectors are not supported by this
     * implementation.
     * @override
     */
    setFileDetector() {}
  
    /**
     * Schedules a command to launch Chrome App with given ID.
     * @param {string} id ID of the App to launch.
     * @return {!Promise<void>} A promise that will be resolved
     *     when app is launched.
     */
    launchApp(id) {
      return this.execute(
        new command.Command(Command.LAUNCH_APP).setParameter('id', id)
      )
    }
  
    /**
     * Schedules a command to get Chromium network emulation settings.
     * @return {!Promise} A promise that will be resolved when network
     *     emulation settings are retrievied.
     */
    getNetworkConditions() {
      return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS))
    }
  
    /**
     * Schedules a command to set Chromium network emulation settings.
     *
     * __Sample Usage:__
     *
     *  driver.setNetworkConditions({
     *    offline: false,
     *    latency: 5, // Additional latency (ms).
     *    download_throughput: 500 * 1024, // Maximal aggregated download throughput.
     *    upload_throughput: 500 * 1024 // Maximal aggregated upload throughput.
     * });
     *
     * @param {Object} spec Defines the network conditions to set
     * @return {!Promise<void>} A promise that will be resolved when network
     *     emulation settings are set.
     */
    setNetworkConditions(spec) {
      if (!spec || typeof spec !== 'object') {
        throw TypeError(
          'setNetworkConditions called with non-network-conditions parameter'
        )
      }
      return this.execute(
        new command.Command(Command.SET_NETWORK_CONDITIONS).setParameter(
          'network_conditions',
          spec
        )
      )
    }
  
    /**
     * Sends an arbitrary devtools command to the browser.
     *
     * @param {string} cmd The name of the command to send.
     * @param {Object=} params The command parameters.
     * @return {!Promise<void>} A promise that will be resolved when the command
     *     has finished.
     * @see <https://chromedevtools.github.io/devtools-protocol/>
     */
    sendDevToolsCommand(cmd, params = {}) {
      return this.execute(
        new command.Command(Command.SEND_DEVTOOLS_COMMAND)
          .setParameter('cmd', cmd)
          .setParameter('params', params)
      )
    }
  
    /**
     * ends an arbitrary devtools command to the browser and get the result.
     *
     * @param {string} cmd The name of the command to send.
     * @param {Object=} params The command parameters.
     * @return {!Promise<void>} A promise that will be resolved when the command
     *     has finished.
     * @see <https://chromedevtools.github.io/devtools-protocol/>
     */
    sendAndGetDevToolsCommand(cmd, params = {}) {
      return this.execute(
        new command.Command(Command.SEND_AND_GET_DEVTOOLS_COMMAND)
          .setParameter('cmd', cmd)
          .setParameter('params', params)
      )
    }
  
    /**
     * Creates a new WebSocket connection.
     * @return {!Promise<resolved>} A new CDP instance.
     */
    async createCDPConnection(target) {
      const caps = await this.getCapabilities()
      const seOptions = caps['map_'].get('se:options') || new Map()
      const vendorInfo =
        caps['map_'].get(this.VENDOR_COMMAND_PREFIX + ':chromeOptions') ||
        new Map()
      const debuggerUrl = seOptions['cdp'] || vendorInfo['debuggerAddress']
      this._wsUrl = await this.getWsUrl(debuggerUrl, target)
  
      return new Promise((resolve, reject) => {
        try {
          this._wsConnection = new WebSocket(this._wsUrl)
        } catch (err) {
          reject(err)
          return
        }
  
        this._wsConnection.on('open', () => {
          this._cdpConnection = new cdp.CdpConnection(this._wsConnection)
          resolve(this._cdpConnection)
        })
  
        this._wsConnection.on('error', (error) => {
          reject(error)
        })
      })
    }
  
    /**
     * Retrieves 'webSocketDebuggerUrl' by sending a http request using debugger address
     * @param {string} debuggerAddress
     * @param {string} target
     * @return {string} Returns parsed webSocketDebuggerUrl obtained from the http request
     */
    async getWsUrl(debuggerAddress, target) {
      if (target && cdpTargets.indexOf(target.toLowerCase()) === -1) {
        throw new error.InvalidArgumentError('invalid target value')
      }
      let path = '/json/version'
  
      if (target === 'page') {
        path = '/json'
      }
      let request = new http.Request('GET', path)
      let client = new http.HttpClient('http://' + debuggerAddress)
      let response = await client.send(request)
      let url = JSON.parse(response.body)['webSocketDebuggerUrl']
      if (target.toLowerCase() === 'page') {
        url = JSON.parse(response.body)[0]['webSocketDebuggerUrl']
      }
  
      return url
    }
  
    /**
     * Sets a listener for Fetch.authRequired event from CDP
     * If event is triggered, it enter username and password
     * and allows the test to move forward
     * @param {string} username
     * @param {string} password
     * @param connection CDP Connection
     */
    async register(username, password, connection) {
      await connection.execute(
        'Network.setCacheDisabled',
        this.getRandomNumber(1, 10),
        {
          cacheDisabled: true,
        },
        null
      )
  
      this._wsConnection.on('message', (message) => {
        const params = JSON.parse(message)
  
        if (params.method === 'Fetch.authRequired') {
          const requestParams = params['params']
          connection.execute(
            'Fetch.continueWithAuth',
            this.getRandomNumber(1, 10),
            {
              requestId: requestParams['requestId'],
              authChallengeResponse: {
                response: 'ProvideCredentials',
                username: username,
                password: password,
              },
            }
          )
        } else if (params.method === 'Fetch.requestPaused') {
          const requestPausedParams = params['params']
          connection.execute(
            'Fetch.continueRequest',
            this.getRandomNumber(1, 10),
            {
              requestId: requestPausedParams['requestId'],
            }
          )
        }
      })
  
      await connection.execute(
        'Fetch.enable',
        1,
        {
          handleAuthRequests: true,
        },
        null
      )
    }
  
    /**
     *
     * @param connection
     * @param callback
     * @returns {Promise<void>}
     */
    async onLogEvent(connection, callback) {
      await connection.execute(
        'Runtime.enable',
        this.getRandomNumber(1, 10),
        {},
        null
      )
  
      this._wsConnection.on('message', (message) => {
        const params = JSON.parse(message)
  
        if (params.method === 'Runtime.consoleAPICalled') {
          const consoleEventParams = params['params']
          let event = {
            type: consoleEventParams['type'],
            timestamp: new Date(consoleEventParams['timestamp']),
            args: consoleEventParams['args'],
          }
  
          callback(event)
        }
      })
    }
  
    /**
     * Set a permission state to the given value.
     *
     * @param {string} name A name of the permission to update.
     * @param {('granted'|'denied'|'prompt')} state State to set permission to.
     * @returns {!Promise<Object>} A promise that will be resolved when the
     *     command has finished.
     * @see <https://w3c.github.io/permissions/#permission-registry> for valid
     *     names
     */
    setPermission(name, state) {
      return this.execute(
        new command.Command(Command.SET_PERMISSION)
          .setParameter('descriptor', { name })
          .setParameter('state', state)
      )
    }
  
    /**
     *
     * @param connection
     * @param callback
     * @returns {Promise<void>}
     */
    async onLogException(connection, callback) {
      await connection.execute(
        'Runtime.enable',
        this.getRandomNumber(1, 10),
        {},
        null
      )
  
      this._wsConnection.on('message', (message) => {
        const params = JSON.parse(message)
  
        if (params.method === 'Runtime.exceptionThrown') {
          const exceptionEventParams = params['params']
          let event = {
            exceptionDetails: exceptionEventParams['exceptionDetails'],
            timestamp: new Date(exceptionEventParams['timestamp']),
          }
  
          callback(event)
        }
      })
    }
  
    /**
     * @param connection
     * @param callback
     * @returns {Promise<void>}
     */
    async logMutationEvents(connection, callback) {
      await connection.execute(
        'Runtime.enable',
        this.getRandomNumber(1, 10),
        {},
        null
      )
      await connection.execute(
        'Page.enable',
        this.getRandomNumber(1, 10),
        {},
        null
      )
  
      await connection.execute(
        'Runtime.addBinding',
        this.getRandomNumber(1, 10),
        {
          name: '__webdriver_attribute',
        },
        null
      )
  
      let mutationListener = ''
      try {
        // Depending on what is running the code it could appear in 2 different places which is why we try
        // here and then the other location
        mutationListener = fs
          .readFileSync(
            './javascript/node/selenium-webdriver/lib/atoms/mutation-listener.js',
            'utf-8'
          )
          .toString()
      } catch {
        mutationListener = fs
          .readFileSync('./lib/atoms/mutation-listener.js', 'utf-8')
          .toString()
      }
  
      this.executeScript(mutationListener)
  
      await connection.execute(
        'Page.addScriptToEvaluateOnNewDocument',
        this.getRandomNumber(1, 10),
        {
          source: mutationListener,
        },
        null
      )
  
      this._wsConnection.on('message', async (message) => {
        const params = JSON.parse(message)
        if (params.method === 'Runtime.bindingCalled') {
          let payload = JSON.parse(params['params']['payload'])
          let elements = await this.findElements({
            css: '*[data-__webdriver_id=' + payload['target'],
          })
  
          if (elements.length === 0) {
            return
          }
  
          let event = {
            element: elements[0],
            attribute_name: payload['name'],
            current_value: payload['value'],
            old_value: payload['oldValue'],
          }
          callback(event)
        }
      })
    }
  
    /**
     * Sends a DevTools command to change the browser's download directory.
     *
     * @param {string} path The desired download directory.
     * @return {!Promise<void>} A promise that will be resolved when the command
     *     has finished.
     * @see #sendDevToolsCommand
     */
    async setDownloadPath(path) {
      if (!path || typeof path !== 'string') {
        throw new error.InvalidArgumentError('invalid download path')
      }
      const stat = await io.stat(path)
      if (!stat.isDirectory()) {
        throw new error.InvalidArgumentError('not a directory: ' + path)
      }
      return this.sendDevToolsCommand('Page.setDownloadBehavior', {
        behavior: 'allow',
        downloadPath: path,
      })
    }
  
    /**
     * Returns the list of cast sinks (Cast devices) available to the Chrome media router.
     *
     * @return {!promise.Thenable<void>} A promise that will be resolved with an array of Strings
     *   containing the friendly device names of available cast sink targets.
     */
    getCastSinks() {
      return this.schedule(
        new command.Command(Command.GET_CAST_SINKS),
        'Driver.getCastSinks()'
      )
    }
  
    /**
     * Selects a cast sink (Cast device) as the recipient of media router intents (connect or play).
     *
     * @param {String} deviceName name of the target device.
     * @return {!promise.Thenable<void>} A promise that will be resolved
     *     when the target device has been selected to respond further webdriver commands.
     */
    setCastSinkToUse(deviceName) {
      return this.schedule(
        new command.Command(Command.SET_CAST_SINK_TO_USE).setParameter(
          'sinkName',
          deviceName
        ),
        'Driver.setCastSinkToUse(' + deviceName + ')'
      )
    }
  
    /**
     * Initiates tab mirroring for the current browser tab on the specified device.
     *
     * @param {String} deviceName name of the target device.
     * @return {!promise.Thenable<void>} A promise that will be resolved
     *     when the mirror command has been issued to the device.
     */
    startCastTabMirroring(deviceName) {
      return this.schedule(
        new command.Command(Command.START_CAST_TAB_MIRRORING).setParameter(
          'sinkName',
          deviceName
        ),
        'Driver.startCastTabMirroring(' + deviceName + ')'
      )
    }
  
    /**
     * Returns an error message when there is any issue in a Cast session.
     * @return {!promise.Thenable<void>} A promise that will be resolved
     *     when the mirror command has been issued to the device.
     */
    getCastIssueMessage() {
      return this.schedule(
        new command.Command(Command.GET_CAST_ISSUE_MESSAGE),
        'Driver.getCastIssueMessage()'
      )
    }
  
    /**
     * Stops casting from media router to the specified device, if connected.
     *
     * @param {String} deviceName name of the target device.
     * @return {!promise.Thenable<void>} A promise that will be resolved
     *     when the stop command has been issued to the device.
     */
    stopCasting(deviceName) {
      return this.schedule(
        new command.Command(Command.STOP_CASTING).setParameter(
          'sinkName',
          deviceName
        ),
        'Driver.stopCasting(' + deviceName + ')'
      )
    }
  
    getRandomNumber(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min)
    }
  }
  
  // PUBLIC API
  
  exports.Driver = Driver
  exports.Options = Options
  exports.ServiceBuilder = ServiceBuilder
  