Skip to content

Commit 5da9dbc

Browse files
authored
Tests: Add Jest unit tests for core Model and Field classes (#340)
1 parent 6e94700 commit 5da9dbc

File tree

9 files changed

+3307
-0
lines changed

9 files changed

+3307
-0
lines changed

tests/js/field.test.js

Lines changed: 746 additions & 0 deletions
Large diffs are not rendered by default.

tests/js/fields.test.js

Lines changed: 407 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
/**
2+
* Unit tests for button_group field type
3+
*/
4+
5+
describe( 'Button Group Field', () => {
6+
let fieldDefinition;
7+
let mockField;
8+
9+
beforeEach( () => {
10+
// Reset mocks
11+
fieldDefinition = null;
12+
13+
// Mock @wordpress/icons
14+
jest.mock( '@wordpress/icons', () => ( { update: 'update-icon' } ), {
15+
virtual: true,
16+
} );
17+
18+
// Mock acf.Field.extend to capture the field definition
19+
global.acf = {
20+
Field: {
21+
extend: jest.fn( ( definition ) => {
22+
fieldDefinition = definition;
23+
return definition;
24+
} ),
25+
},
26+
registerFieldType: jest.fn(),
27+
};
28+
29+
// Load the button_group field module
30+
jest.isolateModules( () => {
31+
require( '../../../assets/src/js/_acf-field-button-group.js' );
32+
} );
33+
34+
// Create a mock field instance with the captured definition
35+
mockField = {
36+
...fieldDefinition,
37+
$: jest.fn(),
38+
get: jest.fn(),
39+
};
40+
} );
41+
42+
afterEach( () => {
43+
delete global.acf;
44+
jest.resetModules();
45+
} );
46+
47+
describe( 'Field Definition', () => {
48+
it( 'should have type "button_group"', () => {
49+
expect( fieldDefinition.type ).toBe( 'button_group' );
50+
} );
51+
52+
it( 'should register click and keydown events', () => {
53+
expect( fieldDefinition.events ).toEqual( {
54+
'click input[type="radio"]': 'onClick',
55+
'keydown label': 'onKeyDown',
56+
} );
57+
} );
58+
} );
59+
60+
describe( '$control()', () => {
61+
it( 'should find .acf-button-group element', () => {
62+
fieldDefinition.$control.call( mockField );
63+
64+
expect( mockField.$ ).toHaveBeenCalledWith( '.acf-button-group' );
65+
} );
66+
} );
67+
68+
describe( '$input()', () => {
69+
it( 'should find checked input element', () => {
70+
fieldDefinition.$input.call( mockField );
71+
72+
expect( mockField.$ ).toHaveBeenCalledWith( 'input:checked' );
73+
} );
74+
} );
75+
76+
describe( 'initialize()', () => {
77+
it( 'should call updateButtonStates', () => {
78+
const updateSpy = jest.fn();
79+
mockField.updateButtonStates = updateSpy;
80+
81+
fieldDefinition.initialize.call( mockField );
82+
83+
expect( updateSpy ).toHaveBeenCalled();
84+
} );
85+
} );
86+
87+
describe( 'setValue()', () => {
88+
it( 'should check the input with matching value and trigger change', () => {
89+
const mockInput = {
90+
prop: jest.fn().mockReturnThis(),
91+
trigger: jest.fn().mockReturnThis(),
92+
};
93+
mockField.$ = jest.fn().mockReturnValue( mockInput );
94+
mockField.updateButtonStates = jest.fn();
95+
96+
fieldDefinition.setValue.call( mockField, 'option2' );
97+
98+
expect( mockField.$ ).toHaveBeenCalledWith(
99+
'input[value="option2"]'
100+
);
101+
expect( mockInput.prop ).toHaveBeenCalledWith( 'checked', true );
102+
expect( mockInput.trigger ).toHaveBeenCalledWith( 'change' );
103+
} );
104+
105+
it( 'should update button states after setting value', () => {
106+
const updateSpy = jest.fn();
107+
mockField.$ = jest.fn().mockReturnValue( {
108+
prop: jest.fn().mockReturnThis(),
109+
trigger: jest.fn().mockReturnThis(),
110+
} );
111+
mockField.updateButtonStates = updateSpy;
112+
113+
fieldDefinition.setValue.call( mockField, 'option1' );
114+
115+
expect( updateSpy ).toHaveBeenCalled();
116+
} );
117+
} );
118+
119+
describe( 'updateButtonStates()', () => {
120+
let mockLabels;
121+
let mockInput;
122+
let mockInputLabel;
123+
124+
beforeEach( () => {
125+
mockInputLabel = {
126+
addClass: jest.fn().mockReturnThis(),
127+
attr: jest.fn().mockReturnThis(),
128+
};
129+
mockInput = {
130+
length: 1,
131+
parent: jest.fn().mockReturnValue( mockInputLabel ),
132+
};
133+
mockLabels = {
134+
removeClass: jest.fn().mockReturnThis(),
135+
attr: jest.fn().mockReturnThis(),
136+
first: jest.fn().mockReturnThis(),
137+
};
138+
} );
139+
140+
it( 'should remove selected class and reset aria attributes on all labels', () => {
141+
const mockControl = {
142+
find: jest.fn().mockReturnValue( mockLabels ),
143+
};
144+
mockField.$control = jest.fn().mockReturnValue( mockControl );
145+
mockField.$input = jest.fn().mockReturnValue( mockInput );
146+
147+
fieldDefinition.updateButtonStates.call( mockField );
148+
149+
expect( mockLabels.removeClass ).toHaveBeenCalledWith( 'selected' );
150+
expect( mockLabels.attr ).toHaveBeenCalledWith(
151+
'aria-checked',
152+
'false'
153+
);
154+
expect( mockLabels.attr ).toHaveBeenCalledWith( 'tabindex', '-1' );
155+
} );
156+
157+
it( 'should mark selected input label with correct attributes', () => {
158+
const mockControl = {
159+
find: jest.fn().mockReturnValue( mockLabels ),
160+
};
161+
mockField.$control = jest.fn().mockReturnValue( mockControl );
162+
mockField.$input = jest.fn().mockReturnValue( mockInput );
163+
164+
fieldDefinition.updateButtonStates.call( mockField );
165+
166+
expect( mockInputLabel.addClass ).toHaveBeenCalledWith(
167+
'selected'
168+
);
169+
expect( mockInputLabel.attr ).toHaveBeenCalledWith(
170+
'aria-checked',
171+
'true'
172+
);
173+
expect( mockInputLabel.attr ).toHaveBeenCalledWith(
174+
'tabindex',
175+
'0'
176+
);
177+
} );
178+
179+
it( 'should set tabindex on first label when no input is checked', () => {
180+
mockInput.length = 0;
181+
const mockControl = {
182+
find: jest.fn().mockReturnValue( mockLabels ),
183+
};
184+
mockField.$control = jest.fn().mockReturnValue( mockControl );
185+
mockField.$input = jest.fn().mockReturnValue( mockInput );
186+
187+
fieldDefinition.updateButtonStates.call( mockField );
188+
189+
expect( mockLabels.first ).toHaveBeenCalled();
190+
expect( mockLabels.attr ).toHaveBeenCalledWith( 'tabindex', '0' );
191+
} );
192+
} );
193+
194+
describe( 'onClick()', () => {
195+
it( 'should call selectButton with parent label', () => {
196+
const selectSpy = jest.fn();
197+
mockField.selectButton = selectSpy;
198+
const mockLabel = { hasClass: jest.fn() };
199+
const mockEl = { parent: jest.fn().mockReturnValue( mockLabel ) };
200+
201+
fieldDefinition.onClick.call( mockField, {}, mockEl );
202+
203+
expect( mockEl.parent ).toHaveBeenCalledWith( 'label' );
204+
expect( selectSpy ).toHaveBeenCalledWith( mockLabel );
205+
} );
206+
} );
207+
208+
describe( 'onKeyDown()', () => {
209+
let mockLabels;
210+
let mockEvent;
211+
let mockLabel;
212+
213+
beforeEach( () => {
214+
mockLabel = {};
215+
mockLabels = {
216+
index: jest.fn().mockReturnValue( 1 ),
217+
length: 3,
218+
eq: jest.fn().mockReturnValue( {
219+
attr: jest.fn().mockReturnThis(),
220+
trigger: jest.fn().mockReturnThis(),
221+
} ),
222+
attr: jest.fn().mockReturnThis(),
223+
};
224+
const mockControl = {
225+
find: jest.fn().mockReturnValue( mockLabels ),
226+
};
227+
mockField.$control = jest.fn().mockReturnValue( mockControl );
228+
mockField.selectButton = jest.fn();
229+
} );
230+
231+
it( 'should select button on Space key (32)', () => {
232+
mockEvent = { which: 32, preventDefault: jest.fn() };
233+
234+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
235+
236+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
237+
expect( mockField.selectButton ).toHaveBeenCalledWith( mockLabel );
238+
} );
239+
240+
it( 'should select button on Enter key (13)', () => {
241+
mockEvent = { which: 13, preventDefault: jest.fn() };
242+
243+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
244+
245+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
246+
expect( mockField.selectButton ).toHaveBeenCalledWith( mockLabel );
247+
} );
248+
249+
it( 'should move to previous button on left arrow (37)', () => {
250+
mockEvent = { which: 37, preventDefault: jest.fn() };
251+
252+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
253+
254+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
255+
expect( mockLabels.eq ).toHaveBeenCalledWith( 0 ); // Previous index
256+
} );
257+
258+
it( 'should move to next button on right arrow (39)', () => {
259+
mockEvent = { which: 39, preventDefault: jest.fn() };
260+
261+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
262+
263+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
264+
expect( mockLabels.eq ).toHaveBeenCalledWith( 2 ); // Next index
265+
} );
266+
267+
it( 'should wrap to last button when at start and pressing left', () => {
268+
mockLabels.index.mockReturnValue( 0 );
269+
mockEvent = { which: 37, preventDefault: jest.fn() };
270+
271+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
272+
273+
expect( mockLabels.eq ).toHaveBeenCalledWith( 2 ); // Last index
274+
} );
275+
276+
it( 'should wrap to first button when at end and pressing right', () => {
277+
mockLabels.index.mockReturnValue( 2 );
278+
mockEvent = { which: 39, preventDefault: jest.fn() };
279+
280+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
281+
282+
expect( mockLabels.eq ).toHaveBeenCalledWith( 0 ); // First index
283+
} );
284+
285+
it( 'should move to previous on up arrow (38)', () => {
286+
mockEvent = { which: 38, preventDefault: jest.fn() };
287+
288+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
289+
290+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
291+
expect( mockLabels.eq ).toHaveBeenCalledWith( 0 );
292+
} );
293+
294+
it( 'should move to next on down arrow (40)', () => {
295+
mockEvent = { which: 40, preventDefault: jest.fn() };
296+
297+
fieldDefinition.onKeyDown.call( mockField, mockEvent, mockLabel );
298+
299+
expect( mockEvent.preventDefault ).toHaveBeenCalled();
300+
expect( mockLabels.eq ).toHaveBeenCalledWith( 2 );
301+
} );
302+
} );
303+
304+
describe( 'selectButton()', () => {
305+
let mockElement;
306+
let mockRadio;
307+
308+
beforeEach( () => {
309+
mockRadio = {
310+
prop: jest.fn().mockReturnThis(),
311+
trigger: jest.fn().mockReturnThis(),
312+
};
313+
mockElement = {
314+
find: jest.fn().mockReturnValue( mockRadio ),
315+
hasClass: jest.fn().mockReturnValue( false ),
316+
};
317+
mockField.updateButtonStates = jest.fn();
318+
mockField.get = jest.fn().mockReturnValue( false );
319+
} );
320+
321+
it( 'should check radio and trigger change', () => {
322+
fieldDefinition.selectButton.call( mockField, mockElement );
323+
324+
expect( mockElement.find ).toHaveBeenCalledWith(
325+
'input[type="radio"]'
326+
);
327+
expect( mockRadio.prop ).toHaveBeenCalledWith( 'checked', true );
328+
expect( mockRadio.trigger ).toHaveBeenCalledWith( 'change' );
329+
} );
330+
331+
it( 'should update button states after selection', () => {
332+
fieldDefinition.selectButton.call( mockField, mockElement );
333+
334+
expect( mockField.updateButtonStates ).toHaveBeenCalled();
335+
} );
336+
337+
it( 'should deselect when allow_null is true and already selected', () => {
338+
mockElement.hasClass.mockReturnValue( true );
339+
mockField.get = jest.fn( ( key ) =>
340+
key === 'allow_null' ? true : false
341+
);
342+
343+
fieldDefinition.selectButton.call( mockField, mockElement );
344+
345+
expect( mockRadio.prop ).toHaveBeenCalledWith( 'checked', false );
346+
} );
347+
348+
it( 'should not deselect when allow_null is false', () => {
349+
mockElement.hasClass.mockReturnValue( true );
350+
mockField.get = jest.fn().mockReturnValue( false );
351+
352+
fieldDefinition.selectButton.call( mockField, mockElement );
353+
354+
// First call is check true, should not be followed by check false
355+
expect( mockRadio.prop ).toHaveBeenCalledWith( 'checked', true );
356+
expect( mockRadio.prop ).not.toHaveBeenCalledWith(
357+
'checked',
358+
false
359+
);
360+
} );
361+
} );
362+
} );

0 commit comments

Comments
 (0)