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

listeners property in DebugElement instance does not represent actual event listeners of the element #22148

Closed
sharikovvladislav opened this issue Feb 10, 2018 · 9 comments
Labels
area: testing Issues related to Angular testing features, such as TestBed freq3: high type: bug/fix
Milestone

Comments

@sharikovvladislav
Copy link

sharikovvladislav commented Feb 10, 2018

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ X ] Bug report  
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

I have a component. There is some HTMLElement in it. At some moment I add some event listener to this element (addEventListener, RxJS subscription of Observable.fromEvent or whatever). Everything works perfectly in production. The problem is in unit tests. Specifically in DebugElement (I suppose). The problem is that listeners array do not represent actual state of event listeners on the element.
I find html element by de.query(By.css('.some-class')) at some moment in unit test. After that something happens, that element gets new event listener (rxjs subscription for instance). After that I want to emulate some event by triggerEventHandler and I want to check, that component reacted correctly. Example:

  it('should work with rxjs fromEvent subscription', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const de = fixture.debugElement;
    const component = fixture.componentInstance;

    expect(component).toBeTruthy();

    component.createKeydownSubscription();

    spyOn(component, 'keydownHandler').and.callThrough();

    de.query(By.css('input')).triggerEventHandler('keydown', {
      keyCode: 13
    });

    expect(component.keydownHandler).toHaveBeenCalled();
  }));

And the createKeydownSubscription code is:

  createKeydownSubscription() {
    this.keydown$ = Observable.fromEvent(this.input.nativeElement, 'keydown');

    this.keydown$.subscribe(event => this.keydownHandler(event));
  }

This unit test does not work.

Also, if you compare eventListeners() array of the element and listeners property of DebugElement of that element, they will be different.

Expected behavior

I expect that listeners array in DebugElement instance represents actual listeners of the element.

Minimal reproduction of the problem with instructions

I created repro as github repo. So the repro is:

  1. git clone git@github.com:sharikovvladislav/ng-rxjs-from-event-test.git
  2. cd ng-rxjs-from-event-test.git
  3. yarn
  4. yarn test

You will get: Executed 2 of 2 (2 FAILED) ERROR (a bit more verbose).

All code is provided in the repro: component and spec

What is the motivation / use case for changing the behavior?

It is the bug. It has to be fixed. Until it is not fixed, it is not comfortable to unit test some cases.

Environment


Angular version: 4.4.6


Browser:
- [ X ] Chrome (desktop) version 63.0.3239 (Mac OS X 10.13.3)
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: XX   v8.9.4
- Platform:   Mac OS X 10.13.3

Others:

- 
@sharikovvladislav
Copy link
Author

sharikovvladislav commented Feb 10, 2018

Possibly it is a feature... I researched a lot and I couldn't find the solution how to correctly handle this. Please, provide some information if so.

Any workaround how to handle this? How to get correct handlers in listeners property of DebugElement?

@mhevery mhevery added the area: core Issues related to the framework runtime label Feb 13, 2018
@sharikovvladislav
Copy link
Author

sharikovvladislav commented Feb 16, 2018

@chuckjaz @mhevery any ideas how to handle this at the moment?

@ngbot ngbot bot modified the milestones: needsTriage, Backlog Feb 26, 2018
@Hojou
Copy link

Hojou commented Mar 28, 2018

I ran into the same problem. It seems .triggerEventHandler does not trigger a real event on the DOM element, but will fake a call to any HostListners.

I've found two workarounds:

  1. Do not use Observable.fromEvent - instead use the HostListener attribute on a method and either emit a message on a Subject that your Observable is based on:
@HostListener() keyDown() {
  this._keyDownSubject.emit({});
}

createKeydownSubscription() {
    this.keydown$ = this._keydownSubject.asObservable();

    this.keydown$.subscribe(event => this.keydownHandler(event));
  }
  1. Do not use .triggerEventHandler and use document.createEvent('CustomEvent') in your test code instead.
debugElement.nativeElement.dispatchEvent(newEvent('keydown'));

