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

In unit test, view children are populated after afterNextRender callback is called rather than before #57313

Open
jnizet opened this issue Aug 9, 2024 · 2 comments
Assignees
Labels
area: testing Issues related to Angular testing features, such as TestBed area: zones P2 The issue is important to a large percentage of users, with a workaround
Milestone

Comments

@jnizet
Copy link
Contributor

jnizet commented Aug 9, 2024

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

core

Is this a regression?

No

Description

Given the following component:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [],
  template: `
    <button (click)="click()">Show inputs and focus the firt one</button>
    @if (inputsShown) {
      <input #foo />
    }
  `,
})
export class AppComponent {
  title = 'repro';

  inputs = viewChildren<ElementRef<HTMLInputElement>>('foo');

  inputsShown = false;
  injector = inject(Injector);

  click() {
    this.inputsShown = true;
    afterNextRender(
      () => {
        console.log('inputs().length = ' + this.inputs().length);
        console.log('input = ' + this.inputs()[0]);
        this.inputs()[0]?.nativeElement.focus();
      },
      {
        injector: this.injector,
      }
    );
  }
}

I expect that clicking the button will make the input appear and give it the focus. And it indeed does that, but not in a unit test.

When running the following unit test, all the expectations pass except the last one, and the console logs show that the length of the view children inside the afterNextRender callback is 0.

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
    }).compileComponents();
  });

  it('should give the focus to the input', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;

    fixture.detectChanges();

    const button = fixture.debugElement.query(By.css('button')).nativeElement;

    button.click();
    fixture.detectChanges();

    // check that the input has been displayed (passes)
    const input = fixture.debugElement.query(By.css('input')).nativeElement;
    expect(input).toBeTruthy();

    // check that the viewChildren is populated (passes)
    expect(app.inputs().length).toBe(1);
    expect(app.inputs()[0].nativeElement).toBe(input);

    // check that it has the focus (fails, and console shows the input from the viewChildren is undefined)
    expect(document.activeElement).toBe(input);
  });
});

The same problem occurs with @ViewChildrenor viewChild is being used.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-kkjfwf?file=repro%2Fsrc%2Fapp%2Fapp.component.spec.ts,repro%2Fsrc%2Fapp%2Fapp.component.ts

Please provide the exception or error you saw

LOG: 'inputs().length = 0'
LOG: 'input = undefined'
[...]
Expected <body>...</body> to be <input>

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

Angular CLI: 18.1.4
Node: 18.20.3
Package Manager: npm 10.2.3
OS: linux x64

Angular: 18.1.4
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1801.4
@angular-devkit/build-angular   18.1.4
@angular-devkit/core            18.1.4
@angular-devkit/schematics      18.1.4
@schematics/angular             18.1.4
rxjs                            7.8.1
typescript                      5.5.4
zone.js                         0.14.10

Anything else?

To run the repro, here are the commands to run in the terminal:

cd repro
npm install
ng serve # to test that everything works fine in real life)
ng test
@atscott
Copy link
Contributor

atscott commented Aug 12, 2024

afterNextRender runs after the application synchronization happens. With ZoneJS, this happens synchronously after the click because the handler is called inside the angular zone. There are two ways to resolve this for your test:

  • Wrap the two statements in an NgZone.run:
   TestBed.inject(NgZone).run(() => {
      button.click();
      fixture.detectChanges();
  });
  • Enable autoDetect for the fixture:
    const fixture = TestBed.createComponent(AppComponent);
    fixture.autoDetectChanges();

@atscott atscott added the area: testing Issues related to Angular testing features, such as TestBed label Aug 12, 2024
@ngbot ngbot bot added this to the needsTriage milestone Aug 12, 2024
@atscott atscott added area: zones P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent labels Aug 12, 2024
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Aug 13, 2024
@atscott atscott added P2 The issue is important to a large percentage of users, with a workaround and removed P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent labels Aug 13, 2024
@jnizet
Copy link
Contributor Author

jnizet commented Aug 13, 2024

Thank you @atscott.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: testing Issues related to Angular testing features, such as TestBed area: zones P2 The issue is important to a large percentage of users, with a workaround
Projects
None yet
Development

No branches or pull requests

3 participants