Search code examples
angularunit-testingkarma-jasminemat-table

Mat Table - renderRows Unit Testing error


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();
    }

   ...
}

Solution

  • 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 ngAfterViewInit for that.

    Have a look here

    The error is shown in your test, because with the first fixture.detectChanges() inside your beforeEach you are triggering ngOnInit. Your tax service mock emits immediately your mock values and since the ViewChild is not initialised until ngAfterViewInit your table is still undefined