Framework:Javascript GridAngular GridReact GridVue Grid

React Grid: Testing AG Grid

We will walk through how you can use testing AG Grid as part of your React application, using default build tools provided when using the Create React App utility.

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=<span ng-non-bindable>{</span>{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,
                cellEditorFramework: 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(`<div>Age: 24</div>`);

    // 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].getFrameworkComponentInstance();
    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(`<div>Age: 50</div>`);
});

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 GitHub Repo.