/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.net.xpc.CrossPageChannelTest');
goog.setTestOnly('goog.net.xpc.CrossPageChannelTest');

const CfgFields = goog.require('goog.net.xpc.CfgFields');
const ChannelStates = goog.require('goog.net.xpc.ChannelStates');
const CrossPageChannel = goog.require('goog.net.xpc.CrossPageChannel');
const CrossPageChannelRole = goog.require('goog.net.xpc.CrossPageChannelRole');
const Disposable = goog.require('goog.Disposable');
const GoogPromise = goog.require('goog.Promise');
const Level = goog.require('goog.log.Level');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const Resolver = goog.require('goog.promise.Resolver');
const TagName = goog.require('goog.dom.TagName');
const TestCase = goog.require('goog.testing.TestCase');
const Timer = goog.require('goog.Timer');
const TransportTypes = goog.require('goog.net.xpc.TransportTypes');
const Uri = goog.require('goog.Uri');
const browser = goog.require('goog.labs.userAgent.browser');
const dispose = goog.require('goog.dispose');
const dom = goog.require('goog.dom');
const log = goog.require('goog.log');
const object = goog.require('goog.object');
const testSuite = goog.require('goog.testing.testSuite');
const xpc = goog.require('goog.net.xpc');
/** @suppress {extraRequire} Needed for G_testRunner.log() */
goog.require('goog.testing.jsunit');


// Set this to false when working on this test.  It needs to be true for
// automated testing, as some browsers (eg IE8) choke on the large numbers of
// iframes this test would otherwise leave active.
/** @const */
const CLEAN_UP_IFRAMES = true;

/** @const */
const IFRAME_LOAD_WAIT_MS = 1000;
const stubs = new PropertyReplacer();
let uniqueId = 0;
let driver;
let accessCheckPromise = null;

