PluginProbe
Extendify / 3.1.1
Extendify v3.1.1
3.2.3 3.2.2 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 All 129 releases
extendify / tests / unit / QuickEdit / components / modals / ProductPriceModal.test.jsx

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

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