function newEvent(eventName: string) {
    const customEvent = document.createEvent('CustomEvent');  // MUST be 'CustomEvent'
    customEvent.initCustomEvent(eventName, false, false);
    return customEvent;
}

It sucks, i would really like .triggerEventHander to instead wrap the logic from my second workaround.

@sharikovvladislav
Copy link
Author

sharikovvladislav commented Apr 23, 2018

I run into the same problem again in absolutely another case.

I have some component with addEventListener (elementRef.nativeElement) and scroll event. When I trigger event with triggerEventHandler this.listeners in DE is empty.

@Hojou yep, your solution works.

@sharikovvladislav
Copy link
Author

sharikovvladislav commented Sep 24, 2018

Any ideas how this work at the moment? Maybe I can take & fix this issue.

I refactored my PR so I add listeners just in template. And this option on how to bind events is not influenced by this bug. Everything works correctly. But this is huge perf problem.

Using template bindings for events like scroll spams tick's rly often. It is problem for huge application. We can not (I don't know the way) run it outside angular using this way of binding.

@sharikovvladislav
Copy link
Author

sharikovvladislav commented Oct 4, 2018

I researched a bit how this works.

So:

  1. listeners are pushed only from listenToElementOutputs function from packages/core/src/view/element.ts file.
  2. The target of that function is to parse output property of node definition.
  3. Output property in node definition is set by angular compiler
  4. Output property contains only events which are defined in the HTML template of the component. So even if I put addEventListener in constructor (sync way) output will not contain this listener.

Am I right with my last statement?

So the option is to use native functions like click/blur/focus of HTMLElement. We can use them though debugElement.nativeElement.<fn>.

Is it correct way to trigger events for HTMLElements? Or triggerEventHandler should handle any types of event bindings (template, addEventListener and stuff).

Weeeell, we also can find this (not explicitly) in the docs: https://angular.io/guide/testing#click-helper.

@pkozlowski-opensource pkozlowski-opensource added the area: testing Issues related to Angular testing features, such as TestBed label Oct 4, 2018
@sharikovvladislav
Copy link
Author

So I finished with that code at the moment:

Unit:

de.query(By.css('.selector')).nativeElement.dispatchEvent(createKeyDownEvent(ESCAPE));

function createKeyDownEvent(keyCode: number) {
  const keydownEvent = new Event('keydown');
  (<any>keydownEvent).keyCode = keyCode;
  return keydownEvent;
}

And the code:


fromEvent(this.myElement.nativeElement, 'keydown')
  .subscribe((event: KeyboardEvent) => {
    this.onKeydown(event);
  })

And for blur.

Unit:

const dataAreaDe = de.query(By.css('.selector'));

dataAreaDe.nativeElement.focus();
dataAreaDe.nativeElement.blur();

Code:

fromEvent(this.myElement.nativeElement, 'blur')
  .subscribe((event: FocusEvent) => {

    this.onBlur(event);
  })

Is that correct way to unit test events which are listened with fromEvent?

@pkozlowski-opensource pkozlowski-opensource added area: testing Issues related to Angular testing features, such as TestBed and removed area: testing Issues related to Angular testing features, such as TestBed area: core Issues related to the framework runtime labels Mar 14, 2020
@ngbot ngbot bot modified the milestones: Backlog, needsTriage Oct 1, 2020
@atscott
Copy link
Contributor

atscott commented Oct 22, 2020

This is working as intended. The Angular DebugElement#triggerEventHandler specifically do not trigger events on listeners added outside of the Angular system. If you want to trigger events on all the listeners, you can do the following:

    const inputEl = de.query(By.css('input')).nativeElement;
    inputEl.eventListeners('keydown').forEach(f => {
      f.call(inputEl, {keyCode: 13});
    });

instead of

    de.query(By.css('input')).triggerEventHandler('keydown', {
      keyCode: 13
    });

@atscott atscott closed this as completed Oct 22, 2020
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: testing Issues related to Angular testing features, such as TestBed freq3: high type: bug/fix
Projects
None yet
Development

No branches or pull requests

7 participants