Jest
Vortex uses Jest as a framework for JavaScript unit testing. Jest tests verify that JavaScript behaviors in custom Drupal modules work correctly in isolation, without requiring a browser or Drupal bootstrap.
Vortex comes with a pre-configured Jest setup.
Usage
- Ahoy
- Docker Compose
ahoy test-js # Run all Jest tests.
docker compose exec cli bash -c "npm test" # Run all Jest tests.
Running tests matching a pattern
ahoy test-js doesn't forward extra arguments, so run subsets through
ahoy cli:
- Ahoy
- Docker Compose
ahoy cli "npm test -- ys_demo"
docker compose exec cli bash -c "npm test -- ys_demo"
Running a specific test by name
- Ahoy
- Docker Compose
ahoy cli "npm test -- -t 'should increment the value'"
docker compose exec cli bash -c \
"npm test -- -t 'should increment the value'"
Writing tests
Drupal JavaScript uses the IIFE pattern ((Drupal) => { ... })(Drupal)
where Drupal is a global object. Tests load these source files using
require() after setting up the required globals.
Test file structure
Test files are placed in a tests/ subdirectory within the js/ directory
of each custom module:
web/modules/custom/my_module/
└── js/
├── my_module.js # Source file (Drupal behavior)
└── tests/
└── my_module.test.js # Jest test file
Test template
/**
* @jest-environment jsdom
*/
describe('Drupal.behaviors.myModule', () => {
beforeEach(() => {
localStorage.clear();
global.Drupal = { behaviors: {} };
jest.resetModules();
// eslint-disable-next-line global-require
require('../my_module.js');
});
afterEach(() => {
delete global.Drupal;
});
it('should attach behavior to the context', () => {
document.body.innerHTML = '<div data-my-module></div>';
Drupal.behaviors.myModule.attach(document);
const el = document.querySelector('[data-my-module]');
expect(el.classList.contains('processed')).toBe(true);
});
});
Loading Drupal behaviors
The require() call executes the source file's IIFE, which receives
global.Drupal as its Drupal parameter and registers the behavior. After
require(), the behavior is accessible via Drupal.behaviors.myModule.
The jest.resetModules() call before require() clears the module cache so
the IIFE re-executes on each test with a fresh global.Drupal object.
Mocking globals
Set globals in beforeEach and clean them up in afterEach:
| Global | Setup | When needed |
|---|---|---|
Drupal | global.Drupal = { behaviors: {} } | Always (required by all Drupal behaviors) |
jQuery | global.jQuery = require('jquery') or a mock | When the source file uses jQuery or $ |
drupalSettings | global.drupalSettings = { path: { baseUrl: '/' } } | When the source file reads drupalSettings |
localStorage | Provided by jsdom; call localStorage.clear() | When the source file uses localStorage |
Testing DOM interactions
The jsdom environment provides document and window. Set up HTML before
each test:
document.body.innerHTML = `
<div data-my-widget>
<button data-action="save">Save</button>
<span data-status></span>
</div>
`;
Drupal.behaviors.myModule.attach(document);
document.querySelector('[data-action="save"]').click();
expect(document.querySelector('[data-status]').textContent).toBe('Saved');
Testing timed behavior
Use Jest fake timers for setTimeout and setInterval:
jest.useFakeTimers();
Drupal.behaviors.myModule.startPolling();
jest.advanceTimersByTime(5000);
expect(fetchSpy).toHaveBeenCalledTimes(5);
jest.useRealTimers();
ESLint compatibility
The eslint.config.mjs includes an override for *.test.js files that adds
the Jest globals and relaxes the nesting limit that test suites routinely
exceed. No additional ESLint configuration is needed for test files.
Boilerplate
Vortex provides a Jest test boilerplate for the demo module that demonstrates testing a counter block with DOM manipulation, localStorage interaction, and event handling.
This boilerplate test runs in the continuous integration pipeline when you install Vortex and can be used as a starting point for writing your own JavaScript tests.
Configuration
See Jest configuration reference.
All global configuration takes place in the
jest.config.js
file.
By default, Jest discovers and runs test files with the .test.js extension
in web/modules/custom/*/js/ directories and their subdirectories.
The configuration uses the jsdom test environment to provide browser
globals like document, window, and localStorage.
Test discovery
The jest.config.js file dynamically scans web/modules/custom/ for
modules that contain a js/ directory, and adds each as a root for test
discovery. Adding a new custom module with JavaScript tests requires no
configuration changes.
Reports
Coverage
Jest collects code coverage automatically when running tests via
ahoy test-js. Coverage is configured in
jest.config.js
with the following settings:
- Source files: all
*.jsfiles inweb/modules/custom/*/js/directories, excluding*.test.jsfiles - Reporters: text (terminal summary), lcov, HTML, and Cobertura XML
- Output directory:
.logs/coverage/jest
After running tests, coverage reports are available at:
| Report | Location |
|---|---|
| Terminal summary | Printed to stdout during test run |
| HTML report | .logs/coverage/jest/lcov-report/index.html |
| LCOV data | .logs/coverage/jest/lcov.info |
| Cobertura XML | .logs/coverage/jest/cobertura.xml |
Continuous integration
In the continuous integration pipeline, Jest runs on the first container only, see Test parallelism.
The Cobertura XML report is used by continuous integration to track coverage trends and can be consumed by CI tools that support the Cobertura format.
Ignoring failures
Set the VORTEX_CI_JEST_IGNORE_FAILURE environment variable to 1 to
ignore failures. The tool still runs and reports violations. See Ignore
tool
failures.