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

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

197 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ProductImageModal opens a wp.media frame on mount, then on selection writes
2 // the picked attachment id through saveProduct({ field: 'image' }) and reloads.
3 // It returns null — no own DOM. Tests pin the frame wiring + save/undo contract
4 // plus the missing-wp.media short-circuit.
5
6 import { act, render } 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/insights', () => ({
18 track: (...args) => mockTrack(...args),
19 }));
20 jest.mock('@quick-edit/state/undo', () => ({
21 pushUndo: (...args) => mockPushUndo(...args),
22 }));
23
24 const importComponent = () =>
25 require('@quick-edit/components/modals/ProductImageModal');
26
27 const makeFrame = ({ selectionId = 99 } = {}) => {
28 const handlers = {};
29 const classes = new Set();
30 const $modal = {
31 __classes: classes,
32 addClass: jest.fn((c) => classes.add(c)),
33 removeClass: jest.fn((c) => classes.delete(c)),
34 hasClass: (c) => classes.has(c),
35 };
36 return {
37 __handlers: handlers,
38 __$modal: $modal,
39 modal: { $el: $modal },
40 on: jest.fn((evt, cb) => {
41 handlers[evt] = handlers[evt] || [];
42 handlers[evt].push(cb);
43 }),
44 open: jest.fn(),
45 content: { mode: jest.fn() },
46 state: () => ({
47 get: () => ({
48 first: () => ({
49 toJSON: () => ({ id: selectionId }),
50 }),
51 }),
52 }),
53 };
54 };
55
56 const fire = (frame, evt) => {
57 for (const cb of frame.__handlers[evt] || []) {
58 cb();
59 }
60 };
61
62 beforeEach(() => {
63 jest.clearAllMocks();
64 document.body.className = '';
65 });
66
67 afterEach(() => {
68 delete window.wp;
69 });
70
71 describe('ProductImageModal — wp.media missing', () => {
72 it('calls onAfterSave(false) immediately when window.wp.media is undefined', () => {
73 delete window.wp;
74 const onAfterSave = jest.fn();
75 const { ProductImageModal } = importComponent();
76 render(<ProductImageModal productId={9} onAfterSave={onAfterSave} />);
77 expect(onAfterSave).toHaveBeenCalledWith(false);
78 });
79 });
80
81 describe('ProductImageModal — frame lifecycle', () => {
82 let frame;
83 beforeEach(() => {
84 frame = makeFrame();
85 window.wp = { media: jest.fn(() => frame) };
86 mockLoadProduct.mockResolvedValue({ image_id: 11 });
87 });
88
89 it('constructs the wp.media frame with image-library config + opens it', () => {
90 const { ProductImageModal } = importComponent();
91 render(<ProductImageModal productId={7} onAfterSave={jest.fn()} />);
92 expect(window.wp.media).toHaveBeenCalledWith(
93 expect.objectContaining({
94 library: { type: 'image' },
95 multiple: false,
96 }),
97 );
98 expect(frame.open).toHaveBeenCalled();
99 });
100
101 it('prefetches the current image id for the undo before-state', () => {
102 const { ProductImageModal } = importComponent();
103 render(<ProductImageModal productId={7} onAfterSave={jest.fn()} />);
104 expect(mockLoadProduct).toHaveBeenCalledWith(7);
105 });
106
107 it('toggles class extendify-quick-edit-mode-browse on frame.modal.$el (NOT body) on open / close', () => {
108 // Round-5 #2: the class lives on the modal element so other
109 // wp.media frames (the AI Agent's "Change image" picker) don't
110 // inherit QE's chrome-hiding CSS and blank out.
111 const { ProductImageModal } = importComponent();
112 render(<ProductImageModal productId={7} onAfterSave={jest.fn()} />);
113 fire(frame, 'open');
114 expect(frame.__$modal.hasClass('extendify-quick-edit-mode-browse')).toBe(
115 true,
116 );
117 expect(
118 document.body.classList.contains('extendify-quick-edit-mode-browse'),
119 ).toBe(false);
120 fire(frame, 'close');
121 expect(frame.__$modal.hasClass('extendify-quick-edit-mode-browse')).toBe(
122 false,
123 );
124 });
125 });
126
127 describe('ProductImageModal — select', () => {
128 let frame;
129 let onAfterSave;
130
131 beforeEach(() => {
132 frame = makeFrame({ selectionId: 909 });
133 window.wp = { media: jest.fn(() => frame) };
134 mockLoadProduct.mockResolvedValue({ image_id: 11 });
135 mockSaveProduct.mockResolvedValue({});
136 onAfterSave = jest.fn();
137 });
138
139 const triggerSelect = async () => {
140 const { ProductImageModal } = importComponent();
141 render(<ProductImageModal productId={7} onAfterSave={onAfterSave} />);
142 // flush the prefetch promise before driving select
143 await act(async () => {});
144 await act(async () => {
145 for (const cb of frame.__handlers.select || []) await cb();
146 });
147 };
148
149 it('calls saveProduct({ productId, field: "image", value: id })', async () => {
150 await triggerSelect();
151 expect(mockSaveProduct).toHaveBeenCalledWith({
152 productId: 7,
153 field: 'image',
154 value: 909,
155 });
156 expect(onAfterSave).toHaveBeenCalledWith(true);
157 expect(mockTrack).toHaveBeenCalledWith('save', {
158 kind: 'product',
159 field: 'image',
160 });
161 });
162
163 it('pushes a product-image undo when the before id differed', async () => {
164 await triggerSelect();
165 expect(mockPushUndo).toHaveBeenCalledWith({
166 kind: 'product-image',
167 productReplay: true,
168 productId: 7,
169 field: 'image',
170 beforeValue: 11,
171 });
172 });
173
174 it('skips the undo push when the before id matched the new id', async () => {
175 mockLoadProduct.mockResolvedValueOnce({ image_id: 909 });
176 await triggerSelect();
177 expect(mockPushUndo).not.toHaveBeenCalled();
178 });
179
180 it('saveProduct rejecting → onAfterSave(false) + save_failed track', async () => {
181 mockSaveProduct.mockRejectedValueOnce(new Error('boom'));
182 await triggerSelect();
183 expect(onAfterSave).toHaveBeenCalledWith(false);
184 expect(mockTrack).toHaveBeenCalledWith('save_failed', {
185 kind: 'product',
186 field: 'image',
187 });
188 });
189
190 it('closing the frame without picking → onAfterSave(false)', () => {
191 const { ProductImageModal } = importComponent();
192 render(<ProductImageModal productId={7} onAfterSave={onAfterSave} />);
193 fire(frame, 'close');
194 expect(onAfterSave).toHaveBeenCalledWith(false);
195 });
196 });
197