Testing Ext JS Applications: What Is Actually Worth Automating
Get a summary of this article:
Ask a team with a ten-year-old Ext JS application how much of it is covered by automated tests and the answer is usually a polite laugh. The reasons are understandable: our components are heavy, they need a DOM, they talk to servers, and the framework’s lifecycle can feel opaque.
But the real problem is usually strategy instead of tooling. When we as teams try to test everything through the browser, we find it slow and brittle, then we just give up…
I want to present you a layered approach: test the cheap, stable things heavily, test the expensive things sparingly, and structure your code so the first group is as large as possible. The snippets use Jasmine, which works with Sencha Test and with Karma-style runners, and target Ext JS 7.

1. The testing pyramid, applied to Ext JS
| Layer | What it covers | Speed | How many |
|---|---|---|---|
| Unit | Models, validators, formatters, controller logic | Milliseconds | Many |
| Component | A view with a real store and DOM, no server | Tens of milliseconds | Some |
| End to end | Critical user journeys against a running app | Seconds | Few |
The rest of this article walks up the pyramid. The key idea is that the architecture decides how much you can push into the bottom layer. If your logic lives in controllers, models, and utility classes (rather than inside anonymous handlers in view configs), most of your tests can be fast unit tests.
2. Unit test models and validators first
Models are the highest-value, lowest-cost target. They hold your field definitions, conversions, and validation rules, and they need no DOM at all.
Ext.define('Acme.model.Account', {
extend: 'Acme.model.Base',
fields: [
{ name: 'id', type: 'int' },
{ name: 'code', type: 'string' },
{ name: 'description', type: 'string' }
],
validators: {
code: { type: 'length', min: 1, max: 20 },
description: 'presence'
}
});
describe('Acme.model.Account', function () {
it('is valid when code and description are present', function () {
var record = Ext.create('Acme.model.Account', {
code: '1.1.01',
description: 'Cash'
});
expect(record.isValid()).toBe(true);
});
it('requires a description', function () {
var record = Ext.create('Acme.model.Account', { code: '1.1.01' });
var validation = record.getValidation();
expect(validation.isValid()).toBe(false);
expect(validation.get('description')).toBe('Must be present');
});
it('rejects codes longer than 20 characters', function () {
var record = Ext.create('Acme.model.Account', {
code: '1234567890123456789012345',
description: 'Too long'
});
expect(record.isValid()).toBe(false);
});
});
These run in milliseconds and protect the rules that every form and grid in your application depends on.
3. Unit test controllers by isolating the view
A ViewController normally gets its view from the framework, but you can instantiate one directly and replace what it talks to. This is exactly why we keep logic in controllers and have them announce events instead of reaching into sibling views.
Ext.define('Acme.view.account.AccountGridController', {
extend: 'Ext.app.ViewController',
alias: 'controller.accountgrid',
onRowDblClick: function (grid, record) {
this.fireViewEvent('accountselected', record);
}
});
describe('Acme.view.account.AccountGridController', function () {
var controller;
beforeEach(function () {
controller = Ext.create('Acme.view.account.AccountGridController');
});
afterEach(function () {
Ext.destroy(controller);
controller = null;
});
it('announces the selected account on double click', function () {
var record = Ext.create('Acme.model.Account', { id: 1 });
spyOn(controller, 'fireViewEvent');
controller.onRowDblClick(null, record);
expect(controller.fireViewEvent)
.toHaveBeenCalledWith('accountselected', record);
});
});
The test never renders anything. It checks the contract: “double click means the controller announces the selection”. If a controller method is hard to test this way, that is usually a sign it is doing too much or reaching into other components.
4. Test stores without a server
Stores are another good target because of all the sorting, filtering, grouping, and any custom logic you attach. Replace the proxy with an in-memory one so nothing touches the network.
describe('Account store behavior', function () {
var store;
beforeEach(function () {
store = Ext.create('Ext.data.Store', {
model: 'Acme.model.Account',
proxy: { type: 'memory' },
data: [
{ id: 1, code: '1.1', description: 'Assets' },
{ id: 2, code: '2.1', description: 'Liabilities' },
{ id: 3, code: '1.2', description: 'Inventory' }
]
});
});
afterEach(function () {
Ext.destroy(store);
});
it('sorts by code', function () {
store.sort('code', 'ASC');
expect(store.first().get('code')).toBe('1.1');
expect(store.last().get('code')).toBe('2.1');
});
it('filters by description', function () {
store.filter('description', 'Inv');
expect(store.getCount()).toBe(1);
expect(store.first().get('id')).toBe(3);
});
});
When you do need to test code that loads data, spy on Ext.Ajax.request (or on the proxy) and call the success callback yourself. You are testing your handling of a response, not whether the network works.
5. Component tests: render, assert, destroy
Some behavior only exists once a component is rendered: column configuration, bindings, visibility rules. For these, render the real view into the document body, give it local data, and always destroy it afterwards. Leaked components are the most common source of flaky Ext JS suites, because they leave DOM nodes, listeners, and global registrations behind.
describe('Acme.view.account.AccountGrid', function () {
var grid;
beforeEach(function () {
grid = Ext.create('Acme.view.account.AccountGrid', {
renderTo: Ext.getBody(),
width: 600,
height: 300,
viewModel: {
stores: {
accounts: {
model: 'Acme.model.Account',
proxy: { type: 'memory' },
data: [{ id: 1, code: '1.1', description: 'Cash' }]
}
}
}
});
// Make sure bindings have been resolved before asserting
grid.getViewModel().notify();
});
afterEach(function () {
Ext.destroy(grid);
grid = null;
});
it('shows one row per account', function () {
expect(grid.getStore().getCount()).toBe(1);
expect(grid.getView().getNodes().length).toBe(1);
});
it('has the expected columns', function () {
var dataIndexes = Ext.Array.pluck(grid.getColumns(), 'dataIndex');
expect(dataIndexes).toEqual(['code', 'description']);
});
});
Two habits make these tests reliable:
- Pass your own view model data instead of letting the component load from the real API.
- Call notify() on the view model (or wait on a condition) before asserting anything that depends on bindings, since they resolve asynchronously.
6. End to end: few tests, critical paths only
End-to-end tests are the most expensive to write and maintain, so be selective. Cover the journeys whose failure would be a real incident, such as logging in, creating a record, and saving a form. Do not re-test validation rules or sorting here; the lower layers already do that, faster.
The biggest source of flakiness in Ext JS end-to-end tests is asynchrony. The DOM exists before the data does, and selectors tied to generated IDs (ext-gen1234) change between runs. Two rules help:
- Locate components by their xtype or a stable itemId, not by generated DOM IDs. Ext.ComponentQuery is built for this.
- Wait for application state, not for a fixed number of seconds.
If you use a general-purpose browser automation tool, you can query the component tree directly from the test:
// Playwright example: wait until the grid has finished loading
await page.waitForFunction(() => {
const grid = Ext.ComponentQuery.query('accountgrid')[0];
return grid && grid.getStore().isLoaded() && !grid.getStore().isLoading();
});
const rowCount = await page.evaluate(
() => Ext.ComponentQuery.query('accountgrid')[0].getStore().getCount()
);
expect(rowCount).toBeGreaterThan(0);
Sencha Test offers purpose-built tooling for this, including component-aware locators and “futures” that wait for a component or condition automatically, so it is worth evaluating if your suite grows beyond a handful of journeys.
7. Make testability a design rule
Most testing pain in Ext JS is architectural pain in disguise. A few rules from earlier in this series pay off directly here:
- Keep logic out of views. If it is in a controller, you can test it without rendering.
- Inject data sources through the view model instead of instantiating stores inside components.
- Define validation on models, so there is one place to test it.
- Always destroy what you create in afterEach, so tests cannot affect each other.
A short checklist
- Are models, validators, and formatters covered by fast unit tests?
- Can you test a controller method without rendering a view?
- Do component tests supply their own data and destroy the component afterwards?
- Do end-to-end tests locate components by xtype or itemId and wait on state instead of timers?
- Are end-to-end tests limited to the journeys that would be real incidents if they broke?
A suite built this way runs in minutes, fails for real reasons, and gives a team the confidence to refactor, which is the whole point of testing in the first place.
The Ext JS data grid can do almost anything an enterprise screen needs: grouping, summaries,…
The data grid gets most of the attention in enterprise React apps, and for good…
Most teams choose a React UI component library by looking at buttons, inputs, themes and…




