Skip to content

Commit f2fb245

Browse files
authored
[#2394] Added Jest for JavaScript unit testing. (#2418)
1 parent a47c487 commit f2fb245

File tree

163 files changed

+5605
-324
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

163 files changed

+5605
-324
lines changed

.ahoy.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,16 @@ commands:
278278
cmd: ahoy cli vendor/bin/phpunit --testsuite=functional "$@"
279279

280280
test-functional-javascript:
281-
aliases: [test-js]
282281
usage: Run PHPUnit functional JavaScript tests.
283282
cmd: ahoy cli vendor/bin/phpunit --testsuite=functional-javascript "$@"
284283
#;> TOOL_PHPUNIT
285284

285+
#;< TOOL_JEST
286+
test-js:
287+
usage: Run Jest JavaScript unit tests.
288+
cmd: ahoy cli "yarn test"
289+
#;> TOOL_JEST
290+
286291
#;< TOOL_BEHAT
287292
test-bdd:
288293
usage: Run BDD tests.

.circleci/config.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ jobs:
363363
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c " \
364364
if [ -n \"${PACKAGE_TOKEN:-}\" ]; then export COMPOSER_AUTH='{\"github-oauth\": {\"github.com\": \"${PACKAGE_TOKEN-}\"}}'; fi && \
365365
COMPOSER_MEMORY_LIMIT=-1 composer --ansi install --prefer-dist"
366+
#;< TOOL_JEST
367+
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c "yarn install --frozen-lockfile"
368+
#;> TOOL_JEST
366369
367370
- run:
368371
name: Provision site
@@ -380,6 +383,12 @@ jobs:
380383
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli ./scripts/vortex/provision.sh
381384
no_output_timeout: 30m
382385

386+
#;< TOOL_JEST
387+
- run:
388+
name: Test with Jest
389+
command: docker compose exec -T cli bash -c "yarn test" || [ "${VORTEX_CI_JEST_IGNORE_FAILURE:-0}" -eq 1 ]
390+
#;> TOOL_JEST
391+
383392
#;< TOOL_PHPUNIT
384393
- run:
385394
name: Test with PHPUnit

.circleci/vortex-test-common.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ jobs:
229229
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c " \
230230
if [ -n \"${PACKAGE_TOKEN:-}\" ]; then export COMPOSER_AUTH='{\"github-oauth\": {\"github.com\": \"${PACKAGE_TOKEN-}\"}}'; fi && \
231231
COMPOSER_MEMORY_LIMIT=-1 composer --ansi install --prefer-dist"
232+
#;< TOOL_JEST
233+
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c "yarn install --frozen-lockfile"
234+
#;> TOOL_JEST
232235
233236
- run:
234237
name: Provision site
@@ -246,6 +249,12 @@ jobs:
246249
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli ./scripts/vortex/provision.sh
247250
no_output_timeout: 30m
248251

252+
#;< TOOL_JEST
253+
- run:
254+
name: Test with Jest
255+
command: docker compose exec -T cli bash -c "yarn test" || [ "${VORTEX_CI_JEST_IGNORE_FAILURE:-0}" -eq 1 ]
256+
#;> TOOL_JEST
257+
249258
#;< TOOL_PHPUNIT
250259
- run:
251260
name: Test with PHPUnit

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ drush/contrib/
4141
!composer.lock
4242
!gherkinlint.json
4343
!package-lock.json
44+
!jest.config.js
4445
!package.json
4546
!patches
4647
!phpcs.xml

.eslintrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@
6161
"operator-linebreak": ["error", "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
6262
"yml/indent": ["error", 2]
6363
},
64+
"overrides": [
65+
{
66+
"files": ["*.test.js"],
67+
"env": { "jest": true },
68+
"rules": {
69+
"no-eval": "off",
70+
"max-nested-callbacks": ["warn", 5],
71+
"jsdoc/check-tag-names": "off"
72+
}
73+
}
74+
],
6475
"settings": {
6576
"jsdoc": {
6677
"tagNamePreference": {

.github/workflows/build-test-deploy.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ jobs:
407407
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c " \
408408
if [ -n \"${PACKAGE_TOKEN:-}\" ]; then export COMPOSER_AUTH='{\"github-oauth\": {\"github.com\": \"${PACKAGE_TOKEN-}\"}}'; fi && \
409409
COMPOSER_MEMORY_LIMIT=-1 composer --ansi install --prefer-dist"
410+
#;< TOOL_JEST
411+
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c "yarn install --frozen-lockfile"
412+
#;> TOOL_JEST
410413
411414
- name: Provision site
412415
run: |
@@ -423,6 +426,13 @@ jobs:
423426
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli ./scripts/vortex/provision.sh
424427
timeout-minutes: 30
425428

429+
#;< TOOL_JEST
430+
- name: Test with Jest
431+
if: ${{ matrix.instance == 0 || strategy.job-total == 1 }}
432+
run: docker compose exec -T cli bash -c "yarn test"
433+
continue-on-error: ${{ vars.VORTEX_CI_JEST_IGNORE_FAILURE == '1' }}
434+
#;> TOOL_JEST
435+
426436
#;< TOOL_PHPUNIT
427437
- name: Test with PHPUnit
428438
if: ${{ matrix.instance == 0 || strategy.job-total == 1 }}

.vortex/CLAUDE.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ When updating template scripts:
8888
**NEVER run without explicit user permission**:
8989

9090
- `ahoy update-snapshots`
91-
- `UPDATE_SNAPSHOTS=1 ./vendor/bin/phpunit`
92-
- Any `UPDATE_SNAPSHOTS=1` command
9391

9492
These modify many files and take 10-15 minutes.
9593

.vortex/docs/.utils/variables/extra/ci.variables.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ VORTEX_CI_BEHAT_IGNORE_FAILURE=0
6969
# Test Behat profile to use in CI. If not set, the `default` profile will be used.
7070
VORTEX_CI_BEHAT_PROFILE=
7171

72+
# Ignore Jest test failures.
73+
VORTEX_CI_JEST_IGNORE_FAILURE=0
74+
7275
# Directory to store test results in CI.
7376
VORTEX_CI_TEST_RESULTS=/tmp/tests
7477

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
sidebar_label: Jest
3+
sidebar_position: 6
4+
---
5+
6+
# Jest
7+
8+
**Vortex** uses [Jest](https://jestjs.io/) as a framework for JavaScript unit
9+
testing. Jest tests verify that JavaScript behaviors in custom Drupal modules
10+
work correctly in isolation, without requiring a browser or Drupal bootstrap.
11+
12+
For running tests, configuration, and CI settings, see the
13+
[Jest tool reference](/docs/tools/jest).
14+
15+
## Test file structure
16+
17+
Test files are co-located with source files in the `js/` directory of each
18+
custom module:
19+
20+
```text
21+
web/modules/custom/my_module/
22+
└── js/
23+
├── my_module.js # Source file (Drupal behavior)
24+
└── my_module.test.js # Jest test file
25+
```
26+
27+
Jest automatically discovers `*.test.js` files in `web/modules/custom/*/js/`
28+
directories. Adding a new module with tests requires no configuration changes.
29+
30+
## Writing tests
31+
32+
Drupal JavaScript uses the IIFE pattern `((Drupal) => { ... })(Drupal)` where
33+
`Drupal` is a global object. Tests load these source files using `eval()` after
34+
setting up the required globals.
35+
36+
### Test template
37+
38+
```javascript
39+
/**
40+
* @jest-environment jsdom
41+
*/
42+
43+
const fs = require('fs');
44+
const path = require('path');
45+
46+
describe('Drupal.behaviors.myModule', () => {
47+
beforeEach(() => {
48+
localStorage.clear();
49+
global.Drupal = { behaviors: {} };
50+
51+
const filePath = path.resolve(__dirname, 'my_module.js');
52+
const code = fs.readFileSync(filePath, 'utf8');
53+
eval(code);
54+
});
55+
56+
afterEach(() => {
57+
delete global.Drupal;
58+
});
59+
60+
it('should attach behavior to the context', () => {
61+
document.body.innerHTML = '<div data-my-module></div>';
62+
Drupal.behaviors.myModule.attach(document);
63+
64+
const el = document.querySelector('[data-my-module]');
65+
expect(el.classList.contains('processed')).toBe(true);
66+
});
67+
});
68+
```
69+
70+
### Loading Drupal behaviors
71+
72+
The `eval(fs.readFileSync(...))` pattern executes the source file's IIFE, which
73+
receives `global.Drupal` as its `Drupal` parameter and registers the behavior.
74+
After `eval()`, the behavior is accessible via `Drupal.behaviors.myModule`.
75+
76+
### Mocking globals
77+
78+
Set globals in `beforeEach` and clean them up in `afterEach`:
79+
80+
| Global | Setup | When needed |
81+
|--------|-------|-------------|
82+
| `Drupal` | `global.Drupal = { behaviors: {} }` | Always — required by all Drupal behaviors |
83+
| `jQuery` | `global.jQuery = require('jquery')` or a mock | When the source file uses `jQuery` or `$` |
84+
| `drupalSettings` | `global.drupalSettings = { path: { baseUrl: '/' } }` | When the source file reads `drupalSettings` |
85+
| `localStorage` | Provided by jsdom; call `localStorage.clear()` | When the source file uses `localStorage` |
86+
87+
### Testing DOM interactions
88+
89+
The `jsdom` environment provides `document` and `window`. Set up HTML before
90+
each test:
91+
92+
```javascript
93+
document.body.innerHTML = `
94+
<div data-my-widget>
95+
<button data-action="save">Save</button>
96+
<span data-status></span>
97+
</div>
98+
`;
99+
100+
Drupal.behaviors.myModule.attach(document);
101+
document.querySelector('[data-action="save"]').click();
102+
103+
expect(document.querySelector('[data-status]').textContent).toBe('Saved');
104+
```
105+
106+
### Testing timed behavior
107+
108+
Use Jest fake timers for `setTimeout` and `setInterval`:
109+
110+
```javascript
111+
jest.useFakeTimers();
112+
113+
Drupal.behaviors.myModule.startPolling();
114+
jest.advanceTimersByTime(5000);
115+
116+
expect(fetchSpy).toHaveBeenCalledTimes(5);
117+
118+
jest.useRealTimers();
119+
```
120+
121+
### ESLint compatibility
122+
123+
The `.eslintrc.json` includes an override for `*.test.js` files that enables
124+
the `jest` environment and allows `eval()`. No additional ESLint configuration
125+
is needed for test files.
126+
127+
## Boilerplate
128+
129+
**Vortex** provides a Jest test boilerplate for the [demo module](https://github.com/drevops/vortex/blob/main/web/modules/custom/ys_demo/js/ys_demo.test.js)
130+
that demonstrates testing a counter block with DOM manipulation, localStorage
131+
interaction, and event handling.
132+
133+
This boilerplate test runs in continuous integration pipeline when you install
134+
**Vortex** and can be used as a starting point for writing your own JavaScript
135+
tests.

0 commit comments

Comments
 (0)