25

I have a medium sized Angular application and for some reasons some of my protractor tests are timing out when run against my live production environment.

I am fairly sure the timeouts happen because of protractor waiting on some asynchronous task. I know about zones and I tried to keep all long running async tasks out of the ngZone (as per the FAQ), but for some reason protractor is still timing out.

It's possible I missed something but I don't know how to debug the problem. Is there any way to find out which part of the code protractor is waiting on?

The NgZone only exposes functions to determine if there are microtasks or macrotasks running, but doesn't tell me which one.

EDIT: A typical example of such a timeout error as shown by protractor:

Failed: Timed out waiting for asynchronous Angular tasks to finish after 11 seconds. This may be because the current page is not an Angular application. Please see the FAQ for more details: https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular

While waiting for element with locator - Locator: By(css selector, [data-e2e='scroll-to-offer'])

The element exists on the page (I have verified this manually), but protractor still times out.

Community
  • 1
  • 1
magnattic
  • 12,638
  • 13
  • 62
  • 115

5 Answers5

31

I had a similar problem, the Testability was unstable and all I knew is that I have some pendingMacroTasks. It's indeed a bit tricky to localize these tasks (you won't go through the whole code base and look for setTimeout).
However I managed to do this eventually.

Relatively simple solution

First thing you can do is open your Dev Tools in Chrome, press Ctrl+O, type zone.js and press Enter.
This will open zone.js source file (not Typescript, yet something).
Inside zone.js look for Zone.prototype.runTask and put a breakpoint inside this method.
Enable the breakpoint after your application is loaded, otherwise it will get hit too many times.

If your Testability is not stabilizing, it probably means that you have some repetitive macro task that reschedules itself. At least that was my case.
If this is the case, the breakpoint will get hit every X time after the application is loaded.

Once the breakpoint is hit, go to task.callback.[[FunctionLocation]] this will take you to (most probably) some setTimeout or setInterval.
To fix it run it outside of Angular zone.

A bit trickier solution

This involves tweaking the Zone.js code, but gives you a more persistent debug information.

  1. Go to node_modules/zone.js/dist/zone.js.
  2. Look for Zone's constructor function: function Zone(parent, zoneSpec)
  3. Add the following inside: this._tasks = {}
  4. Add counter variable in global scope: let counter = 0
  5. Look for Zone.prototype.scheduleTask and add the following right before the call to this._updateTaskCount(task,1):

    task.id = counter++;
    this._tasks[task.id] = task;
    
  6. Look for Zone.prototype.scheduleTask and add the following right before the call to this._updateTaskCount(task,-1):

    delete this._tasks[task.id]
    
  7. Recompile the application and run it

Assuming you did the above tweaks you can always access the pending tasks like this:

tasks = Object.values(window.getAllAngularTestabilities()[0]._ngZone._inner._tasks)

To get all the pending macro tasks that affect the testability state:

tasks.filter(t => t.type === 'macroTask' && t._state === 'scheduled')

After that similarly to the previous solution check out the .callback.[[FunctionLocation]] and fix by running outside of Angular zone.

JeB
  • 11,653
  • 10
  • 58
  • 87
  • What if i'm having trouble recognizing the callback FunctionLoaction. It is from the zone.js file itself. – Eli Sep 09 '19 at 12:06
16

JeB did a great job suggesting his way (some solution is better than nothing). In fact, there is way to get list of pending tasks without patching zone.js file. I get this idea while looking at angular sources: https://github.com/angular/angular/blob/master/packages/core/src/zone/ng_zone.ts#L134

Here is how to:

  1. Import node_modules/zone.js/dist/task-tracking.js file after zone.js
  2. Then in your code get NgZone instance and use it like
import * as _ from "lodash";
...
const ngZone = moduleInstance.injector.get(NgZone);
setInterval(() => {
    var taskTrackingZone = (<any>ngZone)._inner.getZoneWith("TaskTrackingZone");
    if (!taskTrackingZone) {
        throw new Error("'TaskTrackingZone' zone not found! Have you loaded 'node_modules/zone.js/dist/task-tracking.js'?");
    }
    var tasks: any[] = taskTrackingZone._properties.TaskTrackingZone.getTasksFor("macroTask");
    tasks = _.clone(tasks);
    if (_.size(tasks) > 0) {
        console.log("ZONE pending tasks=", tasks);
    }
}, 1000);

Example is placed here: https://stackblitz.com/edit/zonejs-pending-tasks.

How it works:

Stanislav Berkov
  • 5,929
  • 2
  • 30
  • 36
  • I added `import 'zone.js/dist/task-tracking';` to `polyfills.ts` and the above code to `AppModule` constructor. Do not forget that the above `setInterval` itself causes protractor timeouts. – Hodossy Szabolcs Nov 22 '19 at 08:53
  • I add this code into `platformBrowserDynamic().bootstrapModule(AppModule).then( (moduleInstance) => {...});` I.e. into main.ts, not AppModule. Looks like when using in here it does not make protractor waiting. – Stanislav Berkov Nov 22 '19 at 08:57
  • @HodossySzabolcs You're right, this script itself causes protractor timeouts. How you can overcome this is to run that snipped out of Angular ngZone with `ngZone.runOutsideAngular( () => { // snippet here })` – dave0688 Feb 19 '20 at 10:15
1

There is no particular way to track protractor's async tasks. What you might do is investigate which async tasks might be blocking your flow. If you're testing there are 2 main cues for debugging : async tasks in your app code and async tasks in your tests.

In your tests, if you're using something like Jasmine, have you tried something like changing jasmine.DEFAULT_TIMEOUT_INTERVAL;?

Anyway, I don't see a better option to debug this other than start logging all your async tasks on start and on callback.

Francisco
  • 2,018
  • 2
  • 16
  • 16
  • Yes, I want to track async tasks in my app code. The question is how to do that in a way so that I can access it from my protractor test (to help with debugging the timeout problem). Obviously the NgZone already tracks the tasks, because I can [query if there are any pending](https://angular.io/api/core/NgZone#hasPendingMicrotasks), but not which ones. – magnattic Feb 13 '18 at 23:51
  • 1
    @magnattic The list of tasks isn't available through public API, and it's unlikely that such list would make sense for a human, because zone output is barely readable in console. Could possibly be achieved by patching NgZone or Zone. – Estus Flask Feb 14 '18 at 06:34
  • 1
    @estus Thanks taking the time to answer. I know already that it's not in the API, hence the question how to track it or how else to debug the problem. I realize the microtasks are probably not registered in a way that is human-readable, but maybe there is some way to find out which part of the code they were fired from (some sort of stacktrace?). It's just frustrating that this quite common problem with protractor seems impossible to debug unless you know already which part of your code is the problem. Patching the zone seems like a good idea, but I couldn't find any documentation how to do it – magnattic Feb 14 '18 at 17:56
  • @magnattic I'm unable to provide full answer, but I'd suggest to start patching from `Zone.prototype._updateTaskCount`, so you could not just count tasks but keep a `Map` of them. – Estus Flask Feb 14 '18 at 18:28
0

Old post, but the issue is still very much alive. In the meantime, there's a pretty neat solution:

  1. Include "zone.js/plugins/task-tracking"

Then run

private String getTestabilityState() {
    return (String) ((JavascriptExecutor) driver).executeAsyncScript(
        "var seleniumCallback = arguments[0]; " +
        "var result = ''; " +
        "var testabilities = window.getAllAngularTestabilities();" +
        "var expectedAngularCallbacks = testabilities.length;" +
        "var ngCallbackCount = 0; " +
        "for (var i = 0; i < testabilities.length; i++) { " +
        "  var testability = testabilities[i]; " +
        "  testability.whenStable((pendingTasks, tasks) => {" +
        "    if (pendingTasks) { result += JSON.stringify(tasks); }" +
        "    ngCallbackCount++;" +
        "    if (ngCallbackCount >= expectedAngularCallbacks) { seleniumCallback(result); }" +
        "  }, 1);" +
        "}");
}
  • seleniumCallback is the callback function provide by executeAsyncScript
  • For each Testability, call whenStable()
  • The doneCallback signature is

export type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void;

  • didWork is weirdly named. It's true if there are pending tasks, and false is there are non (= ng is stable)
  • The code then increments a ngCallbackCount to determine when to call the seleniumCallback
  • The timeout of 1 ms is needed as whenStable() defaults to waiting until stable if no timeout is provided.

The return value is a length string, typically of a XMLHttpRequest.send.

Simon
  • 2,994
  • 3
  • 28
  • 37
-1

berkov,

I really need your help to get task list using the functionality provided by you.

I added import 'zone.js/dist/task-tracking'; to polyfills.ts and below code to constructor method of AppModule.ts. but it is throwing error Cannot find name 'moduleInstance'.

// app.module.ts

const ngZone = moduleInstance.injector.get(NgZone);
setInterval(() => {
    var taskTrackingZone = (<any>ngZone)._inner.getZoneWith("TaskTrackingZone");
    if (!taskTrackingZone) {
        throw new Error("'TaskTrackingZone' zone not found! Have you loaded 'node_modules/zone.js/dist/task-tracking.js'?");
    }
    var tasks: any[] = taskTrackingZone._properties.TaskTrackingZone.getTasksFor("macroTask");
    tasks = _.clone(tasks);
    if (_.size(tasks) > 0) {
        console.log("ZONE pending tasks=", tasks);
    }
}, 1000);

If it is possible, can you please provide any working example. That will be your great help.

Thank you,

Jignesh Raval

CC: @hodossy-szabolcs

Jignesh Raval
  • 587
  • 7
  • 15