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 / ProductTextModal.test.jsx

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

266 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ProductTextModal loads a product, surfaces a TextControl for `name` or a
2 // TextareaControl for `short_description`, and on Save writes the field via
3 // saveProduct + pushes a `product` undo entry. No-op when unchanged.
4
5 import { act, fireEvent, render, waitFor } from '@testing-library/react';
6
7 const mockLoadProduct = jest.fn();
8 const mockSaveProduct = jest.fn();
9 const mockTrack = jest.fn();
10 const mockPushUndo = jest.fn();
11
12 jest.mock('@quick-edit/lib/api', () => ({
13 loadProduct: (...args) => mockLoadProduct(...args),
14 saveProduct: (...args) => mockSaveProduct(...args),
15 }));
16 jest.mock('@quick-edit/lib/cmd-enter-save', () => ({
17 useCmdEnterSave: jest.fn(),
18 }));
19 jest.mock('@quick-edit/lib/insights', () => ({
20 track: (...args) => mockTrack(...args),
21 }));
22 jest.mock('@quick-edit/state/undo', () => ({
23 pushUndo: (...args) => mockPushUndo(...args),
24 }));
25
26 jest.mock('@wordpress/components', () => ({
27 Modal: ({ title, onRequestClose, children }) => (
28 <div role="dialog" aria-label={title}>
29 <button
30 type="button"
31 data-testid="modal-close"
32 onClick={onRequestClose}
33 />
34 {children}
35 </div>
36 ),
37 Button: ({ children, onClick, disabled, isBusy }) => (
38 <button
39 type="button"
40 onClick={onClick}
41 disabled={disabled}
42 data-busy={String(!!isBusy)}
43 >
44 {children}
45 </button>
46 ),
47 Notice: ({ children, status }) => (
48 <div role="alert" data-status={status}>
49 {children}
50 </div>
51 ),
52 Spinner: () => <span data-testid="spinner" />,
53 TextControl: ({ label, value, onChange }) => (
54 <label>
55 {label}
56 <input
57 aria-label={label}
58 value={value || ''}
59 onChange={(e) => onChange(e.target.value)}
60 />
61 </label>
62 ),
63 TextareaControl: ({ label, value, onChange, rows }) => (
64 <label>
65 {label}
66 <textarea
67 aria-label={label}
68 value={value || ''}
69 rows={rows}
70 onChange={(e) => onChange(e.target.value)}
71 />
72 </label>
73 ),
74 }));
75
76 const importComponent = () =>
77 require('@quick-edit/components/modals/ProductTextModal');
78
79 const clickByText = (text) => {
80 const btn = Array.from(document.querySelectorAll('button')).find(
81 (b) => b.textContent === text,
82 );
83 fireEvent.click(btn);
84 };
85
86 beforeEach(() => {
87 jest.clearAllMocks();
88 });
89
90 describe('ProductTextModal — load', () => {
91 it('renders the Spinner before the load resolves', () => {
92 mockLoadProduct.mockReturnValue(new Promise(() => {}));
93 const { ProductTextModal } = importComponent();
94 render(
95 <ProductTextModal productId={7} field="name" onAfterSave={jest.fn()} />,
96 );
97 expect(document.querySelector('[data-testid="spinner"]')).not.toBeNull();
98 });
99
100 it('surfaces an error Notice on load reject', async () => {
101 mockLoadProduct.mockRejectedValueOnce(new Error('load failed'));
102 const { ProductTextModal } = importComponent();
103 render(
104 <ProductTextModal productId={7} field="name" onAfterSave={jest.fn()} />,
105 );
106 await waitFor(() =>
107 expect(document.querySelector('[role="alert"]')?.textContent).toMatch(
108 /Sorry, something went wrong/i,
109 ),
110 );
111 });
112 });
113
114 describe('ProductTextModal — field=name', () => {
115 beforeEach(() => {
116 mockLoadProduct.mockResolvedValue({ name: 'Old Name' });
117 });
118
119 it('renders a TextControl seeded with the product name', async () => {
120 const { ProductTextModal } = importComponent();
121 render(
122 <ProductTextModal productId={7} field="name" onAfterSave={jest.fn()} />,
123 );
124 await waitFor(() =>
125 expect(
126 document.querySelector('input[aria-label="Product name"]'),
127 ).not.toBeNull(),
128 );
129 expect(
130 document.querySelector('input[aria-label="Product name"]').value,
131 ).toBe('Old Name');
132 });
133
134 it('Save → saveProduct({ field: "name", value }) + product undo + track', async () => {
135 mockSaveProduct.mockResolvedValue({});
136 const onAfterSave = jest.fn();
137 const { ProductTextModal } = importComponent();
138 render(
139 <ProductTextModal productId={7} field="name" onAfterSave={onAfterSave} />,
140 );
141 await waitFor(() =>
142 expect(
143 document.querySelector('input[aria-label="Product name"]'),
144 ).not.toBeNull(),
145 );
146 fireEvent.change(
147 document.querySelector('input[aria-label="Product name"]'),
148 { target: { value: 'New Name' } },
149 );
150 await act(async () => clickByText('Save'));
151 expect(mockSaveProduct).toHaveBeenCalledWith({
152 productId: 7,
153 field: 'name',
154 value: 'New Name',
155 });
156 expect(mockPushUndo).toHaveBeenCalledWith({
157 kind: 'product',
158 productReplay: true,
159 productId: 7,
160 field: 'name',
161 beforeValue: 'Old Name',
162 });
163 expect(mockTrack).toHaveBeenCalledWith('save', {
164 kind: 'product',
165 field: 'name',
166 });
167 expect(onAfterSave).toHaveBeenCalledWith(true);
168 });
169
170 it('no-op when value unchanged: onAfterSave(false), no save call', async () => {
171 const onAfterSave = jest.fn();
172 const { ProductTextModal } = importComponent();
173 render(
174 <ProductTextModal productId={7} field="name" onAfterSave={onAfterSave} />,
175 );
176 await waitFor(() =>
177 expect(
178 document.querySelector('input[aria-label="Product name"]'),
179 ).not.toBeNull(),
180 );
181 await act(async () => clickByText('Save'));
182 expect(mockSaveProduct).not.toHaveBeenCalled();
183 expect(onAfterSave).toHaveBeenCalledWith(false);
184 });
185
186 it('saveProduct rejecting → save_failed + error Notice', async () => {
187 mockSaveProduct.mockRejectedValueOnce(new Error('rejected'));
188 const { ProductTextModal } = importComponent();
189 render(
190 <ProductTextModal productId={7} field="name" onAfterSave={jest.fn()} />,
191 );
192 await waitFor(() =>
193 expect(
194 document.querySelector('input[aria-label="Product name"]'),
195 ).not.toBeNull(),
196 );
197 fireEvent.change(
198 document.querySelector('input[aria-label="Product name"]'),
199 { target: { value: 'Other' } },
200 );
201 await act(async () => clickByText('Save'));
202 expect(mockTrack).toHaveBeenCalledWith('save_failed', {
203 kind: 'product',
204 field: 'name',
205 });
206 expect(document.querySelector('[role="alert"]')?.textContent).toMatch(
207 /Sorry, something went wrong/i,
208 );
209 });
210 });
211
212 describe('ProductTextModal — field=short_description', () => {
213 it('renders a TextareaControl + saves with field=short_description', async () => {
214 mockLoadProduct.mockResolvedValue({
215 short_description: 'old desc',
216 });
217 mockSaveProduct.mockResolvedValue({});
218 const onAfterSave = jest.fn();
219 const { ProductTextModal } = importComponent();
220 render(
221 <ProductTextModal
222 productId={7}
223 field="short_description"
224 onAfterSave={onAfterSave}
225 />,
226 );
227 await waitFor(() =>
228 expect(
229 document.querySelector('textarea[aria-label="Short description"]'),
230 ).not.toBeNull(),
231 );
232 expect(
233 document.querySelector('textarea[aria-label="Short description"]').value,
234 ).toBe('old desc');
235 fireEvent.change(
236 document.querySelector('textarea[aria-label="Short description"]'),
237 { target: { value: 'new desc' } },
238 );
239 await act(async () => clickByText('Save'));
240 expect(mockSaveProduct).toHaveBeenCalledWith({
241 productId: 7,
242 field: 'short_description',
243 value: 'new desc',
244 });
245 expect(onAfterSave).toHaveBeenCalledWith(true);
246 });
247 });
248
249 describe('ProductTextModal — close', () => {
250 it('Cancel calls onAfterSave(false)', async () => {
251 mockLoadProduct.mockResolvedValue({ name: 'x' });
252 const onAfterSave = jest.fn();
253 const { ProductTextModal } = importComponent();
254 render(
255 <ProductTextModal productId={7} field="name" onAfterSave={onAfterSave} />,
256 );
257 await waitFor(() =>
258 expect(
259 document.querySelector('input[aria-label="Product name"]'),
260 ).not.toBeNull(),
261 );
262 clickByText('Cancel');
263 expect(onAfterSave).toHaveBeenCalledWith(false);
264 });
265 });
266