angularangular-testangular-testing-library

Angular test failing because of ngOnit statements not called


Hi I am having an angular 2 component . I have a test . my test is failing as I am getting the following error. the following is the error i recieve when i run ng test.

        Expected spy create to have been called with 
    [ <jasmine.objectContaining(Object({ name: 'test', campaign: Object({ id: '123' }) }))> ]
 but actual calls were [ Object({ name: 'test', campaign: undefined }) ].

the following is my test.

describe('CreateFlightComponent', () => {
  let component: CreateFlightComponent;
  let fixture: ComponentFixture<CreateFlightComponent>;
  let router: Router;

  let getCampaignSpy: jasmine.Spy;
  let createFlightSpy: jasmine.Spy;
  let navigateSpy: jasmine.Spy;
  let toastSpy: jasmine.Spy;
  let getUserSpy: jasmine.Spy;
  let getApprovalPeriodDateSpy: jasmine.Spy;
  let getTagOnsSpy: jasmine.Spy;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [CreateFlightComponent],
      providers: [
        FlightsService,
        CampaignsService,
        ToastService,
        AuthenticationService,
        SettingsService,
        {
          provide: ActivatedRoute,
          useValue: {
            paramMap: of(convertToParamMap({ id: '123' }))
          }
        },
        { provide: ConfigService, useClass: ConfigServiceMock }
      ],
      imports: [
        TranslateModule.forRoot(),
        RouterTestingModule,
        HttpClientTestingModule
      ],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));

  beforeEach(() => {
    const campaignsService = TestBed.get(CampaignsService);
    getCampaignSpy = spyOn(campaignsService, 'get').and.returnValue(
      of({
        id: '123'
      })
    );

    const flightsService = TestBed.get(FlightsService);
    createFlightSpy = spyOn(flightsService, 'create').and.returnValue(
      of({ id: 'f321' })
    );
    getTagOnsSpy = spyOn(flightsService, 'getTagOns').and.returnValue(of([]));
    spyOn(flightsService, 'getTagOnTargetRecipients').and.returnValue(
      of([] as TagOnTargetRecipient[])
    );

    const toastService = TestBed.get(ToastService);
    toastSpy = spyOn(toastService, 'toast');

    const authenticationService = TestBed.get(AuthenticationService);
    getUserSpy = spyOn(authenticationService, 'getUser').and.returnValue(
      of({
        account: { features: [{ code: FeatureCode.PROFILES }] } as Account
      } as User)
    );

    const settingsService = TestBed.get(SettingsService);
    getApprovalPeriodDateSpy = spyOn(
      settingsService,
      'getApprovalPeriodDate'
    ).and.returnValue(of(moment(new Date()).add(7, 'days')));

    router = TestBed.get(Router);
    navigateSpy = spyOn(router, 'navigate');
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(CreateFlightComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  describe('handleSave', () => {

    it('should send a request to create a flight', fakeAsync( () => {
      
      fixture.detectChanges();
      tick();
      component.handleSave({ currentValue: { name: 'test' }, stepIndex: 1 });
  
      expect(createFlightSpy).toHaveBeenCalledWith(
        jasmine.objectContaining({
          name: 'test',
          campaign: { id: '123' }
        })
      );
    }))

});


});

the following is my component class CreateFlightComponent

export class CreateFlightComponent implements OnInit {
  campaign: Campaign;
  isLoading = true;
  isSaving = false;
  hasProfileFeature = false;
  hasLocationFeature = false;
  hasRecipientGatheringFeature = false;
  hasParameterisedContactListFeature = false;
  hasInteractiveSMSFeature = false;
  account: Account;
  inventories: Inventory[] = [];
  tagOns: TagOn[] = [];
  tagOnTargetRecipients: TagOnTargetRecipient[] = [];
  approvalPeriodStartDate: Moment;
  parameterisedContactList: ParameterisedContactList;
  addressProfile: AddressProfile;

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private translateService: TranslateService,
    private flightsService: FlightsService,
    private campaignsService: CampaignsService,
    private toastService: ToastService,
    private authenticationService: AuthenticationService,
    private settingsService: SettingsService
  ) {}

  ngOnInit() {
    this.route.paramMap
      .pipe(
        tap(() => (this.isLoading = true)),
        switchMap(paramMap =>
          combineLatest(
            this.authenticationService.getUser(),
            this.campaignsService.get(paramMap.get('id')),
            this.settingsService.getApprovalPeriodDate(),
            this.flightsService.getTagOns(),
            this.flightsService.getTagOnTargetRecipients(),
            this.flightsService.getInventories()
          )
        )
      )
      .subscribe(
        ([user, campaign, date, tagOns, tagOnTargetRecipients, allInventories]) => {
          this.isLoading = false;

          if (user.account) {
            this.account = user.account;
            this.inventories = this.account.inventories;

            if (this.account.features) {
              this.hasProfileFeature = this.account.features.some(
                feature => feature.code === FeatureCode.PROFILES
              );

              this.hasLocationFeature = this.account.features.some(
                feature => feature.code === FeatureCode.LOCATIONS
              );

              this.hasRecipientGatheringFeature = this.account.features.some(
                feature => feature.code === FeatureCode.RECIPIENT_GATHERING
              );

              this.hasParameterisedContactListFeature = this.account.features.some(
                feature =>
                  feature.code === FeatureCode.PARAMETERISED_CONTACT_LIST
              );

              this.hasInteractiveSMSFeature = this.account.features.some(
                feature =>
                  feature.code === FeatureCode.INTERACTIVE_SMS
              );

              this.addInteractiveSMS(this.hasInteractiveSMSFeature, allInventories, this.inventories )
            }

            if (this.account.addressProfile) {
              this.addressProfile = this.account.addressProfile;
            }
          }

          this.tagOns = tagOns;
          this.tagOnTargetRecipients = tagOnTargetRecipients;
          this.campaign = campaign;
          console.log(JSON.stringify(this.campaign));
          this.approvalPeriodStartDate = date;
        },
        () => {
          this.isLoading = false;
        }
      );
  }

  addInteractiveSMS( hasInteractiveSMSFeature:boolean, allInventories: Inventory[] , inventories: Inventory[]) {
    let interactiveSMS =  allInventories.find(inventory => inventory.code === InventoryCode.INTERACTIVE_SMS);
    if(hasInteractiveSMSFeature && interactiveSMS){
       inventories.push(interactiveSMS);
    }
  }

  handleSave({
    currentValue,
    stepIndex
  }: {
    currentValue: Partial<Flight>;
    stepIndex: number;
  }) {
    const request: Partial<Flight> = {
      ...currentValue,
      campaign: this.campaign
    };
    const titleKey = 'HEADINGS.FLIGHT_CREATED';
    let bodyKey = '';

    this.isSaving = true;

    this.flightsService
      .create(request)
      .pipe(
        switchMap(flight => {
          this.router.navigate(['/flights', flight.id, 'edit'], {
            queryParams: { startStepIndex: stepIndex }
          });

          request.impressionLimit !== flight.impressionLimit
            ? (bodyKey = 'MESSAGES.IMPRESSIONS_CHANGED')
            : (bodyKey = 'MESSAGES.FLIGHT_SAVED_DRAFT');

          return request.impressionLimit !== flight.impressionLimit
            ? this.translateService.get([titleKey, bodyKey], {
                name: request.name,
                impressions: flight.impressionLimit
              })
            : this.translateService.get([titleKey, bodyKey], {
                name: request.name
              });
        })
      )
      .subscribe(
        translations => {
          this.isSaving = false;

          this.toastService.toast({
            type: 'success',
            title: translations[titleKey],
            body: translations[bodyKey],
            icon: 'paper-plane',
            timeout: 10000
          });
        },
        () => {
          this.isSaving = false;
        }
      );
  }

  saveAsDraft(currentValue: Partial<Flight>) {
    const titleKey = 'HEADINGS.FLIGHT_CREATED';
    const bodyKey = 'MESSAGES.FLIGHT_SAVED_DRAFT';

    const request: Partial<Flight> = {
      ...currentValue,
      campaign: this.campaign,
      parameterisedContactList: this.parameterisedContactList,
      status: {
        code: FlightStatusCode.DRAFT
      }
    };

    this.isSaving = true;
    this.flightsService
      .create(request)
      .pipe(
        switchMap(flight => {
          this.router.navigate(['/flights', flight.id, 'edit'], {
            queryParams: { startStepIndex: 1 }
          });

          return this.translateService.get([titleKey, bodyKey], {
            name: request.name
          });
        })
      )
      .subscribe(
        translations => {
          this.isSaving = false;
          this.toastService.toast({
            type: 'success',
            title: translations[titleKey],
            body: translations[bodyKey],
            icon: 'paper-plane',
            timeout: 10000
          });
        },
        () => {
          this.isSaving = false;
        }
      );
  }

  handleUploadFormSubmit(value: ParameterisedContactList) {
    this.parameterisedContactList = value;
  }

  handleCancel() {
    this.router.navigate(['/campaigns', this.campaign.id]);
  }
}