testSuite({

  setUpPage() {
    // This test is insanely slow on IE8 and Safari for some reason.
    TestCase.getActiveTestCase().promiseTimeout = 40 * 1000;

    // Show debug log
    const debugDiv = dom.getElement('debugDiv');
    const logger = log.getLogger('goog.net.xpc');
    log.setLevel(logger, Level.ALL);
    log.addHandler(logger, function(logRecord) {
      const msgElm = dom.createDom(TagName.DIV);
      msgElm.innerHTML = logRecord.getMessage();
      dom.appendChild(debugDiv, msgElm);
    });

    accessCheckPromise = new GoogPromise(function(resolve, reject) {
      const accessCheckIframes = [];

      accessCheckIframes.push(
          create1x1Iframe('nonexistent', 'testdata/i_am_non_existent.html'));
      window.setTimeout(function() {
        accessCheckIframes.push(
            create1x1Iframe('existent', 'testdata/access_checker.html'));
      }, 10);

      // Called from testdata/access_checker.html
      window['sameDomainIframeAccessComplete'] = function() {
        for (let i = 0; i < accessCheckIframes.length; i++) {
          document.body.removeChild(accessCheckIframes[i]);
        }
        resolve();
      };
    });
  },


  setUp() {
    driver = new Driver();
    // Expose driver on the window object, since inner_peer.html uses it to
    // communicate.
    window['driver'] = driver;

    // Ensure that the access check is complete before starting each test.
    return accessCheckPromise;
  },


  tearDown() {
    stubs.reset();
    driver.dispose();
  },


  testCreateIframeSpecifyId() {
    driver.createPeerIframe('new_iframe');

    return Timer.promise(IFRAME_LOAD_WAIT_MS).then(function() {
      driver.checkPeerIframe();
    });
  },


  testCreateIframeRandomId() {
    driver.createPeerIframe();

    return Timer.promise(IFRAME_LOAD_WAIT_MS).then(function() {
      driver.checkPeerIframe();
    });
  },


  testGetRole() {
    const cfg = {};
    cfg[CfgFields.ROLE] = CrossPageChannelRole.OUTER;
    const channel = new CrossPageChannel(cfg);
    // If the configured role is ignored, this will cause the dynamicly
    // determined role to become INNER.
    /** @suppress {visibility} suppression added to enable type checking */
    channel.peerWindowObject_ = window.parent;
    assertEquals(
        'Channel should use role from the config.', CrossPageChannelRole.OUTER,
        channel.getRole());
    channel.dispose();
  },


  // The following batch of tests:
  // * Establishes a peer iframe
  // * Connects an XPC channel between the frames
  // * From the connection callback in each frame, sends an 'echo' request, and
  //   expects a 'response' response.
  // * Reconnects the inner frame, sends an 'echo', expects a 'response'.
  // * Optionally, reconnects the outer frame, sends an 'echo', expects a
  //   'response'.
  // * Optionally, reconnects the inner frame, but first reconfigures it to the
  //   alternate protocol version, simulating an inner frame navigation that
  //   picks up a new/old version.
  //
  // Every valid combination of protocol versions is tested, with both single
  // and double ended handshakes.  Two timing scenarios are tested per
  // combination, which is what the 'reverse' parameter distinguishes.
  //
  // Where single sided handshake is in use, reconnection by the outer frame is
  // not supported, and therefore is not tested.
  //
  // The only known issue migrating to V2 is that once two V2 peers have
  // connected, replacing either peer with a V1 peer will not work.  Upgrading
  // V1 peers to v2 is supported, as is replacing the only v2 peer in a
  // connection with a v1.


  testLifeCycle_v1_v1() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v1_v1_rev() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v1_v1_onesided() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v1_v1_onesided_rev() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v1_v2() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v1_v2_rev() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v1_v2_onesided() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v1_v2_onesided_rev() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 1 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v2_v1() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v2_v1_rev() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v2_v1_onesided() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, false /* reverse */);
  },

  testLifeCycle_v2_v1_onesided_rev() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        1 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        true /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v2_v2() {
    // Test flakes on IE 10+ and Chrome: see b/22873770 and b/18595666.
    if (browser.isIE() || browser.isChrome()) {
      return;
    }

    return checkLifeCycle(
        false /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        false /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v2_v2_rev() {
    return checkLifeCycle(
        false /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, true /* outerFrameReconnectSupported */,
        false /* innerFrameMigrationSupported */, true /* reverse */);
  },


  testLifeCycle_v2_v2_onesided() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        false /* innerFrameMigrationSupported */, false /* reverse */);
  },


  testLifeCycle_v2_v2_onesided_rev() {
    return checkLifeCycle(
        true /* oneSidedHandshake */, 2 /* innerProtocolVersion */,
        2 /* outerProtocolVersion */, false /* outerFrameReconnectSupported */,
        false /* innerFrameMigrationSupported */, true /* reverse */);
  },


  // testConnectMismatchedNames have been flaky on IEs.
  // Flakiness is tracked in http://b/18595666
  // For now, not running these tests on IE.

  testConnectMismatchedNames_v1_v1() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        1 /* innerProtocolVersion */, 1 /* outerProtocolVersion */,
        false /* reverse */);
  },


  testConnectMismatchedNames_v1_v1_rev() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        1 /* innerProtocolVersion */, 1 /* outerProtocolVersion */,
        true /* reverse */);
  },


  testConnectMismatchedNames_v1_v2() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        1 /* innerProtocolVersion */, 2 /* outerProtocolVersion */,
        false /* reverse */);
  },


  testConnectMismatchedNames_v1_v2_rev() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        1 /* innerProtocolVersion */, 2 /* outerProtocolVersion */,
        true /* reverse */);
  },


  testConnectMismatchedNames_v2_v1() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        2 /* innerProtocolVersion */, 1 /* outerProtocolVersion */,
        false /* reverse */);
  },


  testConnectMismatchedNames_v2_v1_rev() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        2 /* innerProtocolVersion */, 1 /* outerProtocolVersion */,
        true /* reverse */);
  },


  testConnectMismatchedNames_v2_v2() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        2 /* innerProtocolVersion */, 2 /* outerProtocolVersion */,
        false /* reverse */);
  },


  testConnectMismatchedNames_v2_v2_rev() {
    if (browser.isIE()) {
      return;
    }

    return checkConnectMismatchedNames(
        2 /* innerProtocolVersion */, 2 /* outerProtocolVersion */,
        true /* reverse */);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testEscapeServiceName() {
    /** @suppress {visibility} suppression added to enable type checking */
    const escape = CrossPageChannel.prototype.escapeServiceName_;
    assertEquals(
        'Shouldn\'t escape alphanumeric name', 'fooBar123',
        escape('fooBar123'));
    assertEquals(
        'Shouldn\'t escape most non-alphanumeric characters',
        '`~!@#$^&*()_-=+ []{}\'";,<.>/?\\',
        escape('`~!@#$^&*()_-=+ []{}\'";,<.>/?\\'));
    assertEquals(
        'Should escape %, |, and :', 'foo%3ABar%7C123%25',
        escape('foo:Bar|123%'));
    assertEquals('Should escape tp', '%25tp', escape('tp'));
    assertEquals('Should escape %tp', '%25%25tp', escape('%tp'));
    assertEquals('Should not escape stp', 'stp', escape('stp'));
    assertEquals('Should not escape s%tp', 's%25tp', escape('s%tp'));
  },


  testSameDomainCheck_noMessageOrigin() {
    const channel = new CrossPageChannel(
        object.create(CfgFields.PEER_HOSTNAME, 'http://foo.com'));
    assertTrue(channel.isMessageOriginAcceptable(undefined));
  },


  testSameDomainCheck_noPeerHostname() {
    const channel = new CrossPageChannel({});
    assertTrue(channel.isMessageOriginAcceptable('http://foo.com'));
  },


  testSameDomainCheck_unconfigured() {
    const channel = new CrossPageChannel({});
    assertTrue(channel.isMessageOriginAcceptable(undefined));
  },


  testSameDomainCheck_originsMatch() {
    const channel = new CrossPageChannel(
        object.create(CfgFields.PEER_HOSTNAME, 'http://foo.com'));
    assertTrue(channel.isMessageOriginAcceptable('http://foo.com'));
  },


  testSameDomainCheck_originsMismatch() {
    const channel = new CrossPageChannel(
        object.create(CfgFields.PEER_HOSTNAME, 'http://foo.com'));
    assertFalse(channel.isMessageOriginAcceptable('http://nasty.com'));
  },


  /** @suppress {checkTypes} suppression added to enable type checking */
  testUnescapeServiceName() {
    /** @suppress {visibility} suppression added to enable type checking */
    const unescape = CrossPageChannel.prototype.unescapeServiceName_;
    assertEquals(
        'Shouldn\'t modify alphanumeric name', 'fooBar123',
        unescape('fooBar123'));
    assertEquals(
        'Shouldn\'t modify most non-alphanumeric characters',
        '`~!@#$^&*()_-=+ []{}\'";,<.>/?\\',
        unescape('`~!@#$^&*()_-=+ []{}\'";,<.>/?\\'));
    assertEquals(
        'Should unescape URL-escapes', 'foo:Bar|123%',
        unescape('foo%3ABar%7C123%25'));
    assertEquals('Should unescape tp', 'tp', unescape('%25tp'));
    assertEquals('Should unescape %tp', '%tp', unescape('%25%25tp'));
    assertEquals('Should not escape stp', 'stp', unescape('stp'));
    assertEquals('Should not escape s%tp', 's%tp', unescape('s%25tp'));
  },


  async testDisposeImmediate() {
    // Given
    driver.createPeerIframe(
        'new_iframe',
        /* oneSidedHandshake= */ false,
        /* innerProtocolVersion= */ 2,
        /* outerProtocolVersion= */ 2,
        /* opt_randomChannelNames= */ true);

    assertEquals(driver.getChannel().state_, ChannelStates.NOT_CONNECTED);
    assertNull(driver.getChannel().transport_);

    // When
    driver.getChannel().dispose();

    // Then
    assertTrue(driver.getChannel().isDisposed());
    // Let any errors caused by erroneous retries happen.
    await Timer.promise(2000);
  },

  async testDisposeBeforePeerNotification() {
    // Given
    driver.createPeerIframe(
        'new_iframe',
        /* oneSidedHandshake= */ false,
        /* innerProtocolVersion= */ 2,
        /* outerProtocolVersion= */ 2,
        /* opt_randomChannelNames= */ true);

    await driver.connectAndWaitForPeer();

    assertEquals(driver.getChannel().state_, ChannelStates.NOT_CONNECTED);
    const transport = driver.getChannel().transport_;

    // When
    driver.getChannel().dispose();

    // Then
    assertNull(driver.getChannel().transport_);
    assertTrue(driver.getChannel().isDisposed());
    assertTrue(transport.isDisposed());
    // Let any errors caused by erroneous retries happen.
    await Timer.promise(2000);
  },



});


