Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Sockets #4300

Open
andreasonny83 opened this issue May 24, 2017 · 15 comments
Open

Web Sockets #4300

andreasonny83 opened this issue May 24, 2017 · 15 comments

Comments

@andreasonny83
Copy link

andreasonny83 commented May 24, 2017

Can't run Protractor test on Angular2+ apps using Firrebase.
Following up from angular/angularfire#779

Bug report

  • Node Version: v7.3.0
  • Protractor Version: 5.1.2
  • Angular Version: 4.1.2
  • Browser(s): chrome
  • Operating System and Version MacOS Sierra 10.12.4
  • Your protractor configuration file
const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    print: function() {}
  },
  beforeLaunch: function() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
  },
  onPrepare() {
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};
  • A relevant example test
import { browser, element, by } from 'protractor';

describe('test App', () => {
  beforeEach(() => {
    browser.get('/');
  });

  it('should display the home page', () => {
    expect<any>(element(by.css('app-root h1')).getText()).toBe('Test App');
  });
});
  • Output from running the test
- 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, app-root h1)
  • Steps to reproduce the bug
    Just including either AngularFireAuth or AngularFireDatabase in an Angular4 AppTestComponent triggers the error.
import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { AngularFireAuth } from 'angularfire2/auth';
import { Observable } from 'rxjs/Observable';
import * as firebase from 'firebase/app';

