Skip to content

eventCoalescing breaks things in a fakeAsync test #56767

@ersimont

Description

@ersimont

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When using eventCoalescing: true in combination with fakeAsync, any event triggered will then block NgZone.onMicrotaskEmpty and .onStable for the rest of the test. Coalescing schedules change detection to be run in a way that the fake async zone cannot see, I think because of the Zone.root.run() used here. This causes the NgZone to be marked as having a microtask pending here, which never gets cleared. So as long as the zone thinks a microtask is pending it will never emit onMicrotaskEmpty nor onStable (see the check here).

Users do not need to use onMicrotaskEmpty or onStable directly to be affected. I noticed this when using an afterRender hook, which is internally triggered by onMicrotaskEmpty. Here is a repro showing that situation:

it('runs coalesced change detection', fakeAsync(() => {
  let ranAfterRenderHook = false;

  @Component({ standalone: true, selector: 'app-inner' })
  class InnerComponent {
    constructor() {
      // this is an inner component so that we can register this hook _after_
      // dispatching an event
      afterRender(() => {
        ranAfterRenderHook = true;
      });
    }
  }

  @Component({
    standalone: true,
    imports: [InnerComponent],
    template: `
      <div class="clickable" (click)="noop()"></div>
      @if (showInner) {
        <app-inner />
      }
    `,
  })
  class OuterComponent {
    showInner = false;
    noop = () => {};
  }

  TestBed.configureTestingModule({
    // change this to `false` and the test passes
    providers: [provideZoneChangeDetection({ eventCoalescing: true })],
  });
  const fixture = TestBed.createComponent(OuterComponent);

  document.querySelector('.clickable')!.dispatchEvent(new MouseEvent('click'));
  tick(); // you'd think this would flush the pending CD task, but it doesn't

  fixture.componentInstance.showInner = true;
  fixture.detectChanges();
  tick(); // If afterRender is delayed, surely this should trigger it? Alas, no!

  expect(ranAfterRenderHook).toBe(true);
}));

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 18.0.5
Node: 20.9.0
Package Manager: npm 10.1.0
OS: win32 x64

Angular: 18.0.4
... animations, cdk, common, compiler, compiler-cli, core, forms
... material, platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1800.5
@angular-devkit/build-angular   18.0.5
@angular-devkit/core            18.0.5
@angular-devkit/schematics      18.0.5
@angular/cli                    18.0.5
@schematics/angular             18.0.5
ng-packagr                      18.0.0
rxjs                            7.8.1
typescript                      5.4.5
zone.js                         0.14.7

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: testingIssues related to Angular testing features, such as TestBed

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions