React Data GridTesting AG Grid
Testing with Jest
If you're using Modules then you will have to make the following configuration changes to accommodate ES Modules - if you're using Packages then this configuration is not required.
In order to test AG Grid with Jest you'll need to make the following configuration changes:
In jest.config.js
add the following lines:
module.exports = {
"transform": {
"^.+\\.(ts|tsx|js|jsx|mjs)$": [
"babel-jest" // or "ts-test" or whichever transformer you're using
]
},
transformIgnorePatterns: ['/node_modules/(?!(@ag-grid-community|@ag-grid-enterprise)/)']
}
Waiting for the Grid to be Initialised
Due to the asynchronous nature of React we cannot simply mount the Grid and assume it'll be ready for testing in the next step - we need to wait for the Grid to be ready before testing it.
We can do this in one of two ways - wait for the gridReady
event to be fired, or wait for the
Grid API to be set.
The first requires a code change and can be tricky to hook into - the latter is unobtrusive and easier to use.
We can create a utility function that will wait for the Grid API to be set for a set amount of time/attempts:
export const ensureGridApiHasBeenSet = component => {
return waitForAsyncCondition(() => {
return component.instance().api !== undefined
}, 5)
};
export const waitForAsyncCondition = (condition, maxAttempts, attempts = 0) => new Promise(function (resolve, reject) {
(function waitForCondition() {
// we need to wait for the gridReady event before we can start interacting with the grid
// in this case we're looking at the api property in our App component, but it could be
// anything (ie a boolean flag)
if (condition()) {
// once our condition has been met we can start the tests
return resolve();
}
attempts++;
if (attempts >= maxAttempts) {
reject("Max timeout waiting for condition")
}
// not set - wait a bit longer
setTimeout(waitForCondition, 10);
})();
});
The first function is what we'll use to wait for the Grid API - the 2nd one is more generic and will be useful for waiting for Grid components to be ready (see later).
We can use ensureGridApiHasBeenSet
before our tests are executed, typically in the beforeEach
hook:
beforeEach((done) => {
component = mount((<GridWithStatefullComponent/>));
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', () => {
expect(agGridReact.api).toBeTruthy();
// ..use the Grid API...
});
We can now safely test the Grid component safe in the knowledge that it's been fully initialised.
Waiting for Grid Components to be Instantiated
In the same way we need to wait for the Grid to be ready we also need to do something similar for user supplied Grid components.
For example, let us suppose a user provides a custom Editor Component and wants to test this within the context of the Grid.
class EditorComponent extends Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value
}
}
render() {
return (
<input type="text"
value={this.state.value}
onChange={this.handleChange}
style={{width: "100%"}} />
)
}
handleChange = (event) => {
this.setState({value: event.target.value});
}
getValue() {
return this.state.value;
}
// for testing
setValue(newValue) {
this.setState({
value: newValue
})
}
isCancelBeforeStart() {
return false;
}
isCancelAfterEnd() {
return false;
};
}
class GridWithStatefullComponent extends Component {
constructor(props) {
super(props);
this.state = {
columnDefs: [{
field: "age",
editable: true,
cellEditor: EditorComponent
}],
rowData: [{ age: 24 }]
};
}
onGridReady(params) {
this.api = params.api;
}
render() {
return (
<div className="ag-theme-balham">
<AgGridReact
columnDefs={this.state.columnDefs}
onGridReady={this.onGridReady.bind(this)}
rowData={this.state.rowData} />
</div>
);
}
}
We can now test this Editor Component by using the Grid API to initiate testing, gain access to the created Editor Instance and then invoke methods on it:
it('cell should be editable and editor component usable', async() => {
expect(component.render().find('.ag-cell-value').html()).toEqual(`,[object Object],`);
// we use the API to start and stop editing
// in a real e2e test we could actually double click on the cell etc
agGridReact.api.startEditingCell({
rowIndex: 0,
colKey: 'age'
});
await waitForAsyncCondition(
() => agGridReact.api.getCellEditorInstances() &&
agGridReact.api.getCellEditorInstances().length > 0, 5)
.then(() => null, () => fail("Editor instance not created within expected time"));
const instances = agGridReact.api.getCellEditorInstances();
expect(instances.length).toEqual(1);
const editorComponent = instances[0];
editorComponent.setValue(50);
agGridReact.api.stopEditing();
await waitForAsyncCondition(
() => agGridReact.api.getCellRendererInstances() &&
agGridReact.api.getCellRendererInstances().length > 0, 5)
.then(() => null, () => fail("Renderer instance not created within expected time"));
expect(component.render().find('.ag-cell-value').html()).toEqual(`,[object Object],`);
});
Note that we make use of the waitForAsyncCondition
utility described above to wait for
the Editor Component to be instantiated.
We also use the Grid API to initiate and end testing as we're can't readily perform double clicks in a unit testing environment (but could if doing e2e with something like Protractor for example).
Testing React Hooks with Enzyme
By default testing libraries won't return an accessible instance of a hook - in order to get access to methods you'll need
to wrap your component with a forwardRef
and then expose methods you want to test with the useImperativeHandle
hook.
import React, {forwardRef, useImperativeHandle, useState} from 'react';
import {AgGridReact} from 'ag-grid-react';
export default forwardRef(function (props, ref) {
const columnDefs = [...columns...];
const rowData = [...rowData...];
const [api, setApi] = useState(null);
const onGridReady = (params) => {
setApi(params.api);
};
useImperativeHandle(ref, () => {
return {
getApi() {
return api;
}
}
});
return (
<AgGridReact
columnDefs={columnDefs}
onGridReady={onGridReady}
rowData={rowData}
/>
);
});
You can then test this hook by accessing it via a ref
:
const ensureGridApiHasBeenSet = async (componentRef) => {
await act(async () => {
await new Promise(function (resolve, reject) {
(function waitForGridReady() {
// access the exposed "getApi" method on our hook
if (componentRef.current.getApi()) {
if (componentRef.current.getApi().getRowNode(8)) {
return resolve();
}
}
setTimeout(waitForGridReady, 10);
})();
})
});
};
beforeEach(async () => {
const ref = React.createRef()
component = mount(<App ref={ref}/>);
agGridReact = component.find(AgGridReact).instance();
await ensureGridApiHasBeenSet(ref);
});
Note that we're accessing exposed getApi
method via the ref
: componentRef.current.getApi()
.
A full working example can be found in the following ,[object Object],.