my test is calling handleSave. i tried to debug the code. I put a debug point in ngOnInit() at this statement this.campaign = campaign within the subscribe method. however it is not hitting the debug there. appreciate if you help

thank you

Please note:

updated to the following code to the following but i get an error.

CreateFlightComponent should send a request to create a flight
[object ErrorEvent] thrown
TypeError: Cannot read property 'next' of undefined



  describe('CreateFlightComponent', () => {
      let component: CreateFlightComponent;
      let fixture: ComponentFixture<CreateFlightComponent>;
      let router: Router;
    
      let getCampaignSpy: jasmine.Spy;
      let createFlightSpy: jasmine.Spy;
      let navigateSpy: jasmine.Spy;
      let toastSpy: jasmine.Spy;
      let getUserSpy: jasmine.Spy;
      let getApprovalPeriodDateSpy: jasmine.Spy;
      let getTagOnsSpy: jasmine.Spy;
      let myActivatedRouteObserver;
      
      beforeEach(() => {
    
        const  myActivatedRouteObservable = new Observable((observer) => {
          myActivatedRouteObserver = observer;
         });
    
      });
    
      beforeEach(async(() => {
    
        TestBed.configureTestingModule({
          declarations: [CreateFlightComponent],
          providers: [
            FlightsService,
            CampaignsService,
            ToastService,
            AuthenticationService,
            SettingsService,
            {
              provide: ActivatedRoute,
              useValue: myActivatedRouteObserver
            },
            { provide: ConfigService, useClass: ConfigServiceMock }
          ],
          imports: [
            TranslateModule.forRoot(),
            RouterTestingModule,
            HttpClientTestingModule
          ],
          schemas: [NO_ERRORS_SCHEMA]
        }).compileComponents();
      }));
    
      beforeEach(() => {
        const campaignsService = TestBed.get(CampaignsService);
        getCampaignSpy = spyOn(campaignsService, 'get').and.returnValue(
          of({
            id: '123'
          })
        );
    
        const flightsService = TestBed.get(FlightsService);
        createFlightSpy = spyOn(flightsService, 'create').and.returnValue(
          of({ id: 'f321' })
        );
        getTagOnsSpy = spyOn(flightsService, 'getTagOns').and.returnValue(of([]));
        spyOn(flightsService, 'getTagOnTargetRecipients').and.returnValue(
          of([] as TagOnTargetRecipient[])
        );
    
        const toastService = TestBed.get(ToastService);
        toastSpy = spyOn(toastService, 'toast');
    
        const authenticationService = TestBed.get(AuthenticationService);
        getUserSpy = spyOn(authenticationService, 'getUser').and.returnValue(
          of({
            account: { features: [{ code: FeatureCode.PROFILES }] } as Account
          } as User)
        );
    
        const settingsService = TestBed.get(SettingsService);
        getApprovalPeriodDateSpy = spyOn(
          settingsService,
          'getApprovalPeriodDate'
        ).and.returnValue(of(moment(new Date()).add(7, 'days')));
    
        router = TestBed.get(Router);
        navigateSpy = spyOn(router, 'navigate');
      });
    
      beforeEach(() => {
        fixture = TestBed.createComponent(CreateFlightComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    
    
    
        it('should send a request to create a flight', fakeAsync( () => {
          
    
          fixture.detectChanges();
    
          myActivatedRouteObserver.next(convertToParamMap({ id: '123' }));
    
          component.handleSave({ currentValue: { name: 'test' }, stepIndex: 1 });
      
          expect(createFlightSpy).toHaveBeenCalledWith(
            jasmine.objectContaining({
              name: 'test',
              campaign: { id: '123' }
            })
          );
        }));
    
    });

Solution

  • To run ngOnInit() from a test, you must call fixture.detectChanges().

    I see that in your code and therefore ngOnInit() should be running. However, your ngOnInit() code is subscribing to an Observable, this.route.paramMap. It is not obvious to me where you are resolving that Observable in your test so your pipes and subscription can run.

    When you set up the ActivatedRoute provider, I'd create my own Observable that you have complete control over.

    Create the Observable:

    let myActivatedRouteObserver;
    constant myActivatedRouteObservable = new Observable((observer) => {
     myActivatedRouteObserver = observer;
    }
    

    Create the provider for ActivatedRoute:

            {
              provide: ActivatedRoute,
              useValue: {
                paramMap: myActivatedRouteObservable
              }
            },
    

    Then when you want to resolve the param map:

    myActivatedRouteObserver.next(convertToParamMap({ id: '123' }));
    

    You'll have to run this after detect changes, because before that the observable is not subscribed to. I suspect that is the root of your issue, the Observable is resolving before it is subscribed to.