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 does not 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 .eslintrc.json includes an override for *.test.js files that enables
the jest environment and allows global-require. 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 will discover and run 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. This
means 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.