/**
 * @param {string} iframeId
 * @param {string} src
 * @return {!HTMLIFrameElement}
 */
function create1x1Iframe(iframeId, src) {
  const iframeAccessChecker = dom.createElement(TagName.IFRAME);
  iframeAccessChecker.id = iframeAccessChecker.name = iframeId;
  iframeAccessChecker.style.width = iframeAccessChecker.style.height = '1px';
  iframeAccessChecker.src = src;
  document.body.insertBefore(iframeAccessChecker, document.body.firstChild);
  return iframeAccessChecker;
}

/**
 * @param {boolean} oneSidedHandshake,
 * @param {number} innerProtocolVersion
 * @param {number} outerProtocolVersion
 * @param {boolean} outerFrameReconnectSupported
 * @param {boolean} innerFrameMigrationSupported
 * @param {boolean} reverse
 * @return {!GoogPromise<undefined>}
 */
function checkLifeCycle(
    oneSidedHandshake, innerProtocolVersion, outerProtocolVersion,
    outerFrameReconnectSupported, innerFrameMigrationSupported, reverse) {
  driver.createPeerIframe(
      'new_iframe', oneSidedHandshake, innerProtocolVersion,
      outerProtocolVersion);
  return driver.connect(
      true /* fullLifeCycleTest */, outerFrameReconnectSupported,
      innerFrameMigrationSupported, reverse);
}

