// 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 testutil = require('./testutil');

const By = require('../../lib/by').By;
const Capabilities = require('../../lib/capabilities').Capabilities;
const Executor = require('../../lib/command').Executor;
const CName = require('../../lib/command').Name;
const error = require('../../lib/error');
const Button = require('../../lib/input').Button;
const Key = require('../../lib/input').Key;
const logging = require('../../lib/logging');
const Session = require('../../lib/session').Session;
const promise = require('../../lib/promise');
const {enablePromiseManager, promiseManagerSuite} = require('../../lib/test/promise');
const until = require('../../lib/until');
const Alert = require('../../lib/webdriver').Alert;
const AlertPromise = require('../../lib/webdriver').AlertPromise;
const UnhandledAlertError = require('../../lib/webdriver').UnhandledAlertError;
const WebDriver = require('../../lib/webdriver').WebDriver;
const WebElement = require('../../lib/webdriver').WebElement;
const WebElementPromise = require('../../lib/webdriver').WebElementPromise;

const assert = require('assert');
const sinon = require('sinon');

const SESSION_ID = 'test_session_id';

// Aliases for readability.
const NativePromise = Promise;
const StubError = testutil.StubError;
const assertIsInstance = testutil.assertIsInstance;
const assertIsStubError = testutil.assertIsStubError;
const throwStubError = testutil.throwStubError;
const fail = (msg) => assert.fail(msg);

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

  // before(function() {
  //   logging.getLogger('webdriver').setLevel(logging.Level.ALL);
  //   logging.installConsoleHandler();
  // });

  // after(function() {
  //   logging.getLogger('webdriver').setLevel(null);
  //   logging.removeConsoleHandler();
  // });

  var driver;
  var flow;
  var uncaughtExceptions;

  beforeEach(function setUp() {
    flow = promise.controlFlow();
    uncaughtExceptions = [];
    flow.on('uncaughtException', onUncaughtException);
  });

  afterEach(function tearDown() {
    if (!promise.USE_PROMISE_MANAGER) {
      return;
    }
    return waitForIdle(flow).then(function() {
      assert.deepEqual([], uncaughtExceptions);
      flow.reset();
    });
  });

  function onUncaughtException(e) {
    uncaughtExceptions.push(e);
  }

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

  function waitForIdle(opt_flow) {
    if (!promise.USE_PROMISE_MANAGER) {
      return Promise.resolve();
    }
    var theFlow = opt_flow || flow;
    return new Promise(function(fulfill, reject) {
      if (theFlow.isIdle()) {
        fulfill();
        return;
      }
      theFlow.once('idle', fulfill);
      theFlow.once('uncaughtException', reject);
    });
  }

  function waitForAbort(opt_flow, opt_n) {
    var n = opt_n || 1;
    var theFlow = opt_flow || flow;
    theFlow.removeAllListeners(
        promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION);
    return new Promise(function(fulfill, reject) {
      theFlow.once('idle', function() {
        reject(Error('expected flow to report an unhandled error'));
      });

      var errors = [];
      theFlow.on('uncaughtException', onError);
      function onError(e) {
        errors.push(e);
        if (errors.length === n) {
          theFlow.removeListener('uncaughtException', onError);
          fulfill(n === 1 ? errors[0] : errors);
        }
      }
    });
  }

  function expectedError(ctor, message) {
    return function(e) {
      assertIsInstance(ctor, e);
      assert.equal(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.deepEqual(this.parameters_, command.getParameters());
      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('testAttachToSession', function() {
    it('sessionIsAvailable', function() {
      let aSession = new Session(SESSION_ID, {'browserName': 'firefox'});
      let executor = new FakeExecutor().
          expect(CName.DESCRIBE_SESSION).
          withParameters({'sessionId': SESSION_ID}).
          andReturnSuccess(aSession).
          end();

      let driver = WebDriver.attachToSession(executor, SESSION_ID);
      return driver.getSession().then(v => assert.strictEqual(v, aSession));
    });

    it('failsToGetSessionInfo', function() {
      let e = new Error('boom');
      let executor = new FakeExecutor().
          expect(CName.DESCRIBE_SESSION).
          withParameters({'sessionId': SESSION_ID}).
          andReturnError(e).
          end();

      let driver = WebDriver.attachToSession(executor, SESSION_ID);
      return driver.getSession()
          .then(() => assert.fail('should have failed!'),
                (actual) => assert.strictEqual(actual, e));
    });

    it('remote end does not recognize DESCRIBE_SESSION command', function() {
      let e = new error.UnknownCommandError;
      let executor = new FakeExecutor().
          expect(CName.DESCRIBE_SESSION).
          withParameters({'sessionId': SESSION_ID}).
          andReturnError(e).
          end();

      let driver = WebDriver.attachToSession(executor, SESSION_ID);
      return driver.getSession().then(session => {
        assert.ok(session instanceof Session);
        assert.strictEqual(session.getId(), SESSION_ID);
        assert.equal(session.getCapabilities().size, 0);
      });
    });

    it('usesActiveFlowByDefault', function() {
      let executor = new FakeExecutor().
          expect(CName.DESCRIBE_SESSION).
          withParameters({'sessionId': SESSION_ID}).
          andReturnSuccess({}).
          end();

      var driver = WebDriver.attachToSession(executor, SESSION_ID);
      assert.equal(driver.controlFlow(), promise.controlFlow());

      return waitForIdle(driver.controlFlow());
    });

    enablePromiseManager(() => {
      it('canAttachInCustomFlow', function() {
        let executor = new FakeExecutor().
            expect(CName.DESCRIBE_SESSION).
            withParameters({'sessionId': SESSION_ID}).
            andReturnSuccess({}).
            end();

        var otherFlow = new promise.ControlFlow();
        var driver = WebDriver.attachToSession(executor, SESSION_ID, otherFlow);
        assert.equal(otherFlow, driver.controlFlow());
        assert.notEqual(otherFlow, promise.controlFlow());

        return waitForIdle(otherFlow);
      });
    });
  });

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

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

    it('happyPathWithCapabilitiesInstance', function() {
      let aSession = new Session(SESSION_ID, {'browserName': 'firefox'});
      let executor = new FakeExecutor().
          expect(CName.NEW_SESSION).
          withParameters({'desiredCapabilities': {'browserName': 'firefox'}}).
          andReturnSuccess(aSession).
          end();

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

    it('handles desired and required capabilities', function() {
      let aSession = new Session(SESSION_ID, {'browserName': 'firefox'});
      let executor = new FakeExecutor().
          expect(CName.NEW_SESSION).
          withParameters({
            'desiredCapabilities': {'foo': 'bar'},
            'requiredCapabilities': {'bim': 'baz'}
          }).
          andReturnSuccess(aSession).
          end();

      let desired = new Capabilities().set('foo', 'bar');
      let required = new Capabilities().set('bim', 'baz');
      var driver = WebDriver.createSession(executor, {desired, required});
      return driver.getSession().then(v => assert.strictEqual(v, aSession));
    });

    it('failsToCreateSession', function() {
      let executor = new FakeExecutor().
          expect(CName.NEW_SESSION).
          withParameters({'desiredCapabilities': {'browserName': 'firefox'}}).
          andReturnError(new StubError()).
          end();

      var 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({'desiredCapabilities': {'browserName': 'firefox'}})
          .andReturnError(new StubError())
          .end();

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

    it('usesActiveFlowByDefault', function() {
      let executor = new FakeExecutor().
          expect(CName.NEW_SESSION).
          withParameters({'desiredCapabilities': {}}).
          andReturnSuccess(new Session(SESSION_ID)).
          end();

      var driver = WebDriver.createSession(executor, {});
      assert.equal(promise.controlFlow(), driver.controlFlow());

      return waitForIdle(driver.controlFlow());
    });

    enablePromiseManager(() => {
      it('canCreateInCustomFlow', function() {
        let executor = new FakeExecutor().
            expect(CName.NEW_SESSION).
            withParameters({'desiredCapabilities': {}}).
            andReturnSuccess({}).
            end();

        var otherFlow = new promise.ControlFlow();
        var driver = WebDriver.createSession(executor, {}, otherFlow);
        assert.equal(otherFlow, driver.controlFlow());
        assert.notEqual(otherFlow, promise.controlFlow());

        return waitForIdle(otherFlow);
      });

      describe('creation failures bubble up in control flow', function() {
        function runTest(...args) {
          let executor = new FakeExecutor()
              .expect(CName.NEW_SESSION)
              .withParameters({'desiredCapabilities': {'browserName': 'firefox'}})
              .andReturnError(new StubError())
              .end();

          WebDriver.createSession(
              executor, {'browserName': 'firefox'}, ...args);
          return waitForAbort().then(assertIsStubError);
        }

        it('no onQuit callback', () => runTest());
        it('has onQuit callback', () => runTest(null, null, function() {}));

        it('onQuit callback failure suppress creation failure', function() {
          let e = new Error('hi!');
          let executor = new FakeExecutor()
              .expect(CName.NEW_SESSION)
              .withParameters({'desiredCapabilities': {'browserName': 'firefox'}})
              .andReturnError(new StubError())
              .end();

          WebDriver.createSession(
              executor, {'browserName': 'firefox'}, null, () => {throw e});
          return waitForAbort().then(err => assert.strictEqual(err, e));
        });
      });
    });
  });

  it('testDoesNotExecuteCommandIfSessionDoesNotResolve', function() {
    var 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();

    var driver = executor.createDriver();
    return driver.getTitle()
        .then(title => assert.equal('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('testCanSuppressCommandFailures', 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).
        expect(CName.GET_TITLE).
            andReturnSuccess('Google Search').
        end();

    var driver = executor.createDriver();
    driver.switchTo().window('foo')
        .catch(v => assert.strictEqual(v, e));
    driver.getTitle();
    return waitForIdle();
  });

  it('testErrorsPropagateUpToTheRunningApplication', 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));
  });

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

    var driver = executor.createDriver();
    return driver.switchTo().window('foo').
        catch(function() { return new StubError; });
        then(assertIsStubError, () => assert.fail());
  });

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

    var driver = executor.createDriver();
    return driver.switchTo().window('foo')
        .catch(throwStubError)
        .then(assert.fail, assertIsStubError);
  });

  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);
  });

  it('testCallbackCommandsExecuteBeforeNextCommand', function() {
    let executor = new FakeExecutor().
        expect(CName.GET_CURRENT_URL).
        expect(CName.GET, {'url': 'http://www.google.com'}).
        expect(CName.CLOSE).
        expect(CName.GET_TITLE).
        end();

    var driver = executor.createDriver();
    driver.getCurrentUrl().then(function() {
      driver.get('http://www.google.com').then(function() {
        driver.close();
      });
    });
    driver.getTitle();

    return waitForIdle();
  });

  enablePromiseManager(() => {
    it('testEachCallbackFrameRunsToCompletionBeforeTheNext', function() {
      let executor = new FakeExecutor().
          expect(CName.GET_TITLE).
          expect(CName.GET_CURRENT_URL).
          expect(CName.GET_CURRENT_WINDOW_HANDLE).
          expect(CName.CLOSE).
          expect(CName.QUIT).
          end();

      var driver = executor.createDriver();
      driver.getTitle().
          // Everything in this callback...
          then(function() {
            driver.getCurrentUrl();
            driver.getWindowHandle();
          }).
          // ...should execute before everything in this callback.
          then(function() {
            driver.close();
          });
      // This should execute after everything above
      driver.quit();

      return waitForIdle();
    });
  });

  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();

      var driver = executor.createDriver();
      return driver.getTitle().
          then(function() {
            return driver.getCurrentUrl();
          }).
          then(function(value) {
            assert.equal('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();

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

  describe('customFunctions', function() {
    it('returnsANonPromiseValue', function() {
      var driver = new FakeExecutor().createDriver();
      return driver.call(() => 'abc123').then(function(value) {
        assert.equal('abc123', value);
      });
    });

    enablePromiseManager(() => {
      it('executionOrderWithCustomFunctions', function() {
        var msg = [];
        let executor = new FakeExecutor().
            expect(CName.GET_TITLE).andReturnSuccess('cheese ').
            expect(CName.GET_CURRENT_URL).andReturnSuccess('tasty').
            end();

        var driver = executor.createDriver();

        var pushMsg = msg.push.bind(msg);
        driver.getTitle().then(pushMsg);
        driver.call(() => 'is ').then(pushMsg);
        driver.getCurrentUrl().then(pushMsg);
        driver.call(() => '!').then(pushMsg);

        return waitForIdle().then(function() {
          assert.equal('cheese is tasty!', msg.join(''));
        });
      });
    });

    it('passingArgumentsToACustomFunction', function() {
      var add = function(a, b) {
        return a + b;
      };
      var driver = new FakeExecutor().createDriver();
      return driver.call(add, null, 1, 2).then(function(value) {
        assert.equal(3, value);
      });
    });

    it('passingPromisedArgumentsToACustomFunction', function() {
      var promisedArg = Promise.resolve(2);
      var add = function(a, b) {
        return a + b;
      };
      var driver = new FakeExecutor().createDriver();
      return driver.call(add, null, 1, promisedArg).then(function(value) {
        assert.equal(3, value);
      });
    });

    it('passingArgumentsAndScopeToACustomFunction', function() {
      function Foo(name) {
        this.name = name;
      }
      Foo.prototype.getName = function() {
        return this.name;
      };
      var foo = new Foo('foo');

      var driver = new FakeExecutor().createDriver();
      return driver.call(foo.getName, foo).then(function(value) {
        assert.equal('foo', value);
      });
    });

    it('customFunctionThrowsAnError', function() {
      var driver = new FakeExecutor().createDriver();
      return driver.call(throwStubError).then(fail, assertIsStubError);
    });

    it('customFunctionSchedulesCommands', function() {
      let executor = new FakeExecutor().
          expect(CName.GET_TITLE).
          expect(CName.CLOSE).
          expect(CName.QUIT).
          end();

      var driver = executor.createDriver();
      driver.call(function() {
        driver.getTitle();
        driver.close();
      });
      driver.quit();
      return waitForIdle();
    });

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

      var driver = executor.createDriver();
      return driver.call(function() {
        var title = driver.getTitle();
        driver.close();
        return title;
      }).then(function(title) {
        assert.equal('Google Search', title);
      });
    });

    it('hasANestedCommandThatFails', function() {
      let executor = new FakeExecutor().
          expect(CName.SWITCH_TO_WINDOW, {
            'name': 'foo',
            'handle': 'foo'
          }).
          andReturnError(new StubError()).
          end();

      var driver = executor.createDriver();
      return driver.call(function() {
        return driver.switchTo().window('foo');
      }).then(fail, assertIsStubError);
    });

    enablePromiseManager(() => {
      it('doesNotCompleteUntilReturnedPromiseIsResolved', function() {
        var order = [];
        var driver = new FakeExecutor().createDriver();

        var d = promise.defer();
        d.promise.then(function() {
          order.push('b');
        });

        driver.call(function() {
          order.push('a');
          return d.promise;
        });
        driver.call(function() {
          order.push('c');
        });

        // timeout to ensure the first function starts its execution before we
        // trigger d's callbacks.
        return new Promise(f => setTimeout(f, 0)).then(function() {
          assert.deepEqual(['a'], order);
          d.fulfill();
          return waitForIdle().then(function() {
            assert.deepEqual(['a', 'b', 'c'], order);
          });
        });
      });
    });

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

      var driver = executor.createDriver();
      driver.call(function() {
        return driver.getTitle();
      }).then(function(title) {
        assert.equal('Google', title);
      });
      return waitForIdle();
    });
  });

  describe('nestedCommands', function() {
    enablePromiseManager(() => {
      it('commandExecutionOrder', function() {
        var msg = [];
        var driver = new FakeExecutor().createDriver();
        driver.call(msg.push, msg, 'a');
        driver.call(function() {
          driver.call(msg.push, msg, 'c');
          driver.call(function() {
            driver.call(msg.push, msg, 'e');
            driver.call(msg.push, msg, 'f');
          });
          driver.call(msg.push, msg, 'd');
        });
        driver.call(msg.push, msg, 'b');
        return waitForIdle().then(function() {
          assert.equal('acefdb', msg.join(''));
        });
      });

      it('basicUsage', function() {
        var msg = [];
        var driver = new FakeExecutor().createDriver();
        var pushMsg = msg.push.bind(msg);
        driver.call(() => 'cheese ').then(pushMsg);
        driver.call(function() {
          driver.call(() => 'is ').then(pushMsg);
          driver.call(() => 'tasty').then(pushMsg);
        });
        driver.call(() => '!').then(pushMsg);
        return waitForIdle().then(function() {
          assert.equal('cheese is tasty!', msg.join(''));
        });
      });

      it('normalCommandAfterNestedCommandThatReturnsAnAction', function() {
        var msg = [];
        let executor = new FakeExecutor().
            expect(CName.CLOSE).
            end();
        var driver = executor.createDriver();
        driver.call(function() {
          return driver.call(function() {
            msg.push('a');
            return driver.call(() => 'foobar');
          });
        });
        driver.close().then(function() {
          msg.push('b');
        });
        return waitForIdle().then(function() {
          assert.equal('ab', msg.join(''));
        });
      });
    });

    it('canReturnValueFromNestedFunction', function() {
      var driver = new FakeExecutor().createDriver();
      return driver.call(function() {
        return driver.call(function() {
          return driver.call(() => 'foobar');
        });
      }).then(function(value) {
        assert.equal('foobar', value);
      });
    });

    it('errorsBubbleUp_caught', function() {
      var driver = new FakeExecutor().createDriver();
      var result = driver.call(function() {
        return driver.call(function() {
          return driver.call(throwStubError);
        });
      }).then(fail, assertIsStubError);
      return Promise.all([waitForIdle(), result]);
    });

    it('errorsBubbleUp_uncaught', function() {
      var driver = new FakeExecutor().createDriver();
      return driver.call(function() {
        return driver.call(function() {
          return driver.call(throwStubError);
        });
      })
      .then(_ => assert.fail('should have failed'), assertIsStubError);
    });

    it('canScheduleCommands', function() {
      let executor = new FakeExecutor().
          expect(CName.GET_TITLE).
          expect(CName.CLOSE).
          end();

      var driver = executor.createDriver();
      driver.call(function() {
        driver.call(function() {
          driver.getTitle();
        });
        driver.close();
      });
      return waitForIdle();
    });
  });

  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() {
      var el = defer();

      var element = new WebElementPromise(driver, el.promise);
      var 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.deepEqual([
          '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();

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

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

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

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

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

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

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

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

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

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

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

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

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

      var 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();

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

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

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

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

    it('webElementPromiseArgumentConversion', function() {
      var 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();

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

    it('argumentConversion', function() {
      var 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();

      var driver = executor.createDriver();
      var 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();
      var driver = executor.createDriver();
      return driver.executeScript('throw Error(arguments[0]);', 'bam').
          then(fail, assertIsStubError);
    });

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

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

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

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

      var 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();

      var 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();

      var 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();

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

    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();

      var driver = executor.createDriver();
      driver.findElement(By.id('foo')).then(function(element) {
        element.click();
      });
      return waitForIdle();
    });

    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();

      var driver = executor.createDriver();
      var element = driver.findElement(By.js('return document.body'));
      element.click();  // just to make sure
      return waitForIdle();
    });

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

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

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

    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();

      var driver = executor.createDriver();
      var element = driver.findElement(function(d) {
        assert.equal(driver, d);
        return d.findElements(By.tagName('a'));
      });
      return element.click();
    });

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

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

      var driver = executor.createDriver();
      return driver.findElements(By.tagName('a'))
          .then(function(elements) {
            return promise.all(elements.map(function(e) {
              assert.ok(e instanceof WebElement);
              return e.getId();
            }));
          })
          .then((actual) => assert.deepEqual(ids, actual));
    });

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

      var 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.deepEqual(ids, actual));
    });

    it('byJs_filtersOutNonWebElementResponses', function() {
      var ids = ['foo', 'bar', 'baz'];
      var 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();

      var driver = executor.createDriver();
      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.deepEqual(ids, actual));
      return waitForIdle();
    });

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

      var 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.deepEqual(['foo'], actual));
    });

    it('byJs_canPassScriptArguments', function() {
      var 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();

      var 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.deepEqual(['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();

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

    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();

      var driver = executor.createDriver();
      var 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.equal(path, 'original/path');
        return Promise.resolve('modified/path');
      };
      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();

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

      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() {
      var a = new WebElement(new FakeExecutor().createDriver(), 'foo');
      return WebElement.equals(a, a).then(assert.ok);
    });

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

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

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

  describe('waiting', function() {
    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();

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

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

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

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

          var driver = executor.createDriver();
          driver.wait(function() {
            return driver.findElements(By.id('foo')).then(els => els.length > 0);
          }, 25);
          return waitForAbort().then(function(e) {
            assert.equal('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);
      });

      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.equal(v, 1));
      });

      it('wait times out', function() {
        let promise = new Promise(resolve => {/* 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.equal(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));
    });

    enablePromiseManager(() => {
      it('alertsBelongToSameFlowAsParentDriver', function() {
        let executor = new FakeExecutor()
            .expect(CName.GET_ALERT_TEXT).andReturnSuccess('hello')
            .end();

        var driver = executor.createDriver();
        var otherFlow = new promise.ControlFlow();
        otherFlow.execute(function() {
          driver.switchTo().alert().then(function() {
            assert.strictEqual(
                driver.controlFlow(), promise.controlFlow(),
                'Alert should belong to the same flow as its parent driver');
          });
        });

        assert.notEqual(otherFlow, driver.controlFlow);
        return Promise.all([
          waitForIdle(otherFlow),
          waitForIdle(driver.controlFlow())
        ]);
      });
    });

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

      var driver = executor.createDriver();
      var alert = driver.switchTo().alert();

      var 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);
    });
  });

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

      var driver = executor.createDriver();
      var otherFlow = new promise.ControlFlow();
      otherFlow.execute(function() {
        driver.findElement({id: 'foo'}).then(function() {
          assert.equal(driver.controlFlow(), promise.controlFlow());
        });
      });

      assert.notEqual(otherFlow, driver.controlFlow);
      return Promise.all([
        waitForIdle(otherFlow),
        waitForIdle(driver.controlFlow())
      ]);
    });
  });

  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();

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

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

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

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

    var driver = new FakeExecutor().createDriver(session);
    var navigateResult = driver.get('some-url').then(fail, assertIsStubError);
    var quitResult = driver.quit().then(fail, assertIsStubError);

    return waitForIdle().then(function() {
      return promise.all(navigateResult, quitResult);
    });
  });

  it('testWebElementCommandsFailIfInitialDriverCreationFailed', function() {
    var session = Promise.reject(new StubError);
    var 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();

    var 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();

    var 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() {
    it('failsIfInitialDriverCreationFailed', function() {
      let session = Promise.reject(new StubError('no session for you'));
      let driver = new FakeExecutor().createDriver(session);
      driver.getSession().catch(function() {});
      return driver.
          actions().
          mouseDown().
          mouseUp().
          perform().
          catch(assertIsStubError);
    });

    describe('mouseMove', function() {
      it('noElement', function() {
        let executor = new FakeExecutor()
            .expect(CName.MOVE_TO, {'xoffset': 0, 'yoffset': 125})
            .andReturnSuccess()
            .end();

        return executor.createDriver().
            actions().
            mouseMove({x: 0, y: 125}).
            perform();
      });

      it('element', function() {
        let executor = new FakeExecutor()
            .expect(CName.FIND_ELEMENT,
                    {using: 'css selector', value: '*[id="foo"]'})
                .andReturnSuccess(WebElement.buildId('abc123'))
            .expect(CName.MOVE_TO,
                    {'element': 'abc123', 'xoffset': 0, 'yoffset': 125})
                .andReturnSuccess()
            .end();

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

    it('supportsMouseDown', function() {
      let executor = new FakeExecutor()
          .expect(CName.MOUSE_DOWN, {'button': Button.LEFT})
              .andReturnSuccess()
          .end();

      return executor.createDriver().
          actions().
          mouseDown().
          perform();
    });

    it('testActionSequence', function() {
      let executor = new FakeExecutor()
          .expect(CName.FIND_ELEMENT,
                  {using: 'css selector', value: '*[id="a"]'})
              .andReturnSuccess(WebElement.buildId('id1'))
          .expect(CName.FIND_ELEMENT,
                  {using: 'css selector', value: '*[id="b"]'})
              .andReturnSuccess(WebElement.buildId('id2'))
          .expect(CName.SEND_KEYS_TO_ACTIVE_ELEMENT,
              {'value': [Key.SHIFT]})
              .andReturnSuccess()
          .expect(CName.MOVE_TO, {'element': 'id1'})
              .andReturnSuccess()
          .expect(CName.CLICK, {'button': Button.LEFT})
              .andReturnSuccess()
          .expect(CName.MOVE_TO, {'element': 'id2'})
              .andReturnSuccess()
          .expect(CName.CLICK, {'button': Button.LEFT})
              .andReturnSuccess()
          .end();

      var driver = executor.createDriver();
      var element1 = driver.findElement(By.id('a'));
      var element2 = driver.findElement(By.id('b'));

      return driver.actions()
          .keyDown(Key.SHIFT)
          .click(element1)
          .click(element2)
          .perform();
    });
  });

  describe('touchActions()', function() {
    it('failsIfInitialDriverCreationFailed', function() {
      let session = Promise.reject(new StubError);
      let driver = new FakeExecutor().createDriver(session);
      driver.getSession().catch(function() {});
      return driver.
          touchActions().
          scroll({x: 3, y: 4}).
          perform().
          catch(assertIsStubError);
    });

    it('testTouchActionSequence', function() {
      let executor = new FakeExecutor()
          .expect(CName.TOUCH_DOWN, {x: 1, y: 2}).andReturnSuccess()
          .expect(CName.TOUCH_MOVE, {x: 3, y: 4}).andReturnSuccess()
          .expect(CName.TOUCH_UP, {x: 5, y: 6}).andReturnSuccess()
          .end();

      var driver = executor.createDriver();
      return driver.touchActions()
          .tapAndHold({x: 1, y: 2})
          .move({x: 3, y: 4})
          .release({x: 5, y: 6})
          .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});
      });

      describe('deprecated API calls setTimeouts()', function() {
        it('implicitlyWait()', function() {
          let executor = new FakeExecutor()
              .expect(CName.SET_TIMEOUT, {implicit: 3})
              .andReturnSuccess()
              .end();
          let driver = executor.createDriver();
          return driver.manage().timeouts().implicitlyWait(3);
        });

        it('setScriptTimeout()', function() {
          let executor = new FakeExecutor()
              .expect(CName.SET_TIMEOUT, {script: 3})
              .andReturnSuccess()
              .end();
          let driver = executor.createDriver();
          return driver.manage().timeouts().setScriptTimeout(3);
        });

        it('pageLoadTimeout()', function() {
          let executor = new FakeExecutor()
              .expect(CName.SET_TIMEOUT, {pageLoad: 3})
              .andReturnSuccess()
              .end();
          let driver = executor.createDriver();
          return driver.manage().timeouts().pageLoadTimeout(3);
        });
      });
    });
  });

  describe('generator support', function() {
    var driver;

    beforeEach(function() {
      driver = new WebDriver(
          new Session('test-session', {}),
          new ExplodingExecutor());
    });

    it('canUseGeneratorsWithWebDriverCall', function() {
      return driver.call(function* () {
        var x = yield Promise.resolve(1);
        var y = yield Promise.resolve(2);
        return x + y;
      }).then(function(value) {
        assert.deepEqual(3, value);
      });
    });

    it('canDefineScopeOnGeneratorCall', function() {
      return driver.call(function* () {
        var x = yield Promise.resolve(1);
        return this.name + x;
      }, {name: 'Bob'}).then(function(value) {
        assert.deepEqual('Bob1', value);
      });
    });

    it('canSpecifyArgsOnGeneratorCall', function() {
      return driver.call(function* (a, b) {
        var x = yield Promise.resolve(1);
        var y = yield Promise.resolve(2);
        return [x + y, a, b];
      }, null, 'abc', 123).then(function(value) {
        assert.deepEqual([3, 'abc', 123], value);
      });
    });

    it('canUseGeneratorWithWebDriverWait', function() {
      var values = [];
      return driver.wait(function* () {
        yield values.push(1);
        values.push(yield promise.delayed(10).then(function() {
          return 2;
        }));
        yield values.push(3);
        return values.length === 6;
      }, 250).then(function() {
        assert.deepEqual([1, 2, 3, 1, 2, 3], values);
      });
    });

    /**
     * @constructor
     * @implements {CommandExecutor}
     */
    function ExplodingExecutor() {}


    /** @override */
    ExplodingExecutor.prototype.execute = function(command, cb) {
      cb(Error('Unsupported operation'));
    };
  });

  describe('wire format', function() {
    const FAKE_DRIVER = new FakeExecutor().createDriver();

    describe('can serialize', function() {
      function runSerializeTest(input, want) {
        let executor = new FakeExecutor().
            expect(CName.NEW_SESSION).
            withParameters({'desiredCapabilities': want}).
            andReturnSuccess({'browserName': 'firefox'}).
            end();
        return WebDriver.createSession(executor, input)
            .getSession();
      }

      it('function as a string', function() {
        function foo() { return 'foo'; }
        return runSerializeTest(foo, '' + foo);
      });

      it('object with toJSON()', function() {
        return runSerializeTest(
            new Date(605728511546),
            '1989-03-12T17:55:11.546Z');
      });

      it('Session', function() {
        return runSerializeTest(new Session('foo', {}), 'foo');
      });

      it('Capabilities', function() {
        var prefs = new logging.Preferences();
        prefs.setLevel(logging.Type.BROWSER, logging.Level.DEBUG);

        var caps = Capabilities.chrome();
        caps.setLoggingPrefs(prefs);

        return runSerializeTest(
            caps,
            {
              'browserName': 'chrome',
              'loggingPrefs': {'browser': 'DEBUG'}
            });
      });

      it('WebElement', function() {
        return runSerializeTest(
            new WebElement(FAKE_DRIVER, 'fefifofum'),
            WebElement.buildId('fefifofum'));
      });

      it('WebElementPromise', function() {
        return runSerializeTest(
            new WebElementPromise(
                FAKE_DRIVER,
                Promise.resolve(new WebElement(FAKE_DRIVER, 'fefifofum'))),
            WebElement.buildId('fefifofum'));
      });

      describe('an array', function() {
        it('with Serializable', function() {
          return runSerializeTest([new Session('foo', {})], ['foo']);
        });

        it('with WebElement', function() {
          return runSerializeTest(
              [new WebElement(FAKE_DRIVER, 'fefifofum')],
              [WebElement.buildId('fefifofum')]);
        });

        it('with WebElementPromise', function() {
          return runSerializeTest(
              [new WebElementPromise(
                  FAKE_DRIVER,
                  Promise.resolve(new WebElement(FAKE_DRIVER, 'fefifofum')))],
              [WebElement.buildId('fefifofum')]);
        });

        it('complex array', function() {
          var expected = [
            'abc', 123, true, WebElement.buildId('fefifofum'),
            [123, {'foo': 'bar'}]
          ];

          var element = new WebElement(FAKE_DRIVER, 'fefifofum');
          var input = ['abc', 123, true, element, [123, {'foo': 'bar'}]];
          return runSerializeTest(input, expected);
        });

        it('nested promises', function() {
          return runSerializeTest(
              ['abc', Promise.resolve([123, Promise.resolve(true)])],
              ['abc', [123, true]]);
        });
      });

      describe('an object', function() {
        it('literal', function() {
          var expected = {sessionId: 'foo'};
          return runSerializeTest({sessionId: 'foo'}, expected);
        });

        it('with sub-objects', function() {
          var expected = {sessionId: {value: 'foo'}};
          return runSerializeTest(
              {sessionId: {value: 'foo'}}, expected);
        });

        it('with values that have toJSON', function() {
          return runSerializeTest(
              {a: {b: new Date(605728511546)}},
              {a: {b: '1989-03-12T17:55:11.546Z'}});
        });

        it('with a Session', function() {
          return runSerializeTest(
              {a: new Session('foo', {})},
              {a: 'foo'});
        });

        it('nested', function() {
          var elementJson = WebElement.buildId('fefifofum');
          var expected = {
            'script': 'return 1',
            'args': ['abc', 123, true, elementJson, [123, {'foo': 'bar'}]],
            'sessionId': 'foo'
          };

          var element = new WebElement(FAKE_DRIVER, 'fefifofum');
          var parameters = {
            'script': 'return 1',
            'args':['abc', 123, true, element, [123, {'foo': 'bar'}]],
            'sessionId': new Session('foo', {})
          };

          return runSerializeTest(parameters, expected);
        });
      });
    });

    describe('can deserialize', function() {
      function runDeserializeTest(original, want) {
        let executor = new FakeExecutor()
            .expect(CName.GET_CURRENT_URL)
            .andReturnSuccess(original)
            .end();
        let driver = executor.createDriver();
        return driver.getCurrentUrl().then(function(got) {
          assert.deepEqual(got, want);
        });
      }

      it('primitives', function() {
        return Promise.all([
            runDeserializeTest(1, 1),
            runDeserializeTest('', ''),
            runDeserializeTest(true, true),
            runDeserializeTest(undefined, undefined),
            runDeserializeTest(null, null)
        ]);
      });

      it('simple object', function() {
        return runDeserializeTest(
            {sessionId: 'foo'},
            {sessionId: 'foo'});
      });

      it('nested object', function() {
        return runDeserializeTest(
            {'foo': {'bar': 123}},
            {'foo': {'bar': 123}});
      });

      it('array', function() {
        return runDeserializeTest(
            [{'foo': {'bar': 123}}],
            [{'foo': {'bar': 123}}]);
      });

      it('passes through function properties', function() {
        function bar() {}
        return runDeserializeTest(
            [{foo: {'bar': 123}, func: bar}],
            [{foo: {'bar': 123}, func: bar}]);
      });
    });
  });
});
