Skip to main content

How to test the usage of AG Grid in Angular applications

AG Grid displays tabular data. To test a component that uses ag-grid-angular, mock the grid with ng-mocks, verify its input and output bindings, and test any custom cell renderers separately.

Let's assume that a component uses ag-grid-angular like this:

<ag-grid-angular
[rowData]="rowData"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[gridOptions]="gridOptions"
(gridReady)="onGridReady($event)"
(rowClicked)="selectedRow = $event.data"
></ag-grid-angular>
<span>{{ selectedRow?.make }}</span>

A test of such a template requires us to:

  • mock ag-grid-angular
  • assert passed inputs
  • assert listeners on outputs
  • provide the Grid API methods used by the component
  • assert custom cell renderers if the grid uses them

Spec file

With MockBuilder, our spec file needs a single line to provide mocks:

beforeEach(() => MockBuilder(TargetComponent, TargetModule));

Where TargetComponent is a component which uses ag-grid-angular, and TargetModule is its module, which imports AgGridModule. The complete component, its Row type, and its module are in test.spec.ts.

Testing inputs of ag-grid-angular

In this test we need to verify that the grid receives the parent's rows, columns, default column settings, and grid options. We also check that replacing the rows updates the binding.

The tools from ng-mocks we need:

After directly assigning a field, mark the parent for checking so the example also works with OnPush change detection.

it('binds inputs', () => {
// Rendering TargetComponent and accessing its instance.
const fixture = MockRender(TargetComponent);
const targetComponent = fixture.point.componentInstance;

// Looking for a debug element of `AgGridAngular`.
const gridEl = ngMocks.reveal<AgGridAngular<Row>>(AgGridAngular);

// Asserting bound properties.
expect(ngMocks.input(gridEl, 'rowData')).toBe(
targetComponent.rowData,
);
expect(ngMocks.input(gridEl, 'columnDefs')).toBe(
targetComponent.columnDefs,
);
expect(ngMocks.input(gridEl, 'defaultColDef')).toBe(
targetComponent.defaultColDef,
);
expect(ngMocks.input(gridEl, 'gridOptions')).toBe(
targetComponent.gridOptions,
);

// Checking that the mock is available through ViewChild and has no grid artifacts.
expect(isMockOf(gridEl.componentInstance, AgGridAngular)).toBe(
true,
);
expect(targetComponent.grid).toBe(gridEl.componentInstance);
expect(targetComponent.grid!.api).toBeUndefined();
expect(ngMocks.formatHtml(gridEl)).toEqual('');

// Updating an input and checking its binding again.
targetComponent.rowData = [{ make: 'Ford', price: 32_000 }];
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();

expect(ngMocks.input(gridEl, 'rowData')).toBe(
targetComponent.rowData,
);
});

Testing outputs of ag-grid-angular

The component listens to rowClicked and displays the selected row's make. To test the binding, emit a row through the mocked output and assert its effect.

The tools from ng-mocks we need:

it('binds outputs', () => {
// Rendering TargetComponent and accessing its instance.
const fixture = MockRender(TargetComponent);
const targetComponent = fixture.point.componentInstance;
const gridEl = ngMocks.reveal(AgGridAngular);

// Simulating an emit.
const data = targetComponent.rowData[0];
expect(targetComponent.selectedRow).toBeUndefined();
ngMocks
.output(gridEl, 'rowClicked')
.emit({ data } as RowClickedEvent<Row>);
fixture.detectChanges();

// Asserting the effect of the emit.
expect(targetComponent.selectedRow).toBe(data);
expect(ngMocks.formatText(fixture)).toEqual('Toyota');
});

Testing gridReady

The approach to test gridReady is the same as above. In this example, onGridReady calls event.api.sizeColumnsToFit(). Supply that method in the event and assert that the handler calls it.

The GridApi<Row> and GridReadyEvent<Row> types come from ag-grid-community. The type assertions describe partial test fixtures; the mock does not create a real Grid API.

