text: /**
   * @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 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.
  var CLEAN_UP_IFRAMES = true;
  
  var IFRAME_LOAD_WAIT_MS = 1000;
  var stubs = new PropertyReplacer();
  var uniqueId = 0;
  var driver;
  var 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');
      logger.setLevel(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.
      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.isVersionOrHigher(10)) ||
          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 */);
    },
  
    testEscapeServiceName() {
      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'));
    },
  
  
    testUnescapeServiceName() {
      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
       */
      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_;
      }
      goog.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
     */
    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
     */
    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
     */
    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
     */
    checkChannelNames_() {
      const outerName = this.channel_.name;
      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.
     */
    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>}
     */
    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
     */
    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_;
      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.
     */
    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
     */
    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
     */
    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.
     */
    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
     */
    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;
  
      const transportType = this.channel_.determineTransportType_();
      switch (transportType) {
        case TransportTypes.NATIVE_MESSAGING:
        case TransportTypes.DIRECT:
          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(
          (res) => this.channel_.peerWindowDeferred_.addCallback(res));
    }
  };
  