@Injectable()
export class FirebaseService {
  constructor(
    private afAuth: AngularFireAuth,
    private db: AngularFireDatabase,
  ) { }
@marcincharezinski
Copy link

marcincharezinski commented May 27, 2017

Yes indeed web sockets are the issue. I had to abandon protractor in my testing as a main framework because of ws protocol.
+1

@NickTomlin
Copy link
Contributor

Very interesting; could one of you produce a small test repo that reproduces this issue? We can then use that to explore some solutions here. Thanks!

@andreasonny83
Copy link
Author

@NickTomlin , I managed to reproduce the bug in one of my current projects hosted on github. I just had to create a separate branch called feature/protractor-bug you can find here: (https://github.com/andreasonny83/online-board/tree/feature/protractor-bug).
There is a commented line you can see here (https://github.com/andreasonny83/online-board/blob/feature/protractor-bug/e2e/app.utils.ts#L7). Switching thebrowser.ignoreSynchronization to true, let me run the Protractor tests. The tests are working if I completely remove Firebase from my project.
Let me know if you find a solution to this.
Thank you.

@jdrorrer
Copy link

jdrorrer commented Jun 2, 2017

I had to call browser.waitForAngularEnabled(false); as well in order for protractor tests to run without getting the same error: Failed: Timed out waiting for asynchronous Angular tasks to finish after 11 seconds...

@fortunella
Copy link

I have a very similar problem. My tests are not failing but after each page load the test waits for about 30 seconds before it proceeds (seems to be a timeout). See my issue #4316

When I remove Firebase everything is fine.

@ZuSe
Copy link

ZuSe commented Jul 4, 2017

The question is how to solve this if you can not remove firebase from the project.
Is there a way to tell protractor to ignore HTTP with status code 101?

@fortunella
Copy link

In my project I solved it by disabling the synchronization. But this has a drawback: I had to insert some sleeps to wait "manually" for the page to get ready. Especially with Angular Material which I use in my project as it uses lots of animations. And there I often have to wait until the animation has finished. But for me this is not a big problem.

@richarddavenport
Copy link

Curious if anyone has had any luck with this? I like @ZuSe's suggestion though.

@crinelap
Copy link

crinelap commented Aug 10, 2017

For me worked this workaround:

    it('test ', function(){
        page.navigateTo();
        browser.sleep(3000);
        browser.waitForAngularEnabled(false);
       
        page.getProjectTitle().then(function(str){expect(str).toBe("Title")});
     
    });

@glebihan
Copy link

I've never used Firebase, so I'm not sure how this could be implemented in this particular case, but I thought I'd post my findings anyway as it might give some pointers as to a possible solution.

I've had similar issues with other web sockets based services and the way I got around it was by using NgZone to initialize the web socket outside of Angular.

Basically, the idea is as follows :

@Injectable()
export class SomeService {

  constructor(
    private ngZone: NgZone
  ) {
    this.ngZone.runOutsideAngular(() => {
      let foo = initWebSocket(...); // Whatever call may set up the web sockets based service, run it outside Angular
      foo.listen('event', e => {
        this.ngZone.run(() => {
          handleEvent(e); // Run callbacks from the web sockets service inside Angular
        });
      });
    });
  }

}

Using this solution has allowed me to properly run protractor tests with web sockets related to Pusher.

Hope it can help.

PhilippeMorier added a commit to PhilippeMorier/spiti-ui that referenced this issue Jan 20, 2018
…vables

- protractor seems to have problems with WebSockets. Therefore, we
  inject `AngularFireAuth` outside `Angular` but reattach needed
  observables ( back into a `NgZone`
- Sources:
  - angular/protractor#4300 (comment)
  - https://stackoverflow.com/questions/42461852/angular-2-inject-service-manually?rq=1
  - kamilkisiela/apollo-angular#320 (comment)

- currently sign in user can be `undefined` at sign-out
- import `AngularFireAuthModule` & `AngularFireDatabaseModule`
- set `allScriptsTimeout` back to its default value of `11000`
@PhilippeMorier
Copy link

I've tried @glebihan's approach by using Injector (it feels ugly 😏). The E2E tests for sign-in/out are working. Unfortunately, the tests where I list my items fail with the famous exception bellow even though the items are loaded and displayed correctly.
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, li)

I tried to explicit 'reattach' the subscription to Angular's change detection with the approach from kamilkisiela/apollo-angular#320 (comment) without any success.

For AngularFireAuth:

public constructor(
  injector: Injector,
  zone: NgZone,
) {
  zone.runOutsideAngular(() => {
    this.afAuth = injector.get(AngularFireAuth);
  });
}

For AngularFireDatabase:

public constructor(
  injector: Injector,
  zone: NgZone,
) {
  zone.runOutsideAngular(() => {
    this.db = injector.get(AngularFireDatabase);
  });

  this.items = this.db
      .list<Item>('/items')
      .valueChanges();
}

@glebihan
Copy link

@PhilippeMorier What I would try for your AngularFireDatabase example is the following.

First declare this.items as an Subject or BehaviorSubject then feed it from a subscription that calls ngZone.run :

public constructor(
  injector: Injector,
  zone: NgZone,
) {
  zone.runOutsideAngular(() => {
    this.db = injector.get(AngularFireDatabase);
    this.db.list<Item>('/items').valueChanges().subscribe(i => {
      this.ngZone.run(() => {
        this.items.next(i);
      });
    });
  });
}

@pavelpykhtin
Copy link

pavelpykhtin commented Apr 25, 2019

Any updates on this issue?
I'm experiencing problem with aspnet/signalr library. Though I've wrapped code that opens a connection into zone.runOutsideAngular protractor is still stucks wating until websoket connection closes.

this.zone.runOutsideAngular(() => {
   this.started = this.connection
        .start()
        .then(_ => {
            console.log('[signaler] started');
            this.isStarted = true;
            this.connection.on(
                'notify',
                (messageType: string, signal: Signal) => {
                    signal.messageType = messageType;
                    this.zone.runGuarded(() => this.signalsSubject$.next(signal));
                }
            );
        })
        .catch();
});

@flensrocker
Copy link

@pavelpykhtin this is my solution (with help from StackOverflow, of course), to have the app and the tests work like expected. The service-consumer don't have to handle with zones, it's all done inside the service.

import { Injectable, NgZone } from "@angular/core";
import { Observable, Observer, TeardownLogic, SchedulerLike, Subscription, asapScheduler, from, of as observableOf } from "rxjs";
import { switchMap, observeOn } from "rxjs/operators";
import * as signalR from "@aspnet/signalr";

export class TheResponse {
}

class LeaveZoneScheduler implements SchedulerLike {
    constructor(
        private zone: NgZone,
        private scheduler: SchedulerLike,
    ) {
    }

    schedule(...args: any[]): Subscription {
        return this.zone.runOutsideAngular(() =>
            this.scheduler.schedule.apply(this.scheduler, args)
        );
    }

    now(): number {
        return this.scheduler.now();
    }
}

export function leaveZone(zone: NgZone, scheduler: SchedulerLike): SchedulerLike {
    return new LeaveZoneScheduler(zone, scheduler);
}

class EnterZoneScheduler implements SchedulerLike {
    constructor(
        private zone: NgZone,
        private scheduler: SchedulerLike,
    ) {
    }

    schedule(...args: any[]): Subscription {
        return this.zone.run(() =>
            this.scheduler.schedule.apply(this.scheduler, args)
        );
    }

    now(): number {
        return this.scheduler.now();
    }
}

export function enterZone(zone: NgZone, scheduler: SchedulerLike): SchedulerLike {
    return new EnterZoneScheduler(zone, scheduler);
}


@Injectable()
export class TheHubService {
    private static _hubConnection: signalR.HubConnection = null;
    private static _huburl: string = "/api/The/Hub";

    constructor(private _zone: NgZone) {
    }

    private static async waitForHubConnectionAsync(): Promise<signalR.HubConnection> {
        if (TheHubService._hubConnection === null) {
            TheHubService._hubConnection = new signalR.HubConnectionBuilder()
                .withUrl(TheHubService._huburl)
                .build();
            await TheHubService._hubConnection
                .start()
                .catch((err: any) => {
                    console.error("error:", err);
                });
        }

        let retry: number = 100; // 100 * 100ms = 10s
        while ((retry > 0) && (TheHubService._hubConnection.state !== signalR.HubConnectionState.Connected)) {
            await new Promise<void>(resolve => setTimeout(resolve, 100));
            retry--;
        }

        if (TheHubService._hubConnection.state !== signalR.HubConnectionState.Connected) {
            throw new Error("can't connect to the hub.");
        }

        return TheHubService._hubConnection;
    }

    getTheResponse(): Observable<TheResponse> {
        return observableOf(true, leaveZone(this._zone, asapScheduler))
            .pipe(
                switchMap((_) => from(TheHubService.waitForHubConnectionAsync())),
                switchMap((connection: signalR.HubConnection) => Observable.create((observer: Observer<TheResponse>): TeardownLogic => {
                    const streamResult: signalR.IStreamResult<TheResponse> = connection.stream("GetTheResponse");
                    const subscription = streamResult.subscribe(observer);
                    return () => subscription.dispose();
                }) as Observable<TheResponse>),
                observeOn(enterZone(this._zone, asapScheduler)),
            );
    }
}

@Malbeth81
Copy link

Malbeth81 commented Feb 7, 2020

I've been investigating a similar issue with our Protractor tests and setting up our websocket outside of the Angular zone didn't change anything, so after following the steps in https://stackoverflow.com/questions/48627074/how-to-track-which-async-tasks-protractor-is-waiting-on to add task tracking to zone-evergreen.js I found that the culprit was Pace calling setTimeout in a loop.

While running the test in non headless mode, entering Pace.stop() in the browser's console (while the test was hanging, having set an longer timeout before hand) would always make the test complete immediately in a success.

https://github.com/HubSpot/pace/blob/master/pace.coffee shows that the default value for trackWebSockets is true. Changing that config fixed the issue and all of our tests are now completing successfully!

Here is the patch that you can apply by executing patch -d ./node_modules/zone.js/dist/ -i ../../../zone-evergreen_debug.patch inside your project's root folder:

--- zone-evergreen.js	2020-02-07 14:46:11.000000000 -0500
+++ zone-evergreen.js	2020-02-07 14:47:34.000000000 -0500
@@ -46,6 +46,8 @@
             this._properties = zoneSpec && zoneSpec.properties || {};
             this._zoneDelegate =
                 new ZoneDelegate(this, this._parent && this._parent._zoneDelegate, zoneSpec);
+            this._taskCount = 0
+            this._tasks = {}
         }
         static assertZonePatched() {
             if (global['Promise'] !== patches['ZoneAwarePromise']) {
@@ -258,7 +260,11 @@
         _updateTaskCount(task, count) {
             const zoneDelegates = task._zoneDelegates;
             if (count == -1) {
+                delete this._tasks[task.id]
                 task._zoneDelegates = null;
+            } else {
+                task.id = this._taskCount++;
+                this._tasks[task.id] = task;
             }
             for (let i = 0; i < zoneDelegates.length; i++) {
                 zoneDelegates[i]._updateTaskCount(task.type, count);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests