MutationObserver is a useful mechanism to watch for changes in the DOM and respond to them.
The MutationObserver interface provides the ability to watch for changes being made to the DOM tree.
Here is an example that watches for a theme class change.
function setUpThemeClassObservers() {
const observer = new MutationObserver(() => {
const themeClass = this.getThemeClass();
this.fireStylesChangedEvent('themeChanged');
});
observer.observe(this.eGridDiv, {
attributes: true,
attributeFilter: ['class'],
});
// we must disconnect otherwise "this" will not be GC'd
// causing a memory leak
return () => observer.disconnect();
}
However, if you forget to disconnect you could be exposing yourself to memory leaks depending on what is accessed from within the MutationObserver
functions.
Wouldn't it be great to have a test that can validate that we disconnect our observers?
Automatic Validation for Code
It turns out that it is possible to validate that every MutationObserver
that is observing the DOM is also disconnected. (You may need more than one test if you have to exercise different code paths in order to setup the MutationObservers)
The idea is to Mock the global MutationObserver with sub mocks for its observe
and disconnect
methods. Before the mock is returned we record it in an array so that we can validate all instances at the end of the test run.
describe('Mutation Observers Disconnected', () => {
let originalMutationObserver: typeof MutationObserver;
const allMockedObservers: any = [];
const mutationObserverMock = jest.fn<MutationObserver, [MutationCallback]>().mockImplementation(() => {
const mock = {
observe: jest.fn(),
disconnect: jest.fn(),
takeRecords: jest.fn(),
};
allMockedObservers.push(mock);
return mock;
});
beforeEach(() => {
// Ensure we can restore the real MutationObserver after the test
originalMutationObserver = global.MutationObserver;
global.MutationObserver = mutationObserverMock;
});
afterEach(() => {
global.MutationObserver = originalMutationObserver;
});
test('observer always disconnected after destroy', async () => {
const api = createGrid();
// Perform some actions if required to exercise the code paths
api.destroy();
expect(allMockedObservers.length).toBeGreaterThan(0);
for (const mock of allMockedObservers) {
expect(mock.observe).toHaveBeenCalled();
expect(mock.disconnect).toHaveBeenCalled();
}
});
});
In this way we can validate that each MutationObserver, setup during the test, is also disconnected at the end of the test.
Stephen Cooper - Senior Developer at AG Grid
Follow me on X @ScooperDev