// 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.

'use strict'

const { StubError, assertIsInstance, assertIsStubError, throwStubError } = require('./testutil')

const error = require('selenium-webdriver/lib/error')
const logging = require('selenium-webdriver/lib/logging')
const promise = require('selenium-webdriver/lib/promise')
const until = require('selenium-webdriver/lib/until')
const { Alert, AlertPromise, WebDriver, WebElement, WebElementPromise } = require('selenium-webdriver/lib/webdriver')
const { By } = require('selenium-webdriver/lib/by')
const { Capabilities } = require('selenium-webdriver/lib/capabilities')
const { Name } = require('selenium-webdriver/lib/command')
const { Session } = require('selenium-webdriver/lib/session')
const assert = require('node:assert')

const CName = Name
const SESSION_ID = 'test_session_id'
const fail = (msg) => assert.fail(msg)

describe('WebDriver', function () {
  const LOG = logging.getLogger('webdriver.test')

  function defer() {
    let d = {}
    let promise = new Promise((resolve, reject) => {
      Object.assign(d, { resolve, reject })
    })
    d.promise = promise
    return d
  }

  function expectedError(ctor, message) {
    return function (e) {
      assertIsInstance(ctor, e)
      assert.strictEqual(message, e.message)
    }
  }

  class Expectation {
    constructor(executor, name, opt_parameters) {
      this.executor_ = executor
      this.name_ = name
      this.times_ = 1
      this.sessionId_ = SESSION_ID
      this.check_ = null
      this.toDo_ = null
      this.withParameters(opt_parameters || {})
    }

    anyTimes() {
      this.times_ = Infinity
      return this
    }

    times(n) {
      this.times_ = n
      return this
    }

    withParameters(parameters) {
      this.parameters_ = parameters
      if (this.name_ !== CName.NEW_SESSION) {
        this.parameters_['sessionId'] = this.sessionId_
      }
      return this
    }

    andReturn(code, opt_value) {
      this.toDo_ = function (command) {
        LOG.info('executing ' + command.getName() + '; returning ' + code)
        return Promise.resolve(opt_value !== void 0 ? opt_value : null)
      }
      return this
    }

    andReturnSuccess(opt_value) {
      this.toDo_ = function (command) {
        LOG.info('executing ' + command.getName() + '; returning success')
        return Promise.resolve(opt_value !== void 0 ? opt_value : null)
      }
      return this
    }

    andReturnError(error) {
      if (typeof error === 'number') {
        throw Error('need error type')
      }
      this.toDo_ = function (command) {
        LOG.info('executing ' + command.getName() + '; returning failure')
        return Promise.reject(error)
      }
      return this
    }

    expect(name, opt_parameters) {
      this.end()
      return this.executor_.expect(name, opt_parameters)
    }

    end() {
      if (!this.toDo_) {
        this.andReturnSuccess(null)
      }
      return this.executor_
    }

    execute(command) {
      assert.deepStrictEqual(command.getParameters(), this.parameters_)
      return this.toDo_(command)
    }
  }

  class FakeExecutor {
    constructor() {
      this.commands_ = new Map()
    }

    execute(command) {
      let expectations = this.commands_.get(command.getName())
      if (!expectations || !expectations.length) {
        assert.fail('unexpected command: ' + command.getName())
        return
      }

      let next = expectations[0]
      let result = next.execute(command)
      if (next.times_ !== Infinity) {
        next.times_ -= 1
        if (!next.times_) {
          expectations.shift()
        }
      }
      return result
    }

    expect(commandName, opt_parameters) {
      if (!this.commands_.has(commandName)) {
        this.commands_.set(commandName, [])
      }
      let e = new Expectation(this, commandName, opt_parameters)
      this.commands_.get(commandName).push(e)
      return e
    }

    createDriver(opt_session) {
      let session = opt_session || new Session(SESSION_ID, {})
      return new WebDriver(session, this)
    }
  }

  /////////////////////////////////////////////////////////////////////////////
  //
  //    Tests
  //
  /////////////////////////////////////////////////////////////////////////////

  describe('testCreateSession', function () {
    it('happyPathWithCapabilitiesHashObject', function () {
      let aSession = new Session(SESSION_ID, { browserName: 'firefox' })
      let executor = new FakeExecutor()
        .expect(CName.NEW_SESSION)
        .withParameters({
          capabilities: {
            alwaysMatch: { browserName: 'firefox' },
            firstMatch: [{}],
          },
        })
        .andReturnSuccess(aSession)
        .end()

      const driver = WebDriver.createSession(executor, {
        browserName: 'firefox',
      })
      return driver.getSession().then((v) => assert.strictEqual(v, aSession))
    })

    it('happy Path With Capabilities Instance', function () {
      let aSession = new Session(SESSION_ID, { browserName: 'firefox' })
      let executor = new FakeExecutor()
        .expect(CName.NEW_SESSION)
        .withParameters({
          capabilities: {
            alwaysMatch: {
              'moz:debuggerAddress': true,
              browserName: 'firefox',
            },
            firstMatch: [{}],
          },
        })
        .andReturnSuccess(aSession)
        .end()

      const driver = WebDriver.createSession(executor, Capabilities.firefox())
      return driver.getSession().then((v) => assert.strictEqual(v, aSession))
    })

    it('drops non-W3C capability names from W3C capabilities', function () {
      let aSession = new Session(SESSION_ID, { browserName: 'firefox' })
      let executor = new FakeExecutor()
        .expect(CName.NEW_SESSION)
        .withParameters({
          capabilities: {
            alwaysMatch: { browserName: 'firefox' },
            firstMatch: [{}],
          },
        })
        .andReturnSuccess(aSession)
        .end()

      const driver = WebDriver.createSession(executor, {
        browserName: 'firefox',
        foo: 'bar',
      })
      return driver.getSession().then((v) => assert.strictEqual(v, aSession))
    })

    it('failsToCreateSession', function () {
      let executor = new FakeExecutor()
        .expect(CName.NEW_SESSION)
        .withParameters({
          capabilities: {
            alwaysMatch: { browserName: 'firefox' },
            firstMatch: [{}],
          },
        })
        .andReturnError(new StubError())
        .end()

      const driver = WebDriver.createSession(executor, {
        browserName: 'firefox',
      })
      return driver.getSession().then(fail, assertIsStubError)
    })

    it('invokes quit callback if it fails to create a session', function () {
      let called = false
      let executor = new FakeExecutor()
        .expect(CName.NEW_SESSION)
        .withParameters({
          capabilities: {
            alwaysMatch: { browserName: 'firefox' },
            firstMatch: [{}],
          },
        })
        .andReturnError(new StubError())
        .end()

      let driver = WebDriver.createSession(executor, { browserName: 'firefox' }, () => (called = true))
      return driver.getSession().then(fail, (err) => {
        assert.ok(called)
        assertIsStubError(err)
      })
    })
  })

  it('testDoesNotExecuteCommandIfSessionDoesNotResolve', function () {
    const session = Promise.reject(new StubError())
    return new FakeExecutor()
      .createDriver(session)
      .getTitle()
      .then((_) => assert.fail('should have failed'), assertIsStubError)
  })

  it('testCommandReturnValuesArePassedToFirstCallback', function () {
    let executor = new FakeExecutor().expect(CName.GET_TITLE).andReturnSuccess('Google Search').end()

    const driver = executor.createDriver()
    return driver.getTitle().then((title) => assert.strictEqual('Google Search', title))
  })

  it('testStopsCommandExecutionWhenAnErrorOccurs', function () {
    let e = new error.NoSuchWindowError('window not found')
    let executor = new FakeExecutor()
      .expect(CName.SWITCH_TO_WINDOW)
      .withParameters({
        name: 'foo',
        handle: 'foo',
      })
      .andReturnError(e)
      .end()

    let driver = executor.createDriver()
    return driver
      .switchTo()
      .window('foo')
      .then(
        (_) => driver.getTitle(), // mock should blow if this gets executed
        (v) => assert.strictEqual(v, e),
      )
  })

  it('testReportsErrorWhenExecutingCommandsAfterExecutingAQuit', function () {
    let executor = new FakeExecutor().expect(CName.QUIT).end()

    let verifyError = expectedError(
      error.NoSuchSessionError,
      'This driver instance does not have a valid session ID ' +
        '(did you call WebDriver.quit()?) and may no longer be used.',
    )

    let driver = executor.createDriver()
    return driver
      .quit()
      .then((_) => driver.get('http://www.google.com'))
      .then(assert.fail, verifyError)
  })

  describe('returningAPromise', function () {
    it('fromACallback', function () {
      let executor = new FakeExecutor()
        .expect(CName.GET_TITLE)
        .expect(CName.GET_CURRENT_URL)
        .andReturnSuccess('http://www.google.com')
        .end()

      const driver = executor.createDriver()
      return driver
        .getTitle()
        .then(function () {
          return driver.getCurrentUrl()
        })
        .then(function (value) {
          assert.strictEqual('http://www.google.com', value)
        })
    })

    it('fromAnErrbackSuppressesTheError', function () {
      let executor = new FakeExecutor()
        .expect(CName.SWITCH_TO_WINDOW, {
          name: 'foo',
          handle: 'foo',
        })
        .andReturnError(new StubError())
        .expect(CName.GET_CURRENT_URL)
        .andReturnSuccess('http://www.google.com')
        .end()

      const driver = executor.createDriver()
      return driver
        .switchTo()
        .window('foo')
        .catch(function (e) {
          assertIsStubError(e)
          return driver.getCurrentUrl()
        })
        .then((url) => assert.strictEqual('http://www.google.com', url))
    })
  })

  describe('WebElementPromise', function () {
    let driver = new FakeExecutor().createDriver()

    it('resolvesWhenUnderlyingElementDoes', function () {
      let el = new WebElement(driver, { ELEMENT: 'foo' })
      return new WebElementPromise(driver, Promise.resolve(el)).then((e) => assert.strictEqual(e, el))
    })

    it('resolvesBeforeCallbacksOnWireValueTrigger', function () {
      const el = defer()

      const element = new WebElementPromise(driver, el.promise)
      const messages = []

      let steps = [
        element.then((_) => messages.push('element resolved')),
        element.getId().then((_) => messages.push('wire value resolved')),
      ]

      el.resolve(new WebElement(driver, { ELEMENT: 'foo' }))
      return Promise.all(steps).then(function () {
        assert.deepStrictEqual(['element resolved', 'wire value resolved'], messages)
      })
    })

    it('isRejectedIfUnderlyingIdIsRejected', function () {
      let element = new WebElementPromise(driver, Promise.reject(new StubError()))
      return element.then(fail, assertIsStubError)
    })
  })

  describe('executeScript', function () {
    it('nullReturnValue', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return document.body;',
          args: [],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      return driver.executeScript('return document.body;').then((result) => assert.strictEqual(null, result))
    })

    it('primitiveReturnValue', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return document.body;',
          args: [],
        })
        .andReturnSuccess(123)
        .end()

      const driver = executor.createDriver()
      return driver.executeScript('return document.body;').then((result) => assert.strictEqual(123, result))
    })

    it('webElementReturnValue', function () {
      const json = WebElement.buildId('foo')

      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return document.body;',
          args: [],
        })
        .andReturnSuccess(json)
        .end()

      const driver = executor.createDriver()
      return driver
        .executeScript('return document.body;')
        .then((element) => element.getId())
        .then((id) => assert.strictEqual(id, 'foo'))
    })

    it('arrayReturnValue', function () {
      const json = [WebElement.buildId('foo')]

      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return document.body;',
          args: [],
        })
        .andReturnSuccess(json)
        .end()

      const driver = executor.createDriver()
      return driver
        .executeScript('return document.body;')
        .then(function (array) {
          assert.strictEqual(1, array.length)
          return array[0].getId()
        })
        .then((id) => assert.strictEqual('foo', id))
    })

    it('objectReturnValue', function () {
      const json = { foo: WebElement.buildId('foo') }

      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return document.body;',
          args: [],
        })
        .andReturnSuccess(json)
        .end()

      const driver = executor.createDriver()
      return driver
        .executeScript('return document.body;')
        .then((obj) => obj['foo'].getId())
        .then((id) => assert.strictEqual(id, 'foo'))
    })

    it('scriptAsFunction', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return (' + function () {} + ').apply(null, arguments);',
          args: [],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      return driver.executeScript(function () {})
    })

    it('simpleArgumentConversion', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return 1;',
          args: ['abc', 123, true, [123, { foo: 'bar' }]],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      return driver.executeScript('return 1;', 'abc', 123, true, [123, { foo: 'bar' }])
    })

    it('webElementArgumentConversion', function () {
      const elementJson = WebElement.buildId('fefifofum')

      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return 1;',
          args: [elementJson],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      return driver.executeScript('return 1;', new WebElement(driver, 'fefifofum'))
    })

    it('webElementPromiseArgumentConversion', function () {
      const elementJson = WebElement.buildId('bar')

      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(elementJson)
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return 1;',
          args: [elementJson],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      const element = driver.findElement(By.id('foo'))
      return driver.executeScript('return 1;', element)
    })

    it('argumentConversion', function () {
      const elementJson = WebElement.buildId('fefifofum')

      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'return 1;',
          args: ['abc', 123, true, elementJson, [123, { foo: 'bar' }]],
        })
        .andReturnSuccess(null)
        .end()

      const driver = executor.createDriver()
      const element = new WebElement(driver, 'fefifofum')
      return driver.executeScript('return 1;', 'abc', 123, true, element, [123, { foo: 'bar' }])
    })

    it('scriptReturnsAnError', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT)
        .withParameters({
          script: 'throw Error(arguments[0]);',
          args: ['bam'],
        })
        .andReturnError(new StubError())
        .end()
      const driver = executor.createDriver()
      return driver.executeScript('throw Error(arguments[0]);', 'bam').then(fail, assertIsStubError)
    })

    it('failsIfArgumentIsARejectedPromise', function () {
      let executor = new FakeExecutor()

      const arg = Promise.reject(new StubError())
      arg.catch(function () {}) // Suppress default handler.

      const driver = executor.createDriver()
      return driver.executeScript(function () {}, arg).then(fail, assertIsStubError)
    })
  })

  describe('executeAsyncScript', function () {
    it('failsIfArgumentIsARejectedPromise', function () {
      const arg = Promise.reject(new StubError())
      arg.catch(function () {}) // Suppress default handler.

      const driver = new FakeExecutor().createDriver()
      return driver.executeAsyncScript(function () {}, arg).then(fail, assertIsStubError)
    })
  })

  describe('findElement', function () {
    it('elementNotFound', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnError(new StubError())
        .end()

      const driver = executor.createDriver()
      return driver.findElement(By.id('foo')).then(assert.fail, assertIsStubError)
    })

    it('elementNotFoundInACallback', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnError(new StubError())
        .end()

      const driver = executor.createDriver()
      return Promise.resolve()
        .then((_) => driver.findElement(By.id('foo')))
        .then(assert.fail, assertIsStubError)
    })

    it('elementFound', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(WebElement.buildId('bar'))
        .expect(CName.CLICK_ELEMENT, { id: WebElement.buildId('bar') })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      const element = driver.findElement(By.id('foo'))
      return element.click()
    })

    it('canUseElementInCallback', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(WebElement.buildId('bar'))
        .expect(CName.CLICK_ELEMENT, { id: WebElement.buildId('bar') })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      return driver.findElement(By.id('foo')).then((e) => e.click())
    })

    it('byJs', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: 'return document.body',
          args: [],
        })
        .andReturnSuccess(WebElement.buildId('bar'))
        .expect(CName.CLICK_ELEMENT, { id: WebElement.buildId('bar') })
        .end()

      const driver = executor.createDriver()
      return driver.findElement(By.js('return document.body')).then((e) => e.click())
    })

    it('byJs_returnsNonWebElementValue', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, { script: 'return 123', args: [] })
        .andReturnSuccess(123)
        .end()

      const driver = executor.createDriver()
      return driver.findElement(By.js('return 123')).then(assert.fail, function (e) {
        assertIsInstance(TypeError, e)
        assert.strictEqual('Custom locator did not return a WebElement', e.message)
      })
    })

    it('byJs_canPassArguments', function () {
      const script = 'return document.getElementsByTagName(arguments[0]);'
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: script,
          args: ['div'],
        })
        .andReturnSuccess(WebElement.buildId('one'))
        .end()
      const driver = executor.createDriver()
      return driver.findElement(By.js(script, 'div'))
    })

    it('customLocator', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENTS, { using: 'css selector', value: '.a' })
        .andReturnSuccess([WebElement.buildId('foo'), WebElement.buildId('bar')])
        .expect(CName.CLICK_ELEMENT, { id: WebElement.buildId('foo') })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      const element = driver.findElement(function (d) {
        assert.strictEqual(driver, d)
        return d.findElements(By.className('a'))
      })
      return element.click()
    })

    it('customLocatorThrowsIfresultIsNotAWebElement', function () {
      const driver = new FakeExecutor().createDriver()
      return driver
        .findElement((_) => 1)
        .then(assert.fail, function (e) {
          assertIsInstance(TypeError, e)
          assert.strictEqual('Custom locator did not return a WebElement', e.message)
        })
    })
  })

  describe('findElements', function () {
    it('returnsMultipleElements', function () {
      const ids = ['foo', 'bar', 'baz']
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENTS, { using: 'css selector', value: '.a' })
        .andReturnSuccess(ids.map(WebElement.buildId))
        .end()

      const driver = executor.createDriver()
      return driver
        .findElements(By.className('a'))
        .then(function (elements) {
          return Promise.all(
            elements.map(function (e) {
              assert.ok(e instanceof WebElement)
              return e.getId()
            }),
          )
        })
        .then((actual) => assert.deepStrictEqual(ids, actual))
    })

    it('byJs', function () {
      const ids = ['foo', 'bar', 'baz']
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: 'return document.getElementsByTagName("div");',
          args: [],
        })
        .andReturnSuccess(ids.map(WebElement.buildId))
        .end()

      const driver = executor.createDriver()

      return driver
        .findElements(By.js('return document.getElementsByTagName("div");'))
        .then(function (elements) {
          return Promise.all(
            elements.map(function (e) {
              assert.ok(e instanceof WebElement)
              return e.getId()
            }),
          )
        })
        .then((actual) => assert.deepStrictEqual(ids, actual))
    })

    it('byJs_filtersOutNonWebElementResponses', function () {
      const ids = ['foo', 'bar', 'baz']
      const json = [
        WebElement.buildId(ids[0]),
        123,
        'a',
        false,
        WebElement.buildId(ids[1]),
        { 'not a web element': 1 },
        WebElement.buildId(ids[2]),
      ]
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: 'return document.getElementsByTagName("div");',
          args: [],
        })
        .andReturnSuccess(json)
        .end()

      const driver = executor.createDriver()
      return driver
        .findElements(By.js('return document.getElementsByTagName("div");'))
        .then(function (elements) {
          return Promise.all(
            elements.map(function (e) {
              assert.ok(e instanceof WebElement)
              return e.getId()
            }),
          )
        })
        .then((actual) => assert.deepStrictEqual(ids, actual))
    })

    it('byJs_convertsSingleWebElementResponseToArray', function () {
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: 'return document.getElementsByTagName("div");',
          args: [],
        })
        .andReturnSuccess(WebElement.buildId('foo'))
        .end()

      const driver = executor.createDriver()
      return driver
        .findElements(By.js('return document.getElementsByTagName("div");'))
        .then(function (elements) {
          return Promise.all(
            elements.map(function (e) {
              assert.ok(e instanceof WebElement)
              return e.getId()
            }),
          )
        })
        .then((actual) => assert.deepStrictEqual(['foo'], actual))
    })

    it('byJs_canPassScriptArguments', function () {
      const script = 'return document.getElementsByTagName(arguments[0]);'
      let executor = new FakeExecutor()
        .expect(CName.EXECUTE_SCRIPT, {
          script: script,
          args: ['div'],
        })
        .andReturnSuccess([WebElement.buildId('one'), WebElement.buildId('two')])
        .end()

      const driver = executor.createDriver()
      return driver
        .findElements(By.js(script, 'div'))
        .then(function (elements) {
          return Promise.all(
            elements.map(function (e) {
              assert.ok(e instanceof WebElement)
              return e.getId()
            }),
          )
        })
        .then((actual) => assert.deepStrictEqual(['one', 'two'], actual))
    })
  })

  describe('sendKeys', function () {
    it('convertsVarArgsIntoStrings_simpleArgs', function () {
      let executor = new FakeExecutor()
        .expect(CName.SEND_KEYS_TO_ELEMENT, {
          id: WebElement.buildId('one'),
          text: '12abc3',
          value: '12abc3'.split(''),
        })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      const element = new WebElement(driver, 'one')
      return element.sendKeys(1, 2, 'abc', 3)
    })

    it('sendKeysWithEmojiRepresentedByPairOfCodePoints', function () {
      let executor = new FakeExecutor()
        .expect(CName.SEND_KEYS_TO_ELEMENT, {
          id: WebElement.buildId('one'),
          text: '\uD83D\uDE00',
          value: ['\uD83D\uDE00'],
        })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      const element = new WebElement(driver, 'one')
      return element.sendKeys('\uD83D\uDE00')
    })

    it('convertsVarArgsIntoStrings_promisedArgs', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(WebElement.buildId('one'))
        .expect(CName.SEND_KEYS_TO_ELEMENT, {
          id: WebElement.buildId('one'),
          text: 'abc123def',
          value: 'abc123def'.split(''),
        })
        .andReturnSuccess()
        .end()

      const driver = executor.createDriver()
      const element = driver.findElement(By.id('foo'))
      return element.sendKeys(Promise.resolve('abc'), 123, Promise.resolve('def'))
    })

    it('sendKeysWithAFileDetector', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(WebElement.buildId('one'))
        .expect(CName.SEND_KEYS_TO_ELEMENT, {
          id: WebElement.buildId('one'),
          text: 'modified/path',
          value: 'modified/path'.split(''),
        })
        .andReturnSuccess()
        .end()

      let driver = executor.createDriver()
      let handleFile = function (d, path) {
        assert.strictEqual(driver, d)
        assert.strictEqual(path, 'original/path')
        return Promise.resolve('modified/path')
      }
      driver.setFileDetector({ handleFile })

      return driver.findElement(By.id('foo')).sendKeys('original/', 'path')
    })

    it('sendKeysWithAFileDetector_handlerError', function () {
      let executor = new FakeExecutor()
        .expect(CName.FIND_ELEMENT, {
          using: 'css selector',
          value: '*[id="foo"]',
        })
        .andReturnSuccess(WebElement.buildId('one'))
        .expect(CName.SEND_KEYS_TO_ELEMENT, {
          id: WebElement.buildId('one'),
          text: 'original/path',
          value: 'original/path'.split(''),
        })
        .andReturnSuccess()
        .end()

      let driver = executor.createDriver()
      let handleFile = function (d, path) {
        assert.strictEqual(driver, d)
        assert.strictEqual(path, 'original/path')
        return Promise.reject('unhandled file error')
      }
      driver.setFileDetector({ handleFile })

      return driver.findElement(By.id('foo')).sendKeys('original/', 'path')
    })
  })

  describe('switchTo()', function () {
    describe('window', function () {
      it('should return a resolved promise when the window is found', function () {
        let executor = new FakeExecutor()
          .expect(CName.SWITCH_TO_WINDOW)
          .withParameters({
            name: 'foo',
            handle: 'foo',
          })
          .andReturnSuccess()
          .end()

        return executor.createDriver().switchTo().window('foo')
      })

      it('should propagate exceptions', function () {
        let e = new error.NoSuchWindowError('window not found')
        let executor = new FakeExecutor()
          .expect(CName.SWITCH_TO_WINDOW)
          .withParameters({
            name: 'foo',
            handle: 'foo',
          })
          .andReturnError(e)
          .end()

        return executor
          .createDriver()
          .switchTo()
          .window('foo')
          .then(assert.fail, (v) => assert.strictEqual(v, e))
      })
    })
  })

  describe('elementEquality', function () {
    it('isReflexive', function () {
      const a = new WebElement(new FakeExecutor().createDriver(), 'foo')
      return WebElement.equals(a, a).then(assert.ok)
    })

    it('failsIfAnInputElementCouldNotBeFound', function () {
      let id = Promise.reject(new StubError())

      const driver = new FakeExecutor().createDriver()
      const a = new WebElement(driver, 'foo')
      const b = new WebElementPromise(driver, id)

      return WebElement.equals(a, b).then(fail, assertIsStubError)
    })
  })

  describe('waiting', function () {
    it('on a condition that always returns true', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let count = 0

      function condition() {
        count++
        return true
      }

      return driver.wait(condition, 1).then(() => assert.strictEqual(1, count))
    })

    it('on a simple counting condition', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let count = 0

      function condition() {
        return ++count === 3
      }

      return driver.wait(condition, 250).then(() => assert.strictEqual(3, count))
    })

    it('on a condition that returns a promise that resolves to true after a short timeout', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()

      let count = 0

      function condition() {
        count += 1
        return new Promise((resolve) => {
          setTimeout(() => resolve(true), 50)
        })
      }

      return driver.wait(condition, 75).then(() => assert.strictEqual(1, count))
    })

    it('on a condition that returns a promise', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()

      let count = 0

      function condition() {
        count += 1
        return new Promise((resolve) => {
          setTimeout(() => resolve(count === 3), 25)
        })
      }

      return driver.wait(condition, 100, null, 25).then(() => assert.strictEqual(3, count))
    })

    it('fails if condition throws', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      return driver.wait(throwStubError, 0, 'goes boom').then(fail, assertIsStubError)
    })

    it('fails if condition returns a rejected promise', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()

      function condition() {
        return new Promise((_, reject) => reject(new StubError()))
      }

      return driver.wait(condition, 0, 'goes boom').then(fail, assertIsStubError)
    })

    it('supports message function if condition exceeds timeout', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let message = () => 'goes boom'
      return driver
        .wait(() => false, 0.001, message)
        .then(fail, (e) => {
          assert.ok(/^goes boom\nWait timed out after \d+ms$/.test(e.message))
        })
    })

    it('handles if the message function throws an error after a condition exceeds timeout', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let message = () => {
        throw new Error('message function error')
      }
      return driver
        .wait(() => false, 0.001, message)
        .then(fail, (e) => {
          assert.ok(/^message function error\nWait timed out after \d+ms$/.test(e.message))
        })
    })

    it('supports message function if condition returns a rejected promise', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let condition = new Promise((res) => setTimeout(res, 100))
      let message = () => 'goes boom'
      return driver.wait(condition, 1, message).then(fail, (e) => {
        assert.ok(/^goes boom\nTimed out waiting for promise to resolve after \d+ms$/.test(e.message))
      })
    })

    it('handles if the message function returns an error after a rejected promise', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let condition = new Promise((res) => setTimeout(res, 100))
      let message = () => {
        throw new Error('message function error')
      }
      return driver.wait(condition, 1, message).then(fail, (e) => {
        assert.ok(/^message function error\nTimed out waiting for promise to resolve after \d+ms$/.test(e.message))
      })
    })

    it('waits forever on a zero timeout', function () {
      let done = false
      setTimeout(() => (done = true), 150)

      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let waitResult = driver.wait(() => done, 0)

      return driver
        .sleep(75)
        .then(function () {
          assert.ok(!done)
          return driver.sleep(100)
        })
        .then(function () {
          assert.ok(done)
          return waitResult
        })
    })

    it('waits forever if timeout omitted', function () {
      let done = false
      setTimeout(() => (done = true), 150)

      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let waitResult = driver.wait(() => done)

      return driver
        .sleep(75)
        .then(function () {
          assert.ok(!done)
          return driver.sleep(100)
        })
        .then(function () {
          assert.ok(done)
          return waitResult
        })
    })

    it('times out when timer expires', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()

      let count = 0
      let wait = driver.wait(
        function () {
          count += 1
          let ms = count === 2 ? 65 : 5
          return promise.delayed(ms).then(function () {
            return false
          })
        },
        60,
        'counting to 3',
      )

      return wait.then(fail, function (e) {
        assert.strictEqual(2, count)
        assert.ok(e instanceof error.TimeoutError, 'Unexpected error: ' + e)
        assert.ok(/^counting to 3\nWait timed out after \d+ms$/.test(e.message))
      })
    })

    it('requires condition to be a promise or function', function () {
      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      assert.throws(() => driver.wait(1234, 0))
    })

    it('promise that does not resolve before timeout', function () {
      let d = defer()

      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      return driver.wait(d.promise, 5).then(fail, (e) => {
        assert.ok(e instanceof error.TimeoutError, 'Unexpected error: ' + e)
        assert.ok(
          /Timed out waiting for promise to resolve after \d+ms/.test(e.message),
          'unexpected error message: ' + e.message,
        )
      })
    })

    it('unbounded wait on promise resolution', function () {
      let messages = []
      let d = defer()

      let executor = new FakeExecutor()
      let driver = executor.createDriver()
      let waitResult = driver.wait(d.promise).then(function (value) {
        messages.push('b')
        assert.strictEqual(1234, value)
      })

      setTimeout(() => messages.push('a'), 5)
      return driver
        .sleep(10)
        .then(function () {
          assert.deepStrictEqual(['a'], messages)
          d.resolve(1234)
          return waitResult
        })
        .then(function () {
          assert.deepStrictEqual(['a', 'b'], messages)
        })
    })

    describe('supports custom wait functions', function () {
      it('waitSucceeds', function () {
        let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([])
          .times(2)
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([WebElement.buildId('bar')])
          .end()

        const driver = executor.createDriver()
        return driver.wait(
          function () {
            return driver.findElements(By.id('foo')).then((els) => els.length > 0)
          },
          200,
          null,
          25,
        )
      })

      it('waitTimesout_timeoutCaught', function () {
        let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([])
          .anyTimes()
          .end()

        const driver = executor.createDriver()
        return driver
          .wait(function () {
            return driver.findElements(By.id('foo')).then((els) => els.length > 0)
          }, 25)
          .then(fail, function (e) {
            assert.strictEqual('Wait timed out after ', e.message.substring(0, 'Wait timed out after '.length))
          })
      })
    })

    describe('supports condition objects', function () {
      it('wait succeeds', function () {
        let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([])
          .times(2)
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([WebElement.buildId('bar')])
          .end()

        let driver = executor.createDriver()
        return driver.wait(until.elementLocated(By.id('foo')), 200, null, 25)
      })

      it('wait times out', function () {
        let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENTS, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess([])
          .anyTimes()
          .end()

        let driver = executor.createDriver()
        return driver
          .wait(until.elementLocated(By.id('foo')), 5)
          .then(fail, (err) => assert.ok(err instanceof error.TimeoutError))
      })
    })

    describe('supports promise objects', function () {
      it('wait succeeds', function () {
        let promise = new Promise((resolve) => {
          setTimeout(() => resolve(1), 10)
        })

        let driver = new FakeExecutor().createDriver()
        return driver.wait(promise, 200).then((v) => assert.strictEqual(v, 1))
      })

      it('wait times out', function () {
        let promise = new Promise(() => {
          /* never resolves */
        })

        let driver = new FakeExecutor().createDriver()
        return driver.wait(promise, 5).then(fail, (err) => assert.ok(err instanceof error.TimeoutError))
      })

      it('wait fails if promise is rejected', function () {
        let err = Error('boom')
        let driver = new FakeExecutor().createDriver()
        return driver.wait(Promise.reject(err), 5).then(fail, (e) => assert.strictEqual(e, err))
      })
    })

    it('fails if not supported condition type provided', function () {
      let driver = new FakeExecutor().createDriver()
      assert.throws(() => driver.wait({}, 5), TypeError)
    })
  })

  describe('alert handling', function () {
    it('alertResolvesWhenPromisedTextResolves', function () {
      let driver = new FakeExecutor().createDriver()
      let deferredText = defer()

      let alert = new AlertPromise(driver, deferredText.promise)

      deferredText.resolve(new Alert(driver, 'foo'))
      return alert.getText().then((text) => assert.strictEqual(text, 'foo'))
    })

    it('cannotSwitchToAlertThatIsNotPresent', function () {
      let e = new error.NoSuchAlertError()
      let executor = new FakeExecutor().expect(CName.GET_ALERT_TEXT).andReturnError(e).end()

      return executor
        .createDriver()
        .switchTo()
        .alert()
        .then(assert.fail, (v) => assert.strictEqual(v, e))
    })

    it('commandsFailIfAlertNotPresent', function () {
      let e = new error.NoSuchAlertError()
      let executor = new FakeExecutor().expect(CName.GET_ALERT_TEXT).andReturnError(e).end()

      const driver = executor.createDriver()
      const alert = driver.switchTo().alert()

      const expectError = (v) => assert.strictEqual(v, e)

      return alert
        .getText()
        .then(fail, expectedError)
        .then(() => alert.accept())
        .then(fail, expectedError)
        .then(() => alert.dismiss())
        .then(fail, expectError)
        .then(() => alert.sendKeys('hi'))
        .then(fail, expectError)
    })
  })

  it('testFetchingLogs', function () {
    let executor = new FakeExecutor()
      .expect(CName.GET_LOG, { type: 'browser' })
      .andReturnSuccess([
        { level: 'INFO', message: 'hello', timestamp: 1234 },
        { level: 'DEBUG', message: 'abc123', timestamp: 5678 },
      ])
      .end()

    const driver = executor.createDriver()
    return driver
      .manage()
      .logs()
      .get('browser')
      .then(function (entries) {
        assert.strictEqual(2, entries.length)

        assert.ok(entries[0] instanceof logging.Entry)
        assert.strictEqual(logging.Level.INFO.value, entries[0].level.value)
        assert.strictEqual('hello', entries[0].message)
        assert.strictEqual(1234, entries[0].timestamp)

        assert.ok(entries[1] instanceof logging.Entry)
        assert.strictEqual(logging.Level.DEBUG.value, entries[1].level.value)
        assert.strictEqual('abc123', entries[1].message)
        assert.strictEqual(5678, entries[1].timestamp)
      })
  })

  it('testCommandsFailIfInitialSessionCreationFailed', function () {
    const session = Promise.reject(new StubError())

    const driver = new FakeExecutor().createDriver(session)
    const navigateResult = driver.get('some-url').then(fail, assertIsStubError)
    const quitResult = driver.quit().then(fail, assertIsStubError)
    return Promise.all([navigateResult, quitResult])
  })

  it('testWebElementCommandsFailIfInitialDriverCreationFailed', function () {
    const session = Promise.reject(new StubError())
    const driver = new FakeExecutor().createDriver(session)
    return driver.findElement(By.id('foo')).click().then(fail, assertIsStubError)
  })

  it('testWebElementCommansFailIfElementCouldNotBeFound', function () {
    let e = new error.NoSuchElementError('Unable to find element')
    let executor = new FakeExecutor()
      .expect(CName.FIND_ELEMENT, {
        using: 'css selector',
        value: '*[id="foo"]',
      })
      .andReturnError(e)
      .end()

    const driver = executor.createDriver()
    return driver
      .findElement(By.id('foo'))
      .click()
      .then(fail, (v) => assert.strictEqual(v, e))
  })

  it('testCannotFindChildElementsIfParentCouldNotBeFound', function () {
    let e = new error.NoSuchElementError('Unable to find element')
    let executor = new FakeExecutor()
      .expect(CName.FIND_ELEMENT, {
        using: 'css selector',
        value: '*[id="foo"]',
      })
      .andReturnError(e)
      .end()

    const driver = executor.createDriver()
    return driver
      .findElement(By.id('foo'))
      .findElement(By.id('bar'))
      .findElement(By.id('baz'))
      .then(fail, (v) => assert.strictEqual(v, e))
  })

  describe('actions()', function () {
    describe('move()', function () {
      it('no origin', function () {
        let executor = new FakeExecutor()
          .expect(CName.ACTIONS, {
            actions: [
              {
                type: 'pointer',
                id: 'default mouse',
                parameters: {
                  pointerType: 'mouse',
                },
                actions: [
                  {
                    duration: 100,
                    origin: 'viewport',
                    type: 'pointerMove',
                    x: 0,
                    y: 125,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                ],
              },
            ],
          })
          .andReturnSuccess()
          .end()

        let driver = executor.createDriver()
        return driver.actions().move({ x: 0, y: 125 }).perform()
      })

      it('origin = element', function () {
        let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENT, {
            using: 'css selector',
            value: '*[id="foo"]',
          })
          .andReturnSuccess(WebElement.buildId('abc123'))
          .expect(CName.ACTIONS, {
            actions: [
              {
                type: 'pointer',
                id: 'default mouse',
                parameters: {
                  pointerType: 'mouse',
                },
                actions: [
                  {
                    duration: 100,
                    origin: WebElement.buildId('abc123'),
                    type: 'pointerMove',
                    x: 0,
                    y: 125,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                ],
              },
            ],
          })
          .end()

        let driver = executor.createDriver()
        let element = driver.findElement(By.id('foo'))
        return driver.actions().move({ x: 0, y: 125, origin: element }).perform()
      })
    })
  })

  describe('manage()', function () {
    describe('setTimeouts()', function () {
      describe('throws if no timeouts are specified', function () {
        let driver
        before(() => (driver = new FakeExecutor().createDriver()))

        it('; no arguments', function () {
          assert.throws(() => driver.manage().setTimeouts(), TypeError)
        })

        it('; ignores unrecognized timeout keys', function () {
          assert.throws(() => driver.manage().setTimeouts({ foo: 123 }), TypeError)
        })

        it('; ignores positional arguments', function () {
          assert.throws(() => driver.manage().setTimeouts(1234, 56), TypeError)
        })
      })

      describe('throws timeout is not a number, null, or undefined', () => {
        let driver
        before(() => (driver = new FakeExecutor().createDriver()))

        function checkError(e) {
          return e instanceof TypeError && /expected "(script|pageLoad|implicit)" to be a number/.test(e.message)
        }

        it('script', function () {
          assert.throws(() => driver.manage().setTimeouts({ script: 'abc' }), checkError)
        })

        it('pageLoad', function () {
          assert.throws(() => driver.manage().setTimeouts({ pageLoad: 'abc' }), checkError)
        })

        it('implicit', function () {
          assert.throws(() => driver.manage().setTimeouts({ implicit: 'abc' }), checkError)
        })
      })

      it('can set multiple timeouts', function () {
        let executor = new FakeExecutor()
          .expect(CName.SET_TIMEOUT, { script: 1, pageLoad: 2, implicit: 3 })
          .andReturnSuccess()
          .end()
        let driver = executor.createDriver()
        return driver.manage().setTimeouts({ script: 1, pageLoad: 2, implicit: 3 })
      })

      it('falls back to legacy wire format if W3C version fails', () => {
        let executor = new FakeExecutor()
          .expect(CName.SET_TIMEOUT, { implicit: 3 })
          .andReturnError(Error('oops'))
          .expect(CName.SET_TIMEOUT, { type: 'implicit', ms: 3 })
          .andReturnSuccess()
          .end()
        let driver = executor.createDriver()
        return driver.manage().setTimeouts({ implicit: 3 })
      })
    })
  })
})
