Why is my reference to ViewChild undefined when mocking the ViewChild

107 Views Asked by At

I have an Angular standalone component that has a child standalone component. In my unit test I want to mock said child component. When I check if the mocked component is rendered via fixture.debugElement, I see that the mocked component is used. But when I try to reference the ViewChild it is always undefined.

Any thoughts?

Angular Version: 17.1.0

Here's the relevant code:

child.component.ts

@Component({
  selector: 'child',
  standalone: true,
  template: 'Child',
})
export class ChildComponent {}

app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent],
  template: '<child></child>'
})
export class AppComponent implements AfterViewInit {
 
  @ViewChild(ChildComponent) child!: ChildComponent;

  ngAfterViewInit(): void {
    // On serving the app this.child is defined
    // In the tests this.child is undefined
    console.log(this.child);
  }
}

app.component.spec.ts

Mocked Child Component

@Component({
  selector: 'child',
  template: `Mocked child`,
  standalone: true,
})
class MockChildComponent {}

Setup

  beforeEach(waitForAsync(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent]
    })
    .overrideComponent(AppComponent, {
      set: {
        imports: [MockChildComponent]
      }
    })
    .compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    appComponent = fixture.componentInstance;

    fixture.detectChanges();
  }));

When I remove overrideComponent I do get a defined reference to the ViewChild. But then it is not mocked of course.

Tests

  // OK
  it('should have the rendered mocked child element', () => {
    expect(
      fixture.debugElement.query(By.css('child')).nativeElement.innerHTML,
    ).toContain('Mocked child');
  });

  // Fail: Expected undefined to be truthy.
  it('should have the mocked child component', () => {
    expect(appComponent.child).toBeTruthy();
  });
1

There are 1 best solutions below

0
Amberjs On

Solved

Of course I fixed it just after posting this. I am my own rubber duck.. I'm posting the solution in case someone else runs into this issue.

To get a correct reference I changed the template in app.component.ts to <child #child></child>:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent],
  template: '<child #child></child>' // Was: <child></child>
})

And changed the ViewChild to reference the temple ate variable instead of the component.

@ViewChild('child') child!: ChildComponent;
// @ViewChild(ChildComponent) child!: ChildComponent;

Now it works as expected.

Edit: moved the solution to an answer post instead of editing it in the question.