PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
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 / lib / api.test.js

api.test.js in Extendify 3.1.0, at tests/unit/QuickEdit/lib/api.test.js

264 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Pins the REST helper shape used by every Quick Edit save path:
2 // /quick-edit/save, /quick-edit/site-identity, /quick-edit/product,
3 // /quick-edit/wp-navigation, /quick-edit/wpforms. fetch is mocked at the
4 // global level so each test can inspect the exact URL, method, headers,
5 // and body the call site emits.
6
7 import * as api from '@quick-edit/lib/api';
8
9 const setExtData = (overrides = {}) => {
10 window.extQuickEditData = {
11 restRoot: 'https://wp.test/wp-json/extendify/v1',
12 nonce: 'qe-nonce',
13 ...overrides,
14 };
15 };
16
17 const mockFetch = (responder) => {
18 global.fetch = jest.fn(responder);
19 };
20
21 const ok = (json) =>
22 Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(json) });
23
24 const fail = (status, json) =>
25 Promise.resolve({
26 ok: false,
27 status,
28 json: () => Promise.resolve(json ?? {}),
29 });
30
31 beforeEach(() => {
32 setExtData();
33 });
34
35 afterEach(() => {
36 delete global.fetch;
37 delete window.extQuickEditData;
38 });
39
40 describe('api.post — request shape', () => {
41 it('POSTs to restRoot + path with JSON body, nonce header, same-origin creds', async () => {
42 mockFetch(() => ok({ ok: true }));
43 await api.post('/foo', { a: 1 });
44 expect(fetch).toHaveBeenCalledTimes(1);
45 const [url, init] = fetch.mock.calls[0];
46 expect(url).toBe('https://wp.test/wp-json/extendify/v1/foo');
47 expect(init.method).toBe('POST');
48 expect(init.credentials).toBe('same-origin');
49 expect(init.headers).toEqual({
50 'Content-Type': 'application/json',
51 'X-WP-Nonce': 'qe-nonce',
52 });
53 expect(JSON.parse(init.body)).toEqual({ a: 1 });
54 });
55
56 it('returns the parsed JSON on a 2xx', async () => {
57 mockFetch(() => ok({ saved: true, html: '<p>hi</p>' }));
58 const out = await api.post('/foo', {});
59 expect(out).toEqual({ saved: true, html: '<p>hi</p>' });
60 });
61 });
62
63 describe('api.post — error mapping', () => {
64 it('throws with message from json.message and exposes status + body', async () => {
65 mockFetch(() => fail(422, { message: 'invalid' }));
66 await expect(api.post('/foo', {})).rejects.toMatchObject({
67 message: 'invalid',
68 status: 422,
69 body: { message: 'invalid' },
70 });
71 });
72
73 it('falls back to json.error when message is absent', async () => {
74 mockFetch(() => fail(400, { error: 'bad shape' }));
75 await expect(api.post('/foo', {})).rejects.toMatchObject({
76 message: 'bad shape',
77 status: 400,
78 });
79 });
80
81 it('falls back to "HTTP <status>" when the body has no message or error', async () => {
82 mockFetch(() => fail(500, {}));
83 await expect(api.post('/foo', {})).rejects.toMatchObject({
84 message: 'HTTP 500',
85 status: 500,
86 });
87 });
88
89 it('treats a body that fails to parse as empty json', async () => {
90 mockFetch(() =>
91 Promise.resolve({
92 ok: false,
93 status: 503,
94 json: () => Promise.reject(new Error('parse failed')),
95 }),
96 );
97 await expect(api.post('/foo', {})).rejects.toMatchObject({
98 message: 'HTTP 503',
99 status: 503,
100 body: {},
101 });
102 });
103 });
104
105 describe('api.get — request shape + error mapping', () => {
106 it('GETs to restRoot + path with the nonce header and no body', async () => {
107 mockFetch(() => ok({ x: 1 }));
108 await api.get('/foo?bar=1');
109 const [url, init] = fetch.mock.calls[0];
110 expect(url).toBe('https://wp.test/wp-json/extendify/v1/foo?bar=1');
111 expect(init.method).toBe('GET');
112 expect(init.credentials).toBe('same-origin');
113 expect(init.headers).toEqual({ 'X-WP-Nonce': 'qe-nonce' });
114 expect(init.body).toBeUndefined();
115 });
116
117 it('throws with body json + status on non-2xx', async () => {
118 mockFetch(() => fail(404, { message: 'not found' }));
119 await expect(api.get('/foo')).rejects.toMatchObject({
120 message: 'not found',
121 status: 404,
122 });
123 });
124 });
125
126 describe('api.save', () => {
127 it('routes payload to POST /quick-edit/save, defaulting translatedContext to null', async () => {
128 mockFetch(() => ok({}));
129 await api.save({ blockId: 12, patches: { content: 'hi' } });
130 const [url, init] = fetch.mock.calls[0];
131 expect(url).toBe('https://wp.test/wp-json/extendify/v1/quick-edit/save');
132 expect(JSON.parse(init.body)).toEqual({
133 blockId: 12,
134 patches: { content: 'hi' },
135 translatedContext: null,
136 });
137 });
138
139 it('forwards the page translatedContext so the server can fail closed on text saves', async () => {
140 setExtData({
141 translatedContext: { isTranslated: true, plugin: 'translatepress' },
142 });
143 mockFetch(() => ok({}));
144 await api.save({
145 blockId: 3,
146 rawBlock: '<!-- wp:heading --><h2>x</h2><!-- /wp:heading -->',
147 });
148 const [, init] = fetch.mock.calls[0];
149 expect(JSON.parse(init.body).translatedContext).toEqual({
150 isTranslated: true,
151 plugin: 'translatepress',
152 });
153 });
154 });
155
156 describe('api.saveSiteIdentity + loadSiteIdentity', () => {
157 it('saveSiteIdentity POSTs the payload to /quick-edit/site-identity', async () => {
158 mockFetch(() => ok({}));
159 await api.saveSiteIdentity({ blogname: 'New' });
160 const [url, init] = fetch.mock.calls[0];
161 expect(url).toBe(
162 'https://wp.test/wp-json/extendify/v1/quick-edit/site-identity',
163 );
164 expect(init.method).toBe('POST');
165 expect(JSON.parse(init.body)).toEqual({ blogname: 'New' });
166 });
167
168 it('loadSiteIdentity GETs /quick-edit/site-identity', async () => {
169 mockFetch(() => ok({ blogname: 'Cur' }));
170 const out = await api.loadSiteIdentity();
171 expect(fetch.mock.calls[0][0]).toBe(
172 'https://wp.test/wp-json/extendify/v1/quick-edit/site-identity',
173 );
174 expect(fetch.mock.calls[0][1].method).toBe('GET');
175 expect(out).toEqual({ blogname: 'Cur' });
176 });
177 });
178
179 describe('api.loadProduct + saveProduct', () => {
180 it('loadProduct URL-encodes the product id and routes to GET', async () => {
181 mockFetch(() => ok({}));
182 await api.loadProduct('42 ?');
183 const [url, init] = fetch.mock.calls[0];
184 expect(url).toBe(
185 'https://wp.test/wp-json/extendify/v1/quick-edit/product?product_id=42%20%3F',
186 );
187 expect(init.method).toBe('GET');
188 });
189
190 it('saveProduct POSTs {product_id, field, value}', async () => {
191 mockFetch(() => ok({}));
192 await api.saveProduct({ productId: 42, field: 'name', value: 'New' });
193 const [url, init] = fetch.mock.calls[0];
194 expect(url).toBe('https://wp.test/wp-json/extendify/v1/quick-edit/product');
195 expect(JSON.parse(init.body)).toEqual({
196 product_id: 42,
197 field: 'name',
198 value: 'New',
199 });
200 });
201 });
202
203 describe('api.saveWpNavigationItem', () => {
204 it('POSTs ref-based nav payload verbatim (navPostId / itemIndex / blockType / patches)', async () => {
205 mockFetch(() => ok({}));
206 await api.saveWpNavigationItem({
207 navPostId: 7,
208 itemIndex: 2,
209 blockType: 'core/navigation-link',
210 patches: { label: 'Home', url: '/' },
211 });
212 const [url, init] = fetch.mock.calls[0];
213 expect(url).toBe(
214 'https://wp.test/wp-json/extendify/v1/quick-edit/wp-navigation',
215 );
216 expect(JSON.parse(init.body)).toEqual({
217 navPostId: 7,
218 itemIndex: 2,
219 blockType: 'core/navigation-link',
220 patches: { label: 'Home', url: '/' },
221 });
222 });
223 });
224
225 describe('api.loadWpFormsField + saveWpFormsField', () => {
226 it('loadWpFormsField URL-encodes both form_id and field_id', async () => {
227 mockFetch(() => ok({}));
228 await api.loadWpFormsField({ formId: '1&', fieldId: '2 ' });
229 const [url] = fetch.mock.calls[0];
230 expect(url).toBe(
231 'https://wp.test/wp-json/extendify/v1/quick-edit/wpforms?form_id=1%26&field_id=2%20',
232 );
233 });
234
235 it('saveWpFormsField POSTs {form_id, field_id, changes}', async () => {
236 mockFetch(() => ok({}));
237 await api.saveWpFormsField({
238 formId: 1,
239 fieldId: 2,
240 changes: { label: 'New' },
241 });
242 const [, init] = fetch.mock.calls[0];
243 expect(JSON.parse(init.body)).toEqual({
244 form_id: 1,
245 field_id: 2,
246 changes: { label: 'New' },
247 });
248 });
249 });
250
251 describe('api — extQuickEditData fallback', () => {
252 it('reads restRoot + nonce from an empty extQuickEditData (URL becomes "undefined<path>")', async () => {
253 // Pinning current behavior: api.js destructures missing fields as
254 // undefined and interpolates them verbatim. If we ever tighten this
255 // to fail loudly, update the test.
256 delete window.extQuickEditData;
257 mockFetch(() => ok({}));
258 await api.get('/foo');
259 const [url, init] = fetch.mock.calls[0];
260 expect(url).toBe('undefined/foo');
261 expect(init.headers['X-WP-Nonce']).toBeUndefined();
262 });
263 });
264