it('handles gridReady', () => {
// Rendering TargetComponent and looking for the grid.
MockRender(TargetComponent);
const gridEl = ngMocks.reveal(AgGridAngular);

// Providing the API method used by the gridReady handler.
let calls = 0;
const api = {
sizeColumnsToFit: () => {
calls += 1;
},
} as GridApi<Row>;

// Simulating an emit.
expect(calls).toBe(0);
ngMocks
.output(gridEl, 'gridReady')
.emit({ api } as GridReadyEvent<Row>);

// Asserting the effect of the emit.
expect(calls).toBe(1);
});

Testing Grid API access through ViewChild

If the component accesses the grid through ViewChild, it might use these members:

@ViewChild(AgGridAngular) public grid?: AgGridAngular<Row>;

public getSelectedRows(): Row[] {
return this.grid!.api.getSelectedRows();
}

The tools from ng-mocks we need:

  • MockInstance: to initialize the mock's api property before rendering
  • MockRender: to render TargetComponent and get its instance

Call MockInstance.scope() in the suite to restore the customization after each test:

MockInstance.scope();
it('provides a Grid API for ViewChild', () => {
// Customizing the mock before rendering TargetComponent.
const selectedRows: Row[] = [{ make: 'Ford', price: 32_000 }];
const api = {
getSelectedRows: () => selectedRows,
} as GridApi<Row>;
MockInstance(AgGridAngular, 'api', api);

// Rendering TargetComponent and accessing its instance.
const targetComponent =
MockRender(TargetComponent).point.componentInstance;

// Asserting access to the API through ViewChild.
expect(targetComponent.grid!.api).toBe(api);
expect(targetComponent.getSelectedRows()).toBe(selectedRows);
});

Testing custom cell renderers

AG Grid creates Angular cell components referenced by a column's cellRenderer. For example, a price column can use this definition:

{ field: 'price', cellRenderer: PriceCellComponent }

A mocked grid receives the column definition but does not create the cell component. Test the cell component separately, supplying the parameters that AG Grid passes to it.

note

agInit and refresh are called by AG Grid. Angular does not invoke them as lifecycle hooks. Cell components are created dynamically, so use MockRender to test their templates. The ngMocks.render helper is for projected templates.

For example, PriceCellComponent uses PriceService to format the supplied value:

@Injectable({ providedIn: 'root' })
class PriceService {
public format(value: number): string {
return `${value}`;
}
}

@Component({
selector: 'ag-grid-price-cell',
standalone: true,
template: '<strong>{{ price }}</strong>',
})
class PriceCellComponent implements ICellRendererAngularComp {
public price = '';

public constructor(private readonly priceService: PriceService) {}

public agInit(params: ICellRendererParams): void {
this.price = this.priceService.format(params.value);
}

public refresh(params: ICellRendererParams): boolean {
this.price = this.priceService.format(params.value);
return true;
}
}

The tools from ng-mocks we need:

The setup provides a predictable result from the formatting service:

beforeEach(() =>
MockBuilder(PriceCellComponent).mock(PriceService, {
format: value => `price: ${value}`,
}),
);

Call agInit with the initial value, then refresh with a new value, and assert the rendered text after each call:

it('refreshes the value without replacing the renderer', () => {
// Rendering the cell component and accessing its instance.
const fixture = MockRender(PriceCellComponent);
const renderer = fixture.point.componentInstance;

// Initializing the cell as AG Grid would.
renderer.agInit({ value: 35_000 } as ICellRendererParams);
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('price: 35000');

// Refreshing the cell with a new value.
expect(
renderer.refresh({ value: 32_000 } as ICellRendererParams),
).toBe(true);
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();

// Asserting that the same renderer displays the new value.
expect(fixture.point.componentInstance).toBe(renderer);
expect(ngMocks.formatText(fixture)).toEqual('price: 32000');
});

cell-renderer.spec.ts also checks initialization on its own and verifies that a mocked grid preserves the renderer configuration without creating cells.