angular / components

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

Unit testing dialogs not intuitive #4259

Closed ghost closed 7 years ago

ghost commented 7 years ago

While writing unit tests for a function that handles a angular material 2 dialog using the example code from material 2 i run into problems. I'm a Jasmine newbie but I didn't had problems to write unit test before. I have to test the result of the afterClose function but i can't get the handle on dialogRef.

For sure there is a workaround but since this was the first function that was really hard to test and someone liked my post on stackoverflow about it maybe refactoring could make it easier ? http://stackoverflow.com/questions/42852816/unit-testing-angular-material-2-dialogs

            let dialogRef = this.dialog.open(ExtractPageDialog, {
                width: this.EXPORT_DIALOG_WIDTH,
                data: {
                    document: this.document
                }
            });
            dialogRef.afterClosed().subscribe((result: any) => {
                if (result) {
                    let fileId = this.document.fileId;
                    this.docProvider.extractPage(this.document.fileId, result.fromPage, result.toPage).subscribe(() => {
                       () => { //totest },
                       (error) => { //totest }
                    });
                } else {
                    //totest
                }
            });

DOCS: https://material.angular.io/components/component/dialog

Proposal, change the example to be easier to test:

dialogRef.afterClosed().subscribe(this.functionName);

functionName(result: any) {
                if (result) {
                    let fileId = this.document.fileId;
                    this.docProvider.extractPage(this.document.fileId, result.fromPage, result.toPage).subscribe(() => {
                       () => { //totest },
                       (error) => { //totest }
                    });
                } else {
                    //totest
                }
            }
}
jelbourn commented 7 years ago

See our own dialog unit tests for examples.

lordgreg commented 6 years ago

Even though this issue has been closed I must say I have never seen such a complex and non-understandable unit tests definition with so many generic variables. overlayContainerElement, viewContainerFixture, ComponentWithChildViewContainer etc..

You have to know each class and its definition perfectly to understand the code. Who would need comments anyways, right?

rvehall commented 6 years ago

Has how unit tests are handled been updated over the past year?

fizxmike commented 6 years ago

I have to say... I think I got this working, but with Angular6... OMG the boilerplate. I'll post if anyone is interested.

TinotendaKay commented 6 years ago

I have to say... I think I got this working, but with Angular6... OMG the boilerplate. I'll post if anyone is interested.

Please share you solution, I am still struggling to test.

fizxmike commented 6 years ago

Angular 6.1.1 Material 6.4.3

40+ lines of boiler plate. I learn more about Angular when writing tests than anything else!

Very simple "save" and "cancel" test for EditDialogComponent:

@Directive({
  // tslint:disable-next-line:directive-selector
  selector: 'view-container-directive'
})
class ViewContainerDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

@Component({
  selector: 'app-view-container-component',
  template: `<view-container-directive></view-container-directive>`,
})
class ViewContainerComponent {
  @ViewChild(ViewContainerDirective) childWithViewContainer: ViewContainerDirective;

  get childViewContainer() {
    return this.childWithViewContainer.viewContainerRef;
  }
}

describe('EditObjectiveDialog', () => {
  let dialog: MatDialog;
  let overlayContainerElement: HTMLElement;

  let testViewContainerRef: ViewContainerRef;
  let viewContainerFixture: ComponentFixture<ViewContainerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        ViewContainerComponent,
        ViewContainerDirective,
        EditDialogComponent,
      ],
      imports: [
        MaterialModule // module of material modules I use
      ],
      providers: [
        { provide: OverlayContainer, useFactory: () => {
          overlayContainerElement = document.createElement('div');
          return { getContainerElement: () => overlayContainerElement };
        }}
      ],
    });

    TestBed.overrideModule(BrowserDynamicTestingModule, {
     // not sure why I needed this, but error message told me to include it
     set: {
        entryComponents: [ EditDialogComponent ]
      }
    });

    TestBed.compileComponents();
  }));

  beforeEach(() => {
    viewContainerFixture = TestBed.createComponent(ViewContainerComponent);
    viewContainerFixture.detectChanges();
    testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
  });

  beforeEach(inject([MatDialog], (d: MatDialog) => {
    dialog = d;
  }));

  describe('Save and Cancel', () => {
    let testStructureObjective: StructureObjective;
    let dialogRef: MatDialogRef<EditDialogComponent, any>;
    let afterCloseCallback: jasmine.Spy;

    beforeEach(() => {
      dialogRef = dialog.open(EditDialogComponent, {
        viewContainerRef: testViewContainerRef,
        data: {
            // DialogData goes here
      }});

      afterCloseCallback = jasmine.createSpy('afterClose callback');
      dialogRef.afterClosed().subscribe(afterCloseCallback);
    });

    it('should return input on save if no edits', async(() => {
      // no edits
      // click save
      const saveButton: DebugElement = viewContainerFixture.debugElement.query(By.css('#saveButton'));
      saveButton.triggerEventHandler('click', null);

      viewContainerFixture.detectChanges();

      viewContainerFixture.whenStable().then(() => {
        expect(afterCloseCallback).toHaveBeenCalledWith({...});
        expect(dialogRef.componentInstance).toBeFalsy(); // is closed
      });
    }));

    it('should return undefined if cancelled', async(() => {
      // no edits
      // click cancel
      const cancelButton: DebugElement = viewContainerFixture.debugElement.query(By.css('#cancelButton'));
      cancelButton.triggerEventHandler('click', null);

      viewContainerFixture.detectChanges();

      viewContainerFixture.whenStable().then(() => {
        expect(afterCloseCallback).toHaveBeenCalledWith(undefined);
        expect(dialogRef.componentInstance).toBeFalsy(); // is closed
      });
    }));

  });
ishwaryamamidi commented 5 years ago

template: <view-container-directive></view-container-directive>,

@fizxmike Could you expain what is app-view-container-component in your code?

alexbjorlig commented 4 years ago

@fizxmike, did you get this to work with Angular v9?

herman-rogers commented 3 years ago

Had to add { static: true } to get the directive to work in Angular 8

export class DialogContainerComponent {
  @ViewChild(DialogContainerDirective, { static: true })
  childWithViewContainer: DialogContainerDirective;

  get childViewContainer() {
    return this.childWithViewContainer.viewContainerRef;
  }
}
angular-automatic-lock-bot[bot] commented 3 years ago

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.