Skip to content
This repository was archived by the owner on Jul 29, 2024. It is now read-only.
This repository was archived by the owner on Jul 29, 2024. It is now read-only.

Bootstrapping should (mostly) work with Browser-initiated navigation (e.g. clicking on links) #3857

Open
@sjelin

Description

@sjelin

Preface

Right now, if anyone clicks a link (or in some other way navigates without using browser.get), we have three major problems:

  1. None of our bootstrapping happens
  2. Users might not be able to update browser.rootEl, browser.ignoreSynchronization, or browser.ng12Hybrid at the correct time.
  3. Selenium doesn't know there's a page load happening, and so sometimes lets commands run against a partially loaded page, or a page might unload while a selenium command is being executed (example).

This is an attempt to deal with problem (1).

Problem (2) is being mitigated by #3847, and possibly either #3858 or #3859.

(3) will be mitigated by #4053

Solution

A browser.get call consists of three phases:

The idea for handling browser-initiated navigation is:

  1. The crucial part of setup can be done in a unload event listener.
  2. Bootstrapping can be be slightly delayed, as long as it happens before any actual commands are issued.
  3. Use redundancy to cover edge cases.

Setup from an unload listener

Setup currently does 3 things:

  1. Disables blocking proxy waiting so that we can interact with the browser more directly
  2. Navigate to a dummy URL so we can tell once navigation has finished
  3. Sets the "NG_DEFER_BOOTSTRAP!" label so that we can load mock modules

Of these steps, only the third is necessary for browser-initiated navigation. To do this, we just need to install an event listener for unload that adds the defer label to window.name:

window.addEventListener('unload', function() {
  var deferLabel = "NG_DEFER_BOOTSTRAP!";
  if (window.name.indexOf(deferLabel) == -1) {
    window.name = deferLabel + window.name;
  }
});

We always install this listener, even if synchronization is disabled. This is a change from the previous behavior, where bootstrap would only get deferred if synchronization was enabled.

It will still be necessary to set the NG_DEFER_BOOTSTRAP!" label in browser.get, since the first page load won’t be preceded by an unload. Additionally, since window.name and unload events are both slightly unreliable, the redundancy is a good thing.

Bootstrapping

The bootstrapping process will be split out into a doBootstrap helper function. It will also change in the following ways:

  • It will manage a variable on the window object to ensure that the same page isn’t bootstrapped multiple times
  • It will add the unload listener described above
  • It will be invoked in several places, including periodically every 100ms or so (details later on).
  • It will be interruptible. See Refactor bootstrapping process and browser.waitForAngular to make them interruptible #4052 for details on what that means. When interrupted, the restart process will be retried from the beginning.
    • Interruptibility will be especially important so that the periodic calls don't mess with anything.
  • It will always resume bootstrap on Angular pages
    • Only AngularJS supports deferring bootstrap at the moment, so really it will only always resume bootstrap for AngularJS apps.
    • Any extraneous NG_DEFER_BOOTSTRAP! labels will be removed.

Relative to the implementation in Protractor 5.1, the new bootstrapping process will be something like:

doBootstrap(timeout = this.getPageTimeout, fromGetFn = false): wdpromise.Promise<void> {
  // If window.__PROTRACTOR_BOOTSTRAP_STATUS_ is undefined, we bootstrap in this call
  // If window.__PROTRACTOR_BOOTSTRAP_STATUS_ is 'started', another call is bootstrapping
  // already and we should wait for it to finish
  // If window.__PROTRACTOR_BOOTSTRAP_STATUS_ is 'complete', bootstrapping has been
  // completed by another call
  return this.executeScriptWithDescription(function() {
    if (window.__PROTRACTOR_BOOTSTRAP_STATUS_ === undefined) {
      window.__PROTRACTOR_BOOTSTRAP_STATUS_ = 'started';
      window.addEventListener('unload', function() {
        if (window.name.indexOf("NG_DEFER_BOOTSTRAP!") == -1) {
          window.name = "NG_DEFER_BOOTSTRAP!" + window.name;
        }
      });
      return 'not started';
    }
    return window.__PROTRACTOR_BOOTSTRAP_STATUS_;
  }, 'Protractor Bootstrap - get/set bootstrap status + add unload listener' )
  .then((bootstrapStatus: string) => {
    if (bootstrapStatus != 'not started') {
      // Bootstrapping is either already finished on this page or is currently being
      // managed by a previous call to doBootstrap.  Either way, we should wait until
      // we're sure bootstrap is complete
      return this.driver.wait(() => {
        return this.executeScriptWithDescription(function() {
          return window.__PROTRACTOR_BOOTSTRAP_STATUS_;,
        }, 'Protractor Bootstrap - get status'))
            .then(
                (status: any) => {
                  return status == 'complete';
                },
                (err: IError) => {
                  if (err.code == 13) {
                    // IE bug - ignore and try again
                    return false;
                  } else {
                    throw err;
                  }
                });
      }, timeout, 'Wait for bootstrap for ' + timeout + 'ms');
    } else {
      // It's our job to bootstrap 

      return this.waitForAngularEnabled().then((waitEnabled) => {
        if (waitEnabled) {
          ... // Plugins.onPageLoad
          ... // testForAngular
          .then((angularVersion: number) => {
            // Load mock modules, but do not resume bootstrap yet.
            // Return a promise for the module names.
          });
         }
      })
      .then((moduleNames?: string[]) => {
        // Try to resume bootstrap
        return this.executeScriptWithDescription(function(moduleNames) {
          if (angular.resumeBootstrap) {
      	    window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__ =
                angular.resumeBootstrap(arguments[0]);
      	  } else {
            window.name = window.name.replace(/^NG_DEFER_BOOTSTRAP!/, '');
          }
        }, 'Protractor Bootstrap - resume bootstrap', moduleNames || []));
      })
      .then(() => {
        if (fromGetFn) {
          ... // Reset bpClient sync
        }
      })
      .then(() => {
        return this.waitForAngularEnabled().then((waitEnabled) => {
          if (waitEnabled) {
            ... // Run plugins.onPageStable
          }
        });
      })
      .then(() => {
        return this.executeScriptWithDescription(function() {
          window.__PROTRACTOR_BOOTSTRAP_STATUS_ = 'complete';
        }, 'Protractor Bootstrap - set status as "complete"')
      });
    }
  });

We then invoke doBootstrap in three places:

  1. At the start of browser.waitForAngular
    • This on its own ensured that the app is bootstrapped before any commands which require synchronization are run.
  2. Periodically, every 100ms or so.
    • Because we are deferring bootstrap, we never want to get into a situation where the app spends a long time waiting for angular.resumeBootstrap, which could happen if a user is doing work which doesn't require browser.waitForAngular.
  3. At the end of browser.get, as we currently do.
    • This is no longer necessary, and indeed won't help at all for browser-initiated navigation, but it doesn't hurt anything either, and reduces reliance on the periodic check.

Edge Case: Interrupted by navigation

As per #4052, we need to refactor bootstrapping to make it more tolerant of interruption (i.e. navigation occurring part way through bootstrapping).

Initially, if we get interrupted, we'll simply give up on the remaining bootstrap work. This is to avoid accidentally running bootstrap code extraneously. As mentioned in #4052 (comment), when we make the changes described in this issue, we will want to change that behavior so that:

  • If bootstrapping is interrupted, we will retrying bootstrapping from the beginning on the new page.
  • If waitForAngular is interrupted, we will restart from the beginning of waitForAngular, including retrying bootstrap.

Edge Case: Blocking Proxy

Pending the Blocking Proxy change described in #4052, this modified bootstrapping process should be compatible with Blocking Proxy.

However, it does go against the spirit of Blocking Proxy to add all this new work to waitForAngular. The point of Blocking Proxy is that this kind of work should be done automatically for you. Pending #4064 and angular/blocking-proxy#16, we should be able to add a angular_bootstrap_barrier.ts file to do bootstrapping before each command. We can execute this intentionally by doing a blank executeScript at the end of browser.get/periodically.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions