Skip to content

Commit c505a46

Browse files
committed
Add Jest front-end test suite
1 parent 580b724 commit c505a46

File tree

8 files changed

+5006
-1
lines changed

8 files changed

+5006
-1
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Front-end tests
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
8+
jobs:
9+
js-tests:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: '20'
16+
cache: 'npm'
17+
- name: Install dependencies
18+
run: npm ci
19+
- name: Run front-end tests
20+
run: npm test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/vendor/
22
/tmp/
3+
/node_modules/

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ add_filter('blc_load_retry_delay', function (int $delay): int {
4747
## Développement
4848
- PHP 7.3 ou supérieur et [Composer](https://getcomposer.org/) sont requis pour installer les dépendances de développement.
4949
- Installer les dépendances : `composer install`.
50-
- Exécuter la suite de tests : `vendor/bin/phpunit`.
50+
- Installer les dépendances front : `npm install`.
51+
- Exécuter les tests PHP : `vendor/bin/phpunit`.
52+
- Exécuter les tests front : `npm test` (ou `composer test:js`).
5153

5254
## Auteur
5355
Jérôme Le Gousse

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
"psr-4": {
88
"Tests\\": "tests/"
99
}
10+
},
11+
"scripts": {
12+
"test:js": "npm test"
1013
}
1114
}

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: '@wordpress/jest-preset-default',
3+
testEnvironment: 'jsdom',
4+
testMatch: ['**/__tests__/**/*.test.js']
5+
};
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
const path = require('path');
2+
3+
describe('blc-admin-scripts modal interactions', () => {
4+
let $;
5+
6+
const defaultMessages = {
7+
empty: 'Veuillez saisir une URL.',
8+
invalid: 'Veuillez saisir une URL valide.',
9+
same: "La nouvelle URL doit être différente de l'URL actuelle.",
10+
genericError: 'Une erreur est survenue. Veuillez réessayer.',
11+
prefixedError: 'Erreur : '
12+
};
13+
14+
function setupDom() {
15+
document.body.innerHTML = `
16+
<div id="blc-modal" class="blc-modal" role="presentation" aria-hidden="true">
17+
<div class="blc-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="blc-modal-title">
18+
<button type="button" class="blc-modal__close" aria-label="Fermer"></button>
19+
<h2 id="blc-modal-title" class="blc-modal__title"></h2>
20+
<p class="blc-modal__message"></p>
21+
<div class="blc-modal__error" role="alert" aria-live="assertive"></div>
22+
<div class="blc-modal__field">
23+
<label for="blc-modal-url" class="blc-modal__label"></label>
24+
<input type="url" id="blc-modal-url" class="blc-modal__input" value="" />
25+
</div>
26+
<div class="blc-modal__actions">
27+
<button type="button" class="button button-secondary blc-modal__cancel">Annuler</button>
28+
<button type="button" class="button button-primary blc-modal__confirm">Mettre à jour</button>
29+
</div>
30+
</div>
31+
</div>
32+
<table>
33+
<tbody id="the-list">
34+
<tr data-row-id="row-1" style="opacity: 1;">
35+
<td>
36+
<a
37+
href="#"
38+
class="blc-edit-link"
39+
data-url="https://old.example"
40+
data-postid="42"
41+
data-row-id="row-1"
42+
data-occurrence-index="0"
43+
data-nonce="nonce"
44+
>Modifier</a>
45+
<button
46+
type="button"
47+
class="blc-unlink"
48+
data-url="https://old.example"
49+
data-postid="42"
50+
data-row-id="row-1"
51+
data-occurrence-index="0"
52+
data-nonce="nonce"
53+
>Supprimer</button>
54+
</td>
55+
</tr>
56+
</tbody>
57+
</table>
58+
`;
59+
}
60+
61+
beforeEach(() => {
62+
jest.resetModules();
63+
jest.useFakeTimers();
64+
setupDom();
65+
$ = require('jquery');
66+
// Exécute immédiatement les callbacks `$(document).ready(...)`.
67+
$.fn.ready = function (fn) {
68+
fn.call(document, $);
69+
return this;
70+
};
71+
// En environnement JSDOM, `:visible` renvoie `false` par défaut : on le force à `true`
72+
// pour pouvoir tester la gestion du focus dans la modale.
73+
$.expr.pseudos.visible = () => true;
74+
$.expr.pseudos.hidden = () => false;
75+
// Simplifie les animations jQuery utilisées lors de la suppression de lignes.
76+
$.fn.fadeOut = function (_duration, callback) {
77+
if (typeof callback === 'function') {
78+
this.each(function () {
79+
callback.call(this);
80+
});
81+
}
82+
return this;
83+
};
84+
$.post = jest.fn();
85+
global.jQuery = $;
86+
global.$ = $;
87+
window.jQuery = $;
88+
window.$ = $;
89+
global.ajaxurl = 'admin-ajax.php';
90+
delete window.blcAdminMessages;
91+
require(path.resolve(__dirname, '../blc-admin-scripts.js'));
92+
expect($('#blc-modal').length).toBe(1);
93+
expect($('#blc-modal').attr('tabindex')).toBe('-1');
94+
});
95+
96+
afterEach(() => {
97+
jest.runOnlyPendingTimers();
98+
jest.useRealTimers();
99+
document.body.innerHTML = '';
100+
delete global.jQuery;
101+
delete global.$;
102+
delete window.jQuery;
103+
delete window.$;
104+
delete global.ajaxurl;
105+
});
106+
107+
function openEditModal() {
108+
const link = $('#the-list .blc-edit-link');
109+
expect($('#the-list').length).toBe(1);
110+
expect(link.length).toBe(1);
111+
link.trigger('click');
112+
jest.advanceTimersByTime(20);
113+
return $('#blc-modal');
114+
}
115+
116+
function mockAjaxHandlers() {
117+
let doneHandler = () => {};
118+
let failHandler = () => {};
119+
// Simule l'objet renvoyé par `$.post` pour piloter manuellement les callbacks `.done()`/`.fail()`.
120+
$.post.mockImplementation(() => ({
121+
done(handler) {
122+
doneHandler = handler;
123+
return this;
124+
},
125+
fail(handler) {
126+
failHandler = handler;
127+
return this;
128+
}
129+
}));
130+
return {
131+
triggerSuccess(response) {
132+
doneHandler(response);
133+
},
134+
triggerFailure(error) {
135+
failHandler(error);
136+
}
137+
};
138+
}
139+
140+
test('opens and closes the modal via edit link interactions', () => {
141+
const modal = openEditModal();
142+
143+
expect(modal.hasClass('is-open')).toBe(true);
144+
expect(document.body.classList.contains('blc-modal-open')).toBe(true);
145+
146+
modal.find('.blc-modal__cancel').trigger('click');
147+
148+
expect(modal.hasClass('is-open')).toBe(false);
149+
expect(document.body.classList.contains('blc-modal-open')).toBe(false);
150+
});
151+
152+
test('validates user input before submitting changes', () => {
153+
const modal = openEditModal();
154+
const input = modal.find('.blc-modal__input');
155+
const confirm = modal.find('.blc-modal__confirm');
156+
const error = modal.find('.blc-modal__error');
157+
158+
input.val(' ');
159+
confirm.trigger('click');
160+
expect(error.text()).toBe(defaultMessages.empty);
161+
162+
input.val('https://exa mple.com');
163+
confirm.trigger('click');
164+
expect(error.text()).toBe(defaultMessages.invalid);
165+
166+
input.val('https://old.example');
167+
confirm.trigger('click');
168+
expect(error.text()).toBe(defaultMessages.same);
169+
});
170+
171+
test('keeps focus trapped inside the modal when tabbing', () => {
172+
const modal = openEditModal();
173+
const closeButton = modal.find('.blc-modal__close');
174+
const input = modal.find('.blc-modal__input');
175+
const cancelButton = modal.find('.blc-modal__cancel');
176+
const confirmButton = modal.find('.blc-modal__confirm');
177+
178+
input[0].focus();
179+
const tabFromInput = $.Event('keydown', { key: 'Tab' });
180+
input.trigger(tabFromInput);
181+
expect(tabFromInput.isDefaultPrevented()).toBe(true);
182+
expect(document.activeElement).toBe(cancelButton[0]);
183+
184+
confirmButton[0].focus();
185+
const tabFromConfirm = $.Event('keydown', { key: 'Tab' });
186+
confirmButton.trigger(tabFromConfirm);
187+
expect(tabFromConfirm.isDefaultPrevented()).toBe(true);
188+
expect(document.activeElement).toBe(closeButton[0]);
189+
190+
closeButton[0].focus();
191+
const shiftTabFromClose = $.Event('keydown', { key: 'Tab', shiftKey: true });
192+
closeButton.trigger(shiftTabFromClose);
193+
expect(shiftTabFromClose.isDefaultPrevented()).toBe(true);
194+
expect(document.activeElement).toBe(confirmButton[0]);
195+
});
196+
197+
test('closes the modal and removes the row when the AJAX response succeeds', () => {
198+
const ajax = mockAjaxHandlers();
199+
const modal = openEditModal();
200+
const input = modal.find('.blc-modal__input');
201+
const confirm = modal.find('.blc-modal__confirm');
202+
const row = $('#the-list tr');
203+
204+
input.val('https://new.example');
205+
confirm.trigger('click');
206+
207+
expect(modal.hasClass('is-submitting')).toBe(true);
208+
expect(confirm.prop('disabled')).toBe(true);
209+
expect(row.css('opacity')).toBe('0.5');
210+
211+
ajax.triggerSuccess({ success: true });
212+
213+
expect(modal.hasClass('is-open')).toBe(false);
214+
expect($('#the-list tr').length).toBe(0);
215+
expect(document.body.classList.contains('blc-modal-open')).toBe(false);
216+
});
217+
218+
test('restores the UI and surfaces an error when the AJAX response fails', () => {
219+
const ajax = mockAjaxHandlers();
220+
const modal = openEditModal();
221+
const input = modal.find('.blc-modal__input');
222+
const confirm = modal.find('.blc-modal__confirm');
223+
const row = $('#the-list tr');
224+
225+
input.val('https://new.example');
226+
confirm.trigger('click');
227+
ajax.triggerSuccess({ success: false, data: { message: 'Serveur indisponible' } });
228+
229+
expect(modal.hasClass('is-open')).toBe(true);
230+
expect(modal.hasClass('is-submitting')).toBe(false);
231+
expect(confirm.prop('disabled')).toBe(false);
232+
expect(row.css('opacity')).toBe('1');
233+
expect(modal.find('.blc-modal__error').text()).toBe(`${defaultMessages.prefixedError}Serveur indisponible`);
234+
});
235+
236+
test('shows a generic error message when the AJAX request is rejected', () => {
237+
const ajax = mockAjaxHandlers();
238+
const modal = openEditModal();
239+
const input = modal.find('.blc-modal__input');
240+
const confirm = modal.find('.blc-modal__confirm');
241+
const row = $('#the-list tr');
242+
243+
input.val('https://new.example');
244+
confirm.trigger('click');
245+
ajax.triggerFailure(new Error('Network error'));
246+
247+
expect(modal.hasClass('is-open')).toBe(true);
248+
expect(modal.hasClass('is-submitting')).toBe(false);
249+
expect(confirm.prop('disabled')).toBe(false);
250+
expect(row.css('opacity')).toBe('1');
251+
expect(modal.find('.blc-modal__error').text()).toBe(defaultMessages.genericError);
252+
});
253+
});

0 commit comments

Comments
 (0)