PluginProbe
Extendify / 3.1.1
Extendify v3.1.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / unit / QuickEdit / components / modals / WPFormsFieldModal.test.jsx

WPFormsFieldModal.test.jsx in Extendify 3.1.1, at tests/unit/QuickEdit/components/modals/WPFormsFieldModal.test.jsx

336 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // WPFormsFieldModal loads a single WPForms field via loadWpFormsField, surfaces
2 // label / placeholder / description / required, and on Save sends ONLY the
3 // changed keys through saveWpFormsField — choices, validation, and conditional
4 // logic are explicitly untouched. The placeholder field is hidden for the
5 // `name` composite type and for select/checkbox/radio.
6
7 import { act, fireEvent, render, waitFor } from '@testing-library/react';
8
9 const mockLoadWpFormsField = jest.fn();
10 const mockSaveWpFormsField = jest.fn();
11 const mockTrack = jest.fn();
12 const mockPushUndo = jest.fn();
13
14 jest.mock('@quick-edit/lib/api', () => ({
15 loadWpFormsField: (...args) => mockLoadWpFormsField(...args),
16 saveWpFormsField: (...args) => mockSaveWpFormsField(...args),
17 }));
18 jest.mock('@quick-edit/lib/cmd-enter-save', () => ({
19 useCmdEnterSave: jest.fn(),
20 }));
21 jest.mock('@quick-edit/lib/insights', () => ({
22 track: (...args) => mockTrack(...args),
23 }));
24 jest.mock('@quick-edit/state/undo', () => ({
25 pushUndo: (...args) => mockPushUndo(...args),
26 }));
27
28 jest.mock('@wordpress/components', () => ({
29 Modal: ({ title, onRequestClose, children }) => (
30 <div role="dialog" aria-label={title}>
31 <button
32 type="button"
33 data-testid="modal-close"
34 onClick={onRequestClose}
35 />
36 {children}
37 </div>
38 ),
39 Button: ({ children, onClick, disabled, isBusy }) => (
40 <button
41 type="button"
42 onClick={onClick}
43 disabled={disabled}
44 data-busy={String(!!isBusy)}
45 >
46 {children}
47 </button>
48 ),
49 Notice: ({ children, status }) => (
50 <div role="alert" data-status={status}>
51 {children}
52 </div>
53 ),
54 Spinner: () => <span data-testid="spinner" />,
55 TextControl: ({ label, value, onChange }) => (
56 <label>
57 {label}
58 <input
59 aria-label={label}
60 value={value || ''}
61 onChange={(e) => onChange(e.target.value)}
62 />
63 </label>
64 ),
65 TextareaControl: ({ label, value, onChange, rows }) => (
66 <label>
67 {label}
68 <textarea
69 aria-label={label}
70 value={value || ''}
71 rows={rows}
72 onChange={(e) => onChange(e.target.value)}
73 />
74 </label>
75 ),
76 ToggleControl: ({ label, checked, onChange }) => (
77 <label>
78 {label}
79 <input
80 type="checkbox"
81 aria-label={label}
82 checked={!!checked}
83 onChange={(e) => onChange(e.target.checked)}
84 />
85 </label>
86 ),
87 }));
88
89 const importComponent = () =>
90 require('@quick-edit/components/modals/WPFormsFieldModal');
91
92 const fill = (label, value) => {
93 const input = document.querySelector(
94 `input[aria-label="${label}"], textarea[aria-label="${label}"]`,
95 );
96 fireEvent.change(input, { target: { value } });
97 };
98
99 const toggle = (label) => {
100 const input = document.querySelector(
101 `input[type="checkbox"][aria-label="${label}"]`,
102 );
103 fireEvent.click(input);
104 };
105
106 const clickByText = (text) => {
107 const btn = Array.from(document.querySelectorAll('button')).find(
108 (b) => b.textContent === text,
109 );
110 fireEvent.click(btn);
111 };
112
113 beforeEach(() => {
114 jest.clearAllMocks();
115 });
116
117 describe('WPFormsFieldModal — load', () => {
118 it('shows the Spinner before the load resolves', () => {
119 mockLoadWpFormsField.mockReturnValue(new Promise(() => {}));
120 const { WPFormsFieldModal } = importComponent();
121 render(
122 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={jest.fn()} />,
123 );
124 expect(document.querySelector('[data-testid="spinner"]')).not.toBeNull();
125 });
126
127 it('seeds all four inputs from the loaded field', async () => {
128 mockLoadWpFormsField.mockResolvedValue({
129 type: 'text',
130 label: 'Your name',
131 placeholder: 'e.g. Ada',
132 description: 'How to address you',
133 required: true,
134 });
135 const { WPFormsFieldModal } = importComponent();
136 render(
137 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={jest.fn()} />,
138 );
139 await waitFor(() =>
140 expect(
141 document.querySelector('input[aria-label="Label"]'),
142 ).not.toBeNull(),
143 );
144 expect(document.querySelector('input[aria-label="Label"]').value).toBe(
145 'Your name',
146 );
147 expect(
148 document.querySelector('input[aria-label="Placeholder"]').value,
149 ).toBe('e.g. Ada');
150 expect(
151 document.querySelector(
152 'textarea[aria-label="Description (shown below the field)"]',
153 ).value,
154 ).toBe('How to address you');
155 expect(
156 document.querySelector('input[aria-label="Required field"]').checked,
157 ).toBe(true);
158 });
159
160 it('renders a Notice when the load rejects', async () => {
161 mockLoadWpFormsField.mockRejectedValueOnce(new Error('field 404'));
162 const { WPFormsFieldModal } = importComponent();
163 render(
164 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={jest.fn()} />,
165 );
166 await waitFor(() =>
167 expect(document.querySelector('[role="alert"]')?.textContent).toMatch(
168 /Sorry, something went wrong/i,
169 ),
170 );
171 });
172 });
173
174 describe('WPFormsFieldModal — placeholder visibility', () => {
175 const cases = [
176 ['text', true],
177 ['textarea', true],
178 ['email', true],
179 ['name', false],
180 ['select', false],
181 ['checkbox', false],
182 ['radio', false],
183 ];
184
185 for (const [type, expected] of cases) {
186 it(`type=${type} → placeholder ${expected ? 'visible' : 'hidden'}`, async () => {
187 mockLoadWpFormsField.mockResolvedValue({
188 type,
189 label: 'x',
190 placeholder: '',
191 description: '',
192 required: false,
193 });
194 const { WPFormsFieldModal } = importComponent();
195 render(
196 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={jest.fn()} />,
197 );
198 await waitFor(() =>
199 expect(
200 document.querySelector('input[aria-label="Label"]'),
201 ).not.toBeNull(),
202 );
203 const placeholder = document.querySelector(
204 'input[aria-label="Placeholder"]',
205 );
206 if (expected) {
207 expect(placeholder).not.toBeNull();
208 } else {
209 expect(placeholder).toBeNull();
210 }
211 });
212 }
213 });
214
215 describe('WPFormsFieldModal — save', () => {
216 it('sends ONLY the changed keys + tracks save with the field type', async () => {
217 mockLoadWpFormsField.mockResolvedValue({
218 type: 'text',
219 label: 'Your name',
220 placeholder: 'Ada',
221 description: 'How to address you',
222 required: false,
223 });
224 mockSaveWpFormsField.mockResolvedValue({});
225 const onAfterSave = jest.fn();
226 const { WPFormsFieldModal } = importComponent();
227 render(
228 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={onAfterSave} />,
229 );
230 await waitFor(() =>
231 expect(
232 document.querySelector('input[aria-label="Label"]'),
233 ).not.toBeNull(),
234 );
235 fill('Label', 'Your full name');
236 toggle('Required field');
237 await act(async () => clickByText('Save'));
238 expect(mockSaveWpFormsField).toHaveBeenCalledWith({
239 formId: 12,
240 fieldId: 'f-3',
241 changes: { label: 'Your full name', required: true },
242 });
243 // pushUndo carries the INVERSE of the changes bag so performUndo's
244 // wpformsReplay branch can shallow-merge the originals back in.
245 expect(mockPushUndo).toHaveBeenCalledWith({
246 kind: 'wpforms-field',
247 wpformsReplay: true,
248 formId: 12,
249 fieldId: 'f-3',
250 changes: { label: 'Your name', required: false },
251 });
252 expect(mockTrack).toHaveBeenCalledWith('save', {
253 kind: 'wpforms_field',
254 type: 'text',
255 });
256 expect(onAfterSave).toHaveBeenCalledWith(true);
257 });
258
259 it('no-op when nothing changed → onAfterSave(false), no save call', async () => {
260 mockLoadWpFormsField.mockResolvedValue({
261 type: 'text',
262 label: 'Your name',
263 placeholder: '',
264 description: '',
265 required: false,
266 });
267 const onAfterSave = jest.fn();
268 const { WPFormsFieldModal } = importComponent();
269 render(
270 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={onAfterSave} />,
271 );
272 await waitFor(() =>
273 expect(
274 document.querySelector('input[aria-label="Label"]'),
275 ).not.toBeNull(),
276 );
277 await act(async () => clickByText('Save'));
278 expect(mockSaveWpFormsField).not.toHaveBeenCalled();
279 expect(onAfterSave).toHaveBeenCalledWith(false);
280 });
281
282 it('saveWpFormsField rejecting → save_failed + error Notice', async () => {
283 mockLoadWpFormsField.mockResolvedValue({
284 type: 'text',
285 label: 'x',
286 placeholder: '',
287 description: '',
288 required: false,
289 });
290 mockSaveWpFormsField.mockRejectedValueOnce(new Error('boom'));
291 const { WPFormsFieldModal } = importComponent();
292 render(
293 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={jest.fn()} />,
294 );
295 await waitFor(() =>
296 expect(
297 document.querySelector('input[aria-label="Label"]'),
298 ).not.toBeNull(),
299 );
300 fill('Label', 'y');
301 await act(async () => clickByText('Save'));
302 expect(mockTrack).toHaveBeenCalledWith('save_failed', {
303 kind: 'wpforms_field',
304 type: 'text',
305 });
306 expect(document.querySelector('[role="alert"]')?.textContent).toMatch(
307 /Sorry, something went wrong/i,
308 );
309 expect(mockPushUndo).not.toHaveBeenCalled();
310 });
311 });
312
313 describe('WPFormsFieldModal — close', () => {
314 it('Cancel calls onAfterSave(false)', async () => {
315 mockLoadWpFormsField.mockResolvedValue({
316 type: 'text',
317 label: 'x',
318 placeholder: '',
319 description: '',
320 required: false,
321 });
322 const onAfterSave = jest.fn();
323 const { WPFormsFieldModal } = importComponent();
324 render(
325 <WPFormsFieldModal formId={12} fieldId="f-3" onAfterSave={onAfterSave} />,
326 );
327 await waitFor(() =>
328 expect(
329 document.querySelector('input[aria-label="Label"]'),
330 ).not.toBeNull(),
331 );
332 clickByText('Cancel');
333 expect(onAfterSave).toHaveBeenCalledWith(false);
334 });
335 });
336