/**
 * @param {number} innerProtocolVersion
 * @param {number} outerProtocolVersion
 * @param {boolean} reverse
 * @return {!GoogPromise<undefined>}
 */
function checkConnectMismatchedNames(
    innerProtocolVersion, outerProtocolVersion, reverse) {
  driver.createPeerIframe(
      'new_iframe', false /* oneSidedHandshake */, innerProtocolVersion,
      outerProtocolVersion, true /* opt_randomChannelNames */);
  return driver.connect(
      false /* fullLifeCycleTest */, false /* outerFrameReconnectSupported */,
      false /* innerFrameMigrationSupported */, reverse /* reverse */);
}



/**
 * Driver for the tests for CrossPageChannel.
 * @unrestricted
 */
const Driver = class extends Disposable {
  constructor() {
    super();

    /**
     * The peer iframe.
     * @type {!Element}
     * @private
     * @suppress {checkTypes} suppression added to enable type checking
     */
    this.iframe_ = null;

    /**
     * The channel to use.
     * @type {?CrossPageChannel}
     * @private
     */
    this.channel_ = null;

    /**
     * Outer frame configuration object.
     * @type {?Object}
     * @private
     */
    this.outerFrameCfg_ = null;

    /**
     * The initial name of the outer channel.
     * @type {?string}
     * @private
     */
    this.initialOuterChannelName_ = null;

    /**
     * Inner frame configuration object.
     * @type {?Object}
     * @private
     */
    this.innerFrameCfg_ = null;

    /**
     * The contents of the payload of the 'echo' request sent by the inner
     * frame.
     * @type {?string}
     * @private
     */
    this.innerFrameEchoPayload_ = null;

    /**
     * The contents of the payload of the 'echo' request sent by the outer
     * frame.
     * @type {?string}
     * @private
     */
    this.outerFrameEchoPayload_ = null;

    /**
     * A resolver which fires its promise when the inner frame receives an echo.
     * @type {!Resolver}
     * @private
     */
    this.innerFrameResponseReceived_ = GoogPromise.withResolver();

    /**
     * A resolver which fires its promise when the outer frame receives an echo.
     * @type {!Resolver}
     * @private
     */
    this.outerFrameResponseReceived_ = GoogPromise.withResolver();
  }

  /** @override */
  disposeInternal() {
    // Required to make this test perform acceptably (and pass) on slow
    // browsers, esp IE8.
    if (CLEAN_UP_IFRAMES) {
      dom.removeNode(this.iframe_);
      delete this.iframe_;
    }
    dispose(this.channel_);
    this.innerFrameResponseReceived_.promise.cancel();
    this.outerFrameResponseReceived_.promise.cancel();
    super.disposeInternal();
  }

  /**
   * Returns the child peer's window object.
   * @return {!Window} Child peer's window.
   * @private
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  getInnerPeer_() {
    return this.iframe_.contentWindow;
  }

  /**
   * Sets up the configuration objects for the inner and outer frames.
   * @param {string=} opt_iframeId If present, the ID of the iframe to use,
   *     otherwise, tells the channel to generate an iframe ID.
   * @param {boolean=} opt_oneSidedHandshake Whether the one sided handshake
   *     config option should be set.
   * @param {string=} opt_channelName The name of the channel to use, or null
   *     to generate one.
   * @param {number=} opt_innerProtocolVersion The native transport protocol
   *     version used in the inner iframe.
   * @param {number=} opt_outerProtocolVersion The native transport protocol
   *     version used in the outer iframe.
   * @param {boolean=} opt_randomChannelNames Whether the different ends of the
   *     channel should be allowed to pick differing, random names.
   * @return {string} The name of the created channel.
   * @private
   * @suppress {missingReturn} suppression added to enable type checking
   */
  setConfiguration_(
      opt_iframeId, opt_oneSidedHandshake, opt_channelName,
      opt_innerProtocolVersion, opt_outerProtocolVersion,
      opt_randomChannelNames) {
    const cfg = {};
    if (opt_iframeId) {
      cfg[CfgFields.IFRAME_ID] = opt_iframeId;
    }
    cfg[CfgFields.PEER_URI] = 'testdata/inner_peer.html';
    if (!opt_randomChannelNames) {
      const channelName = opt_channelName || 'test_channel' + uniqueId++;
      cfg[CfgFields.CHANNEL_NAME] = channelName;
    }
    cfg[CfgFields.LOCAL_POLL_URI] = 'does-not-exist.html';
    cfg[CfgFields.PEER_POLL_URI] = 'does-not-exist.html';
    cfg[CfgFields.ONE_SIDED_HANDSHAKE] = !!opt_oneSidedHandshake;
    cfg[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] = opt_outerProtocolVersion;
    function resolveUri(fieldName) {
      cfg[fieldName] =
          Uri.resolve(window.location.href, cfg[fieldName]).toString();
    }
    resolveUri(CfgFields.PEER_URI);
    resolveUri(CfgFields.LOCAL_POLL_URI);
    resolveUri(CfgFields.PEER_POLL_URI);
    this.outerFrameCfg_ = cfg;
    this.innerFrameCfg_ = object.clone(cfg);
    this.innerFrameCfg_[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] =
        opt_innerProtocolVersion;
  }

  /**
   * Creates an outer frame channel object.
   * @return {string}
   * @private
   * @suppress {checkTypes} suppression added to enable type checking
   */
  createChannel_() {
    if (this.channel_) {
      this.channel_.dispose();
    }
    this.channel_ = new CrossPageChannel(this.outerFrameCfg_);
    this.channel_.registerService('echo', goog.bind(this.echoHandler_, this));
    this.channel_.registerService(
        'response', goog.bind(this.responseHandler_, this));

    return this.channel_.name;
  }

  /**
   * Checks the names of the inner and outer frames meet expectations.
   * @private
   * @suppress {undefinedVars} suppression added to enable type checking
   */
  checkChannelNames_() {
    const outerName = this.channel_.name;
    /**
     * @suppress {missingProperties} suppression added to enable type checking
     */
    const innerName = this.getInnerPeer_().channel.name;
    const configName = this.innerFrameCfg_[CfgFields.CHANNEL_NAME] || null;

    // The outer channel never changes its name.
    assertEquals(this.initialOuterChannelName_, outerName);
    // The name should be as configured, if it was configured.
    if (configName) {
      assertEquals(configName, innerName);
    }
    // The names of both ends of the channel should match.
    assertEquals(innerName, outerName);
    G_testRunner.log('Channel name: ' + innerName);
  }

  /**
   * Returns the configuration of the xpc.
   * @return {?Object} The configuration of the xpc.
   */
  getInnerFrameConfiguration() {
    return this.innerFrameCfg_;
  }

  /**
   * Creates the peer iframe.
   * @param {string=} opt_iframeId If present, the ID of the iframe to create,
   *     otherwise, generates an iframe ID.
   * @param {boolean=} opt_oneSidedHandshake Whether a one sided handshake is
   *     specified.
   * @param {number=} opt_innerProtocolVersion The native transport protocol
   *     version used in the inner iframe.
   * @param {number=} opt_outerProtocolVersion The native transport protocol
   *     version used in the outer iframe.
   * @param {boolean=} opt_randomChannelNames Whether the ends of the channel
   *     should be allowed to pick differing, random names.
   * @return {!Array<string>} The id of the created iframe and the name of the
   *     created channel.
   * @suppress {missingReturn} suppression added to enable type checking
   */
  createPeerIframe(
      opt_iframeId, opt_oneSidedHandshake, opt_innerProtocolVersion,
      opt_outerProtocolVersion, opt_randomChannelNames) {
    let expectedIframeId;

    if (opt_iframeId) {
      expectedIframeId = opt_iframeId = opt_iframeId + uniqueId++;
    } else {
      // Have createPeerIframe() generate an ID
      stubs.set(xpc, 'getRandomString', function(length) {
        return '' + length;
      });
      expectedIframeId = 'xpcpeer4';
    }
    assertNull(
        'element[id=' + expectedIframeId + '] exists',
        dom.getElement(expectedIframeId));

    this.setConfiguration_(
        opt_iframeId, opt_oneSidedHandshake, undefined /* opt_channelName */,
        opt_innerProtocolVersion, opt_outerProtocolVersion,
        opt_randomChannelNames);
    const channelName = this.createChannel_();
    this.initialOuterChannelName_ = channelName;
    this.iframe_ = this.channel_.createPeerIframe(document.body);

    assertEquals(expectedIframeId, this.iframe_.id);
  }

  /**
   * Checks if the peer iframe has been created.
   */
  checkPeerIframe() {
    assertNotNull(this.iframe_);
    const peer = this.getInnerPeer_();
    assertNotNull(peer);
    assertNotNull(peer.document);
  }

  /**
   * Starts the connection. The connection happens asynchronously.
   * @param {boolean} fullLifeCycleTest
   * @param {boolean} outerFrameReconnectSupported
   * @param {boolean} innerFrameMigrationSupported
   * @param {boolean} reverse
   * @return {!GoogPromise<undefined>}
   * @suppress {checkTypes} suppression added to enable type checking
   */
  connect(
      fullLifeCycleTest, outerFrameReconnectSupported,
      innerFrameMigrationSupported, reverse) {
    if (!this.isTransportTestable_()) {
      return;
    }

    // Set the criteria for the initial handshake portion of the test.
    this.reinitializePromises_();

    this.innerFrameResponseReceived_.promise.then(
        this.checkChannelNames_, null, this);

    if (fullLifeCycleTest) {
      this.innerFrameResponseReceived_.promise.then(goog.bind(
          this.testReconnects_, this, outerFrameReconnectSupported,
          innerFrameMigrationSupported));
    }

    this.continueConnect_(reverse);
    return this.innerFrameResponseReceived_.promise;
  }

  /**
   * @param {boolean} reverse
   * @private
   * @suppress {missingProperties} suppression added to enable type checking
   */
  continueConnect_(reverse) {
    // Wait until the peer is fully established.  Establishment is sometimes
    // very slow indeed, especially on virtual machines, so a fixed timeout is
    // not suitable.  This wait is required because we want to take precise
    // control of the channel startup timing, and shouldn't be needed in
    // production use, where the inner frame's channel is typically not started
    // by a DOM call as it is here.
    if (!this.getInnerPeer_() || !this.getInnerPeer_().instantiateChannel) {
      window.setTimeout(goog.bind(this.continueConnect_, this, reverse), 100);
      return;
    }

    const connectFromOuterFrame = goog.bind(
        this.channel_.connect, this.channel_,
        goog.bind(this.outerFrameConnected_, this));
    const innerConfig = this.innerFrameCfg_;
    /**
     * @suppress {missingProperties} suppression added to enable type checking
     */
    const connectFromInnerFrame = goog.bind(
        this.getInnerPeer_().instantiateChannel, this.getInnerPeer_(),
        innerConfig);

    // Take control of the timing and reverse of each frame's first SETUP call.
    // If these happen to fire right on top of each other, that tends to mask
    // problems that reliably occur when there is a short delay.
    window.setTimeout(connectFromOuterFrame, reverse ? 1 : 10);
    window.setTimeout(connectFromInnerFrame, reverse ? 10 : 1);
  }

  /**
   * Called by the outer frame connection callback.
   * @private
   */
  outerFrameConnected_() {
    const payload = this.outerFrameEchoPayload_ = xpc.getRandomString(10);
    this.channel_.send('echo', payload);
  }

  /**
   * Called by the inner frame connection callback in inner_peer.html.
   * @suppress {missingProperties} suppression added to enable type checking
   */
  innerFrameConnected() {
    const payload = this.innerFrameEchoPayload_ = xpc.getRandomString(10);
    this.getInnerPeer_().sendEcho(payload);
  }

  /**
   * The handler function for incoming echo requests.
   * @param {string} payload The message payload.
   * @private
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  echoHandler_(payload) {
    assertTrue('outer frame should be connected', this.channel_.isConnected());
    const peer = this.getInnerPeer_();
    assertTrue('child should be connected', peer.isConnected());
    this.channel_.send('response', payload);
  }

  /**
   * The handler function for incoming echo responses.
   * @param {string} payload The message payload.
   * @private
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  responseHandler_(payload) {
    assertTrue('outer frame should be connected', this.channel_.isConnected());
    const peer = this.getInnerPeer_();
    assertTrue('child should be connected', peer.isConnected());
    assertEquals(this.outerFrameEchoPayload_, payload);
    this.outerFrameResponseReceived_.resolve(true);
  }

  /**
   * The handler function for incoming echo replies. Called from
   * inner_peer.html.
   * @param {string} payload The message payload.
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  innerFrameGotResponse(payload) {
    assertTrue('outer frame should be connected', this.channel_.isConnected());
    const peer = this.getInnerPeer_();
    assertTrue('child should be connected', peer.isConnected());
    assertEquals(this.innerFrameEchoPayload_, payload);
    this.innerFrameResponseReceived_.resolve(true);
  }

  /**
   * The second phase of the standard test, where reconnections of both the
   * inner and outer frames are performed.
   * @param {boolean} outerFrameReconnectSupported Whether outer frame
   *     reconnects are supported, and should be tested.
   * @param {boolean} innerFrameMigrationSupported
   * @private
   */
  testReconnects_(outerFrameReconnectSupported, innerFrameMigrationSupported) {
    G_testRunner.log('Performing inner frame reconnect');
    this.reinitializePromises_();
    this.innerFrameResponseReceived_.promise.then(
        this.checkChannelNames_, null, this);

    if (outerFrameReconnectSupported) {
      this.innerFrameResponseReceived_.promise.then(goog.bind(
          this.performOuterFrameReconnect_, this,
          innerFrameMigrationSupported));
    } else if (innerFrameMigrationSupported) {
      this.innerFrameResponseReceived_.promise.then(
          this.migrateInnerFrame_, null, this);
    }

    this.performInnerFrameReconnect_();
  }

  /**
   * Initializes the promise resolvers and clears the echo payloads, ready for
   * another sub-test.
   * @private
   */
  reinitializePromises_() {
    this.innerFrameEchoPayload_ = null;
    this.outerFrameEchoPayload_ = null;
    this.innerFrameResponseReceived_.promise.cancel();
    this.innerFrameResponseReceived_ = GoogPromise.withResolver();
    this.outerFrameResponseReceived_.promise.cancel();
    this.outerFrameResponseReceived_ = GoogPromise.withResolver();
  }

  /**
   * Get the inner frame to reconnect, and repeat the echo test.
   * @private
   * @suppress {missingProperties} suppression added to enable type checking
   */
  performInnerFrameReconnect_() {
    const peer = this.getInnerPeer_();
    peer.instantiateChannel(this.innerFrameCfg_);
  }

  /**
   * Get the outer frame to reconnect, and repeat the echo test.
   * @private
   */
  performOuterFrameReconnect_(innerFrameMigrationSupported) {
    G_testRunner.log('Closing channel');
    this.channel_.close();

    // If there is another channel still open, the native transport's global
    // postMessage listener will still be active.  This will mean that messages
    // being sent to the now-closed channel will still be received and
    // delivered, such as transport service traffic from its previous
    // correspondent in the other frame.  Ensure these messages don't cause
    // exceptions.
    try {
      this.channel_.xpcDeliver(xpc.TRANSPORT_SERVICE, 'payload');
    } catch (e) {
      fail('Should not throw exception');
    }

    G_testRunner.log('Reconnecting outer frame');
    this.reinitializePromises_();
    this.innerFrameResponseReceived_.promise.then(
        this.checkChannelNames_, null, this);
    if (innerFrameMigrationSupported) {
      this.outerFrameResponseReceived_.promise.then(
          this.migrateInnerFrame_, null, this);
    }
    this.channel_.connect(goog.bind(this.outerFrameConnected_, this));
  }

  /**
   * Migrate the inner frame to the alternate protocol version and reconnect it.
   * @private
   */
  migrateInnerFrame_() {
    G_testRunner.log('Migrating inner frame');
    this.reinitializePromises_();
    const innerFrameProtoVersion =
        this.innerFrameCfg_[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION];
    this.innerFrameResponseReceived_.promise.then(
        this.checkChannelNames_, null, this);
    this.innerFrameCfg_[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] =
        innerFrameProtoVersion == 1 ? 2 : 1;
    this.performInnerFrameReconnect_();
  }

  /**
   * Determines if the transport type for the channel is testable.
   * Some transports are misusing global state or making other
   * assumptions that cause connections to fail.
   * @return {boolean} Whether the transport is testable.
   * @private
   */
  isTransportTestable_() {
    let testable = false;

    /** @suppress {visibility} suppression added to enable type checking */
    const transportType = this.channel_.determineTransportType_();
    switch (transportType) {
      case TransportTypes.NATIVE_MESSAGING:
        testable = true;
        break;
    }

    return testable;
  }

  /** @return {?CrossPageChannel} */
  getChannel() {
    return this.channel_;
  }

  /**
   * Begin, but don't finish, connection to a peer.
   *
   * @return {!Promise<undefined>} A timing hook for the unstable period between
   *     the creation of the peer and the connection notification from that
   * peer.
   */
  connectAndWaitForPeer() {
    this.channel_.connect();
    // Set a listener for when the peer exists.
    return new Promise(
        /** @suppress {visibility} suppression added to enable type checking */
        (res) => this.channel_.peerWindowDeferred_.addCallback(res));
  }
};
