I'm using a mat table in a component and call renderRows after updating the table, which works perfectly. However, in my unit tests, I'm getting the below error.
An error was thrown in afterAll Failed: Cannot read property 'renderRows' of undefined error properties: Object({ longStack: 'TypeError: Cannot read property 'renderRows' of undefined at SafeSubscriber._next (http://localhost:9876/karma_webpack/src/app/product-management/tax-configuration/tax-configuration.component.ts:80:23)
spec.ts file ->
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TaxConfigurationComponent } from './tax-configuration.component';
import { MatTableModule } from '@angular/material/table';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NotificationService } from 'src/app/services/custom/notification.service';
import { TaxConfigurationService } from 'src/app/services/products/tax-configuration.service';
import { MockTaxConfigurationService } from 'src/app/services/products/tax-configuration.service.mock.spec';
import { throwError } from 'rxjs';
import { MatButtonModule } from '@angular/material/button';
describe('TaxConfigurationComponent', () => {
let component: TaxConfigurationComponent;
let fixture: ComponentFixture<TaxConfigurationComponent>;
let _notificationService: NotificationService;
let _taxService: TaxConfigurationService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TaxConfigurationComponent],
imports: [
BrowserAnimationsModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
MatButtonModule,
],
providers: [{ provide: TaxConfigurationService, useClass: MockTaxConfigurationService }],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TaxConfigurationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
_taxService = TestBed.inject(TaxConfigurationService);
_notificationService = TestBed.inject(NotificationService);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should populate tax table on init', () => {
expect(component.dataSource.length).toBeGreaterThan(0);
});
it('should show an error notification when "getTaxConfig()" errors out', () => {
spyOn(_notificationService, 'startNotification').and.callThrough();
spyOn(_taxService, 'getTaxConfig').and.returnValue(throwError('Error'));
component.ngOnInit();
expect(_notificationService.startNotification).toHaveBeenCalledWith(
'An error occurred while fetching data.',
'nc-notification--error',
'priority_high'
);
});
});
component.ts file ->
ngOnInit(): void {
this.state = true;
this.taxForm = new FormGroup({});
this.populateTaxConfigTable();
}
populateTaxConfigTable(): void {
this._taxService.getTaxConfig().subscribe((results) => {
results.forEach((result) => {
const rowEntry = {
name: result.resourceName,
category: result.resourceCategory,
id: result.resourceId,
tsc: new FormControl(result.taxTsc, [
Validators.required,
Validators.pattern(regexPattern),
Validators.max(100),
Validators.min(0),
]),
ot: new FormControl(result.taxOt, [
Validators.required,
Validators.pattern(regexPattern),
Validators.max(100),
Validators.min(0),
]),
vat: new FormControl(result.taxVat, [
Validators.required,
Validators.pattern(regexPattern),
Validators.max(100),
Validators.min(0),
]),
};
const tscControlName = rowEntry.id + 'tsc';
const otControlName = rowEntry.id + 'ot';
const vatControlName = rowEntry.id + 'vat';
this.taxForm.addControl(tscControlName, rowEntry.tsc);
this.taxForm.addControl(otControlName, rowEntry.ot);
this.taxForm.addControl(vatControlName, rowEntry.vat);
this.dataSource.push(rowEntry);
});
this.table.renderRows();
this.state = false;
}, (error) => {
this._notificationService.startNotification('An error occurred while fetching data.',
'nc-notification--error', 'priority_high');
});
}
When I comment the this.table.renderRows, unit tests are running without any issues. Any idea about the issue here?
Edit:
MockTaxCongfigurationService
export class MockTaxConfigurationService {
getTaxConfig(): Observable<ResourceTaxes[]> {
return of([mockResourceTaxes, mockResourceTaxes]);
}
updateTaxConfig(data: TaxPostData[]): Observable<TaxResponseData[]> {
return of([mockTaxResponseData]);
}
}
Using viewChild ->
export class TaxConfigurationComponent implements OnInit {
@ViewChild(MatTable) table: MatTable<any>;
displayedColumns: string[] = ['name', 'tsc', 'ot', 'vat'];
taxForm: FormGroup;
dataSource: TaxTableData[] = [];
state = false; // Loading state
shouldDisableSave = false;
constructor(
private _notificationService: NotificationService,
private _taxService: TaxConfigurationService) {}
ngOnInit(): void {
this.state = true;
this.taxForm = new FormGroup({});
this.populateTaxConfigTable();
}
...
}
I think you unit test actually reveals a problem which you didn't encounter because your real tax configuration service just took long enough, until the table was actually initialised.
A view child is not available in onInit. It is set a little bit later in the life cycle. You would need to use
ngAfterViewInitfor that.Have a look here
The error is shown in your test, because with the first
fixture.detectChanges()inside yourbeforeEachyou are triggeringngOnInit. Your tax service mock emits immediately your mock values and since theViewChildis not initialised untilngAfterViewInityour table is stillundefined