angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.24k stars 6.7k forks source link

bug(Dialog): MatTestDialogOpener (This constructor is not compatible with Angular Dependency Injection) #27703

Open bederuijter opened 11 months ago

bederuijter commented 11 months ago

Is this a regression?

The previous version in which this bug was not present was

No response

Description

The MatDialog API mentions a MatTestDialogOpener that can be used when writing tests that involve a mat-dialog.

Instead, when using the MatTestDialogOpener when writing my test it throws an error:

 NG0202: This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.
    This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

    Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

Reproduction

I don't know how to provide a StackBlitz reproduction that involve a test. Using the mat dialog opener test I was still able to reproduce the error (In the hopes of eliminating user error 🙂)

import {Component, Inject} from '@angular/core';
import {fakeAsync, TestBed, flush} from '@angular/core/testing';
import {MatTestDialogOpenerModule, MatTestDialogOpener} from '@angular/material/dialog/testing';
import {MAT_DIALOG_DATA, MatDialogRef, MatDialogState} from '@angular/material/dialog';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';

describe('MDC-based MatTestDialogOpener', () => {
  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [MatTestDialogOpenerModule, NoopAnimationsModule],
      declarations: [ExampleComponent],
    });

    TestBed.compileComponents();
  }));

  it('should open a dialog when created', fakeAsync(() => {
    const fixture = TestBed.createComponent(MatTestDialogOpener.withComponent(ExampleComponent));
    flush();
    expect(fixture.componentInstance.dialogRef.getState()).toBe(MatDialogState.OPEN);
    expect(document.querySelector('mat-dialog-container')).toBeTruthy();
  }));

  it('should throw an error if no dialog component is provided', () => {
    expect(() => TestBed.createComponent(MatTestDialogOpener)).toThrow(
      Error('MatTestDialogOpener does not have a component provided.'),
    );
  });

  it('should pass data to the component', fakeAsync(() => {
    const config = {data: 'test'};
    TestBed.createComponent(MatTestDialogOpener.withComponent(ExampleComponent, config));
    flush();
    const dialogContainer = document.querySelector('mat-dialog-container');
    expect(dialogContainer!.innerHTML).toContain('Data: test');
  }));

  it('should get closed result data', fakeAsync(() => {
    const config = {data: 'test'};
    const fixture = TestBed.createComponent(
      MatTestDialogOpener.withComponent<ExampleComponent, ExampleDialogResult>(
        ExampleComponent,
        config,
      ),
    );
    flush();
    const closeButton = document.querySelector('#close-btn') as HTMLElement;
    closeButton.click();
    flush();
    expect(fixture.componentInstance.closedResult).toEqual({reason: 'closed'});
  }));
});

interface ExampleDialogResult {
  reason: string;
}

/** Simple component for testing MatTestDialogOpener. */
@Component({
  template: `
    Data: {{data}}
    <button id="close-btn" (click)="close()">Close</button>
  `,
})
class ExampleComponent {
  constructor(
    public dialogRef: MatDialogRef<ExampleComponent, ExampleDialogResult>,
    @Inject(MAT_DIALOG_DATA) public data: any,
  ) {}

  close() {
    this.dialogRef.close({reason: 'closed'});
  }
}

Expected Behavior

To be able to write tests using the MatTestDialogOpener

Actual Behavior

MatTestDialogOpener throwing an error related to the Dependency injection

NG0202: This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.
    This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

    Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

      at ɵɵinvalidFactoryDep (../../../../node_modules/@angular/core/fesm2022/core.mjs:668:11)
      at NodeInjectorFactory.MatTestDialogOpener2_Factory [as factory] (../../../../ng:/MatTestDialogOpener2/ɵfac.js:5:48)
      at getNodeInjectable (../../../../node_modules/@angular/core/fesm2022/core.mjs:4669:44)
      at createRootComponent (../../../../node_modules/@angular/core/fesm2022/core.mjs:13264:35)
      at ComponentFactory.create (../../../../node_modules/@angular/core/fesm2022/core.mjs:13122:25)
      at initComponent (../../../../node_modules/@angular/core/fesm2022/testing.mjs:26421:51)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:421:30)
      at FakeAsyncTestZoneSpec.Object.<anonymous>.FakeAsyncTestZoneSpec.onInvoke (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:4779:37)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:3088:43)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:420:56)
      at Object.onInvoke (../../../../node_modules/@angular/core/fesm2022/core.mjs:26321:33)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:420:56)
      at Zone.Object.<anonymous>.Zone.run (../../../../node_modules/zone.js/bundles/zone-testing-bundle.umd.js:175:47)
      at NgZone.run (../../../../node_modules/@angular/core/fesm2022/core.mjs:26175:28)
      at _TestBedImpl.createComponent (../../../../node_modules/@angular/core/fesm2022/testing.mjs:26424:41)
      at Function.createComponent (../../../../node_modules/@angular/core/fesm2022/testing.mjs:26231:37)

Environment

Theodode commented 10 months ago

I have the same issue with version 15, i tried another way to use MatTestDialogOpener but i came to the same result.

**Error: NG0202:** This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.   
This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.   
Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

Here is my MatTestDialogOpener implementation into my before each.

I was wondering that MatTestDialogOpener can wrap my dialog component into a mocked component, so i can test my dialog outside of any parent component context. Thats why I start with calling MatTestDialogOpener.withComponent to create my wrapper component.

Is it the correct way to do this ?

beforeEach(async () => {
    DialogWrapperComponent = MatTestDialogOpener.withComponent(ResultsComponent, { data: questionary });

    await TestBed.configureTestingModule({
      declarations: [
        DialogWrapperComponent,
        ResultsComponent
      ],
      imports: [
        SharedModule //Contians MatDialogModule
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(DialogWrapperComponent);
    componentWrapper = fixture.componentInstance;
    component = componentWrapper.dialogRef.componentInstance;
    compiled = fixture.nativeElement;
    fixture.detectChanges();
});

Environment Angular: 15.0.4 CDK/Material: 15.0.3 Browser(s): Edge Operating System (e.g. Windows, macOS, Ubuntu): Windows 10

selangley-wa commented 4 months ago

Doing some Googling - and looking at the error message - looks like something that is expected is missing for dependency injection. Not sure where, yet.

Tangentially-related issues: https://github.com/thymikee/jest-preset-angular/issues/467 https://github.com/angular/angular/issues/45155

I also confirmed this behavior - the errors - is consistent in Angular+Material 14.x, 15.x, 16.x, and 17.x when added to an empty, new Angular application created via: ng new

I'm now discovered that if you copy the files: dialog-opener.spec.ts dialog-opener.ts

from the Angular Material directory: src/material/dialog/testing/

and place them in your Angular directory: src/app/

and then modify the imports in dialog-opener.spec.ts to import directly from your local version of dialog-opener.ts like so: import {MatTestDialogOpenerModule, MatTestDialogOpener} from './dialog-opener';

then the tests in dialog-opener.spec.ts will run successfully locally.

This probably explains why the Angular Material team had no problems running the tests in dialog-opener.spec.ts successfully when run in the Angular Material project.