1
votes

I made some updates to an Angular component and it broke some unit tests. All test specs are broken so to me it seems like something to do with initialization in the beforeEach calls. I have tried to research the problem extensively but haven't had any success.

The error I get back from running my unit tests is: TypeError: Cannot read property 'subscribe' of undefined

The only calls made to subscribe are:

this.ruleDataFieldOption$.subscribe
...
this.submitted$.subscribe
...
this.dataFieldControl.valueChanges.subscribe

I tried to explicitly initialize ruleDataFieldOption$ by setting it to of(mockRuleDataFields) but that did not work. I also tried moving the block of code from the second beforeEach call into the promise of the async beforeEach call (i.e. .compileComponents().then( () => {...} ) and that didn't work either.

client-rules-condition.component.spec.ts

const mockDataFieldService = {
  getRuleDataFields: () => {}
} as RuleDataFieldService;

const mockStore = {
  select: td.function('.select'),
  dispatch: td.function('.dispatch'),
  pipe: td.function('.pipe'),
} as Store<any>;
let dispatch;
let pipe;

const mockConfigService = {
  getConfiguration: td.func(() => {
    return mockNotificationConfig
  })
} as ConfigService;

const mockNotificationConfig = {
  Env: null,
  apiBaseUrl: 'blah'
} as MclNotificationConfig;

const mockRuleDataFields: RuleDataFieldDefinition[] = [
  {name: 'data field 1', dataFieldId: 'mock data field guid', category: 'mock category'} as RuleDataFieldDefinition
];

fdescribe('ClientRulesConditionComponent', () => {
  let component: ClientRulesConditionComponent;
  let fixture: ComponentFixture<ClientRulesConditionComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ClientRulesConditionComponent ],
      imports: [ReactiveFormsModule],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        {provide: Store, useFactory: () => mockStore},
        {provide: RuleDataFieldService, useValue: mockRuleDataFields},
        HttpClientTestingModule,

        // {provide: ConfigService, useFactory: () => mockConfigService},
        // {provide: MclNotificationConfig, useFactory: () => mockNotificationConfig},
        // HttpHandler, HttpClient
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    dispatch = spyOn(mockStore, 'dispatch');
    pipe = spyOn(mockStore, 'pipe').and.callThrough();
    fixture = TestBed.createComponent(ClientRulesConditionComponent);
    component = fixture.componentInstance;
    const fb: FormBuilder = new FormBuilder();
    component.conditionForm = fb.group({
      name: fb.control('', [Validators.required]),
      triggerWhen: fb.control('', [Validators.required]),
      conditions: fb.array([
        fb.group({
          dataField: fb.control('df1'),
          conditionToMatch: fb.control('ctm1'),
          value: fb.control('v1')
        }),
        fb.group({
          dataField: fb.control('df2'),
          conditionToMatch: fb.control('ctm2'),
          value: fb.control('v2')
        })
      ])
    });
    component.indexInConditionArray = 1;
    component.submitted$ = of(false);
    component.ruleDataFieldOption$ = of(mockRuleDataFields);
    fixture.detectChanges();
  });


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

    // Confirm conditionToMatch and dataField get actions have been dispatched
    expect(dispatch).toHaveBeenCalledTimes(2);

    // Confirm that we load the correct set of form controls and they have the correct value associated with them
    const conditionsArray = component.conditionForm.controls['conditions'] as FormArray;
    const conditionGroup = conditionsArray.at(component.indexInConditionArray) as FormGroup;
    expect(component.dataFieldControl).toEqual(conditionGroup.controls['dataField'] as FormControl);
    expect(component.dataFieldControl.value).toEqual('df2');
    expect(component.matchingConditionControl).toEqual(conditionGroup.controls['conditionToMatch'] as FormControl);
    expect(component.matchingConditionControl.value).toEqual('ctm2');
    expect(component.valueControl).toEqual(conditionGroup.controls['value'] as FormControl);
    expect(component.valueControl.value).toEqual('v2');

    // Confirm the conditionToMatch and dataField selectors were wired up
    expect(pipe).toHaveBeenCalledTimes(2);

    // Confirm the expected value for the 'submitted' flag got emitted
    expect(component.submitted).toBeFalsy();
  });
});
.
.
.

client-rules-condition.component.ts

@Component({
  selector: 'nd-client-rules-condition',
  templateUrl: './client-rules-condition.component.html',
  styleUrls: ['./client-rules-condition.component.scss']
})
export class ClientRulesConditionComponent implements OnInit {

  @Input() conditionForm: FormGroup;
  @Input() indexInConditionArray: number;
  @Input() submitted$: Observable<boolean>;
  @Output() removeCondition: EventEmitter<number> = new EventEmitter();

  conditionToMatchOption$: Observable<ConditionToMatch[]>;

  valueControl: FormControl;
  matchingConditionControl: FormControl;
  dataFieldControl: FormControl;

  private static NPI_DATA_FIELD_GUID: string;

  validValuePattern: string;
  invalidValueError: string;

  // Emits the candidate rule fields that eventually come back from the database.
  //    The 'data fields' combo box in the markup is wired up to this.
  ruleDataFieldOption$: Observable<RuleDataFieldDefinition[]>;

  // Tracks whether the parent form has been submitted.
  submitted: boolean;

  constructor(private _store: Store<state.AppState>) { }

  ngOnInit() {
    this._store.dispatch(new GetConditionToMatchOptions(''));

    // Dispatch Action that causes the rule-data-field options to get retrieved from the server and slammed into the global state.
    this._store.dispatch(new GetRuleDataFieldsOptions(''));

    this.conditionToMatchOption$ = this._store.pipe(select(getConditionToMatchOption));

    // Wire up the selector that cause the rule-data-field options to get pulled from the global state when they are set/updated
    this.ruleDataFieldOption$ = this._store.pipe(select(getRuleDataFieldsSelector));

    // Load GUID for NPI field to use to distinguish between different Data Field dropdown items to apply different validation in Value To Match
    this.ruleDataFieldOption$.subscribe(dataFields => {
      if (ClientRulesConditionComponent.NPI_DATA_FIELD_GUID == null) {
        for (let df of dataFields) {
          if (df.name.toLowerCase().includes('npi') || df.name.toLowerCase().includes('national provider identifier')) {
            ClientRulesConditionComponent.NPI_DATA_FIELD_GUID = df.dataFieldId.toUpperCase();
            break;
          }
        }
      }
    });
    .
    .
    .

I am confident that it is something minor and I just need my unit tests to pass.

1
Too much code, but the first thing that pops out is spyOn(mockStore, 'dispatch') should probably return an Observable.The Head Rush
I agree with @The Head Rush, it‘s quite a lot of code not really relevant to the actual issue. But I don‘t think it‘s the dispatch spy, since that‘s just a method called. But rather the select spy does not return an observable. And you need to know that with the first fixture.detectChanges the ngOnInit gets called. So you are setting the ruleDataField$ with a fixed value and then call onInit and it gets replaced with your select spy which only returns null.Erbsenkoenig

1 Answers

0
votes

Try this, in one of the before each declare the new observable.

component.ruleDataFieldOption$ = new Observable<ConditionToMatch[]>;

I don't know if the same rules apply to the observable it self as the Subject but you can give it a try.